十四、Metal - Metal Shader language (著色語言規(guī)范)總結

音視頻開發(fā):OpenGL + OpenGL ES + Metal 系列文章匯總

Metal的頂點函數(shù)和片元函數(shù)的書寫尤其自己的語法規(guī)范,因此這里進行語法規(guī)范總結

主要內(nèi)容:

  1. 變量的數(shù)據(jù)類型
  2. 函數(shù)修飾符
  3. 變量的地址空間修飾符
  4. 變量的屬性修飾符
  5. 內(nèi)建變量修飾符

1、Metal著色器語言認識

  • Metal著色器語言是用來編寫3D圖形渲染邏輯、并行Metal計算核心邏輯的一門編程語言,并且當使用Metal框架來完成APP的某些功能時也需要使用Metal編程語言。
  • Metal語言使用Clang 和LLVM進行編譯處理,編譯器對于在GPU上的代碼執(zhí)行效率有更好的控制。
  • Metal基于C++ 11.0語言設計的,在C++基礎上多了一些擴展和限制,主要用來編寫在GPU上執(zhí)行的圖像渲染邏輯代碼以及通用并行計算邏輯代碼。
  • Metal 像素坐標系統(tǒng):Metal中紋理 或者 幀緩存區(qū)attachment的像素使用的坐標系統(tǒng)的原點是左上角。

Metal語言相較C++11.0的限制

  1. Metal中不支持C++11.0的如下特性
    1. Lambda表達式
    2. 遞歸函數(shù)調(diào)用
    3. 動態(tài)轉換操作符
    4. 類型識別
    5. 對象創(chuàng)建new和銷毀delete操作符
    6. 操作符noexcept
    7. go跳轉
    8. 變量存儲修飾符 register 和thread_local
    9. 虛函數(shù)修飾符
    10. 派生類
    11. 異常處理
  2. C++標準庫在Metal語言中也不可使用
  3. Metal語言對于指針使用的限制
    1. 函數(shù)名不能出現(xiàn)main
    2. Metal圖形和并行計算函數(shù)用到的入?yún)ⅲū热缰羔?/ 引用),如果是指針 / 引用必須使用地址空間修飾符(比如device、threadgroup、constant)
    3. 不支持函數(shù)指針

2、數(shù)據(jù)類型

2.1 基本數(shù)據(jù)類型

包括標量、向量、矩陣。

2.1.1 標量

標量類型:


標量類型.png

注意:

  1. 新增half的浮點類型,是正常的float浮點型的一半,只有2個字節(jié)大小。
  2. 通過sizeof操作符得到的數(shù)據(jù)類型的結果就用size_t來接收,8個字節(jié)大小。因為存儲的是數(shù)據(jù)類型,所以是8個字節(jié)大小。

代碼:

bool a = true;
char b = 5;
int  d = 15;
//用于表示內(nèi)存空間
size_t c = 1;
ptrdiff_t f = 2;

2.1.2 向量

支持類型:
booln、charn、shortn、intn、ucharn、ushortn、uintn、halfn、floatn

注意:

  1. 這里的 n 表示向量的維度,最多不超過4維向量
  2. 數(shù)據(jù)類型后面的n并不是n本身,而是表示幾維向量

定義:
可以看到,基本上可以看做是一個數(shù)組來使用

//直接賦值初始化
bool2 A= {1,2};
//通過內(nèi)建函數(shù)float4初始化
float4 pos = float4(1.0,2.0,3.0,4.0);

//通過下標從向量中獲取某個值
float x = pos[0];
float y = pos[1];

tips:在OpenGL ES的GLSL語言中float類型數(shù)據(jù)不可以使用f,例如2.0f,在著色器中書寫時,是不能加f,寫成2.0,而在Metal中則可以寫成2.0f,其中f可以是大寫,也可以是小寫,原因也好理解,glsl語言是作為字符串來使用,而metal并不是。

使用規(guī)則

有兩種:

  1. 直接通過下標來使用
  2. 通過字母來使用,有兩種xyzw/rgba(分別代表頂點坐標和色值)

