前言
文章翻譯是的apple的Metal開發(fā)文檔demo:Using a Render Pipeline to Render Primitives
),項目下載鏈接
)
概覽
在《使用Metal填充一個view》中,您學(xué)習(xí)了如何設(shè)置MTKView對象,以及如何使用渲染過程更改視圖的內(nèi)容,該示例只是將視圖的內(nèi)容擦除為背景色,這個例子將為你演示怎樣為渲染通道配置一個渲染管線來繪制一個彩色2D的三角形到view上,這個例子將為每個頂點配置一個位置和顏色,渲染管道使用該數(shù)據(jù)渲染三角形,并且根據(jù)三角形的頂點顏色采用插值的方式來繪制。

注意
這個xcode的項目包含了運行在macOS,ios和tvOS上的工程,Matal不支持在ios和tvOS的模擬器上運行,所以ios和tvOS的工程需要真機測試,默認(rèn)選擇的工程是macOS平臺的工程。
理解Metal的渲染管道
渲染管道執(zhí)行一系列渲染指令和將數(shù)據(jù)傳遞到渲染過程的目標(biāo)位置,渲染管道有許多階段,一些使用著色器編程,另一些使用固定或可配置的行為,這個例子主要關(guān)注三個主要的階段:頂點階段,光柵化階段和片元階段,頂點階段和片元階段是可以編程的,所以你可以使用 Metal Shading Language (MSL)語言來為他們寫一些函數(shù),光柵化階段只要固定的行為。
Ps:
從這個描敘看所謂的渲染管道類似與opengGL ES中的program,而Metal Shading Language (MSL)類似與Opengl shader language,這樣有了對比,可能更有助于我們理解Matel中的這些概念。
圖1 Matal圖像渲染管道的主要階段

