渲染一個(gè)簡(jiǎn)單的二維三角形
概觀
在使用Metal繪制到屏幕時(shí),您學(xué)習(xí)了如何設(shè)置MTKView
對(duì)象并使用渲染過(guò)程更改視圖的內(nèi)容只祠。該示例只是將視圖的內(nèi)容刪除為背景顏色。此示例顯示如何配置渲染管道并將其用作渲染過(guò)程的一部分,以將簡(jiǎn)單的2D彩色三角形繪制到視圖中挣轨。該示例為每個(gè)頂點(diǎn)提供位置和顏色,渲染管道使用該數(shù)據(jù)渲染三角形轩猩,在為三角形頂點(diǎn)指定的顏色之間插入顏色卷扮。
注意
Xcode項(xiàng)目包含在macOS荡澎,iOS和tvOS上運(yùn)行示例的方案。iOS或者tvOS模擬器不支持Metal晤锹,因此iOS和tvOS方案需要物理設(shè)備來(lái)運(yùn)行示例衔瓮。默認(rèn)方案是macOS。
了解Metal Render Pipeline
一個(gè)渲染管線流程繪圖命令和數(shù)據(jù)寫(xiě)入到一個(gè)渲染通道的目標(biāo)抖甘。渲染管道有許多階段热鞍,一些使用著著色器編程,另一些使用固定或可配置的行為編程衔彻。此示例主要關(guān)注管道的三個(gè)階段:頂點(diǎn)階段薇宠、光柵化階段和片段階段。頂點(diǎn)階段和片段階段是可編程的艰额,因此您可以使用MSL為他們編寫(xiě)函數(shù)澄港。光柵化階段具有固定的行為。
圖1 Metal圖形化渲染管道的主要階段
渲染從繪圖命令開(kāi)始柄沮,該命令包括頂點(diǎn)的計(jì)數(shù)和要渲染的基元類(lèi)型回梧。例如,以下是此示例中的繪圖命令:
renderEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
頂點(diǎn)階段為每個(gè)頂點(diǎn)提供數(shù)據(jù)祖搓。當(dāng)處理了足夠的頂點(diǎn)時(shí)狱意,渲染管道柵格化基元,確定渲染目標(biāo)中的哪些像素基于基元的邊界內(nèi)拯欧。片段階段確定要寫(xiě)入這些元素的渲染目標(biāo)的值详囤。
在本示例的其余部分,您將看到如何編寫(xiě)頂點(diǎn)和片段函數(shù)镐作,如何創(chuàng)建渲染管道狀態(tài)對(duì)象藏姐,以及最后如何編碼使用此管道的繪制命令。
決定自定義渲染管道如何處理數(shù)據(jù)
頂點(diǎn)函數(shù)為單個(gè)頂點(diǎn)生成數(shù)據(jù)该贾,片段函數(shù)生成單個(gè)片段的數(shù)據(jù)羔杨,但您可以決定它們的工作方式。您可以考慮目標(biāo)來(lái)配置管道的各個(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í)行的變換類(lèi)型殴玛,輸入坐標(biāo)在自定義坐標(biāo)空間中定義,以視圖中心的像素為單位進(jìn)行測(cè)量添祸。
聲明一個(gè)AAPLVertex
結(jié)構(gòu)滚粟,使用SIMD矢量類(lèi)型來(lái)保存位置和顏色數(shù)據(jù)。要共享結(jié)構(gòu)在內(nèi)存中的布局方式的單個(gè)定義刃泌,請(qǐng)?jiān)谕ㄓ妙^文件中聲明結(jié)構(gòu)凡壤,并將其導(dǎo)入著色器(Metal shader)
和app中。
typedef struct
{
vector_float2 position;
vector_float4 color;
} AAPLVertex;
SIMD類(lèi)型在MSL中很常見(jiàn)耙替,您也應(yīng)該在應(yīng)用程序中使用simd
庫(kù)亚侠。SIMD類(lèi)型包含特定數(shù)據(jù)類(lèi)型的多個(gè)通道,因此將位置聲明為vector_float2
包含兩個(gè)32位浮點(diǎn)數(shù)(他將保存x和y坐標(biāo))俗扇。顏色需要使用vector_float4
硝烂,因?yàn)樗鼈冇?個(gè)通道R、G铜幽、B滞谢、A。
在應(yīng)用程序中除抛,使用常亮數(shù)組輸入指定數(shù)據(jù):
let triangleVertices: [AAPLVertex] =
[AAPLVertex(position: [250, -250], color: [1, 0, 0, 1]),
AAPLVertex(position: [-250, -250], color: [0, 1, 0, 1]),
AAPLVertex(position: [0, 250], color: [0, 0, 1, 1])]
頂點(diǎn)階段為頂點(diǎn)生成數(shù)據(jù)狮杨,因此需要提供顏色和變換位置。使用SIMD類(lèi)型聲明RasterizerData
包含位置和顏色值的結(jié)構(gòu)镶殷。
typedef struct
{
float4 position [[position]];
float4 color;
} RasterizerData;
輸出位置(下面詳細(xì)描述)必須定義為vector_float4
禾酱,顏色聲明為輸入數(shù)據(jù)結(jié)構(gòu)的顏色。
您需要告訴Metal光柵化數(shù)據(jù)中哪個(gè)字段提供位置數(shù)據(jù)绘趋,因?yàn)?strong>Metal不對(duì)結(jié)構(gòu)中的字段強(qiáng)制執(zhí)行任何特定的命名約定。position
使用[[position]]
屬性限定符注釋該字段以聲明此字段包含輸出位置颗管。
片段函數(shù)只是將光柵化階段的數(shù)據(jù)傳遞給后期階段陷遮,因=因此它不需要任何其他參數(shù)。
聲明頂點(diǎn)函數(shù)
聲明頂點(diǎn)函數(shù)垦江,包括其輸入?yún)?shù)及輸出數(shù)據(jù)帽馋。就像使用kernel
關(guān)鍵字聲明計(jì)算函數(shù)一樣,您使用vertex
關(guān)鍵字聲明頂點(diǎn)函數(shù)比吭。
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
constant AAPLVertex *vertices [[buffer(AAPLVertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(AAPLVertexInputIndexViewportSize)]])
第一個(gè)參數(shù)是vertexID
绽族,使用[[vertex_id]]
屬性限定符,他還是一個(gè)Metal關(guān)鍵字衩藤。執(zhí)行渲染命令時(shí)吧慢,GPU會(huì)多次調(diào)用頂點(diǎn)函數(shù),為每一個(gè)頂點(diǎn)生成唯一值赏表。
第二個(gè)參數(shù)vertices
是一個(gè)包含訂單數(shù)據(jù)的數(shù)組检诗,使用AAPLVertex
先前定義的結(jié)構(gòu)匈仗。
要將位置轉(zhuǎn)換為Metal的坐標(biāo),該函數(shù)需要繪制三角形的視口大蟹昊拧(以像素為單位)悠轩,因此將其存儲(chǔ)在viewportSizePointer
參數(shù)中。
第二個(gè)和第三個(gè)參數(shù)具有[buffer(n)]
屬性限定符攻泼,默認(rèn)情況下火架,Metal會(huì)自動(dòng)為參數(shù)表分配每個(gè)參數(shù)的插槽。將[[buffer(n)]]
限定符添加到緩沖區(qū)參數(shù)時(shí)忙菠,可以明確告知Metal使用哪個(gè)插槽何鸡。明確聲明插槽可以更輕松地修改著色器,而無(wú)需更改應(yīng)用程序代碼只搁。在共享頭文件中聲明兩個(gè)指標(biāo)的常量音比。
函數(shù)的輸出是一個(gè)RasterizerData
結(jié)構(gòu)。
寫(xiě)入頂點(diǎn)函數(shù)
您的頂點(diǎn)函數(shù)必須生成輸出結(jié)構(gòu)的兩個(gè)字段氢惋。使用vertexID
參數(shù)索引到數(shù)組并讀取頂點(diǎn)的輸入數(shù)據(jù)洞翩。另外,檢索視口尺寸焰望。
float2 pixelSpacePosition = vertices[vertexID].position.xy;
vector_float2 viewportSize = vector_float2(*viewportSizePointer);
頂點(diǎn)函數(shù)必須在clip-space
坐標(biāo)空間中提供位置數(shù)據(jù)骚亿,這是使用四維同質(zhì)矢量(x, y, z, w)指定的3D點(diǎn)。光柵化階段采用輸出位置并將x熊赖,y和z坐標(biāo)除以w来屠,以在標(biāo)準(zhǔn)化設(shè)備坐標(biāo)中生成3D點(diǎn)。標(biāo)準(zhǔn)化設(shè)備坐標(biāo)與視口大小無(wú)關(guān)震鹉。
標(biāo)準(zhǔn)化設(shè)備坐標(biāo)使用左手坐標(biāo)系并映射到視口中的位置俱笛。基元被剪切到此坐標(biāo)系中的框传趾,然后進(jìn)行柵格化迎膜。剪切框的左下角位于(x,y)
坐標(biāo)的(-1浆兰,-1.0)
磕仅,右上角位于(1.0, 1.0)
。正z
值指向遠(yuǎn)離相機(jī)(進(jìn)入屏幕)簸呈。坐標(biāo)的可見(jiàn)部分位于(近剪輯平面)和(遠(yuǎn)剪輯平面)之間榕订。
圖2 標(biāo)準(zhǔn)化設(shè)備坐標(biāo)系
[圖片上傳失敗...(image-f39984-1565142312808)]
您的頂點(diǎn)函數(shù)需要將輸入坐標(biāo)系轉(zhuǎn)化為此輸出坐標(biāo)系。
因?yàn)檫@是一個(gè)2D應(yīng)用程序而不需要同質(zhì)坐標(biāo)蜕便,所以首先默認(rèn)值寫(xiě)入輸出坐標(biāo)劫恒,其w
值設(shè)置為1.0
,其他坐標(biāo)設(shè)置為0.0
玩裙。這意味著坐標(biāo)已在標(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類(lèi)型執(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;
寫(xiě)一個(gè)片段函數(shù)
一個(gè)片段是一個(gè)可能改變的渲染目標(biāo)喧务。光柵化器確定渲染目標(biāo)的哪些像素被基元覆蓋。僅渲染像素中心在三角形內(nèi)部的片段枉圃。
圖3 光柵化階段生成的碎片
片段函數(shù)處理來(lái)自光柵化器的輸入信息功茴,用于單個(gè)位置,并計(jì)算每個(gè)渲染目標(biāo)的輸出值孽亲。這些片段由管道中的后續(xù)階段處理坎穿,最終寫(xiě)入渲染目標(biāo)。
注意
片段被稱為可能更改的原因是因?yàn)槠坞A段之后的管道階段可以配置為拒絕某些片段或更改寫(xiě)入渲染目標(biāo)的內(nèi)容。在此示例中,片段階段計(jì)算的所有值都按原樣寫(xiě)入渲染目標(biāo)摩瞎。
此示例中的片段著色器接收與頂點(diǎn)著色器輸出中聲明的相同參數(shù)。使用fragment
關(guān)鍵字聲明片段函數(shù)孵延。他需要一個(gè)參數(shù),與頂點(diǎn)階段提供的結(jié)構(gòu)RasterizerData
相同亲配。添加[[stage_in]]
限定符以指示此參數(shù)是由光柵化器生成尘应。
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
如果您的片段函數(shù)寫(xiě)入多個(gè)渲染目標(biāo),則它必須為每個(gè)渲染目標(biāo)聲明一個(gè)包含字段的結(jié)構(gòu)吼虎。由于此示例僅具有單個(gè)渲染目標(biāo)犬钢,因此您可以直接將浮點(diǎn)矢量指定為函數(shù)的輸出。此輸出的是要渲染目標(biāo)的顏色思灰。
光柵化階段計(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ù)已經(jīng)完成拳亿,您可以創(chuàng)建使用他們的渲染管道。首先愿伴,獲取默認(rèn)庫(kù)并獲取每個(gè)函數(shù)的MTLFunction對(duì)象肺魁。
let defaultLibrary = device?.makeDefaultLibrary()
let vertexFunction = defaultLibrary?.makeFunction(name: "vertexShader")
let framentFunction = defaultLibrary?.makeFunction(name: "fragmentShader")
接下里,創(chuàng)建一個(gè)MTLRenderPipelineState對(duì)象隔节,渲染管道有更多要配置的階段鹅经,因此您使用MTLRenderPipelineDescriptor來(lái)配置管道寂呛。
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.label = "Simple Pipeline"
pipelineStateDescriptor.vertexFunction = vertexFunction
pipelineStateDescriptor.fragmentFunction = framentFunction
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat
do {
try pipelineState = device?.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
} catch {
assert(false, "Failed to created pipline state: \(error.localizedDescription)")
}
除了指定頂點(diǎn)和片段函數(shù)之外,還要聲明管道將繪制的所有渲染目標(biāo)的像素格式瘾晃。像素格式(MTLPixelFormat)定義像素?cái)?shù)據(jù)的存儲(chǔ)器布局贷痪。對(duì)于簡(jiǎn)單格式,此定義包括每個(gè)像素的字節(jié)數(shù)蹦误,存儲(chǔ)在像素種的數(shù)據(jù)通道以及這些通道的位布局劫拢。此示例只有一個(gè)渲染目標(biāo)并且由視圖提供,因此將視圖的像素格式復(fù)制到渲染管道描述符中强胰。渲染管道狀態(tài)必須使用與渲染過(guò)程指定的像素格式舱沧,因此他們始終相同。
當(dāng)Metal創(chuàng)建渲染管道狀態(tài)對(duì)象偶洋,管道配置為將片段函數(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è)部分杏节。
renderEncoder?.setViewport(MTLViewport(originX: 0.0,
originY: 0.0,
width: Double(viewportSize.x),
height: Double(viewportSize.y),
znear: 0.0,
zfar: 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ù)狸眼,因此沒(méi)有要設(shè)置的參數(shù)藤树。
renderEncoder?.setVertexBytes(triangleVertices,
length: MemoryLayout<AAPLVertex>.size * triangleVertices.count,
index: Int(AAPLVertexInputIndexVertices.rawValue))
renderEncoder?.setVertexBytes(&viewportSize,
length: MemoryLayout<vector_uint2>.size,
index: Int(AAPLVertexInputIndexViewportSize.rawValue))
編碼繪圖命令
指定基元的類(lèi)型,起始索引和頂點(diǎn)數(shù)拓萌。渲染三角形時(shí)岁钓,將vertexID
參數(shù)值為0、1和2來(lái)調(diào)用頂點(diǎn)函數(shù)。
使用顏色插值進(jìn)行實(shí)驗(yàn)
在此示例中屡限,顏色值在三角形中進(jìn)行插值品嚣。這通常是你想要的,但有時(shí)你想要一個(gè)頂點(diǎn)生成一個(gè)值钧大,并在整個(gè)基元上保持不變翰撑。在頂點(diǎn)函數(shù)的輸出上指定flat
屬性限定符以執(zhí)行此操作。現(xiàn)在試試吧拓型。在示例項(xiàng)目中找到RasterizerData
的定義额嘿,并將[[flat]]
限定符添加到其顏色字段中。
float4 color [[flat]];
再次運(yùn)行該示例劣挫。 渲染管道在三角形上均勻地使用第一個(gè)頂點(diǎn)(稱為激發(fā)頂點(diǎn))的顏色值册养,并忽略其他兩個(gè)頂點(diǎn)的顏色。 您可以使用平面著色和插值的混合压固,只需在頂點(diǎn)函數(shù)的輸出上添加或省略平坦限定符即可球拦。 “金屬著色語(yǔ)言”規(guī)范定義了您還可以用來(lái)修改光柵化行為的其他屬性限定符。