Metal框架詳細解析(三) —— 渲染簡單的2D三角形(一)

版本記錄

版本號 時間
V1.0 2018.10.05 星期五

前言

很多做視頻和圖像的,相信對這個框架都不是很陌生,它渲染高級3D圖形,并使用GPU執行數據并行計算。接下來的幾篇我們就詳細的解析這個框架。感興趣的看下面幾篇文章。
1. Metal框架詳細解析(一)—— 基本概覽
2. Metal框架詳細解析(二) —— 器件和命令(一)

Overview

在上一篇中,您學習了如何編寫使用Metal的應用程序并向GPU發出基本渲染命令。

在本示例中,您將學習如何在Metal中渲染基本幾何體。 特別是,您將學習如何使用頂點數據和SIMD類型,配置圖形渲染管道,編寫GPU函數以及發出繪制調用。


The Metal Graphics Rendering Pipeline - Metal圖形渲染管道

Metal圖形渲染管道由多個圖形處理單元(GPU)階段組成,一些是可編程的,一些是固定的,用于執行繪圖命令。 Metal將管道的輸入,過程和輸出定義為應用于某些數據的一組渲染命令。 在最基本的形式中,管道接收頂點作為輸入并將像素渲染為輸出。 此示例主要關注管道的三個主要階段:頂點函數,光柵化階段和片段函數。 頂點函數和片段函數是可編程階段。 光柵化階段是固定的。

MTLRenderPipelineState對象表示圖形渲染管道。 可以使用MTLRenderPipelineDescriptor對象配置此管道的許多階段,該對象定義了Metal處理輸入頂點到渲染輸出像素的大部分方式。


Vertex Data - 頂點數據

頂點只是兩個或多個線相交的空間中的一個點。 通常,頂點表示為定義特定幾何的笛卡爾坐標的集合,以及與每個坐標相關聯的可選數據。

此示例呈現由三個頂點組成的簡單2D三角形,每個頂點包含三角形角的位置和顏色。

Position是必需的頂點屬性,而color是可選的。 對于此示例,管道使用兩個頂點屬性將彩色三角形渲染到drawable的特定區域。


Use SIMD Data Types - 使用SIMD數據類型

頂點數據通常從包含從專用建模軟件導出的3D模型數據的文件加載。詳細模型可能包含數千個具有許多屬性的頂點,但最終它們都以某種形式的數組陣列結束,這些陣列經過特殊打包,編碼并發送到GPU。

示例的三角形為其三個頂點中的每一個定義了2D位置(x,y)和RGBA顏色(紅色,綠色,藍色,alpha)。這種相對少量的數據被直接硬編碼到結構數組中,其中數組的每個元素代表單個頂點。用作數組元素的數據類型的結構定義了每個頂點的內存布局。

頂點數據和一般的3D圖形數據通常用矢量數據類型定義,簡化了常見的圖形算法和GPU處理。此示例使用SIMD庫提供的優化矢量數據類型來表示三角形的頂點。 SIMD庫獨立于MetalMetalKit,但強烈建議用于開發Metal應用程序,主要是因為它的便利性和性能優勢。

三角形的2D位置組件由vector_float2 SIMD數據類型聯合表示,該類型包含兩個32位浮點值。類似地,三角形的RGBA顏色分量用vector_float4 SIMD數據類型聯合表示,該數據類型包含四個32位浮點值。然后將這兩個屬性組合成單個AAPLVertex結構。

typedef struct
{
    // Positions in pixel space
    // (e.g. a value of 100 indicates 100 pixels from the center)
    vector_float2 position;

    // Floating-point RGBA colors
    vector_float4 color;
} AAPLVertex;

三角形的三個頂點直接硬編碼為AAPLVertex元素數組,從而定義每個頂點的精確屬性值。

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 } },
};

Set a Viewport - 設置視口

視口指定Metal渲染內容的drawable區域。 視口是具有x和y偏移,寬度和高度以及近和遠平面的3D區域(盡管這里不需要這兩個,因為此示例僅渲染2D內容)。

為管道分配自定義視口需要通過調用setViewport:方法將MTLViewport結構編碼為渲染命令編碼器。 如果未指定視口,Metal會設置一個默認視口,其大小與用于創建渲染命令編碼器的drawable相同。


Write a Vertex Function - 寫一個頂點函數