渲染從一個包含了頂點數(shù)量和渲染的原始的圖像類型的指令開始,舉例來說,下面這個就是例子中的一個渲染指令。
// Draw the triangle. 畫一個三角形
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:3];
頂點階段為每個頂點提供數(shù)據(jù),當(dāng)處理了足夠多的頂點時,渲染管道光柵化基本體,確定渲染目標(biāo)中哪些像素位于基本體的邊界內(nèi)。片段階段確定要寫入這些像素的渲染目標(biāo)的顏色值。
在本示例的其余部分中,您將看到如何編寫頂點和片元函數(shù),如何創(chuàng)建渲染管道狀態(tài)對象,以及如何對使用此管道的draw命令進行編碼。
你的自定義渲染管道如何處理數(shù)據(jù)
頂點函數(shù)為單個頂點生成數(shù)據(jù),片元函數(shù)為單個片元生成數(shù)據(jù),但是你決定它們怎么生成數(shù)據(jù),您在配置管道的各個階段時要考慮目標(biāo),這意味著您知道希望管道生成什么以及它如何生成這些結(jié)果。
決定將哪些數(shù)據(jù)傳遞到渲染管道中,以及將哪些數(shù)據(jù)傳遞到管道的后期階段。通常有三個地方:
1.管道的輸入,由應(yīng)用程序提供并傳遞到vertex階段
2.頂點階段的輸出,傳遞到光柵化階段
3.片段階段的輸入,由應(yīng)用程序提供或由光柵化階段生成。
在這個例子中,輸入的管道的數(shù)據(jù)是頂點的位置和顏色,為了演示通常在頂點函數(shù)中執(zhí)行的轉(zhuǎn)換類型,輸入坐標(biāo)在自定義坐標(biāo)空間中定義,以距離視圖中心的像素為單位進行測量。這些坐標(biāo)需要轉(zhuǎn)換成Metal的坐標(biāo)系
聲明一個AAPLVertex的結(jié)構(gòu)體,使用SIMD向量類型保存位置和顏色數(shù)據(jù)
要共享一個在內(nèi)存中定義的結(jié)構(gòu)體,請在公共頭文件中聲明該結(jié)構(gòu)體,并將其導(dǎo)入到Metal和應(yīng)用程序中。
typedef struct
{
vector_float2 position;
vector_float4 color;
} AAPLVertex; // 定義的公共結(jié)構(gòu)體
SIMD的類型在Metal Shading language中很常見,你在你的程序中也應(yīng)該使用 simd library,SIMD類型包含特定數(shù)據(jù)類型的多個通道,所以把位置聲明為一個包含兩個32位float值的vector_float2類型(保存x和y的坐標(biāo)值)顏色值保存在一個vector_float4類型中,它有紅,綠,藍和透明度4個通道。
在程序中,使用常量數(shù)組指定輸入數(shù)據(jù):
static const AAPLVertex triangleVertices[] =
{
// 2D positions, RGBA colors
{ { 250, -250 }, { 1, 0, 0, 1 } },
{ { -250, -250 }, { 0, 1, 0, 1 } },
{ { 0, 250 }, { 0, 0, 1, 1 } },
};
頂點階段為頂點生成數(shù)據(jù),因此它需要提供顏色和變換的位置。聲明包含位置和顏色值的RasterizerData結(jié)構(gòu)體,使用SIMD的類型
typedef struct
{
//此成員的 [[positing]] 屬性表示此值是此結(jié)構(gòu)從頂點函數(shù)返回的頂點信息。
float4 position [[position]];
//由于此成員沒有特殊屬性,因此光柵化器使用其他三角形頂點的值插值其值,然后將插值值傳遞給每個三角形中的片元
float4 color;
} RasterizerData;
輸出位置(在下面詳細描述)必須定義為vector_float4,顏色聲明為輸入數(shù)據(jù)結(jié)構(gòu)中的color屬性
您需要告訴Metal光柵化數(shù)據(jù)中的哪個字段提供了位置數(shù)據(jù),因為Metal沒有對結(jié)構(gòu)中的字段強制任何特定的命名約定,請使用[[position]]屬性限定符注釋位置字段,以聲明此字段保留輸出位置。
fragment函數(shù)只是將光柵化階段的數(shù)據(jù)傳遞給后期階段,因此不需要任何額外的參數(shù)。
定義一個頂點函數(shù)
聲明頂點函數(shù),包括它的輸入?yún)?shù)和它輸出的數(shù)據(jù)。與使用kernel關(guān)鍵字聲明計算函數(shù)類似,您也可以使用vertex關(guān)鍵字聲明頂點函數(shù)
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
constant AAPLVertex *vertices [[buffer(AAPLVertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(AAPLVertexInputIndexViewportSize)]])
第一個參數(shù)vertexID使用了[[vertex_id]]屬性限定符,這是另一個Metal關(guān)鍵字。執(zhí)行render命令時,GPU多次調(diào)用頂點函數(shù),為每個頂點生成一個唯一的值。
第二個參數(shù)vertices是一個包含頂點數(shù)據(jù)的數(shù)組,使用前面定義的AAPLVertex結(jié)構(gòu)。
若要將位置轉(zhuǎn)換為Metal的坐標(biāo),該函數(shù)需要繪制三角形的視區(qū)的大?。ㄒ韵袼貫閱挝唬虼怂鎯υ趘iewportSizePointer參數(shù)中
第二個和第三個參數(shù)具有[[buffer(n)]]屬性限定符。默認(rèn)情況下,Metal會自動為每個參數(shù)在參數(shù)表中分配插槽。將[[buffer(n)]]限定符添加到緩沖區(qū)參數(shù)時,將明確告訴Metal要使用哪個插槽。顯式聲明槽可以更容易地修改著色器,而無需更改應(yīng)用程序代碼。聲明共享頭文件中兩個指示符的常量。
函數(shù)返回一個RasterizerData的結(jié)構(gòu)體
實現(xiàn)頂點函數(shù)
頂點函數(shù)必須生成輸出結(jié)構(gòu)的兩個字段。使用vertexID參數(shù)索引到頂點數(shù)組并讀取頂點的輸入數(shù)據(jù)。另外,檢索視口尺寸。
float2 pixelSpacePosition = vertices[vertexID].position.xy;
// Get the viewport size and cast to float.
vector_float2 viewportSize = vector_float2(*viewportSizePointer);
頂點函數(shù)必須在剪輯空間坐標(biāo)中提供位置數(shù)據(jù),剪輯空間坐標(biāo)是使用四維均勻向量(x,y,z,w)指定的三維點。光柵化階段獲取輸出位置,并將x、y和z坐標(biāo)除以w,以生成標(biāo)準(zhǔn)化設(shè)備坐標(biāo)中的3D點。標(biāo)準(zhǔn)化設(shè)備坐標(biāo)與視區(qū)大小無關(guān)
圖2 歸一化設(shè)備坐標(biāo)系

