Metal学习笔记(二)-- Metal Shading Language学习

跟OpenGL ES类似,Metal也有自己的着色器语言,相比更接近C语言的GLSL,Metal Shading Language的语言更接近于C++。而且,Metal有着和OpenGL管道相同的渲染流程,即:顶点着色器->图元装配->光栅化->片元着色器->提交缓冲区,不同的是,Metal中将着色器描述为函数,但本质是相同的。

但是毕竟是苹果的亲儿子,Xcode不仅可以提供了创建Metal文件的入口,还能使用Clang+LLVM对Metal文件进行编译链接,这相比于要开发者手敲GLSL的OpenGL ES来说,无疑是比较优势的一点,毕竟仅仅是以字符串形式存在的着色器来说,查找错误是一个麻烦的过程。

但是Metal的语法仿佛继承了OC语言恶心的特质,学习起来还是需要一定成本的,本文将对Metal Shading Language做一个学习归纳,方便日后查看。

Metal Shading Language 基本语法

Metal Shading Language使用的是 Clang 和 LLVM 进行编译处理的,它基于C++ 11.0 语言设计,但它也不完全等同与C++的语言,Metal不支持以下特征:

  • Lambda表达式
  • 递归函数调用
  • 动态转换操作符
  • 类型识别
  • 对象创建new和销毁delete操作符
  • 操作符noexcept
  • goto跳转
  • 变量存储修饰符register和thread_local
  • 虚函数修饰符
  • 派生类
  • 异常处理
  • C++标准库

另外,Metal像素坐标系统使用的原点是左上角,这与OpenGL的左下角是不同的,所以我们使用Metal渲染纹理时不再需要翻转。

而且,Metal函数名不能命名为Main,这点和GLSL是不同的。

Metal支持结构体和枚举。

基本数据类型

Metal 有以下基本数据类型:

类型 描述
bool 布尔类型,true/false
char 有符号8-bit整数
unsigned char / uchar 无符号8-bit整数
short 有符号16-bit整数
unsigned short / ushort 无符号32-bit整数
int 有符号32-bit整数
unsigned int / uint 无符号32-bit整数
half 16-bit浮点数
float 32-bit浮点数
size_t 无符号64-bit整数
prtdiff_t 64-bit有符号整数,表示两个指针的差
void 表示一个空的值集合

与OpenGL不同的是,Metal支持数字后缀表示字面量类型,如0.4f,0.5h。

指针类型

Metal Shading Language 也是支持指针类型的,同样是使用 * 表示指针,但是在Metal Shading Language中,对指针的使用以下限制:

  • Metal图型和并行计算函数用的的入参如果是指针或者应用类型,必须使用地址空间修饰符(如device,threadgroup,constant)
  • 不支持函数指针

向量和矩阵数据类型

向量

Metal Shading Language 使用基本数据类型+维度表示向量,如float3表示每个维度类型为float的3维向量,它的定义可以写为:

short4 vector1 = {1,2,3,4};
float3 vector2 = float3(1.f, 2.f, 3.f);

和GLSL类似,它也可以使用.xyzw或者.rgba读取或赋予对应的值:

short2 vector3 = vector1.xy; // vector3 = (1, 2)
float2 vector4 = vector2.rb; // vector4 = (1.f, 3.f)

向量读取的是后实际是使用索引值,所以向量的xyzw(或rgba)可以颠倒和重复:

vector3.xy = vector3.yx; // vector3 = (2, 1)
vector4.rg = vector4.rr; // vector4 = (1.f, 1,f)

另外,因为xyzw(或rgba)读取的索引值,所以不能读取越界的索引,也不能xyzw和rgba混用,如

vector3.zw // 错误,二维向量没有zw值
vector4.xg // 错误,xyzw和rgba不能混用

向量支持如下类型:

  • booln
  • charn
  • shortn
  • intn
  • ucharn
  • ushortn
  • uintn
  • halfn
  • floatn

0<n≤4,表示维度

矩阵

矩阵支持如下类型:

  • halfnxm
  • floatnxm

n、m分别表示矩阵的行数和列数

它可以这么定义:

half2x2 matrix = half2x2(1.h,2.h,3.h,4.h);
matrix = {2.h,2.h,2.h,2.h};

纹理类型

纹理类型是一个句柄,它指向一个一维/二维/三维纹理数据。他是这么定义的:

texture1d<T, access a = access::sample>
texture2d<T, access a = access::sample>
texture3d<T, access a = access::sample>

T表示数据类型,设定了从纹理中读取的或是向纹理中写入时的颜色类型,可以是half,float,short,int等

access是一个枚举值,定义了访问权利:

enum class access {
    sample, // 默认值, 纹理对象可以被采样,采样一维时使用或不使用都从纹理中读取数据
    read, // 不使用采样器,一个图形渲染函数或一个并行计算函数可以读取纹理对象
    write // 一个图形渲染函数或者一个并行函数可以像纹理对象写入数据
}

示例如下:

void foo(texture2d<float> imgA[[ texture(0) ]],
         texture2d<float, access::read> imgB [[ texture(1) ]],
         texture2d<float, access::write> imgC [[ texture(2) ]])

采样器类型

采样器类型决定了如何对一个纹理进行采样操作,在Metal框架中有一个对应着着色器语言的采样器对象MTLSamplerState,这个对象作为图形渲染着色器函数参数或者并行计算函数的参数传递。
需要注意的是,Metal程序中,初始化采样器必须使用constexpr修饰符声明。

constexpr sampler s(coord::normalized)

例中括号内表示采样器的采样参数,是一个枚举值,Metal中采样器以下几种类型:

枚举名称 有效值 描述
coord {normalized, pixel} 从纹理中采样时,纹理坐标是否需要归一化
filter {nearest, linear} 纹理采用的过滤方式,放大/缩小的过滤方式
min_filter {nearest, linear} 纹理采用的缩小过滤方式
mag_filter {nearest, linear} 纹理采用的放大过滤方式
s_address, t_address, r_address {clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat} 设置s、t、r坐标的寻址模式
address {clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat} 设置所有的纹理坐标寻址模式
compare_func {none, less, less_equal, greater, greater_equal, equal, not_equal} 为使用r纹理坐标做shadow map,设置比较测试逻辑,这个状态值只可以在Metal Shading Language中完成

例:

constexpr sampler s(coord::pixel, address::clamp_to_zero, filter::linear);
constexpr sampler a(address::repeat);

修饰符

修饰符是Metal Shading Language中比较难理解的点,Metal中的修饰符有函数修饰符、地址空间修饰符和传递修饰符3种

函数修饰符

函数修饰符位于函数之前,例如:

kernel void foo(...) {
 ...
}

Metal有以下3种函数修饰符

  1. kernel:表示该函数是一个数据并行计算函数,他可以分配在一维/二维/三维线程组中去执行。
  2. vertex:表示该函数是一个顶点着色函数,它将顶点数据刘总的每个顶点数据执行一次然后为每个顶点生成数据输出到绘制管线。
  3. fragment:表示该函数是一个片元着色函数,它将为片元数据流中的每个片元和其关联执行一次然后将每个片元生成的颜色数据输出到绘制管线中。

只有图形着色器函数才可以被vertex和fragment修饰,对于图形着色函数,返回值类型可以辨别出他是为顶点做计算还是为每个像素做计算,图型函数返回值可以为void,但这意味着该函数不产生数据输出到绘制管线,这是一个无意义的动作。

函数修饰符有以下注意点

  1. 使用kernel修饰的函数,其返回值类型必须是void类型;
  2. 一个被函数修饰符修饰的函数不能调用其他被函数修饰符修饰的函数;

地址空间修饰符

地址空间修饰符用于变量或者参数,位于变量或参数类型之前,例如:

device float4 *color;

空间修饰符表示一个变量被分配在哪一片内存区域,所有着色器函数(vertex,fragment,kernel)的参数,如果是指针或者应用,都必须带有地址空间修饰符。

空间修饰符有以下4种。

  • device:设备地址空间
  • threadgroup:线程组地址空间
  • constant:常量地址空间
  • thread:线程地址空间

对于图形着色器函数,其指针或者应用类型参数必须定位为device或constant地址空间;
对于并行计算函数,其指针或者应用类型参数必须定义为device或threadgroup或constant地址空间;

