iOS 开发者的 Metal 养成计划「4」 —— Metal 着色器语言和图形渲染

发布于:2018-01-25 15:59,阅读数:1011,点赞数:0


# 引言 在前几篇文章中介绍了 Metal 中的命令系统。但光有命令系统还是无法画出任何图形,这里还需要着色器程序的参与。看完本文以后,读者就可以把图像渲染从 Metal API 到着色器一整条的流程走通了。 # Metal Shader System ## Metal Shading Language Metal 的着色器用的是一门专门的语言,叫`Metal Shading Language`(大概可以叫`MSL`?)。既然是一门专门的语言,资料就只有官方文档了:[「Metal Shading Language Specification」](https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf)。 它是一门单独的语言,但它不是全新的。它是 C++14 的一个子集,然后做了不多的扩展。和`GLSL`比,尤其是熟悉 C++ 的同学,写起来要爽得多。这门语言包括了 C++ 中的模板类和模板方法、宏替换、宏替换等内容,可以以面向对象方式写出更复杂的着色器。 > 在这里建议读者仔细阅读上面提供的文档,文档里详细描述了语言支持的范围,各类数据类型等开发者所需要的一切内容。在本文中仅会就实际使用方法做简单介绍。 在使用 Metal 时,若是用于渲染图像,则与传统的 OpenGL 中相同,需要提供`顶点着色器函数`和`片元着色器函数`,若是用于 GPU 运算时只需要完成`核函数`即可。在 MSL 中,在内存/显存与着色器数据交换上与 OpenGL 有较大区别,下面来仔细看。 ## 图像渲染 与传统 OpenGL 渲染图像的管线流程相同,在 Metal 着色器中渲染图像时数据的走向如下图所示: ![](//cdn.blog.yuusann.com/img/corpus/18008_1.jpg) 大致步骤描述如下: - 提前将顶点数据、纹理上传至显存,保存于`MTLBuffer`或`MTLTexture`对象中。这里的数据并不一定是都输入数据,还可以是输出数据。如渲染图片到另一张`MTLTexture`里,或保存数据到另一个`MTLBuffer`里,这些都可以。 - 绑定句柄。与 GLSL 不同的是,MTL 在着色器函数的输入参数中标明了每个参数的句柄。函数一般会有多个参数,因此在绑定时需要指明哪个对象对应着色器函数中是哪一个参数。这些句柄可以自由更改,具体的实现参考下文。 - 定义顶点着色器函数,接收 Metal API 中传入的参数,做运算后函数的返回值会被作为片元着色器函数的输入。 - 定义片元着色器,接收顶点着色器传出的数据,计算每个片元的颜色。片元着色器的返回值是一个思维向量,即是这个片元的颜色 RGBA 值。 流程比较清晰,下面来看看具体实现。 ### Vertex Shader 首先定义两个简单的数据结构,`VertexIn`结构体表示的是 Metal API 像顶点着色器的输入结构。结构体内包含两个成员,数据类型中`float3`和`float2`前者一个是三维的浮点类型向量,后者是二维浮点类型向量。而前缀`packed`表示的是这个结构是被打包在一起的。 ```cpp struct VertexIn { packed_float3 position; packed_float2 texCoords; }; ``` 在上传顶点数据时,通常会把同一个点的多个参数打包在一起上传。这句话是什么意思呢,绘制图形时,每一个顶点会对应一个位置坐标,一个贴图坐标,如果有的话还有法线坐标等其他坐标。我们不会把这些参数拆成多个数组,而是直接打包起来放在一个数组里。上传的定点数组的结构大概是这样的: |顶点坐标X|顶点坐标Y|顶点坐标Z|纹理坐标X|纹理坐标Y|法线坐标X|法线坐标Y|法线坐标Z|...| |:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:| |a|b|c|d|e|f|g|h|...| |i|j|k|l|m|n|o|p|...| |p|s|t|u|v|w|x|y|...| 每一行代表一个顶点,把描述一个顶点的所有参数都打包在一起,组合成一个数组。读取数据时根据步长和偏移就可以读取到对应的数据了。`packed`就是这个意思。 以下结构是顶点着色器的输出结构。同样是一个结构体,包含了两个向量。顶点着色器是处理顶点的着色器,因此它的输出必须含有顶点输出。在第一个用于描述顶点的向量中,追加了`[[position]]`标签。 这是本系列文中首次出现这样的标签,这样的标签描述的是这个变量的句柄。即上文中提到从 Metal API 向着色器函数传递参数时用到的参数句柄。在下面介绍函数时也会出现这样的标签。 这里的`[[position]]`标签固定表示这个向量是顶点向量。第二个参数描述的是贴图坐标。顶点着色器中一般不会处理贴图坐标,因此这个参数是直接从 Metal API 绑定的数据里取出来存在这里的。 这样的一个结构体包含了一个顶点的空间坐标和纹理坐标,这些数据作为顶点着色器的输出,将被传递到片元着色器作为输入。 ```cpp struct VertexOut { float4 position [[position]]; float2 texCoords; }; ``` 一个简单的顶点着色器函数大概长这样: ```cpp vertex VertexOut vertex_surface(device VertexIn *vertices [[buffer(0)]], uint vertexId [[vertex_id]]) { VertexOut out; // 输入的顶点是一个三维向量,在 MSL 中顶点是一个四维向量。 // 这一行构造了一个四维向量。 out.position = float4(vertices[vertexId].position, 1); out.texCoords = vertices[vertexId].texCoords; return out; } ``` 函数前缀为`vertex`,返回值为上文定义的`VertexOut`结构体,函数名是个`vertex_surface`,第一个参数的类型为上文定义的`VertexIn`结构体的指针,句柄为`[[buffer(0)]]`,意思为`buffer`类型的参数的`0`号。第二个参数为`uint`类型,句柄为`vertex_id`,这个句柄固定表示这是绘制时的索引。那么关于第一个参数前的`device`关键字,描述的是这个参数的内存空间属性,关于这个参数参考官方文档的`4.2`节,会有详细的介绍。 至此我们已经完成了一个简单的顶点着色器了。它什么都没干,仅仅是把从 Metal API 绑定的数据从`VertexIn`结构体封装成`VertexOut`结构体并交给片元着色器。 ### Fragment Shader 片元着色器用于处理片元,它的返回值是这个片元的颜色,是一个四维向量。 ```cpp fragment half4 fragment_texture(VertexOut fragmentIn [[stage_in]], texture2d<float, access::sample> tex2d [[texture(0)]], sampler sampler2d [[sampler(0)]]) { float2 texCoords = fragmentIn.texCoords; half3 surfaceColor = half3(tex2d.sample(sampler2d, texCoords).rgb); return half4(surfaceColor, 1);; } ``` 片元着色器函数前缀为`fragment`,返回值是一个四维向量。第一个参数是从顶点着色器输出的`VertexOut`结构体,第二个参数是一个二维纹理,数据类型为浮点,以采样方式访问,句柄为`[[texture(0)]]`。可以看出,各个类型的句柄是相互独立的,都可以从`0`开始。第三个参数是一个纹理采样器。 函数体中仅仅是完成了一个图片采样的工作。从输入的结构体中取出纹理坐标,利用采样器对纹理采样,拿到对应坐标的颜色值的`RGB`值,最后返回这个片元的颜色值。 以上就完成了一个最简单的图像采样片元着色器。 ## Metal API 至此,我们已经有了顶点着色器和片元着色器了。那么到底怎么使用这两个着色器呢? 在本系列的前几篇文章中提到过`MTLRenderPipelineDescriptor`这个类。在构造渲染管线时,需要构造这个类的实例,在这里可以设置着色器函数。使用`MTLLibrary`的`makeFunction`方法,传入函数名即可。 ```swift pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_surface") pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_texture") ``` 那么如何给着色器函数传入参数呢?在命令编码阶段,编码器有一系列`setxxx`方法来传入参数。这些方法都有`index`参数,用于指定这个参数对应的句柄。 例如,顶点着色器中拥有句柄为`[[buffer(0)]]`的参数,下面代码中的第一行设置了一个`MTLBuffer`参数,`index`为`0`,对应的就是这个参数。代码的第二行对应的参数句柄应是`[[texture(0)]]`。 ```swift encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) encoder.setFragmentTexture(texture, index: 0) encoder.setFragmentSamplerState(samplerState, index: 0) ``` 以上三行代码就完成了参数绑定,着色器函数运行时已经能够取得正确的参数了。 # 小结 至此,我们已经把图像渲染的整条路走通了。结合之前的文章,已经可以在屏幕上渲染图像了。那么在下一篇文章,将把 GPU 并发运算的路走通。


评论:0条


返回列表

返回主页