頂點函數(也稱為頂點著色器vertex shader)的主要任務是處理傳入的頂點數據并將每個頂點映射到視口中的位置。 這樣,管道中的后續階段可以引用此視口位置并將像素渲染到drawable中的精確位置。 頂點函數通過將任意頂點坐標轉換為標準化設備坐標(也稱為剪輯空間坐標clip-space coordinates)來完成此任務。

剪輯空間Clip space是一個2D坐標系,它將視口區域沿x軸和y軸映射到[-1.0,1.0]范圍。 視口的左下角映射到(-1.0,-1.0),右上角映射到(1.0,1.0),中心映射到(0.0,0.0)。

頂點函數對于繪制的每個頂點執行一次。 在此示例中,對于每個幀,繪制三個頂點以構成三角形。 因此,頂點函數每幀執行三次。

頂點函數是用Metal著色語言Metal shading language編寫的,它基于C ++ 14。Metal著色語言代碼可能看起來類似于傳統的C / C ++代碼,但兩者根本不同。 傳統的C / C ++代碼通常在CPU上執行,而Metal著色語言代碼專門在GPU上執行。 GPU提供了更大的處理帶寬,并且可以在大量頂點和片段上并行工作。 但是,它具有比CPU少的內存,不能有效地處理控制流操作,并且通常具有更高的延遲。

此示例中的頂點函數稱為vertexShader,這是它的聲明。

vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
             constant AAPLVertex *vertices [[buffer(AAPLVertexInputIndexVertices)]],
             constant vector_uint2 *viewportSizePointer [[buffer(AAPLVertexInputIndexViewportSize)]])

1. Declare Vertex Function Parameters - 聲明頂點函數參數

第一個參數vertexID使用[[vertex_id]]屬性限定符并保存當前正在執行的頂點的索引。當繪制調用使用此頂點函數時,此值從0開始,并在每次調用vertexShader函數時遞增。使用[[vertex_id]]屬性限定符的參數通常用于索引包含頂點的數組。

第二個參數vertices是包含頂點的數組,每個頂點定義為AAPLVertex數據類型。指向此結構的指針定義了這些頂點的數組。

第三個也是最后一個參數viewportSizePointer包含視口的大小,并具有vector_uint2數據類型。

verticesviewportSizePointer參數都使用SIMD數據類型,這些類型是C和Metal著色語言代碼都能理解的類型。因此,示例可以在共享AAPLShaderTypes.h標頭中定義AAPLVertex結構,該結構包含在AAPLRenderer.mAAPLShaders.metal代碼中。因此,共享頭確保三角形頂點的數據類型在Objective-C聲明(triangleVertices)中與在Metal著色語言聲明(vertices)中相同。在Metal應用程序中使用SIMD數據類型可確保內存布局在CPU / GPU聲明中完全匹配,并有助于將頂點數據從CPU發送到GPU。

注意:對AAPLVertex結構的任何更改都會同等地影響AAPLRenderer.mAAPLShaders.metal代碼。

verticesviewportSizePointer參數都使用[[buffer(index)]]屬性限定符。 AAPLVertexInputIndexVerticesAAPLVertexInputIndexViewportSize的值是用于在AAPLRenderer.mAAPLShaders.metal代碼中標識和設置頂點函數輸入的索引。

2. Declare Vertex Function Return Values - 聲明頂點函數返回值

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 clipSpacePosition [[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;

頂點函數必須通過[[position]]屬性限定符為clipSpacePosition成員使用返回每個頂點的剪輯空間位置值。 聲明此屬性后,管道的下一個階段(柵格化rasterization)使用clipSpacePosition值來標識三角形角的位置,并確定要渲染的像素。

3. Process Vertex Data - 處理頂點數據

示例頂點函數的主體對輸入頂點做兩件事:

  • 1) 執行坐標系轉換,將生成的頂點剪輯空間位置寫入out.clipSpacePosition返回值。
  • 2) 將頂點顏色傳遞給out.color返回值。

要獲取輸入頂點,vertexID參數用于索引頂點數組。

float2 pixelSpacePosition = vertices[vertexID].position.xy;

此示例從每個vertices元素的position成員獲取2D頂點坐標,并將其轉換為寫入out.clipSpacePosition返回值的剪輯空間位置。 每個頂點輸入位置相對于從視口中心開始的x和y方向上的像素數定義。 因此,為了將這些像素空間位置轉換為剪輯空間位置,頂點函數除以視口大小的一半。

out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);

最后,頂點函數訪問每個vertices元素的color成員并將其傳遞給out.color返回值,而不執行任何修改。

out.color = vertices[vertexID].color;

RasterizerData返回值的內容現在已完成,結構將傳遞到管道中的下一個階段。