標(biāo)準(zhǔn)化設(shè)備坐標(biāo)使用左手坐標(biāo)系并映射到視口中的位置?;倔w被剪裁到這個坐標(biāo)系中的一個框中,然后光柵化。剪輯框的左下角位于(-1.0,-1.0)的(x,y)坐標(biāo),右上角位于(1.0,1.0)。正z值指向遠離相機(進入屏幕)的方向。z坐標(biāo)的可見部分介于0.0(近剪裁平面)和1.0(遠剪裁平面)之間。
將輸入坐標(biāo)系轉(zhuǎn)換為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)系

因為這是一個二維應(yīng)用程序,不需要同質(zhì)坐標(biāo),所以首先向輸出坐標(biāo)寫入一個默認(rèn)值,w值設(shè)置為1.0,其他坐標(biāo)設(shè)置為0.0。這意味著坐標(biāo)已經(jīng)在標(biāo)準(zhǔn)化設(shè)備坐標(biāo)空間中,頂點函數(shù)應(yīng)該在該坐標(biāo)空間中生成(x,y)坐標(biāo)。將輸入位置除以視窗大小的一半以生成標(biāo)準(zhǔn)化設(shè)備坐標(biāo)。由于此計算是使用SIMD類型執(zhí)行的,因此可以使用一行代碼同時分割兩個通道。執(zhí)行除法并將結(jié)果放入輸出位置的x和y通道。
out.position = vector_float4(0.0, 0.0, 0.0, 1.0);
out.position.xy = pixelSpacePosition / (viewportSize / 2.0);
最后,將顏色值復(fù)制到out.color返回值中
實現(xiàn)一個片元函數(shù)
片元是對渲染目標(biāo)的可能更改。光柵化器確定基本體覆蓋渲染目標(biāo)的哪些像素。僅渲染像素中心位于三角形內(nèi)部的片元。
圖3光柵化階段產(chǎn)生的碎片

片段函數(shù)處理來自光柵化器的單個位置的傳入信息,并計算每個渲染目標(biāo)的輸出值。這些片段值由管道中的后期處理,最終寫入呈現(xiàn)目標(biāo)
注意
片段之所以稱為可能的更改,是因為可以將片段階段之后的管道階段配置為拒絕某些片段或更改寫入呈現(xiàn)目標(biāo)的內(nèi)容。在此示例中,fragment stage計算的所有值都按原樣寫入到呈現(xiàn)目標(biāo)。
此示例中的片段著色器接收頂點著色器輸出中聲明的相同參數(shù)。使用fragment關(guān)鍵字聲明fragment函數(shù)。它只需要一個參數(shù),即vertex stage提供的光柵化數(shù)據(jù)結(jié)構(gòu)。添加[[stage_in]]屬性限定符以指示此參數(shù)是由光柵化器生成的。
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
如果片段函數(shù)寫入多個渲染目標(biāo),則它必須為每個渲染目標(biāo)聲明一個包含字段的結(jié)構(gòu)。由于此示例只有一個渲染目標(biāo),因此可以直接指定浮點向量作為函數(shù)的輸出。此輸出是要寫入渲染目標(biāo)的顏色。
光柵化階段計算每個片段參數(shù)的值,并用它們調(diào)用片段函數(shù)。光柵化階段將其顏色參數(shù)計算為三角形頂點處顏色的混合。片段離頂點越近,頂點對最終顏色的貢獻就越大。
圖4插值片段顏色