1.设备地址空间

指的是设备内存池分配出来的缓存对象,它是可读可写的,一个缓存对象可以被声明为一个标量、向量或者是用户自定义结构体的指针或应用。
另外注意的是,纹理对象总是在设备地址空间分配内存,device地址空间修饰符不必出现在纹理类型定义中,一个纹理对象的内容无法直接访问,Metal提供读写纹理的内建函数。

例:

device float4 *color;
struct Foo {
    float a[3];
    int b[2];
}
device Foo *my_info;

2.线程组地址空间

指的是用于为并行计算着色函数分配内存变量的空间,这些变量被一个线程组的所有线程共享。
在线程组地址空间分配的变量不能用与图形绘制着色函数。
在并行计算着色函数中,在线程组地址空间分配的变量为一个线程组使用,声明周期和线程组相同。
例:

kernel void my_func(threadgroup float *a [[threadgroup(0)]]) {
    threadgroup float x;
    
    threadgroup float b[10];
}

3.常量地址空间

指向的缓存对象也是设备内存池分配,但他是只读的,它修饰的变量必须在定义的时候初始化,用来初始化的值必须是编译时的常量,修饰的变量在程序域的生命周期和程序一样,在程序中的并行计算着色函数或者图形绘制着色函数调用时,它的的值都会保持不变。
值得注意的是,常量地址空间的指针或引用也可以作为函数的参数,为声明的常量赋值会产生编译错误,声明常量但是没有赋予初值也会编译错误;
例如:

constant float samplers[] = {1.f, 2.f, 3.f, 3.f};

sampler[4] = {3,3,3,3}; // 编译失败

constant float a; // 编译失败

4.线程地址空间

指向每个线程准备的地址空间,这个线程地址空间定义的变量在其他线程不可见。可以在图形绘制函数或者并行计算函数中使用。
例:

kernel void my_func(...) {
    float x;
    thread float = &x;
}

传递修饰符

对于图形绘制或并行着色器函数来说,输入/输出都需要通过参数传递(除了常量地址空间变量和程序域中定义的采样器以外),参数可以以下之一:

  • device buffer - 设备缓存,一个指向设备地址空间的任意数据类型的指针或者引用
  • constant buffer - 常量缓存区,一个指向常量地址空间的任意数据类型的引用
  • texture - 纹理对象
  • sampler - 采样器对象
  • threadgroup - 在线程组中供各线程共享的缓存

注意的是,着色器的缓存(device 和 constant)不能重名。

对于每个着色器来说,一个变量传递修饰符是必须指定的,它用来设置一个缓冲、纹理、采样器的位置。
传递修饰符位于参数变量之后,用[[]]表示,并在()内指定它的位置。
通常我们有以下几种类型可以从外部传入着色器:

  • [[buffer(index)]] // device buffer / constant buffer
  • [[texture(index)]] // texture
  • [[sampler(index)]] // sampler
  • [[threadgroup(index)]] // threadgroup

index是一个unsigned integer类型的值,它表示一个缓存、纹理、采样器参数的位置。

参考代码如下:

kernel void add_vectors(const device float4 *inA [[buffer(0)]],
                        const device float4 *inB [[buffer(1)]],
                        device float4 *out [[buffer(2)]],
                        uint id [[thread_position_in_grid]]) {
                        
                        out[id] = inA[id] + inB[id];
                        }

thread_position_in_grid: 用于表示当前节点在多线程网络中的位置,不需要用户传入

内建变量属性修饰符

除了以上的传递修饰符,Metal还提供了内建的传递修饰符。常见的有以下几种:

  • [[vertex_id]] 顶点id表示符,即当前是第几个顶点
  • [[position]] 顶点信息,表述片元的窗口相对坐标(x, y, z, 1/w)
  • [[point_size]] 点的大小
  • [[color(m)]] 颜色,m编译前得确定,表示传入颜色附着点m
  • [[stage_in]] 表示由顶点着色函数输出经过光栅化生成传入片元函数的数据,一个顶点和片元函数都只能有一个参数被声明为stage_in
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353