一、编程指南PDF下载链接(中英文档)
-
1、Metal编程指南PDF链接
https://github.com/dennie-lee/ios_tech_record/raw/main/Metal学习PDF/Metal 编程指南.pdf -
2、Metal着色语言(Metal Shader Language:简称MSL)编程指南PDF链接
https://github.com/dennie-lee/ios_tech_record/raw/main/Metal学习PDF/Metal 着色语言指南.pdf
二、渲染流程图
三、内容前述
此示例展示如何配置渲染管线并将其用作渲染通道的一部分,以将简单的图片渲染到视图中和立体锥体中,效果图如下:
四、Metal渲染管线
渲染管线处理绘图命令并将数据写入渲染通道的目标。此示例侧重于管道的三个主要阶段:顶点阶段、光栅化阶段和片段阶段。
渲染从绘图命令开始,其中包括顶点数和要渲染的图元类型。例如,这是此示例中的绘图命令:(参数数据解析请看下一步)
commandEncoder.drawIndexedPrimitives(type: .triangle, indexCount: 6,
indexType: .uint32, indexBuffer: vertexIndexs,
indexBufferOffset: 0)
五、Metal渲染管道处理数据
这里使用的是顶点索引的方式关联顶点数据。例如顶点数据:
var vertexs:[Float] = [
//顶点坐标 //纹理坐标 //颜色值
1, -1, 1,1, 1.0,0.0,0.0,1.0,//长方形的右下角
-1,-1, 0,1, 1.0,0.0,0.0,1.0,//长方形的左下角
-1, 1, 0,0, 1.0,0.0,0.0,1.0,//长方形的左上角
1, 1, 1,0, 1.0,0.0,0.0,1.0,//长方形的右上角
]
顶点参考坐标系如图一,因为只是把图片渲染到视图上,相当于是2D坐标(二维),只看最前面那个面即可;纹理坐标参考图二
颜色值有四个通道RGBA:红色、绿色、蓝色、透明度,所以是四个数据。
顶点索引值数据,索引0、1、2对应的顶点以顺时针的顺序构成一个三角形;0、2、3对应的顶点以顺时针的顺序构成一个三角形,两个三角形组合起来渲染构成长方形(Metal默认是以顺时针的方式渲染,也可以设置成逆时针,此处使用默认的方式):
let indexs : [Int32] = [0,1,2,0,2,3]
vertexIndexs = device?.makeBuffer(bytes: indexs,
length: MemoryLayout<Int32>.size * 6,
options: .storageModeShared)
MTLPrimitiveType绘制类型有如下五种:(第四步的参数1)
@available(iOS 8.0, *)
public enum MTLPrimitiveType : UInt, @unchecked Sendable {
case point = 0
case line = 1
case lineStrip = 2
case triangle = 3
case triangleStrip = 4
}
绘制方式如下图所示:
在此示例中,管道的输入数据是顶点的位置、纹理坐标位置和自定义的颜色(自定义颜色:可堆叠在图片上)
声明一个VertexInput结构,保存顶点坐标、纹理坐标和颜色数据
typedef struct {
//顶点坐标
float2 position;
//纹理坐标
float2 textureCoordinate;
//颜色
float4 color;
} VertexInput;
顶点阶段为顶点生成数据,需要提供变换后的位置和颜色。声明一个包含顶点位置、纹理位置和颜色值的RasterizerData结构
typedef struct {
//顶点坐标
float4 position [[position]];
//纹理坐标
float2 textureCoordinate;
//颜色
float4 color;
} RasterizerData;
位置声明为float4意味着它包含四个32位浮点值(将保存x、y坐标、z左边和w值);纹理位置声明为float2意味着它包含两个32位浮点值(将保存x和y坐标);颜色使用float4存储,因为它们有四个通道:红色、绿色、蓝色和alpha。
因为Metal需要知道光栅化数据中的哪个字段提供位置数据,而Metal不会对结构中的字段强制执行任何特定的命名约定,所以使用[[position]]属性限定符注释位置字段以声明该字段保存输出位置。
六、定义和编写顶点着色函数
声明顶点函数,包括它的输入参数和它输出的数据。就像使用kernel关键字声明计算函数一样,使用vertex关键字声明顶点函数。(kernel:可查看这篇文章https://blog.csdn.net/qqwyuli/article/details/130785820)
//[[vertex_id]] :顶点id标识符,并不由开发者传递
//属性修饰符"[[buffer(index)]]" 为着色函数参数设定了缓存的位置
vertex RasterizerData vertexShader(uint vertexId [[vertex_id]],
constant VertexInput *vertexs [[buffer(0)]]){
RasterizerData out;
out.position = vector_float4(0.0,0.0,0.0,1.0);
out.position.xy = vertexs[vertexId].position;
//纹理坐标
out.textureCoordinate = vertexs[vertexId].textureCoordinate;
//自定义传过来的颜色
out.color = vertexs[vertexId].color;
return out;
};
第一个参数vertexID使用[[vertex_id]]属性限定符,Metal的一个关键字。执行渲染命令时,GPU会多次调用顶点函数,为每个顶点生成一个唯一值。
第二个参数 vertices 是一个包含顶点数据的数组,使用之前定义的Vertex结构
第二个具有[[buffer(n)]]属性限定符。默认情况下,Metal会自动为每个参数分配参数表中的槽。当将[[buffer(n)]]限定符添加到缓冲区参数时,明确地告诉Metal要使用哪个插槽。显式声明插槽可以更轻松地修改着色器,而无需更改应用程序代码.
对命令的参数进行编码时,设置顶点函数的参数
commandEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
七、计算纹理位置和编写片段着色函数(片元着色函数)
对纹理进行采样以从纹理中的某个位置计算颜色。要对纹理数据进行采样,片段函数需要纹理坐标和对要采样的纹理的引用。除了从光栅化器阶段传入的参数外,还传入一个带有 texture2d 类型和 [[texture(index)]] 属性限定符的colorTexture参数。此参数是对要采样的 MTLTexture 对象的引用。
fragment float4 fragmentShader(RasterizerData input [[stage_in]],
texture2d<float> colorTexture [[texture(0)]])
使用内置纹理 sample() 函数对纹素数据进行采样。sample()函数有两个参数:一个采样器 (textureSampler),它描述了希望如何采样纹理,以及纹理坐标(input.textureCoordinate),它描述了纹理中要采样的位置。sample()函数从纹理中获取一个或多个像素并返回从这些像素计算出的颜色。
当渲染到的区域与纹理的大小不同时,采样器可以使用不同的算法来准确计算sample()函数应该返回的纹素颜色。设置mag_filter模式来指定当面积大于纹理大小时采样器应该如何计算返回的颜色,设置min_filter模式来指定当面积小于纹理大小时采样器应该如何计算返回颜色质地。为两个过滤器设置线性模式会使采样器对给定纹理坐标周围像素的颜色进行平均,从而产生更平滑的输出图像。
constexpr sampler textureSampler (mag_filter::linear,min_filter::linear);
//float4 color = colorTexture.sample(textureSampler, input.textureCoordinate) * input.color;
float4 color = colorTexture.sample(textureSampler, input.textureCoordinate);
对命令的参数进行编码时,设置片段函数的纹理参数。此示例使用索引0来识别Metal着色语言代码中的纹理。
commandEncoder.setFragmentTexture(texture, index: 0)
完整的片段函数
fragment float4 fragmentShader(RasterizerData input [[stage_in]],
texture2d<float> colorTexture [[texture(0)]]){
constexpr sampler textureSampler (mag_filter::linear,min_filter::linear);
//float4 color = colorTexture.sample(textureSampler, input.textureCoordinate) * input.color;
float4 color = colorTexture.sample(textureSampler, input.textureCoordinate);
return color;
};
八、创建渲染Pipeline State对象
顶点函数和片元函数已经完成,可以创建一个使用它们的渲染管道。首先,获取默认库并为每个函数获取一个MTLFunction对象和创建一个 MTLRenderPipelineState 对象。渲染管道有更多阶段需要配置,可以使用 MTLRenderPipelineDescriptor 来配置管道。
var library = device?.makeDefaultLibrary()
if let url = Bundle.main.url(forResource: "TextureShaders", withExtension: "metal"){
library = try? device?.makeLibrary(URL: url)
}
let descriptor = MTLRenderPipelineDescriptor()
descriptor.vertexFunction = library?.makeFunction(name: "vertexShader")
descriptor.fragmentFunction = library?.makeFunction(name: "fragmentShader")
descriptor.colorAttachments[0].pixelFormat = viewColorPixelformat
pipelineState = try? device?.makeRenderPipelineState(descriptor: descriptor)
九、设置视口
现在有了管道的渲染管道状态对象,将要渲染图片纹理。可以使用渲染命令编码器来执行此操作。首先,设置视口,以便 Metal 知道要绘制到渲染目标的哪一部分。
commandEncoder.setViewport(MTLViewport(originX: 0, originY: 0,
width: viewSize.width, height: viewSize.height,
znear: -1.0, zfar: 1.0))
十、设置渲染Pipeline State
为要使用的管道设置渲染管道状态。
commandEncoder.setRenderPipelineState(pipelineState)
十一、将参数数据发送到顶点函数
通常使用缓冲区(MTLBuffer)将数据传递给着色器。然而,当只需要将少量数据传递给顶点函数时,就像这里的情况一样,将数据直接复制到命令缓冲区中。
commandEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
十二、对绘图命令进行编码
指定图元的种类、起始索引和索引数。渲染纹理图片时,将使用vertexID参数的值和顶点索引调用顶点函数。
commandEncoder.drawIndexedPrimitives(type: .triangle, indexCount: 6,
indexType: .uint32, indexBuffer: vertexIndexs,
indexBufferOffset: 0)
与使用Metal绘制到屏幕一样,结束编码过程并提交命令缓冲区。但是,可以使用同一组步骤对更多渲染命令进行编码。最终图像呈现为好像命令是按照指定的顺序处理的。(为了性能,允许 GPU 并行处理命令甚至部分命令,只要最终结果看起来是按顺序呈现的。)
十三、完整代码
例子:github链接:https://github.com/dennie-lee/MetalDrawTextureDemo