在上篇文章中我們學(xué)會了如何使用Metal來繪制視圖內(nèi)容,在這個(gè)篇章中我們將展示如何使用自定義渲染管道來繪制一個(gè)2D彩色圖形墩莫。該示例為每個(gè)頂點(diǎn)提供了位置和顏色芙委,并且渲染管線使用該數(shù)據(jù)渲染三角形,在為三角形頂點(diǎn)指定的顏色之間插入顏色值狂秦。
Metal渲染管道
一個(gè)渲染管線流程繪圖命令和數(shù)據(jù)寫入到一個(gè)渲染通道的目標(biāo)灌侣。渲染管線具有許多階段,其中一些階段使用著色器進(jìn)行編程裂问,而其他階段則具有固定或可配置的行為侧啼。該示例著重于流水線的三個(gè)主要階段:頂點(diǎn)階段牛柒,柵格化階段和片元階段。頂點(diǎn)階段和片元階段是可編程的痊乾,因此您可以使用Metal Shading Language
(MSL)為其編寫函數(shù)皮壁。光柵化階段具有固定的行為。
下圖:Metal圖形渲染管線的主要階段
渲染從繪制命令開始哪审,該命令包括一個(gè)頂點(diǎn)數(shù)和要渲染的圖元類型蛾魄。例如,這是此示例中的繪圖命令:
// Draw the triangle.
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:3];
頂點(diǎn)階段為每個(gè)頂點(diǎn)提供數(shù)據(jù)湿滓。處理完足夠的頂點(diǎn)后滴须,渲染管線將對圖元進(jìn)行柵格化,確定渲染目標(biāo)中的哪些像素位于圖元的邊界內(nèi)茉稠。片元階段確定要寫入這些像素的渲染目標(biāo)的值。
在本示例的其余部分把夸,您將看到如何編寫頂點(diǎn)和片元函數(shù)而线,如何創(chuàng)建渲染管道狀態(tài)對象,最后如何對使用該管道的繪圖命令進(jìn)行編碼恋日。
自定義渲染管道如何使用數(shù)據(jù)
頂點(diǎn)函數(shù)為單個(gè)頂點(diǎn)生成數(shù)據(jù)膀篮,片元函數(shù)為單個(gè)片元生成數(shù)據(jù),但是您可以決定它們的工作方式岂膳。
決定哪些數(shù)據(jù)要傳遞到渲染管道誓竿,哪些數(shù)據(jù)要傳遞到管道的后續(xù)階段。通常有三個(gè)地方可以做到這一點(diǎn):
- 管道的輸入谈截,由應(yīng)用程序提供并傳遞到頂點(diǎn)階段筷屡。
- 頂點(diǎn)階段的輸出,傳遞到光柵化階段簸喂。
- 片元階段的輸入毙死,由您的應(yīng)用程序提供或由柵格化階段生成。
在此示例中喻鳄,管道的輸入數(shù)據(jù)是頂點(diǎn)的位置及其顏色扼倘。為了演示您通常在頂點(diǎn)函數(shù)中執(zhí)行的變換類型,輸入坐標(biāo)是在自定義坐標(biāo)空間中定義的除呵,以自視圖中心的像素為單位再菊。這些坐標(biāo)需要轉(zhuǎn)換為Metal的坐標(biāo)系。
AAPLVertex使用SIMD向量類型聲明結(jié)構(gòu)颜曾,以保存位置和顏色數(shù)據(jù)纠拔。要共享有關(guān)結(jié)構(gòu)在內(nèi)存中的布局方式的單一定義,請?jiān)诠矘?biāo)頭中聲明該結(jié)構(gòu)泛豪,然后將其導(dǎo)入Metal著色器和應(yīng)用程序中绿语。
typedef struct
{
vector_float2 position;
vector_float4 color;
} AAPLVertex;
SIMD類型包含特定數(shù)據(jù)類型的多個(gè)通道秃症,因此將位置聲明為vector_float2
意味著它包含兩個(gè)32位浮點(diǎn)值(將保存x和y坐標(biāo)。)使用vector_float4
存儲顏色吕粹,因此它們具有四個(gè)通道-紅色种柑,綠色,藍(lán)色和Alpha匹耕。
在應(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
// Vertex shader outputs and fragment shader inputs
typedef struct
{
// The [[position]] attribute of this member indicates that this value
// is the clip space position of the vertex when this structure is
// returned from the vertex function.
float4 position [[position]];
// Since this member does not have a special attribute, the rasterizer
// interpolates its value with the values of the other triangle vertices
// and then passes the interpolated value to the fragment shader for each
// fragment in the triangle.
float4 color;
} RasterizerData;
您需要告訴Metal柵格化數(shù)據(jù)中的哪個(gè)字段提供位置數(shù)據(jù),因?yàn)镸etal不會對結(jié)構(gòu)中的字段實(shí)施任何特定的命名約定既鞠。position用[[position]]
屬性限定符注釋該字段煤傍,以聲明該字段占據(jù)輸出位置。
片元函數(shù)只是將柵格化階段的數(shù)據(jù)傳遞到以后的階段嘱蛋,因此不需要任何其他參數(shù)蚯姆。
聲明頂點(diǎn)函數(shù)
聲明頂點(diǎn)函數(shù),包括其輸入?yún)?shù)和其輸出的數(shù)據(jù)洒敏。就像使用kernel關(guān)鍵字聲明計(jì)算函數(shù)一樣龄恋,您可以使用關(guān)鍵字聲明頂點(diǎn)函數(shù)vertex。
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會多次調(diào)用頂點(diǎn)函數(shù)显押,從而為每個(gè)頂點(diǎn)生成唯一的值。vertexID[[vertex_id]]
第二個(gè)參數(shù)傻挂,vertices是一個(gè)使用AAPLVertex先前定義的結(jié)構(gòu)包含頂點(diǎn)數(shù)據(jù)的數(shù)組煮落。
要將位置轉(zhuǎn)換為Metal的坐標(biāo),此函數(shù)需要繪制三角形的視口大杏荒薄(以像素為單位)蝉仇,因此將其存儲在參數(shù)中。viewportSizePointer
第二個(gè)和第三個(gè)參數(shù)具有[[buffer(n)]]屬性限定符殖蚕。默認(rèn)情況下轿衔,Metal自動在參數(shù)表中為每個(gè)參數(shù)分配插槽。將[[buffer(n)]]限定詞添加到緩沖區(qū)參數(shù)時(shí)睦疫,您明確告訴Metal使用哪個(gè)插槽害驹。顯式聲明插槽可以使您更輕松地修改著色器,而無需更改應(yīng)用程序代碼蛤育。在共享頭文件中聲明兩個(gè)索引的常量宛官。
該函數(shù)的輸出是一個(gè)結(jié)構(gòu)葫松。RasterizerData
編寫頂點(diǎn)函數(shù)
您的頂點(diǎn)函數(shù)必須生成輸出結(jié)構(gòu)的兩個(gè)字段。使用自變量索引數(shù)組并讀取頂點(diǎn)的輸入數(shù)據(jù)底洗。
// Index into the array of positions to get the current vertex.
// The positions are specified in pixel dimensions (i.e. a value of 100
// is 100 pixels from the origin).
float2 pixelSpacePosition = vertices[vertexID].position.xy;
// Get the viewport size and cast to float.
vector_float2 viewportSize = vector_float2(*viewportSizePointer);
頂點(diǎn)函數(shù)必須在剪貼空間坐標(biāo)中提供位置數(shù)據(jù)腋么,該數(shù)據(jù)是使用四維均勻向量(x,y,z,w)指定的3D點(diǎn)。光柵化階段需要的輸出位置亥揖,并且將x珊擂,y和z由坐標(biāo)w,以生成一個(gè)3D點(diǎn)歸一化設(shè)備坐標(biāo)费变。規(guī)范化的設(shè)備坐標(biāo)與視口大小無關(guān)摧扇。
下圖:規(guī)范化設(shè)備坐標(biāo)系
規(guī)范化的設(shè)備坐標(biāo)使用左手坐標(biāo)系并映射到視口中的位置。在圖元坐標(biāo)系中將圖元裁剪到一個(gè)框挚歧,然后進(jìn)行柵格化扛稽。剪輯框的左下角為的(x,y)坐標(biāo),右上角為的坐標(biāo)滑负。正z值指向遠(yuǎn)離相機(jī)的位置(進(jìn)入屏幕)在张。坐標(biāo)的可見部分在(近裁剪平面)和(遠(yuǎn)裁剪平面)之間。(-1.0,-1.0)(1.0,1.0)z0.01.0
將輸入坐標(biāo)系轉(zhuǎn)換為規(guī)范化的設(shè)備坐標(biāo)系橙困。
因?yàn)檫@是2D應(yīng)用程序瞧掺,并且不需要同質(zhì)坐標(biāo)耕餐,所以首先將默認(rèn)值寫入輸出坐標(biāo)凡傅,將w值設(shè)置為,將其他坐標(biāo)設(shè)置為肠缔。這意味著坐標(biāo)已經(jīng)在歸一化設(shè)備坐標(biāo)空間中夏跷,并且頂點(diǎn)函數(shù)應(yīng)該在該坐標(biāo)空間中生成(x,y)坐標(biāo)明未。將輸入位置除以視口大小的一半以生成標(biāo)準(zhǔn)化的設(shè)備坐標(biāo)槽华。由于此計(jì)算是使用SIMD類型執(zhí)行的,因此可以使用一行代碼同時(shí)分割兩個(gè)通道趟妥。執(zhí)行除法猫态,然后將結(jié)果放入輸出位置的x和y通道中。
// To convert from positions in pixel space to positions in clip-space,
// divide the pixel coordinates by half the size of the viewport.
out.position = vector_float4(0.0, 0.0, 0.0, 1.0);
out.position.xy = pixelSpacePosition / (viewportSize / 2.0);
最后披摄,將顏色值復(fù)制到返回值中亲雪。
out.color = vertices[vertexID].color;
編寫片元函數(shù)
下圖:柵格化階段生成的片元
片元函數(shù)處理來自光柵化器的單個(gè)位置的傳入信息,并計(jì)算每個(gè)渲染目標(biāo)的輸出值疚膊。這些片元值由流水線中的后續(xù)階段處理义辕,最終寫入渲染目標(biāo)。
此示例中的片元著色器接收的參數(shù)與頂點(diǎn)著色器的輸出中聲明的參數(shù)相同寓盗。使用fragment關(guān)鍵字聲明片元功能灌砖。它只接受一個(gè)參數(shù)璧函,即頂點(diǎn)階段提供的結(jié)構(gòu)。添加屬性限定符[[stage_in]]
以指示此參數(shù)由光柵化器生成基显。
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
如果您的片元函數(shù)寫入多個(gè)渲染目標(biāo)蘸吓,則必須為每個(gè)渲染目標(biāo)聲明一個(gè)帶有字段的結(jié)構(gòu)。由于此示例僅具有一個(gè)渲染目標(biāo)续镇,因此您可以直接將浮點(diǎn)向量指定為函數(shù)的輸出美澳。此輸出是要寫入渲染目標(biāo)的顏色。
光柵化階段為每個(gè)片元的參數(shù)計(jì)算值摸航,并使用它們調(diào)用片元函數(shù)制跟。柵格化階段將其顏色參數(shù)計(jì)算為三角形頂點(diǎn)處顏色的混合。片元離頂點(diǎn)越近酱虎,該頂點(diǎn)對最終顏色的貢獻(xiàn)就越大雨膨。
下圖:插值的片元顏色
返回插值的顏色作為函數(shù)的輸出。
return in.color;
創(chuàng)建渲染管道狀態(tài)對象
現(xiàn)在功能已完成读串,您可以創(chuàng)建使用它們的渲染管道聊记。首先,獲取默認(rèn)庫并MTLFunction為每個(gè)函數(shù)獲取一個(gè)對象恢暖。
// Load all the shader files with a .metal file extension in the project.
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
接下來創(chuàng)建一個(gè)MTLRenderPipelineState
對象排监,渲染管道又很多的可配置項(xiàng),你可以使用MTLRenderPipelineDescriptor
來進(jìn)行配置杰捂。
// Configure a pipeline descriptor that is used to create a pipeline state.
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)和片元功能之外舆床,您還聲明管道將繪制到的所有渲染目標(biāo)的像素格式。像素格式(MTLPixelFormat)定義像素?cái)?shù)據(jù)的存儲布局嫁佳。對于簡單格式挨队,此定義包括每個(gè)像素的字節(jié)數(shù),存儲在一個(gè)像素中的數(shù)據(jù)通道數(shù)以及這些通道的位布局蒿往。由于此樣本僅具有一個(gè)渲染目標(biāo)盛垦,并且由視圖提供,因此將視圖的像素格式復(fù)制到渲染管道描述符中瓤漏。渲染管線狀態(tài)必須使用與渲染通道指定的像素格式兼容的像素格式腾夯。在此示例中,渲染通道和管線狀態(tài)對象都使用視圖的像素格式蔬充,因此它們始終相同蝶俱。
當(dāng)Metal創(chuàng)建渲染管線狀態(tài)對象時(shí),將管線配置為將片元函數(shù)的輸出轉(zhuǎn)換為渲染目標(biāo)的像素格式娃惯。如果要指定其他像素格式跷乐,則需要創(chuàng)建其他管道狀態(tài)對象。您可以在針對不同像素格式的多個(gè)管線中重復(fù)使用相同的著色器趾浅。
設(shè)置視口
既然已經(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 }];
設(shè)置渲染管線狀態(tài)
設(shè)置要使用的管道的渲染管道狀態(tài)如输。
[renderEncoder setRenderPipelineState:_pipelineState];
將參數(shù)數(shù)據(jù)發(fā)送到頂點(diǎn)函數(shù)
通常鼓黔,您使用緩沖區(qū)(MTLBuffer)將數(shù)據(jù)傳遞到著色器。但是不见,當(dāng)您只需要向頂點(diǎn)函數(shù)傳遞少量數(shù)據(jù)時(shí)(如此處所示)澳化,可以將數(shù)據(jù)直接復(fù)制到命令緩沖區(qū)中。
該示例將兩個(gè)參數(shù)的數(shù)據(jù)復(fù)制到命令緩沖區(qū)中稳吮。從樣本中定義的數(shù)組復(fù)制頂點(diǎn)數(shù)據(jù)缎谷。視口數(shù)據(jù)是從用于設(shè)置視口的同一變量中復(fù)制的。
在此示例中灶似,fragment函數(shù)僅使用從光柵化器接收到的數(shù)據(jù)列林,因此沒有要設(shè)置的參數(shù)。
// Pass in the parameter data.
[renderEncoder setVertexBytes:triangleVertices
length:sizeof(triangleVertices)
atIndex:AAPLVertexInputIndexVertices];
[renderEncoder setVertexBytes:&_viewportSize
length:sizeof(_viewportSize)
atIndex:AAPLVertexInputIndexViewportSize];
與使用Metal繪制到屏幕一樣酪惭,您結(jié)束編碼過程并提交命令緩沖區(qū)希痴。但是,您可以使用相同的步驟對更多的渲染命令進(jìn)行編碼春感。渲染最終圖像砌创,就像按指定順序處理命令一樣。(為了提高性能甥厦,只要最終結(jié)果看上去已經(jīng)按順序呈現(xiàn)纺铭,GPU便可以并行處理命令或什至部分命令寇钉。)