SceneKit杂谈 - 如何使用Metal Shader自定义SCNMaterial的效果

577 阅读2分钟

前言

本篇文章主要记录如何使用SCNMaterial + Metal Shader实现自定义材质效果

编写一个基本的Metal Shader

Shader主要包含下面的部分

Vertex Function输入输出数据结构

struct VertexInput {
  float3 position  [[attribute(SCNVertexSemanticPosition)]];
  float2 uv [[attribute(SCNVertexSemanticTexcoord0)]];
};

struct VertexOut {
  float4 position [[position]];
  float2 uv;
};

这里的VertexInput通过attribute(SCNVertexSemanticXXX)和SceneKit约定好的顶点格式进行映射,VertexOut则是标准的Metal Shader写法,主要用于Fragment Function的输入

SceneKit通用输入Buffer结构

在Metal中,使用Buffer来传递uniform变量,定义NodeBuffer结构来接受SceneKit SCNNode的通用uniform变量

struct NodeBuffer {
  float4x4 modelTransform;
  float4x4 modelViewProjectionTransform;
  float4x4 modelViewTransform;
  float4x4 normalTransform;
  float2x3 boundingBox;
};

Vertex Function

这个是标准的Metal Vertex Function,输入参数是VertexInputNodeBuffer

vertex VertexOut textureSamplerVertex(VertexInput in [[ stage_in ]], constant NodeBuffer& scn_node [[buffer(1)]]) {
  VertexOut out;
  out.position = scn_node.modelViewProjectionTransform * float4(in.position, 1.0);
  out.uv = in.uv;
  return out;
}

使用NodeBuffer中的mvp矩阵对原始位置进行变换。

Fragment Function

fragment float4 textureSamplerFragment(VertexOut out [[ stage_in ]], texture2d<float, access::sample> diffuse [[texture(0)]]) {
    constexpr sampler textureSampler(coord::normalized, filter::linear, address::repeat);
    return diffuse.sample(textureSampler, out.uv);
}

Fragment Function就是简单的使用uv对纹理采样,返回对应的颜色

SCNMaterial使用自定义Metal Shader

mat.program = [SCNProgram program];
mat.program.vertexFunctionName = @"textureSamplerVertex";
mat.program.fragmentFunctionName = @"textureSamplerFragment";

创建SCNProgram并指定Metal Shader中的vertexFunctionNamefragmentFunctionName,然后赋值给SCNMaterial的program即可

设置纹理

通过下面的代码可以将图片赋值给Fragment Function中diffuse参数

[mainCanvasMaterial setValue:[SCNMaterialProperty materialPropertyWithContents:img] forKey:@"diffuse"];

img是UIImage类型变量,SceneKit会在底层将UIImage转成MTLTexture,绑定到Metal Shader的diffuse变量,也就是索引为0的纹理上。

如何传递自定义Buffer数据

比如自定义一个表示缩放的数据结构来缩放纹理

struct ScaleParams {
    float2 scale;
};

在Vertex Function 或者 Fragment Function中增加参数

vertex VertexOut textureSamplerVertex(VertexInput in [[ stage_in ]], constant NodeBuffer& scn_node [[buffer(1)]], constant ScaleParams &scaleParams [[buffer(2)]]) {
  VertexOut out;
  out.position = scn_node.modelViewProjectionTransform * float4(in.position, 1.0);
    float offsetX = 0.5 * (1.0 - 1.0 / scaleParams.scale.x);
    float offsetY = 0.5 * (1.0 - 1.0 / scaleParams.scale.y);
  out.uv = float2(offsetX + in.uv.x / scaleParams.scale.x, offsetY + in.uv.y / scaleParams.scale.y);
  return out;
}

[[buffer(2)]]表示ScaleParams绑定到索引为2的buffer位置,这也是标准的Metal Shader做法。

在oc代码中,使用setValue:forKey:设置该buffer值

[mainCanvasMaterial setValue:[NSData dataWithBytes:&scale length:sizeof(simd_float2)] forKey:@"scaleParams"];

这里直接将NSData传递给SCNMaterial,在底层会把NSData传递给对应索引的MTLBuffer