函數(shù)返回插值后的顏色值
return in.color;
創(chuàng)建渲染管道狀態(tài)對象
現(xiàn)在這些函數(shù)已經(jīng)完成,可以創(chuàng)建使用它們的渲染管道。首先,獲取默認(rèn)庫并為每個函數(shù)獲取MTLFunction對象。
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
接下來,創(chuàng)建MTLRenderPipelineState對象。渲染管道有更多要配置的階段,因此可以使用MTLRenderPipelineDescriptor來配置管道。
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
pipelineStateDescriptor.label = @"Simple Pipeline";
pipelineStateDescriptor.vertexFunction = vertexFunction;
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
error:&error];
除了指定頂點和片段函數(shù)外,還需要聲明管道將繪制到的所有渲染目標(biāo)的像素格式。像素格式(MTLPixelFormat)定義像素數(shù)據(jù)的內(nèi)存布局。對于簡單格式,此定義包括每個像素的字節(jié)數(shù)、存儲在像素中的數(shù)據(jù)通道數(shù)以及這些通道的位布局。由于此示例只有一個呈現(xiàn)目標(biāo),并且由視圖提供,請將視圖的像素格式復(fù)制到呈現(xiàn)管道描述符中。渲染管道狀態(tài)必須使用與渲染過程指定的像素格式兼容的像素格式。在此示例中,渲染過程和管道狀態(tài)對象都使用視圖的像素格式,因此它們始終相同
當(dāng)Metal創(chuàng)建渲染管道狀態(tài)對象時,管道被配置為將片段函數(shù)的輸出轉(zhuǎn)換為渲染目標(biāo)的像素格式。如果要以不同的像素格式為目標(biāo),則需要創(chuàng)建不同的管道狀態(tài)對象??梢栽诙鄠€針對不同像素格式的管道中重用相同的著色器
設(shè)置視口
現(xiàn)在已經(jīng)有了管道的渲染管道狀態(tài)對象,您將渲染三角形。使用渲染命令編碼器執(zhí)行此操作。首先,設(shè)置視口,以便Metal知道要繪制到渲染目標(biāo)的哪個部分。
// Set the region of the drawable to draw into.
[renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, 0.0, 1.0 }];
Set the Render Pipeline State
為要使用的管道設(shè)置渲染管道狀態(tài)。
[renderEncoder setRenderPipelineState:_pipelineState];
將參數(shù)數(shù)據(jù)發(fā)送到頂點函數(shù)
通常,使用緩沖區(qū)(MTLBuffer)將數(shù)據(jù)傳遞給著色器。但是,當(dāng)您只需要向vertex函數(shù)傳遞少量數(shù)據(jù)時(如這里的情況),請將數(shù)據(jù)直接復(fù)制到命令緩沖區(qū)中。
示例將兩個參數(shù)的數(shù)據(jù)復(fù)制到命令緩沖區(qū)中。頂點數(shù)據(jù)從示例中定義的數(shù)組中復(fù)制。從用于設(shè)置視口的同一變量復(fù)制視口數(shù)據(jù)。
在此示例中,fragment函數(shù)僅使用從光柵化器接收的數(shù)據(jù),因此沒有要設(shè)置的參數(shù)。
[renderEncoder setVertexBytes:triangleVertices
length:sizeof(triangleVertices)
atIndex:AAPLVertexInputIndexVertices];
[renderEncoder setVertexBytes:&_viewportSize
length:sizeof(_viewportSize)
atIndex:AAPLVertexInputIndexViewportSize];
對繪圖命令進行編碼
指定基本體的類型、起始索引和頂點數(shù)。當(dāng)三角形被渲染時,頂點函數(shù)被調(diào)用,頂點參數(shù)的值為0、1和2。
// Draw the triangle.
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:3];
與使用Metal繪制屏幕一樣,結(jié)束編碼過程并提交命令緩沖區(qū)。但是,可以使用相同的步驟集對更多渲染命令進行編碼。最終的圖像將呈現(xiàn)為命令是按照指定的順序處理的。(為了提高性能,GPU可以并行處理命令,甚至部分命令,只要最終結(jié)果看起來是按順序呈現(xiàn)的)
顏色插值實驗
在這個示例中,顏色值是通過三角形插值的。這通常是您想要的,但有時您希望一個值由一個頂點生成,并在整個基本體中保持不變。為此,請在頂點函數(shù)的輸出上指定平面屬性限定符。現(xiàn)在試試這個。在示例項目中查找光柵化數(shù)據(jù)的定義,并將[[flat]]限定符添加到其顏色字段中。
float4 color [[flat]];
再次運行樣本。渲染管道使用來自第一個頂點(稱為激發(fā)頂點)的顏色值均勻地穿過三角形,并且它忽略來自其他兩個頂點的顏色。只需在頂點函數(shù)的輸出中添加或省略平面限定符,就可以混合使用平面著色和插值。Metal Shading Language specification規(guī)范定義了其他屬性限定符,您也可以使用這些限定符來修改光柵化行為。