Metal学习——Metal着色器语言(MSL)(一)

前面学习了Metal框架的基本组成和运行方式,知道了要如何科学使用的它。然而我们最终的目的还是要使用这套框架来优雅的操作GPU单元进行图像处理,也就回到了Shader的编写上了。

我们知道OpenGL的Shader编写语言GLSL的语法上是非常类似C的,而Metal的Shader编写语言MSL则是直接基于C++14实现的一个子集。

Metal Shading Language官方文档文档中通过7个模块来介绍Metal:

  • “简介”是对文档的介绍,介绍了Metal和C++ 14之间的异同。
  • “数据类型”列出了Metal数据类型,包括表示向量,矩阵,缓冲区,纹理和采样器的类型。它还讨论了类型对齐和类型转换。
  • “运算符”列出了“Metal”运算符。
  • “函数和变量声明”详细说明了如何声明函数和变量,有时使用限制它们使用方式的属性。
  • “Metal Standard Library”定义了一系列内置Metal函数。
  • “编译器”详细介绍了Metal编译器的选项,包括预处理器指令,数学内在函数选项和控制优化的选项。
  • “数值符合性”描述了表示浮点数的要求,包括数学运算的准确性。

接下来这里围绕着一个简单的MSL脚本来学习Metal的语法,之后再详细学习各个部分的细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <metal_stdlib> // 导入Metal标准库
using namespace metal;

// 定义一个结构体用于传递顶点着色器的输出
struct SingleInputVertexIO {
float4 position [[position]];
float2 textureCoordinate [[user(texturecoord)]];
};

// 顶点着色器
vertex SingleInputVertexIO oneInputVertex(device packed_float2 *position [[buffer(0)]],
device packed_float2 *texturecoord [[buffer(1)]],
uint vid [[vertex_id]]) {
SingleInputVertexIO outputVertices;
outputVertices.position = float4(position[vid], 0, 1.0);
outputVertices.textureCoordinate = texturecoord[vid];

return outputVertices;
}

// 片段着色器
fragment half4 passthroughFragment(SingleInputVertexIO fragmentInput [[stage_in]],
texture2d<half> inputTexture [[texture(0)]]) {
constexpr sampler quadSampler;
half4 color = inputTexture.sample(quadSampler, fragmentInput.textureCoordinate);

return color;
}

在Metal中用[[…]]包含的这种变量实际上着色器的内置变量/内置函数,就像OpenGL中我们通过gl_Position函数访问顶点着色器的像素位置信息。

可以看做着色器属性访问符号,在这里简化了语法可以十分的方便访问着色器的内置对象/方法(getter/setter函数)。

这是一个简单的纹理绘制脚本,我们把它分成三个部分来解析。

结构体部分

我们定义了一个名为SingleInputVertexIO的结构体,它内部包含两个变量position和textureCoordinate。因为我们贴一张2D的纹理图一般只需要知道纹理的顶点坐标和纹理坐标就够了。

[[position]] 表示裁剪空间中输入到GPU中的原始坐标信息,固定是一个四维(xyzw)的向量。

[[user(name_xxx)]] 表示为片段着色器的函数参数指定一个(ImageBlock)图像块数据类型的属性名称为name_xxx,和OpenGL中varying限定符的作用十分相似。ImageBlock是一种二维图像独有的数据结构,它表示一个像素点的(x,y)坐标。简单的说就是定义一个name_xxx的二维图像坐标的片段着色器参数,理解起来有点绕,具体可以参考文档的2.10选节。

顶点着色器部分

首先,在Metal中支持三种函数(kernel, vertex, fragment)的定义,而我们这里图像处理主要集中在编写vertex和fragment的Shader。

1
2
3
vertex SingleInputVertexIO oneInputVertex(device packed_float2 *position [[buffer(0)]],
device packed_float2 *texturecoord [[buffer(1)]],
uint vid [[vertex_id]])

接着我们来看顶点着色器脚本的函数声明,显然这里定义了一个叫oneInputVertex其中返回值为SingleInputVertexIO结构类型的顶点着色器。

[[buffer(n)]] 表示第n个设备和常量缓存区,即外部通过setVertexBuffer设置到encoder中的MTLBuffer对象。同时我们在外部实际上是把这个对象的指针传入设备中,所以我们这里声明为指针类型,而在Metal中指针对象必须要声明它的空间属性(Address Space Attributes)。

其中在iOS中支持device/threadgroup/constant/thread/threadgroup_imageblock这5种空间属性。具体可参考文档4.2/4.3.1选节

packed_float2 表示压缩的2维float向量,将一组相关的数据打包在一起传递给GPU称作数据打包传输,我们可通过这个数组直接访问相关的数据(顶点坐标/法线等)从而减少了调用次数提高了效率。

[[vertex_id]] 表示每个顶点的标识符,而每个顶点缓冲区的真正地址基于这个标识符的相对偏移n来访问。

1
2
3
4
5
6
7
outputVertices.position = float4(position[vid], 0, 1.0);
相当于OpenGL中
gl_Position = vec4(x, y, 0 , 1.0);

device packed_float2 *texturecoord [[buffer(1)]]
outputVertices.textureCoordinate = texturecoord[vid];
相当于这个管线单元中缓存区buffer(1)的对象赋值给.textureCoordinate

片段着色器部分

1
2
fragment half4 passthroughFragment(SingleInputVertexIO fragmentInput [[stage_in]],
texture2d<half> inputTexture [[texture(0)]])

[[stage_in]] 表示顶点着色器的输出/片段着色器的输入,它可以是一个结构体或者一个变量,结构体中变量可以是标量或者向量,具体参考4.3.5选节。

[[texture(n)]] 表示这个管线单元的纹理对象,外部通过setFragmentTexture函数输入到管线中。

总结

这里通过一个示例来介绍MSL的基本结构和编写方法,其他内置对象/函数的含义可以详细查阅官方文档来得知。这是一个使用Metal实现的滤镜链框架