Rasterization - 光柵化

頂點函數執行三次后,對于每個三角形的頂點執行一次,管道中的下一個階段,即柵格化開始。

光柵化是管道光柵化器單元產生碎片的階段。 片段包含原始預像素數據,用于生成渲染到drawable的像素。 對于由頂點函數生成的每個完整三角形,光柵化器確定目標可繪制的哪些像素被三角形覆蓋。 它通過測試drawable中每個像素的中心是否在三角形內部來實現。 在下圖中,僅生成像素中心位于三角形內部的片段。 這些片段顯示為灰色方塊。

柵格化還確定發送到管道中下一個階段的值:片段函數。在管道的早期,頂點函數輸出RasterizerData結構的值,該結構包含剪輯空間位置(clipSpacePosition)和顏色(color)clipSpacePosition成員使用所需的[[position]]屬性限定符,指示這些值直接用于確定三角形的片段覆蓋區域。color成員沒有屬性限定符,表示應該在三角形的片段中插入這些值。

在將每個頂點值轉換為每個片段值之后,光柵化器將color值傳遞給片段函數。此轉換使用固定插值函數,該函數計算從三角形的三個頂點的color值派生的單個加權顏色。插值函數的權重(也稱為重心坐標barycentric coordinates)是每個頂點位置與片段中心的相對距離。例如:

  • 如果片段正好位于三角形的中間,與每個三角形的三個頂點等距,則每個頂點的顏色加權1/3。 在下圖中,這顯示為三角形中心的灰色片段(0.33,0.33,0.33)

  • 如果一個片段非常靠近一個頂點并且距離另外兩個非常遠,則將近頂點的顏色加權為1,將遠點的顏色加權為0。在下圖中,這顯示為偏紅色 片段(0.5,0.25,0.25)靠近三角形的右下角。

  • 如果片段位于三角形的邊緣,在三個頂點中的兩個頂點的中間,則每個邊緣定義頂點的顏色加權1/2,非邊緣頂點的顏色加權0。在下圖中, 這顯示為三角形左邊緣的青色片段(0.0,0.5,0.5)

由于光柵化是固定的管道階段,因此無法通過自定義Metal著色語言代碼修改其行為。 在光柵化器創建片段及其關聯值之后,結果將傳遞到管道中的下一個階段。


Write a Fragment Function - 寫一個片段函數

片段函數(也稱為片段著色器fragment shader)的主要任務是處理傳入的片段數據并計算可繪制像素的顏色值。

此示例中的片段函數稱為fragmentShader,這是它的簽名。

fragment float4 fragmentShader(RasterizerData in [[stage_in]])

該函數有一個參數in,它使用由頂點函數返回的相同RasterizerData結構。 [[stage_in]]屬性限定符表示此參數來自光柵化器。 該函數返回一個四分量浮點向量,其中包含要呈現給drawable的最終RGBA顏色值。

此示例演示了一個非常簡單的片段函數,該函數返回光柵化器的插值color值,無需進一步處理。 每個片段將其插值color值渲染到三角形中的對應像素。

return in.color;

Obtain Function Libraries and Create a Pipeline - 獲取函數庫并創建管道

在構建示例時,Xcode會編譯AAPLShaders.metal文件以及Objective-C代碼。但是,Xcode無法在構建時鏈接vertexShaderfragmentShader函數;相反,應用程序需要在運行時顯式鏈接這些函數。

Metal著色語言代碼分兩個階段編譯:

  • 1) 前端編譯在構建時在Xcode中發生.metal文件從高級源代碼編譯為中間表示(IR)文件。

  • 2) 后端編譯在運行時在物理設備中進行。然后將IR文件編譯為低級機器代碼。

每個GPU系列都有不同的指令集。因此,Metal shading語言代碼只能在運行時由物理設備本身完全編譯為本機GPU代碼。前端編譯通過將IR存儲在打包在示例的.app包中的default.metallib文件中來減少一些編譯開銷。

default.metallib文件是Metal著色語言函數庫,由運行時通過調用newDefaultLibrary方法檢索的MTLLibrary對象表示。從該庫中,可以檢索由MTLFunction對象表示的特定函數。

// Load all the shader files with a .metal file extension in the project
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];

// Load the vertex function from the library
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];

// Load the fragment function from the library
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];

這些MTLFunction對象用于創建表示圖形渲染管道的MTLRenderPipelineState對象。調用MTLDevice對象的newRenderPipelineStateWithDescriptor:error:方法開始后端編譯過程,該過程鏈接vertexShaderfragmentShader函數,從而產生完全編譯的管道。