通過下標來使用

//通過for循環(huán)對一個向量進行運算
float4 VB;
for(int i = 0; i < 4 ; i++)
{
    VB[i] = pos[i] * 2.0f;
}

通過字母使用

單個字母:

int4 test = int4(0,1,2,3);
int a = test.x; //獲取的向量元素0
int b = test.y; //獲取的向量元素1
int c = test.z; //獲取的向量元素2
int d = test.w; //獲取的向量元素3

int e = test.r; //獲取的向量元素0
int f = test.g; //獲取的向量元素1
int g = test.b; //獲取的向量元素2
int h = test.a; //獲取的向量元素3

多個字母:

float4 c;
c.xyzw = float4(1.0f,2.0f,3.0f,4.0f);
c.z = 1.0f;
c.xy = float2(3.0f,4.0f);
c.xyz = float3(3.0f,4.0f,5.0f);

注意:

  1. 可以把xyzw/rgba分別看做下標為0123即可
  2. 所以它可以亂序訪問
  3. 但是賦值時不可以重復訪問,取值時可以重復
  4. xyzw和rbga兩種不能混用

代碼:

float4 pos = float4(1.0f,2.0f,3.0f,4.0f);
//向量分量逆序訪問
float4 swiz = pos.wxyz;  //swiz = (4.0,1.0,2.0,3.0);
//向量分量重復訪問
float4 dup = pos.xxyy;  //dup = (1.0f,1.0f,2.0f,2.0f);

//可以僅對 xw / wx 修改
//pos = (5.0f,2.0,3.0,6.0)
pos.xw = float2(5.0f,6.0f);

//pos = (8.0f,2.0f,3.0f,7.0f)
pos.wx = float2(7.0f,8.0f);

//可以僅對 xyz 進行修改
//pos = (3.0f,5.0f,9.0f,7.0f);
pos.xyz = float3(3.0f,5.0f,9.0f);

float2 pos;
pos.x = 1.0f; //合法
pos.z = 1.0f; //非法,pos是二維向量,沒有z這個索引

float3 pos2;
pos2.z = 1.0f; //合法
pos2.w = 1.0f; //非法

// 賦值 時 分量不可重復,取值 時 分量可重復
//非法,x出現(xiàn)2次
pos.xx = float2(3.0,4.0f);
pos.xy = swiz.xx;

//向量中xyzw與rgba兩組分量不能混合使用
float4 pos4 = float4(1.0f,2.0f,3.0f,4.0f);
pos4.x = 1.0f;
pos4.y = 2.0f;
//非法,.rgba與.xyzw 混合使用
pos4.xg = float2(2.0f,3.0f);
////非法,.rgba與.xyzw 混合使用
float3 coord = pos4.ryz;

2.1.3 矩陣

有兩種類型,halfnxm、floatnxm。
nxm表示n行m列,最多就是4行4列??梢园丫仃嚳醋鲆粋€二維數(shù)組來使用

1. float4 類型向量的構造方式

//float4類型向量的所有可能構造方式
//1個一維向量,表示一行都是x
float4(float x);/
//4個一維向量 --> 4維向量
float4(float x,float y,float z,float w);
//2個二維向量 --> 4維向量
float4(float2 a,float2 b);
//1個二維向量+2個一維向量 --> 4維向量
float4(float2 a,float b,float c);
float4(float a,float2 b,float c);
float4(float a,float b,float2 c);
//1個三維向量+1個一維向量 --> 4維向量
float4(float3 a,float b);
float4(float a,float3 b);
//1個四維向量 --> 4維向量
float4(float4 x);

2.float3 類型向量的構造方式

//float3類型向量的所有可能的構造的方式
//1個一維向量
float3(float x);
//3個一維向量
float3(float x,float y,float z);
//1個一維向量 + 1個二維向量
float3(float a,float2 b);
//1個二維向量 + 1個一維向量
float3(float2 a,float b);
//1個三維向量
float3(float3 x);

3. float2 類型向量的構造方式

