[MetalKit]4-Using-MetalKit-part-3使用MetalKit3

蘋果API搬運工發表於2017-12-14

本系列文章是對 metalkit.org 上面MetalKit內容的全面翻譯和學習.

MetalKit系統文章目錄


上一節我說我們將學習Metal shading language.在學之前,我們先做一些程式碼清理和重構.從下載前一節的原始碼 source code開始.我們將從重構render() 函式開始.所以讓我們取出vertex bufferrender pipeline state,並建立3個新的函式放進去,這樣我們的舊函式就減少到這樣:

var vertex_buffer: MTLBuffer!
var rps: MTLRenderPipelineState! = nil

func render() {
    device = MTLCreateSystemDefaultDevice()
    createBuffer()
    registerShaders()
    sendToGPU()
}
複製程式碼

我們先對createBuffer() 函式做一些改變.回憶上一節vertex dataFloat型別的陣列,像這樣:

let vertex_data:[Float] = [-1.0, -1.0, 0.0, 1.0,
                            1.0, -1.0, 0.0, 1.0,
                            0.0,  1.0, 0.0, 1.0]
複製程式碼

讓我們把它轉換成更好的格式,一個帶有兩個vector_float4型別成員的結構體,一個position另一個是color:

struct Vertex {
    var position: vector_float4
    var color: vector_float4
}
複製程式碼

你可能會好奇vector_float4到底是什麼樣的資料型別.從蘋果官方文件中我們發現,這種向量型別是一種clang基礎型別,比傳統的SIMD型別更適合向量-向量向量-標量的算術運算.它可以通過類似陣列下標來訪問向量的成員分量,具體作法是用 . 操作符和元件名稱訪問(x,y,z,w,或它們的組合).除了 .xyzw元件名外,下面的子向量也能通過:.lo / .hi(向量的前半部分和後半部分)來輕鬆訪問,還有奇偶位的 .even / .odd子向量:

vector_float4 x = 1.0f;         // x = { 1, 1, 1, 1 }.

vector_float3 y = { 1, 2, 3 };  // y = { 1, 2, 3 }.

x.xyz = y.zyx;                  // x = { 1/3, 1/2, 1, 1 }.

x.w = 0;                        // x = { 1/4, 1/3, 1/2, 0 }.
複製程式碼

讓我們返回到createBuffer()用新的結構體來替換vertex-data:

func createBuffer() {
    let vertex_data = [Vertex(position: [-1.0, -1.0, 0.0, 1.0], color: [1, 0, 0, 1]),
                       Vertex(position: [ 1.0, -1.0, 0.0, 1.0], color: [0, 1, 0, 1]),
                       Vertex(position: [ 0.0,  1.0, 0.0, 1.0], color: [0, 0, 1, 1])]
    vertex_buffer = device!.newBufferWithBytes(vertex_data, length: sizeof(Vertex) * 3, options:[])
}
複製程式碼

你看,通過簡單地將它轉成結構體陣列,我們可以輕易建立頂點資料.

同時,我們保持頂點位置仍在上次的位置上,並且我們為每個頂點新增單獨的顏色(紅,綠,藍).接下來,是registerShaders() 函式.我們無需改變舊程式碼,只需要將它移動到新的地方:

func registerShaders() {
    let library = device!.newDefaultLibrary()!
    let vertex_func = library.newFunctionWithName("vertex_func")
    let frag_func = library.newFunctionWithName("fragment_func")
    let rpld = MTLRenderPipelineDescriptor()
    rpld.vertexFunction = vertex_func
    rpld.fragmentFunction = frag_func
    rpld.colorAttachments[0].pixelFormat = .BGRA8Unorm
    do {
        try rps = device!.newRenderPipelineStateWithDescriptor(rpld)
    } catch let error {
        self.print("\(error)")
    }
}
複製程式碼

最後,我們對sendToGPU() 函式也做同樣的操作,不改變舊程式碼只移動到新地方:

func sendToGPU() {
    if let rpd = currentRenderPassDescriptor, drawable = currentDrawable {
        rpd.colorAttachments[0].clearColor = MTLClearColorMake(0.5, 0.5, 0.5, 1.0)
        let command_buffer = device!.newCommandQueue().commandBuffer()
        let command_encoder = command_buffer.renderCommandEncoderWithDescriptor(rpd)
        command_encoder.setRenderPipelineState(rps)
        command_encoder.setVertexBuffer(vertex_buffer, offset: 0, atIndex: 0)
        command_encoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
        command_encoder.endEncoding()
        command_buffer.presentDrawable(drawable)
        command_buffer.commit()
    }
}
複製程式碼

接下來讓我們轉移到Shaders.metal檔案.這時我們做兩處修改.首先,給我們的Vertex結構體新增一個color成員,這樣我們就可以在CPUGPU之間來回傳遞資料:

struct Vertex {
    float4 position [[position]];
    float4 color; 
};
複製程式碼

其次,我們替換上次在fragment著色器中使用的硬編碼的顏色:

fragment float4 fragment_func(Vertex vert [[stage_in]]) {
    return float4(0.7, 1, 1, 1);
}
複製程式碼

替換為每個頂點自帶的實際顏色(通過vertex_buffer傳遞到GPU):

fragment float4 fragment_func(Vertex vert [[stage_in]]) {
    return vert.color;
}
複製程式碼

如果你執行程式,你看到一個更漂亮的彩色三角形:

chapter04.png

你也許會奇怪,為什麼我們只傳遞給三個頂點對應顏色,但頂點之間的顏色卻是漸變的?要理解這些,就必須先理解兩種著色器的不同及它們在圖形管線中角色的不同.讓我們看看任一個著色器的語法(這裡先頂點著色器作例子):

vertex Vertex vertex_func(constant Vertex *vertices [[buffer(0)]], uint vid [[vertex_id]])
複製程式碼

第一個關鍵詞,是函式限定符只能使用vertex, fragmentkernel.下一個關鍵詞是返回值型別.接下來是帶有圓括號引數的函式name名稱.Metal shading language限定了指標的使用,必須用device,threadgroupconstant修飾符來宣告,這些修飾符指定了函式變數或引數分配到的記憶體區域.[[...]] 語法是用來宣告屬性,例如資源位置,著色器輸入,以及在著色器與CPU之間來回傳遞的內建變數.

Metal使用 [[ buffer(index)]] 屬性來標識出位置,讓deviceconstant buffer的引數型別能夠區分.內建的輸入變數和輸出變數被用來在圖形函式(頂點和片段)與固定圖形管線流程之間傳遞資料.在我們例子中 [[vertex_id]] 是傳遞過程中每個頂點的識別符號.Metal接收頂點函式和光柵產生的片段的輸出,來產生輸入到片段函式的各個片段.每個片段輸入依靠 [[stage_in]] 屬性修飾符來標識.

vertex shader用指向頂點列表的指標作為第一個引數.我們可以用第2個引數vid來索引vertices頂點,其中的vid被賦值成vertex_id,它告訴Metal插入當前正在被處理的頂點的索引作為第2個引數.然後只需傳遞每個頂點(包括位置和顏色)給fragment shader片段著色器去處理.fragment shader片段著色器所作的操作是,取出從vertex shader頂點著色器中傳過來的頂點,直接傳給每個畫素而無需改變輸入資料.頂點著色器執行頻率不高(本例中只需3次-每個頂點1次),但fragment shader片段著色器執行幾千次-每個需要繪製的畫素一次.

所以你可能仍然會問:"ok,但是顏色漸變到底怎麼回事呢?" 現在你理解了每個著色器的作用及執行頻率,你可以認為任一個畫素點的顏色都是它的附近畫素顏色的平均值.例如,在red紅green綠顏色畫素正中間的畫素顏色將會是yellow黃,只是因為fragment shader片段著色器用平均數來產生顏色插值:0.5 * red + 0.5 * green.同樣的,在red紅blue藍正中間的顏色會是magenta品紅,在blue藍green綠正中間的顏色會是cyan青.就這樣,剩餘部分畫素都是用初始顏色的插值,最終結果就是你看到的漸變範圍.

原始碼source code 已釋出在Github上.

下次見!

相關文章