MTLRenderPipelineState對象包含由MTLRenderPipelineDescriptor對象配置的其他管道設置。除頂點和片段函數外,此示例還配置colorAttachments數組中第一個條目的pixelFormat值。此示例僅渲染到單個目標,即視圖的drawable(colorAttachments [0]),其像素格式由視圖本身(colorPixelFormat)配置。視圖的像素格式定義了每個像素的內存布局;在創建管道時,Metal必須能夠引用此布局,以便它可以正確呈現fragment函數生成的顏色值。

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];

Send Vertex Data to a Vertex Function - 將頂點數據發送到頂點函數

創建管道后,可以將其分配給渲染命令編碼器。 此操作將由該特定管道處理所有后續渲染命令。

[renderEncoder setRenderPipelineState:_pipelineState];

此示例使用setVertexBytes:length:atIndex:方法將頂點數據發送到頂點函數。如前所述,示例的vertexShader函數的簽名有兩個參數,verticesviewportSizePointer,它們使用[[buffer(index)]]屬性限定符。 setVertexBytes:length:atIndex:方法中index參數的值映射到[[buffer(index)]]屬性限定符中具有相同index值的參數。因此,調用setVertexBytes:length:atIndex:方法為特定的頂點函數參數設置特定的頂點數據。

AAPLVertexInputIndexVerticesAAPLVertexInputIndexViewportSize值在AAPLRenderer.mAAPLShaders.metal文件之間共享的AAPLShaderTypes.h標頭中定義。該示例將這些值用于setVertexBytes:length:atIndex:方法的index參數以及與同一頂點函數對應的[[buffer(index)]]屬性限定符。通過減少由于硬編碼整數(可能將錯誤的數據發送到錯誤的參數)導致的潛在索引不匹配,可以跨不同文件共享這些值,從而使示例更加健壯。

此示例將以下頂點數據發送到頂點函數:

  • 使用AAPLVertexInputIndexVertices索引值將triangleVertices指針發送到vertices參數
  • 使用AAPLVertexInputIndexViewportSize索引值將_viewportSize指針發送到viewportSizePointer參數
// You send a pointer to the `triangleVertices` array also and indicate its size
// The `AAPLVertexInputIndexVertices` enum value corresponds to the `vertexArray`
// argument in the `vertexShader` function because its buffer attribute also uses
// the `AAPLVertexInputIndexVertices` enum value for its index
[renderEncoder setVertexBytes:triangleVertices
                       length:sizeof(triangleVertices)
                      atIndex:AAPLVertexInputIndexVertices];

// You send a pointer to `_viewportSize` and also indicate its size
// The `AAPLVertexInputIndexViewportSize` enum value corresponds to the
// `viewportSizePointer` argument in the `vertexShader` function because its
//  buffer attribute also uses the `AAPLVertexInputIndexViewportSize` enum value
//  for its index
[renderEncoder setVertexBytes:&_viewportSize
                       length:sizeof(_viewportSize)
                      atIndex:AAPLVertexInputIndexViewportSize];

Draw the Triangle - 繪制三角形

設置管道及其關聯的頂點數據后,發出繪制調用會執行管道并繪制樣本的單個三角形。該示例將單個繪圖命令編碼到渲染命令編碼器(render command encoder)中。

三角形是Metal中的幾何圖元,需要繪制三個頂點。其他基元包括需要兩個頂點的線,或者只需要一個頂點的點。 drawPrimitives:vertexStart:vertexCount:方法允許您準確指定要繪制的基元類型以及要使用的從先前設置的頂點數據派生的頂點數據。為vertexStart參數設置0表示繪圖應以頂點數組中的第一個頂點開始。這意味著頂點函數的vertexID參數的第一個值(使用[[vertex_id]]屬性限定符)將為0。設置為vertexCount參數的3表示應繪制三個頂點,從而生成一個三角形。 (也就是說,對于vertexID參數,頂點函數執行三次,值為0,1和2)。

// Draw the 3 vertices of our triangle
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
                  vertexStart:0
                  vertexCount:3];

此調用是對單個三角形的渲染命令進行編碼所需的最后一次調用。 繪圖完成后,渲染循環可以結束編碼,提交命令緩沖區,并呈現包含渲染三角形的drawable

后記

本篇主要講述了您學習如何在Metal中渲染基本幾何體,感興趣的給個贊或者關注~~~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容