[MetalKit]Using MetalKit part 3使用MetalKit3

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

MetalKit系統(tǒng)文章目錄


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

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

func render() {
    device = MTLCreateSystemDefaultDevice()
    createBuffer()
    registerShaders()
    sendToGPU()
}

我們先對createBuffer()函數做一些改變.回憶上一節(jié)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到底是什么樣的數據類型.從蘋果官方文檔中我們發(fā)現,這種向量類型是一種clang基礎類型,比傳統(tǒng)的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:[])
}

你看,通過簡單地將它轉成結構體數組,我們可以輕易創(chuàng)建頂點數據.

同時,我們保持頂點位置仍在上次的位置上,并且我們?yōu)槊總€頂點添加單獨的顏色(紅,綠,藍).接下來,是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修飾符來聲明,這些修飾符指定了函數變量或參數分配到的內存區(qū)域.[[...]]語法是用來聲明屬性,例如資源位置,著色器輸入,以及在著色器與CPU之間來回傳遞的內置變量.

Metal使用[[ buffer(index) ]]屬性來標識出位置,讓deviceconstant buffer的參數類型能夠區(qū)分.內置的輸入變量和輸出變量被用來在圖形函數(頂點和片段)與固定圖形管線流程之間傳遞數據.在我們例子中[[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 已發(fā)布在Github上.
下次見!

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

推薦閱讀更多精彩內容