渲染一個(gè)簡(jiǎn)單的二維三角形。
本文內(nèi)容來自蘋果
本文主要講述使用Metal繪制到屏幕時(shí),將視圖的內(nèi)容刪除為背景顏色。顯示如何配置渲染管道并將其用作渲染過程的一部分,以簡(jiǎn)單的2D彩色三角形繪制到視圖中。為每個(gè)頂點(diǎn)提供位置和顏色,渲染管道使用該數(shù)據(jù)渲染三角形,在為三角形頂點(diǎn)指定的顏色之間插入顏色值。
注意
iOS或tvOS模擬器不支持Metal,因此iOS和tvOS方案需要物理設(shè)備來運(yùn)行示例
了解Metal Render Pipeline
一個(gè)渲染管線流程繪圖命令和數(shù)據(jù)寫入到一個(gè)渲染通道的目標(biāo)。渲染管道有許多階段,一些使用著色器編程,另一些使用固定或可配置的行為編程。此示例主要關(guān)注管道的三個(gè)主要階段:頂點(diǎn)階段,光柵化階段和片段階段。頂點(diǎn)階段和片段階段是可編程的,因此您可以使用金屬著色語言(MSL)為它們編寫函數(shù)。光柵化階段具有固定的行為。
圖1 Metal圖形渲染管道的主要階段
渲染從繪圖命令開始,該命令包括頂點(diǎn)計(jì)數(shù)和要渲染的基元類型。例如,以下是此示例中的繪圖命令:
// Draw the triangle.
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:3];
頂點(diǎn)階段為每個(gè)頂點(diǎn)提供數(shù)據(jù)。當(dāng)處理了足夠的頂點(diǎn)時(shí),渲染管道柵格化基元,確定渲染目標(biāo)中的哪些像素位于基元的邊界內(nèi)。片段階段確定要寫入這些像素的渲染目標(biāo)的值。
在本示例的其余部分中,您將看到如何編寫頂點(diǎn)和片段函數(shù),如何創(chuàng)建渲染管道狀態(tài)對(duì)象,以及最后如何編碼使用此管道的繪制命令。
自定義渲染管道如何處理數(shù)據(jù)
頂點(diǎn)函數(shù)為單個(gè)頂點(diǎn)生成數(shù)據(jù)
,片段函數(shù)生成單個(gè)片段的數(shù)據(jù)
,但您可以決定它們的工作方式。您可以考慮目標(biāo)來配置管道的各個(gè)階段,這意味著您知道管道要生成什么以及如何生成這些結(jié)果。
確定要傳遞到渲染管道的數(shù)據(jù)以及將哪些數(shù)據(jù)傳遞到管道的后續(xù)階段。通常有三個(gè)地方可以執(zhí)行此操作:
- 管道的輸入,由您的應(yīng)用程序提供并傳遞到頂點(diǎn)階段。
- 頂點(diǎn)階段的輸出,傳遞給光柵化階段。
- 片段階段的輸入,由您的應(yīng)用提供或由光柵化階段生成。
管道的輸入數(shù)據(jù)是頂點(diǎn)的位置及其顏色。通常在頂點(diǎn)函數(shù)中執(zhí)行的變換類型,輸入坐標(biāo)在自定義坐標(biāo)空間中定義,以視圖中心的像素為單位進(jìn)行測(cè)量。這些坐標(biāo)需要轉(zhuǎn)換為Metal的坐標(biāo)系。
聲明一個(gè)PLVertex結(jié)構(gòu),使用SIMD矢量類型來保存位置和顏色數(shù)據(jù)。要共享結(jié)構(gòu)在內(nèi)存中的布局方式的單個(gè)定義,請(qǐng)?jiān)谕ㄓ脴?biāo)頭中聲明結(jié)構(gòu),并將其導(dǎo)入Metal shader和app中。
typedef struct
{
vector_float2 position;
vector_float4 color;
} AAPLVertex;
SIMD類型在Metal Shading Language中很常見,您也應(yīng)該使用simd庫在應(yīng)用程序中使用它們。SIMD類型包含特定數(shù)據(jù)類型的多個(gè)通道
,因此將位置聲明為包含兩個(gè)32位浮點(diǎn)值(它將保存x和y坐標(biāo))。顏色使用a存儲(chǔ),因此它們有四個(gè)通道 - 紅色,綠色,藍(lán)色和alpha。vector_float2vector_float4
在應(yīng)用程序中,使用常量數(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 } },
};
頂點(diǎn)階段為頂點(diǎn)生成數(shù)據(jù),因此需要提供顏色和變換位置。使用SIMD類型聲明包含位置和顏色值的結(jié)構(gòu)。RasterizerData
typedef struct
{
//此成員的[[位置]]屬性表示此值
//是此結(jié)構(gòu)為
//從Vertex函數(shù)返回。
float4 position [[position]];
//由于此成員沒有特殊屬性,因此光柵化器
//用其他三角形頂點(diǎn)的值插入其值
//然后將插值值傳遞給每個(gè)
//三角形中的片段。
float4 color;
} RasterizerData;
輸出位置(下面詳細(xì)描述)必須定義為a 。顏色聲明為輸入數(shù)據(jù)結(jié)構(gòu)中的顏色。vector_float4
您需要告訴Metal光柵化數(shù)據(jù)中的哪個(gè)字段提供位置數(shù)據(jù),因?yàn)镸etal不對(duì)結(jié)構(gòu)中的字段強(qiáng)制執(zhí)行任何特定的命名約定。position使用[[position]]屬性限定符注釋該字段以聲明此字段包含輸出位置。
片段函數(shù)只是將光柵化階段的數(shù)據(jù)傳遞給后期階段,因此它不需要任何其他參數(shù)。
聲明頂點(diǎn)函數(shù)
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
constant AAPLVertex *vertices [[buffer(AAPLVertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(AAPLVertexInputIndexViewportSize)]])
第一個(gè)參數(shù)是使用屬性限定符,它是另一個(gè)Metal關(guān)鍵字。執(zhí)行渲染命令時(shí),GPU會(huì)多次調(diào)用頂點(diǎn)函數(shù),為每個(gè)頂點(diǎn)生成唯一值。vertexID[[vertex_id]]
第二個(gè)參數(shù)vertices是一個(gè)包含頂點(diǎn)數(shù)據(jù)的數(shù)組,使用AAPLVertex先前定義的結(jié)構(gòu)。
要將位置轉(zhuǎn)換為Metal的坐標(biāo),該函數(shù)需要繪制三角形的視口大小(以像素為單位),因此將其存儲(chǔ)在參數(shù)中。viewportSizePointer
第二個(gè)和第三個(gè)參數(shù)具有[[buffer(n)]]屬性限定符。默認(rèn)情況下,Metal會(huì)自動(dòng)為參數(shù)表分配每個(gè)參數(shù)的插槽。將[[buffer(n)]]限定符添加到緩沖區(qū)參數(shù)時(shí),可以明確告知Metal使用哪個(gè)插槽。明確聲明插槽可以更輕松地修改著色器,而無需更改應(yīng)用程序代碼。在共享頭文件中聲明兩個(gè)指標(biāo)的常量。
函數(shù)的輸出是一個(gè)結(jié)構(gòu)。RasterizerData
寫入頂點(diǎn)函數(shù)
您的頂點(diǎn)函數(shù)必須生成輸出結(jié)構(gòu)的兩個(gè)字段。使用參數(shù)索引到數(shù)組并讀取頂點(diǎn)的輸入數(shù)據(jù)。另外,檢索視口尺寸。vertexIDvertices
float2 pixelSpacePosition = vertices[vertexID].position.xy;
// 獲取視區(qū)大小并將其轉(zhuǎn)換為float
vector_float2 viewportSize = vector_float2(*viewportSizePointer);
頂點(diǎn)函數(shù)必須在剪輯空間坐標(biāo)中提供位置數(shù)據(jù),這是使用四維同質(zhì)矢量(x,y,z,w)指定的3D點(diǎn)。光柵化階段獲取輸出位置并將,,和坐標(biāo)分開x,以在標(biāo)準(zhǔn)化設(shè)備坐標(biāo)中生成3D點(diǎn)。標(biāo)準(zhǔn)化設(shè)備坐標(biāo)與視口大小無關(guān)。yzw
圖2標(biāo)準(zhǔn)化設(shè)備坐標(biāo)系
標(biāo)準(zhǔn)化設(shè)備坐標(biāo)使用左手坐標(biāo)系并映射到視口中的位置。基元被剪切到此坐標(biāo)系中的框,然后進(jìn)行柵格化。剪切框的左下角位于(x,y)坐標(biāo)處,右上角位于。正-Z值指向遠(yuǎn)離相機(jī)(進(jìn)入屏幕)。坐標(biāo)的可見部分位于(近剪裁平面)和(遠(yuǎn)剪裁平面)之間。
將輸入坐標(biāo)系轉(zhuǎn)換為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)系。
因?yàn)檫@是一個(gè)2D應(yīng)用程序而且不需要同質(zhì)坐標(biāo),所以首先將默認(rèn)值寫入輸出坐標(biāo),其w值設(shè)置為,其他坐標(biāo)設(shè)置為。這意味著坐標(biāo)已經(jīng)在標(biāo)準(zhǔn)化設(shè)備坐標(biāo)空間中,并且頂點(diǎn)函數(shù)應(yīng)該在該坐標(biāo)空間中生成(x,y)坐標(biāo)。將輸入位置除以視口大小的一半以生成標(biāo)準(zhǔn)化設(shè)備坐標(biāo)。由于使用SIMD類型執(zhí)行該計(jì)算,因此可以使用單行代碼同時(shí)劃分兩個(gè)通道。執(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
out.color = vertices[vertexID].color;
寫一個(gè)片段函數(shù)
一個(gè)片段是一個(gè)可能改變的渲染目標(biāo)。光柵化器確定渲染目標(biāo)的哪些像素被基元覆蓋。僅渲染像素中心在三角形內(nèi)部的片段。
圖3光柵化階段生成的碎片
片段函數(shù)處理來自光柵化器的輸入信息,用于單個(gè)位置,并計(jì)算每個(gè)渲染目標(biāo)的輸出值。這些片段值由管道中的后續(xù)階段處理,最終寫入渲染目標(biāo)。
注意
片段被稱為可能的更改的原因是因?yàn)槠坞A段之后的管道階段可以配置為拒絕某些片段或更改寫入渲染目標(biāo)的內(nèi)容。在此示例中,片段階段計(jì)算的所有值都按原樣寫入渲染目標(biāo)。
此示例中的片段著色器接收與頂點(diǎn)著色器輸出中聲明的相同參數(shù)。使用fragment關(guān)鍵字聲明片段函數(shù)。它需要一個(gè)參數(shù),與頂點(diǎn)階段提供的結(jié)構(gòu)相同。添加屬性限定符以指示此參數(shù)是由光柵化器生成的。RasterizerData[[stage_in]]
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
光柵化階段計(jì)算每個(gè)片段參數(shù)的值,并使用它們調(diào)用片段函數(shù)。光柵化階段將其顏色參數(shù)計(jì)算為三角形頂點(diǎn)處顏色的混合。片段離頂點(diǎn)越近,頂點(diǎn)對(duì)最終顏色的貢獻(xiàn)越大。
圖4插值片段顏色
將插值顏色作為函數(shù)的輸出返回。
return in.color;
創(chuàng)建渲染管道狀態(tài)對(duì)象
現(xiàn)在函數(shù)已完成,您可以創(chuàng)建使用它們的渲染管道。首先,獲取默認(rèn)庫并獲取MTLFunction
每個(gè)函數(shù)的對(duì)象。
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
接下來,創(chuàng)建一個(gè)對(duì)象。渲染管道有更多要配置的階段,因此您使用a 來配置管道。
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];
除了指定頂點(diǎn)和片段函數(shù)之外,還要聲明管道將繪制的所有渲染目標(biāo)的像素格式。像素格式()定義像素?cái)?shù)據(jù)的存儲(chǔ)器布局。對(duì)于簡(jiǎn)單格式,此定義包括每個(gè)像素的字節(jié)數(shù),存儲(chǔ)在像素中的數(shù)據(jù)通道數(shù)以及這些通道的位布局。由于此示例只有一個(gè)渲染目標(biāo)并且由視圖提供,因此將視圖的像素格式復(fù)制到渲染管道描述符中。渲染管道狀態(tài)必須使用與渲染過程指定的像素格式兼容的像素格式。在此示例中,渲染過程和管道狀態(tài)對(duì)象都使用視圖的像素格式,因此它們始終相同。MTLPixelFormat
當(dāng)Metal創(chuàng)建渲染管道狀態(tài)對(duì)象時(shí),管道配置為將片段函數(shù)的輸出轉(zhuǎn)換為渲染目標(biāo)的像素格式。如果要定位不同的像素格式,則需要?jiǎng)?chuàng)建不同的管道狀態(tài)對(duì)象。您可以在針對(duì)不同像素格式的多個(gè)管道中重復(fù)使用相同的著色器。
設(shè)置視口
現(xiàn)在您已擁有管道的渲染管道狀態(tài)對(duì)象,您將渲染三角形。您可以使用渲染命令編碼器執(zhí)行此操作。首先,設(shè)置視口,以便Metal知道要繪制的渲染目標(biāo)的哪個(gè)部分。
// Set the region of the drawable to draw into.
[renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, 0.0, 1.0 }];
設(shè)置渲染管道狀態(tài)
設(shè)置要使用的管道的渲染管道狀態(tài)。
[renderEncoder setRenderPipelineState:_pipelineState];
將參數(shù)數(shù)據(jù)發(fā)送到頂點(diǎn)函數(shù)
通常,您使用buffers(MTLBuffer
)將數(shù)據(jù)傳遞給著色器。但是,當(dāng)您需要將少量數(shù)據(jù)傳遞給頂點(diǎn)函數(shù)時(shí)(如此處所示),將數(shù)據(jù)直接復(fù)制到命令緩沖區(qū)中。
該示例將兩個(gè)參數(shù)的數(shù)據(jù)復(fù)制到命令緩沖區(qū)中。頂點(diǎn)數(shù)據(jù)從樣本中定義的數(shù)組中復(fù)制。視口數(shù)據(jù)是從用于設(shè)置視口的相同變量中復(fù)制的。
在此示例中,片段函數(shù)僅使用從光柵化器接收的數(shù)據(jù),因此沒有要設(shè)置的參數(shù)。
[renderEncoder setVertexBytes:triangleVertices
length:sizeof(triangleVertices)
atIndex:AAPLVertexInputIndexVertices];
[renderEncoder setVertexBytes:&_viewportSize
length:sizeof(_viewportSize)
atIndex:AAPLVertexInputIndexViewportSize];
編碼繪圖命令
指定基元的類型,起始索引和頂點(diǎn)數(shù)。渲染三角形時(shí),將為參數(shù)調(diào)用頂點(diǎn)函數(shù),值為0,1和2 。vertexID
// Draw the triangle.
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:3];
與使用Metal繪制屏幕一樣,您可以結(jié)束編碼過程并提交命令緩沖區(qū)。但是,您可以使用同一組步驟編碼更多渲染命令。渲染最終圖像,就好像命令按指定順序處理一樣。(為了提高性能,允許GPU并行處理命令甚至部分命令,只要最終結(jié)果看起來按順序呈現(xiàn)。)
使用顏色插值進(jìn)行實(shí)驗(yàn)
在此示例中,顏色值在三角形中進(jìn)行插值。這通常是你想要的,但有時(shí)候你想要一個(gè)頂點(diǎn)生成一個(gè)值,并在整個(gè)基元上保持不變。flat
在頂點(diǎn)函數(shù)的輸出上指定屬性限定符以執(zhí)行此操作。現(xiàn)在試試吧。在示例項(xiàng)目中找到定義并將限定符添加到其字段中。RasterizerData[[flat]]color``float4 color [[flat]];
再次運(yùn)行該示例。渲染管道在三角形上均勻地使用第一個(gè)頂點(diǎn)(稱為激發(fā)頂點(diǎn))的顏色值,并忽略其他兩個(gè)頂點(diǎn)的顏色。您可以使用平面著色和插值的混合,只需flat
在頂點(diǎn)函數(shù)的輸出上添加或省略限定符即可。“ 金屬著色語言”規(guī)范定義了您還可以用來修改光柵化行為的其他屬性限定符。