渲染一個簡單的二維三角形
概觀
在使用Metal繪制到屏幕時,您學習了如何設置MTKView
對象并使用渲染過程更改視圖的內容。該示例只是將視圖的內容刪除為背景顏色。此示例顯示如何配置渲染管道并將其用作渲染過程的一部分,以將簡單的2D彩色三角形繪制到視圖中。該示例為每個頂點提供位置和顏色,渲染管道使用該數據渲染三角形,在為三角形頂點指定的顏色之間插入顏色。
注意
Xcode項目包含在macOS,iOS和tvOS上運行示例的方案。iOS或者tvOS模擬器不支持Metal,因此iOS和tvOS方案需要物理設備來運行示例。默認方案是macOS。
了解Metal Render Pipeline
一個渲染管線流程繪圖命令和數據寫入到一個渲染通道的目標。渲染管道有許多階段,一些使用著著色器編程,另一些使用固定或可配置的行為編程。此示例主要關注管道的三個階段:頂點階段、光柵化階段和片段階段。頂點階段和片段階段是可編程的,因此您可以使用MSL為他們編寫函數。光柵化階段具有固定的行為。
圖1 Metal圖形化渲染管道的主要階段
渲染從繪圖命令開始,該命令包括頂點的計數和要渲染的基元類型。例如,以下是此示例中的繪圖命令:
renderEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
頂點階段為每個頂點提供數據。當處理了足夠的頂點時,渲染管道柵格化基元,確定渲染目標中的哪些像素基于基元的邊界內。片段階段確定要寫入這些元素的渲染目標的值。
在本示例的其余部分,您將看到如何編寫頂點和片段函數,如何創建渲染管道狀態對象,以及最后如何編碼使用此管道的繪制命令。
決定自定義渲染管道如何處理數據
頂點函數為單個頂點生成數據,片段函數生成單個片段的數據,但您可以決定它們的工作方式。您可以考慮目標來配置管道的各個階段,這意味著您知道管道要生成什么以及如何生成這些結果。
確定要傳遞到渲染管道的數據以及將哪些數據傳遞到管道的后續階段。通常有三個地方可以執行此操作:
- 管道的輸入,由您的應用程序提供并傳遞到頂點。
- 頂點階段的輸出,傳遞光柵化階段。
- 片段階段的輸入,由您的應用提供或光柵化階段生成。
在此示例中,管道的輸入數據是頂點的位置及顏色。為了演示通常在頂點函數中執行的變換類型,輸入坐標在自定義坐標空間中定義,以視圖中心的像素為單位進行測量。
聲明一個AAPLVertex
結構,使用SIMD矢量類型來保存位置和顏色數據。要共享結構在內存中的布局方式的單個定義,請在通用頭文件中聲明結構,并將其導入著色器(Metal shader)
和app中。
typedef struct
{
vector_float2 position;
vector_float4 color;
} AAPLVertex;
SIMD類型在MSL中很常見,您也應該在應用程序中使用simd
庫。SIMD類型包含特定數據類型的多個通道,因此將位置聲明為vector_float2
包含兩個32位浮點數(他將保存x和y坐標)。顏色需要使用vector_float4
,因為它們有4個通道R、G、B、A。
在應用程序中,使用常亮數組輸入指定數據:
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])]
頂點階段為頂點生成數據,因此需要提供顏色和變換位置。使用SIMD類型聲明RasterizerData
包含位置和顏色值的結構。
typedef struct
{
float4 position [[position]];
float4 color;
} RasterizerData;
輸出位置(下面詳細描述)必須定義為vector_float4
,顏色聲明為輸入數據結構的顏色。
您需要告訴Metal光柵化數據中哪個字段提供位置數據,因為Metal不對結構中的字段強制執行任何特定的命名約定。position
使用[[position]]
屬性限定符注釋該字段以聲明此字段包含輸出位置。
片段函數只是將光柵化階段的數據傳遞給后期階段,因=因此它不需要任何其他參數。
聲明頂點函數
聲明頂點函數,包括其輸入參數及輸出數據。就像使用kernel
關鍵字聲明計算函數一樣,您使用vertex
關鍵字聲明頂點函數。
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
constant AAPLVertex *vertices [[buffer(AAPLVertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(AAPLVertexInputIndexViewportSize)]])
第一個參數是vertexID
,使用[[vertex_id]]
屬性限定符,他還是一個Metal關鍵字。執行渲染命令時,GPU會多次調用頂點函數,為每一個頂點生成唯一值。
第二個參數vertices
是一個包含訂單數據的數組,使用AAPLVertex
先前定義的結構。
要將位置轉換為Metal的坐標,該函數需要繪制三角形的視口大小(以像素為單位),因此將其存儲在viewportSizePointer
參數中。
第二個和第三個參數具有[buffer(n)]
屬性限定符,默認情況下,Metal會自動為參數表分配每個參數的插槽。將[[buffer(n)]]
限定符添加到緩沖區參數時,可以明確告知Metal使用哪個插槽。明確聲明插槽可以更輕松地修改著色器,而無需更改應用程序代碼。在共享頭文件中聲明兩個指標的常量。
函數的輸出是一個RasterizerData
結構。
寫入頂點函數
您的頂點函數必須生成輸出結構的兩個字段。使用vertexID
參數索引到數組并讀取頂點的輸入數據。另外,檢索視口尺寸。
float2 pixelSpacePosition = vertices[vertexID].position.xy;
vector_float2 viewportSize = vector_float2(*viewportSizePointer);
頂點函數必須在clip-space
坐標空間中提供位置數據,這是使用四維同質矢量(x, y, z, w)指定的3D點。光柵化階段采用輸出位置并將x,y和z坐標除以w,以在標準化設備坐標中生成3D點。標準化設備坐標與視口大小無關。
標準化設備坐標使用左手坐標系并映射到視口中的位置。基元被剪切到此坐標系中的框,然后進行柵格化。剪切框的左下角位于(x,y)
坐標的(-1,-1.0)
,右上角位于(1.0, 1.0)
。正z
值指向遠離相機(進入屏幕)。坐標的可見部分位于(近剪輯平面)和(遠剪輯平面)之間。
圖2 標準化設備坐標系
[圖片上傳失敗...(image-f39984-1565142312808)]
您的頂點函數需要將輸入坐標系轉化為此輸出坐標系。
因為這是一個2D應用程序而不需要同質坐標,所以首先默認值寫入輸出坐標,其w
值設置為1.0
,其他坐標設置為0.0
。這意味著坐標已在標準化設備坐標空間中,并且頂點函數應該在該坐標空間中生成(x, y)
坐標。將輸入位置除以視口大小一半以生成標準化設備坐標。由于使用SIMD類型執行該計算,因此可以使用單行代碼同時劃分兩個通道。執行除法并將結果放在輸出位置的x
和y
通道中。
out.position = vector_float4(0.0, 0.0, 0.0, 1.0);
out.position.xy = pixelSpacePosition / (viewportSize / 2.0);
最后將顏色值復制到返回值的out.color
中。
out.color = vertices[vertexID].color;
寫一個片段函數
一個片段是一個可能改變的渲染目標。光柵化器確定渲染目標的哪些像素被基元覆蓋。僅渲染像素中心在三角形內部的片段。
圖3 光柵化階段生成的碎片
片段函數處理來自光柵化器的輸入信息,用于單個位置,并計算每個渲染目標的輸出值。這些片段由管道中的后續階段處理,最終寫入渲染目標。
注意
片段被稱為可能更改的原因是因為片段階段之后的管道階段可以配置為拒絕某些片段或更改寫入渲染目標的內容。在此示例中,片段階段計算的所有值都按原樣寫入渲染目標。
此示例中的片段著色器接收與頂點著色器輸出中聲明的相同參數。使用fragment
關鍵字聲明片段函數。他需要一個參數,與頂點階段提供的結構RasterizerData
相同。添加[[stage_in]]
限定符以指示此參數是由光柵化器生成。
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
如果您的片段函數寫入多個渲染目標,則它必須為每個渲染目標聲明一個包含字段的結構。由于此示例僅具有單個渲染目標,因此您可以直接將浮點矢量指定為函數的輸出。此輸出的是要渲染目標的顏色。
光柵化階段計算每個片段參數的值,并使他們調用片段函數。光柵化階段將其顏色參數計算為三角形頂點處顏色的混合。片段離頂點越近,頂點對最終顏色的貢獻越大。
圖4 插值片段顏色
將插值顏色作為函數的輸出返回
return in.color;
創建渲染管道狀態對象
現在函數已經完成,您可以創建使用他們的渲染管道。首先,獲取默認庫并獲取每個函數的MTLFunction對象。
let defaultLibrary = device?.makeDefaultLibrary()
let vertexFunction = defaultLibrary?.makeFunction(name: "vertexShader")
let framentFunction = defaultLibrary?.makeFunction(name: "fragmentShader")
接下里,創建一個MTLRenderPipelineState對象,渲染管道有更多要配置的階段,因此您使用MTLRenderPipelineDescriptor來配置管道。
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)")
}
除了指定頂點和片段函數之外,還要聲明管道將繪制的所有渲染目標的像素格式。像素格式(MTLPixelFormat)定義像素數據的存儲器布局。對于簡單格式,此定義包括每個像素的字節數,存儲在像素種的數據通道以及這些通道的位布局。此示例只有一個渲染目標并且由視圖提供,因此將視圖的像素格式復制到渲染管道描述符中。渲染管道狀態必須使用與渲染過程指定的像素格式,因此他們始終相同。
當Metal創建渲染管道狀態對象,管道配置為將片段函數的輸出轉換為渲染目標的像素格式。如果要定位不同的像素格式,則需要創建不同的管道狀態對象。您可以在針對不同像素格式的多個管道中重復使用相同的著色器。
設置視口
現在您已擁有管道的渲染管道狀態對象,您將渲染三角形。您可以使用渲染命令編碼器執行此操作。首先,設置視口,以便Metal知道要繪制渲染目標的哪個部分。
renderEncoder?.setViewport(MTLViewport(originX: 0.0,
originY: 0.0,
width: Double(viewportSize.x),
height: Double(viewportSize.y),
znear: 0.0,
zfar: 1.0))
設置渲染管道狀態
設置要使用的管道的渲染管道狀態。
renderEncoder?.setRenderPipelineState(pipelineState)
將參數數據發送到頂點函數
通常,您使用buffers
(MTLBuffer)將數據傳遞給著色器。但是,當您需要將少量數據傳遞給頂點函數時(如此處所示),將數據直接復制到命令緩沖區中。
該示例將兩個參數的數據復制到命令緩沖區中。頂點數據從樣本中定義的數組中復制。視口數據是從用于設置視口的相同變量中復制。
在此示例中,片段函數僅使用光柵化器接受的數據,因此沒有要設置的參數。
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))
編碼繪圖命令
指定基元的類型,起始索引和頂點數。渲染三角形時,將vertexID
參數值為0、1和2來調用頂點函數。
使用顏色插值進行實驗
在此示例中,顏色值在三角形中進行插值。這通常是你想要的,但有時你想要一個頂點生成一個值,并在整個基元上保持不變。在頂點函數的輸出上指定flat
屬性限定符以執行此操作。現在試試吧。在示例項目中找到RasterizerData
的定義,并將[[flat]]
限定符添加到其顏色字段中。
float4 color [[flat]];
再次運行該示例。 渲染管道在三角形上均勻地使用第一個頂點(稱為激發頂點)的顏色值,并忽略其他兩個頂點的顏色。 您可以使用平面著色和插值的混合,只需在頂點函數的輸出上添加或省略平坦限定符即可。 “金屬著色語言”規范定義了您還可以用來修改光柵化行為的其他屬性限定符。