//float2類型向量的所有可能的構造方式
//1個一維向量
float2(float x);
//2個一維向量
float2(float x,float y);
//1個二維向量
float2(float2 x);

2.2 Metal其他類型

有兩種,紋理類型和采樣器類型。

2.2.1 紋理類型

紋理類型是一個句柄,指向一維/二維/三維紋理數(shù)據(jù),而紋理數(shù)據(jù)對應一個紋理的某個level的mipmap的全部或者一部分。

紋理類型的定義:

  • texture1d<T, access a = access::sample>
  • texture2d<T, access a = access::sample>
  • texture3d<T, access a = access::sample>
  1. texture1d,texture2d,texture3d都表示這是一個紋理類型,分別定義的是一維/二維/三維。
  2. T是一個泛型,表示從紋理中讀取數(shù)據(jù) 或是 寫入時的顏色類型,T可以是half、float、short、int等。
  3. access表示紋理訪問權限,當access沒寫時,默認是sample 。

紋理訪問權限:

宏定義:

enum class access {
    sample, 
    read, 
    write
};
  • sample: 紋理對象可以被采樣(即使用采樣器去紋理中讀取數(shù)據(jù),相當于OpenGL ES的GLSL中sampler2D),采樣一維這時使用 或者 不使用都可以從紋理中讀取數(shù)據(jù)(即可讀可寫可采樣)。
  • read:不使用采樣器,一個圖形渲染函數(shù) 或者 一個并行計算函數(shù)可以讀取紋理對象(即僅可讀)。
  • write:一個圖形渲染函數(shù) 或者 一個并行計算可以向紋理對象寫入數(shù)據(jù)(即 可讀可寫)。
2.2.2 采樣器類型 Samplers

對采樣器設置采樣器類型,決定了對這個紋理進行采樣時的操作方式。在Metal框架中通過采樣器的對象MTLSamplerState進行設置采樣器類型,這個對象作為圖形渲染著色器函數(shù)參數(shù) 或是 并行計算函數(shù)的參數(shù)傳遞。
有以下幾種狀態(tài):

  1. coord:
    • 內(nèi)容:從紋理中采樣時,紋理坐標是否需要歸一化
    • 參數(shù):enum class coord { normalized, pixel };
  2. filter
    1. 描述:紋理采樣過濾方式,統(tǒng)一設置,包括放大/縮小過濾方式
    2. 參數(shù):enum class filter { nearest, linear };
  3. min_filter
    1. 描述:設置紋理采樣的縮小過濾方式
    2. 參數(shù):enum class min_filter { nearest, linear };鄰近過濾、線性過濾
  4. mag_filter
    1. 描述:設置紋理采樣的放大過濾方式
    2. 參數(shù):enum class min_filter { nearest, linear };鄰近過濾、線性過濾
  5. s_address、t_address、r_address
    1. 描述:設置紋理s、t、r坐標(對應紋理坐標的x、y、z)的尋址方式
    2. 參數(shù):enum class s_address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };
    3. 參數(shù):t坐標:enum class t_address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };
    4. 參數(shù):r坐標:enum class r_address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };
  6. address
    1. 描述:設置所有紋理坐標的尋址方式
    2. 參數(shù):enum class address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };
  7. mip_filter
    1. 描述:設置紋理采樣的mipMap過濾模式, 如果是none,那么只有一層紋理生效;
    2. 參數(shù):enum class mip_filter { none, nearest, linear };

定義:

/*
constexpr:修飾符(必須寫)
sampler:類型
s:采樣器變量名稱
參數(shù)
    - coord: 是否需要歸一化,不需要歸一化,用的是像素pixel
    - address: 地址環(huán)繞方式
    - filter: 過濾方式
*/
constexpr sampler s(coord::pixel, address::clamp_to_zero, filter::linear);

constexpr sampler a(coord::normalized);

constexpr sampler b(address::repeat);

注意:constexpr作為修飾符必須寫

3、函數(shù)修飾符

函數(shù)修飾符用來修飾函數(shù),放在函數(shù)的最前面,即位于函數(shù)返回值的前面。有三種,kernel、vertex、fragment。

