Swift

ShaderView

ShaderView 是一个 SwiftUI 包,旨在集成和显示 Metal 着色器。它简化了在 SwiftUI 应用程序中使用用 .metal 编写的自定义着色器的过程。

设置

将任何 .metal 文件添加到项目中,可以为空文件。这将确保可以创建一个 metal 库并且可以编译默认着色器。

ShaderView()

ShaderView() 允许您显示 .metal 文件中定义的任何着色器。要使用您的自定义着色器,只需在初始化 ShaderView 时指定着色器的名称。

基本用法

ShaderView()

参数

Metal 代码集成

当仅使用其中一个默认着色器(defaultFragmentShader 或 defaultVertexShader)时,至关重要的是要使顶点着色器的输出与片元着色器的输入相匹配。以下是一些默认布局,可确保您的着色器与该软件包一起使用,尤其是在您选择仅使用其中一个默认着色器时。

软件包所需结构体的定义

我的软件包使用这些结构体将信息传递给着色器。即使在使用自定义着色器时,也必须将它们包含在 .metal 文件中,因为软件包的缓冲区需要它们。

struct VertexOutput {
    float4 position [[position]];
    float2 screenCoord;
};
struct Viewport {
    float2 size; 
};

片元着色器的默认定义

这是一个确保与软件包兼容的模板。自定义着色器时,除了 shaderInput 之外,保持参数相同。ShaderInput 可以替换为自定义参数,有关更多信息,请参见下面的“自定义着色器输入”部分。

fragment float4 customFragmentShader(VertexOutput in [[stage_in]],
                              constant Viewport& viewport [[buffer(0)]],
                              constant ShaderInput& shaderInput [[buffer(1)]]){
  // Shader logic... 
  return float4(red, green, blue, alpha); //Standard return format
}

顶点着色器的默认定义

这是一个确保与软件包兼容的模板。自定义时,请确保具有相同的输入和输出类型。

vertex VertexOutput customVertexShader(uint vertexID [[vertex_id]],
                                     constant Viewport& viewport [[buffer(0)]]) {
    // Shader logic... 
    return VertexOutput; //Standard return format compatable with the default fragment shader
}

自定义着色器输入

ShaderInputable

提供给 ShaderView 的任何着色器输入都必须符合此协议。它需要软件包用于将数据转换为 Metal 友好格式并管理其更改的方法。

public protocol ShaderInputable: AnyObject, ObservableObject{
    init(time: Float)
    
    var time: Float {get set}
    var onChange: (() -> Void)? { get set }

    func updateProperties(from input: any ShaderInputable)
    func metalData() -> Data
}
组件

使用协议的示例

class ShaderInputableExample: ShaderInputable {

    var time: Float = 0.0
    var onChange: (() -> Void)?
    
    //New variable example. Changing this value triggers the 'onChange' closure, allowing the shader to respond to changes in its active state.
    var isActive: Bool = true{
        didSet{
            onChange?()
        }
    }

    required init(time: Float){
        self.time = time
        self.isActive = true
    }

    func updateProperties(from input: any ShaderInputable){
        guard let input = input as? ShaderInputableExample else {
            return
        }
        self.isActive = input.isActive;
    }

    func metalData() -> Data {
        var metalInput = CustomMetalShaderInput(time: self.time, isActive: self.isActive)
        return Data(bytes: &metalInput, count: MemoryLayout<CustomMetalShaderInput>.size)
    }
}

子类化默认 shaderInput 的示例代码

class SubclassedShaderInput: ShaderInput {

    //New variable example. Changing this value triggers the 'onChange' closure, allowing the shader to respond to changes in its active state.
    var isActive: Bool {
           didSet {
               onChange?()
           }
       }

    required init(time: Float) {
        self.isActive = true
        super.init(time: time)
    }

    override func updateProperties(from input: any ShaderInputable){
        guard let input = input as? SubclassedShaderInput else {
            return
        }
        self.isActive = input.isActive;
    }

    override func metalData() -> Data {
        var metalInput = CustomMetalShaderInput(time: self.time, isActive: self.isActive)
        return Data(bytes: &metalInput, count: MemoryLayout<CustomMetalShaderInput>.size)
    }
}

自定义着色器输入数据的示例

为了使 Metal 能够正确读取着色器输入数据,缓冲区大小和期望值需要匹配。建议定义自定义着色器输入结构,该结构也将在 Metal 中以相同的方式定义。

用于 metalData() 的自定义着色器输入结构示例

此示例与上述的 SubclassedShaderInput 和 ShaderInputableExample 兼容。

struct CustomMetalShaderInput {
    var time: Float
    var isActive: Bool //new variable example
    var padding: [UInt8] = Array(repeating: 0, count: 3) //needed for difference in size of boolean in swift vs metal
}

ShaderViewLogger

要更改日志级别,请使用 ShaderViewLogger.setLogLevel(level: newLevel),其中 newLevel 是以下任何级别:

public enum ShaderViewLogLevel: Int {
    case none = 0
    case error = 1
    case debug = 2
}