kernel: 表示該函數(shù)是一個數(shù)據(jù)并行計算著色函數(shù),它可以被分配在一維/二維/三維線程組中去執(zhí)行,表示函數(shù)要并行計算,其返回值類型必須是void類型,是一個高并發(fā)函數(shù)。
vertex: 表示該函數(shù)是一個頂點著色函數(shù),它將為頂點數(shù)據(jù)流中的每個頂點數(shù)據(jù)執(zhí)行一次,然后為每個頂點生成數(shù)據(jù)輸出到繪制管線。
fragment: 表示該函數(shù)是一個片元著色函數(shù),它將為片元數(shù)據(jù)流中的每個片元 和其相關聯(lián)的數(shù)據(jù)執(zhí)行一次,然后將每個片元生成的顏色數(shù)據(jù)輸出到繪制管線中。

代碼:

//并行計算函數(shù)(kernel)
kernel void CCTestKernelFunctionA(int a,int b)
{ 
    /*
     注意:
     1. 使用kernel 修飾的函數(shù)返回值必須是void 類型
     2. 一個被函數(shù)修飾符修飾過的函數(shù),不允許在調(diào)用其他的被函數(shù)修飾過的函數(shù). 非法
     3. 被函數(shù)修飾符修飾過的函數(shù),只允許在客戶端對其進行操作. 不允許被普通的函數(shù)調(diào)用.
     */
     
    //不可以的!
    //一個被函數(shù)修飾符修飾過的函數(shù),不允許在調(diào)用其他的被函數(shù)修飾過的函數(shù). 非法
    CCTestKernelFunctionB(1,2);//非法,錯誤調(diào)用?。。?    CCTestVertexFunctionB(1,2);//非法,錯誤調(diào)用!??!
    
    //可以! 你可以調(diào)用普通函數(shù).而且在Metal 不僅僅只有這3種被修飾過的函數(shù).普通函數(shù)也可以存在
    CCTest();
    
}

//并行計算函數(shù)
kernel void CCTestKernelFunctionB(int a,int b)
{
    .....
}

//頂點函數(shù)
vertex int CCTestVertexFunctionB(int a,int b)
{
    .....
}

//片元函數(shù)
fragment int CCTestVertexFunctionB(int a,int b)
{
    .....
}

//普通函數(shù)
void CCTest()
{
    .....
}

注意:

  1. 使用kernel修飾的函數(shù),其返回值類型必須是void類型
  2. Metal中并不是所有函數(shù)都需要上述3個修飾符修飾,是可以在Metal中定義普通函數(shù)的,即不帶任何修飾符的函數(shù)。
  3. 被函數(shù)修飾符修飾的函數(shù)不能相互調(diào)用,只能調(diào)用普通函數(shù),也不能被普通函數(shù)調(diào)用被函數(shù)修飾符修飾的函數(shù)。這樣容易理解,它們各自尤其特殊的含義,是要被系統(tǒng)調(diào)用的。
  4. 只有圖形著色函數(shù)才可以被vertex和fragment修飾,對于圖形著色函數(shù),通過返回值類型可以辨認出是為頂點計算還是像素計算,其返回值也可以是void,意味著不產(chǎn)生數(shù)據(jù)輸出到繪制管線,是一個無意義的動作。

4、變量的地址空間修飾符

地址空間修飾符用來表示一個變量或參數(shù)要分配在哪一片區(qū)域。有device、Threadgroup、constant、Thread四種。

注意事項:

  1. 所有的著色函數(shù)(vertex、fragment、kernel)的參數(shù),如果是指針/引用,都必須帶有地址空間修飾符號
  2. 對于圖形著色器函數(shù)(即vertex/fragment修飾的函數(shù)),其指針/引用類型的參數(shù)必須定義為 device、constant地址空間。
  3. 對于并行計算函數(shù)(即kernel修飾的函數(shù)),其指針/引用類型的參數(shù)必須定義為 device、threadgroup、constant。
  4. 并不是所有的變量都需要修飾符,也可以定義普通變量(即無修飾符的變量)。

具體使用:

//變量/參數(shù)地址空間修飾符
void CCTestFouncitionE(device int *g_data,
                       threadgroup int *l_data,
                       constant float *c_data
                       )
{
    //...
    
}

4.1 device:設備地址空間修飾符

設備地址空間指向設備內(nèi)存池分配出來的緩存對象(設備指顯存,即GPU),即GPU空間分配的緩存對象,它是可讀可寫的。
這個緩存對象可以存儲變量和用戶自定義結構體的指針/引用。

代碼:

// 設備地址空間: device 用來修飾指針.引用
//1.修飾指針變量
device float4 *color;

struct CCStruct{
    float a[3];
    int b[2];
};
//2.修飾結構體類的指針變量
device CCStruct *my_CS;

注意:

  1. 紋理對象總是在設備地址空間分配內(nèi)存,即紋理對象默認分配在顯存中
  2. device地址空間修飾符不必出現(xiàn)在紋理類型定義中
  3. 一個紋理對象的內(nèi)容無法直接訪問,Metal提供讀寫紋理的內(nèi)建函數(shù),通過內(nèi)建函數(shù)訪問紋理對象

4.2 constant:常量地址空間修飾符

constant指向的緩存對象也是存儲在顯存中,但是僅可讀。

代碼:

constant float samples[] = { 1.0f, 2.0f, 3.0f, 4.0f };

//對一個常量地址空間的變量進行修改也會失敗,因為它只讀的
sampler[4] = {3,3,3,3}; //編譯失敗; 

//定義為常量地址空間聲明時不賦初值也會編譯失敗
constant float a;

4.3 threadgroup:線程組地址空間修飾符

線程組地址空間用于為并行計算著色器函數(shù)分配內(nèi)存變量,這些變量被一個線程組的所有線程共享。
在線程組地址空間分配的變量不能用于圖形繪制著色函數(shù)(即頂點著色函數(shù) / 片元著色函數(shù)),即在圖形繪制著色函數(shù)中不能使用線程組。可以暫時不用關注。

代碼:

/*
 1. threadgroup 被并行計算計算分配內(nèi)存變量, 這些變量被一個線程組的所有線程共享. 在線程組分配變量不能被用于圖像繪制.
 2. thread 指向每個線程準備的地址空間. 在其他線程是不可見切不可用的
 */
kernel void CCTestFouncitionF(threadgroup float *a)
{
    //在線程組地址空間分配一個浮點類型變量x
    threadgroup float x;
    
    //在線程組地址空間分配一個10個浮點類型數(shù)的數(shù)組y;
    threadgroup float y[10];
    
}

4.4 thread:線程地址空間修飾符

線程地址空間指向每個線程準備的地址空間,也是在GPU中,該線程的地址空間定義的變量在其他線程不可見(即變量不共享)
在圖形繪制著色函數(shù) 或者 并行計算著色函數(shù)中聲明的變量,在線程地址空間分配存儲

代碼:

kernel void CCTestFouncitionG(void)
{
    //在線程空間分配空間給x,p
    float x;
    thread float p = &x;
}

5、變量的屬性修飾符

在函數(shù)的傳遞參數(shù)中,除了常量地址空間變量和程序域定義的采樣器以外,也即是需要從外界傳入的參數(shù)需要使用屬性修飾符。

作用: 標識從客戶端傳遞資源到服務器端的定位。也就是OpenGL ES中的通道location。

屬性修飾符類型有五種:

  • device buffer 設備緩存:一個指向設備地址空間的任意數(shù)據(jù)類型的指針/引用
  • 常量緩存:一個指向常量地址空間的任意數(shù)據(jù)類型的指針/引用
  • 紋理對象
  • 采樣器對象
  • 在線程組中供線程共享的緩存

定義:

在代碼中如何表現(xiàn):
 1.已知條件:device buffer(設備緩存)/constant buffer(常量緩存)
 代碼表現(xiàn):[[buffer(index)]]
 解讀:不變的buffer ,index 可以由開發(fā)者來指定.
 
 2.已知條件:texture Object(紋理對象)
 代碼表現(xiàn): [[texture(index)]]
 解讀:不變的texture ,index 可以由開發(fā)者來指定.
 
 3.已知條件:sampler Object(采樣器對象)
 代碼表示: [[sampler(index)]]
 解讀:不變的sampler ,index 可以由開發(fā)者來指定.
 
 4.已知條件:threadgroup Object(線程組對象)
 代碼表示: [[threadgroup(index)]]
 解讀:不變的threadgroup ,index 可以由開發(fā)者來指定.

注意:

  1. index是一個unsigned interger類型的值,表示了一個緩存、紋理、采樣器參數(shù)的位置(即在函數(shù)參數(shù)索引表中的位置,相當于OpenGl ES中的location)
  2. 從語法上來說,屬性修飾符的聲明位置應該位于參數(shù)變量名之后

代碼:

//并行計算著色器函數(shù)add_vectros ,實現(xiàn)2個設備地址空間中的緩存A與緩存B相加.然后將結果寫入到緩存out.
//屬性修飾符"(buffer(index))" 為著色函數(shù)參數(shù)設定了緩存的位置
//thread_position_in_grid:用于表示當前節(jié)點在多線程網(wǎng)格中的位置,并不需要開發(fā)者傳遞,是Metal自帶的。
/*
 kernel:并行計算函數(shù)修飾符
 void:函數(shù)返回值類型
 add_vectros:函數(shù)名
 const device float4 *inA [[buffer(0)]]:定義了一個float4類型的指針,指向一個4維向量空間,放在設備內(nèi)存空間(即顯存GPU中)
    - const device:只決定放在哪里
    - inA:變量名
    - [[buffer(0)]] 對應 buffer中0這個id
 */
kernel void add_vectros(
                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];
}

//著色函數(shù)的多個參數(shù)使用不同類型的屬性修飾符的情況
//紋理讀取的方式的sampler,即采樣器,[[sampler(0)]]表示采樣器的緩存id
kernel void my_kernel(device float4 *p [[buffer(0)]],
                      texture2d<float> img [[texture(0)]],
                      sampler sam [[sampler(0)]])
{
    //.....
    
}

6、內(nèi)建變量修飾符

對于特殊的變量提供了內(nèi)建的修飾符直接使用,有四種。

  • [[vertex_id]] :頂點id標識符,并不由開發(fā)者傳遞
  • [[position]]:在頂點著色函數(shù)中,表示當前的頂點信息,類型是float4、還可以表示描述了片元的窗口的相對坐標(x,y,z,1/w),即該像素點在屏幕上的位置信息。
  • [[point_size]] :點的大小,類型是float
  • [[color(m)]] :顏色,m在編譯前就必須確定
  • [[stage_in]] :片元著色函數(shù)使用的單個片元輸入數(shù)據(jù)是由頂點著色函數(shù)輸出然后經(jīng)過光柵化生成的(即由頂點著色函數(shù)之后的顏色傳遞到片元著色函數(shù)),類似于GLSL中的varying傳遞紋理/顏色

注意:

  1. 頂點和片元著色器函數(shù)都只能有一個參數(shù)被聲明為使用stage_in修飾符(即有且僅有一個)
  2. 對于一個使用了stage_in修飾符的自定義結構體,其成員可以為一個整型/浮點類型標量,或是整型/浮點類型向量

代碼:

//定義了片元輸入的結構體,
struct MyFragmentOutput {
      // color attachment 0 顏色附著點0
     float4 clr_f [[color(0)]]; 
     // color attachment 1 顏色附著點1
     int4 clr_i [[color(1)]]; 
     // color attachment 2 顏色附著點2
     uint4 clr_ui [[color(2)]]; 
};

fragment MyFragmentOutput my_frag_shader( ... ) 
{
    MyFragmentOutput f;
    ....
    f.clr_f = ...;
    ....
    return f; 
}


更多語法規(guī)范可查看:著色器語言指南

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容