[MetalKit]15-Using-MetalKit-part-9使用MetalKit9

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

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

MetalKit系統文章目錄


我打賭很多讀者很相信MetalKit系列,所以今天我重回這個系列,我們將學習如何在Metal中繪製3D內容.讓我們繼續我們在playground中的工作,繼續本系列的第8部分 part 8.

在本章結束時,我們將渲染一個3D立方體,但是首先讓我們繪製一個2D矩形並複用這個矩形的邏輯來建議立方體的所有面.讓我們修改vertex_data陣列來儲存4個頂點而不是原來三角形的3個頂點:

let vertex_data = [
    Vertex(pos: [-1.0, -1.0, 0.0,  1.0], col: [1, 0, 0, 1]),
    Vertex(pos: [ 1.0, -1.0, 0.0,  1.0], col: [0, 1, 0, 1]),
    Vertex(pos: [ 1.0,  1.0, 0.0,  1.0], col: [0, 0, 1, 1]),
    Vertex(pos: [-1.0,  1.0, 0.0,  1.0], col: [1, 1, 1, 1])
]
複製程式碼

最有意思的部分來了.矩形和其它複雜幾何體都是由三角形組成,並且大部分頂點屬於2個或更多三角形,就不需要給這些頂點建立複本了,因為我們有一種辦法通過index buffer索引緩衝器來複用它們,這種方法可以從vertex buffer頂點緩衝器中儲存頂點索引到列表中,來追蹤那些將要用到的頂點的順序.所以讓我們建立這樣一份索引列表:

let index_data: [UInt16] = [
    0, 1, 2, 2, 3, 0
]
複製程式碼

為了理解這些索引是如何被儲存的,讓我們看下面這幅圖:

chapter09_1.jpg

對於前方的面(矩形)來說,我們用到的頂點儲存在vertex buffer頂點緩衝器03號位.稍後我們將新增另外4個頂點.前面是由兩個三角形構成.我們先用頂點0,12繪製一個三角形,然後用頂點2,30再繪製一個三角形.請注意,正如期待的那樣,有兩個頂點被重用了.還要注意的是繪製是以clockwise順時針完成的.這是Metal中預設的正面繞序,但是也能被設定為counterclockwise逆時針的.

然後我們需要建立index_buffer:

var index_buffer: MTLBuffer!
複製程式碼

下一步,我們需要在createBuffers()函式中把index_data賦值給index buffer:

index_buffer = device!.newBufferWithBytes(index_data, length: sizeof(UInt16) * index_data.count , options: [])
複製程式碼

最後,在drawRect(:)函式中我們需要將drawPrimitives呼叫:

command_encoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
複製程式碼

替換為drawIndexedPrimitives呼叫:

command_encoder.drawIndexedPrimitives(.Triangle, indexCount: index_buffer.length / sizeof(UInt16), indexType: MTLIndexType.UInt16, indexBuffer: index_buffer, indexBufferOffset: 0)
複製程式碼

在playground主頁面中,看看新產生的影象:

chapter09_2.png

現在我們知道如何繪製一個矩形了,讓我們看看如何繪製更多矩形!

let vertex_data = [
    Vertex(pos: [-1.0, -1.0,  1.0, 1.0], col: [1, 0, 0, 1]),
    Vertex(pos: [ 1.0, -1.0,  1.0, 1.0], col: [0, 1, 0, 1]),
    Vertex(pos: [ 1.0,  1.0,  1.0, 1.0], col: [0, 0, 1, 1]),
    Vertex(pos: [-1.0,  1.0,  1.0, 1.0], col: [1, 1, 1, 1]),
    Vertex(pos: [-1.0, -1.0, -1.0, 1.0], col: [0, 0, 1, 1]),
    Vertex(pos: [ 1.0, -1.0, -1.0, 1.0], col: [1, 1, 1, 1]),
    Vertex(pos: [ 1.0,  1.0, -1.0, 1.0], col: [1, 0, 0, 1]),
    Vertex(pos: [-1.0,  1.0, -1.0, 1.0], col: [0, 1, 0, 1])
]
let index_data: [UInt16] = [
    0, 1, 2, 2, 3, 0,   // front

    1, 5, 6, 6, 2, 1,   // right

    3, 2, 6, 6, 7, 3,   // top

    4, 5, 1, 1, 0, 4,   // bottom

    4, 0, 3, 3, 7, 4,   // left

    7, 6, 5, 5, 4, 7,   // back

]
複製程式碼

現在我們有了準備渲染的整個立方體,讓我們來到MathUtils.swift中,在modelMatrix()中註釋掉rotationtranslation呼叫,只保留縮放倍數為0.5.你將很可能看到一個像這樣的影象:

chapter09_3.png

呃,但是仍然是一個矩形!是的,因為我們仍沒有depth深度概念所以立方體看起來只是個平的.是時候來點數學魔法了.我們不需要使用Matrix矩陣結構體因為simd框架給我們提供了類似的資料結構和數學函式,我們可以直接使用.我們能輕易用matrix_float4x4代替自定義的Matrix結構體來重寫我們的轉換函式.

但是你可能會問,如何在我們2D螢幕上顯示3D物體.這個過程將每個畫素經過一系列變換.首先,modelMatrix() 將畫素從物體空間轉換到世界空間.這個矩陣是我們已經知道的,負責平移,旋轉和縮放的那個.新增新的重寫過的函式後,modelMatrix應該看起來像這樣:

func modelMatrix() -> matrix_float4x4 {
    let scaled = scalingMatrix(0.5)
    let rotatedY = rotationMatrix(Float(M_PI)/4, float3(0, 1, 0))
    let rotatedX = rotationMatrix(Float(M_PI)/4, float3(1, 0, 0))
    return matrix_multiply(matrix_multiply(rotatedX, rotatedY), scaled)
}
複製程式碼

你注意到用到的matrix_multiply函式因為Matrix結構體已經不能用了.同時,因為所有畫素將經歷同樣的變換,我們想要把矩陣儲存為一個Uniform並傳遞到vertex shader.為此,讓我們建立一個新的結構體:

struct Uniforms {
    var modelViewProjectionMatrix: matrix_float4x4
}
複製程式碼

回到createBuffers()函式,讓我們用傳遞modelMatrix時用到的緩衝器指標來傳遞全域性變數到著色器:

let modelViewProjectionMatrix = modelMatrix()
var uniforms = Uniforms(modelViewProjectionMatrix: modelViewProjectionMatrix)
memcpy(bufferPointer, &uniforms, sizeof(Uniforms))
複製程式碼

在playground主頁面中,看看新產生的影象:

chapter09_4.png

呃...立方體看上去差不多了,可是有些地方沒了.下一步變換,畫素將從世界空間攝像機空間.我們在螢幕上看到的所有東西都是被一個虛擬攝像機觀察到的,它通過帶有near最近far最遠平面的frustum平頭截體(金字塔形)來限制觀察(攝像機)空間:

chapter09_5.png

回到MathUtils.swift讓我們建立viewMatrix():

func viewMatrix() -> matrix_float4x4 {
    let cameraPosition = vector_float3(0, 0, -3)
    return translationMatrix(cameraPosition)
}
複製程式碼

下一步的變換,畫素將從camera space攝像機空間變換到clip space裁剪空間.這裡,所有不在clip space裁剪空間裡面的頂點將被判斷,看三角形被culled剔除(所有頂點都在裁剪空間外)或clipped to bounds截斷(某些頂點在外面某些在內部).projectionMatrix() 會幫我們計算邊界並判斷頂點在哪裡:

func projectionMatrix(near: Float, far: Float, aspect: Float, fovy: Float) -> matrix_float4x4 {
    let scaleY = 1 / tan(fovy * 0.5)
    let scaleX = scaleY / aspect
    let scaleZ = -(far + near) / (far - near)
    let scaleW = -2 * far * near / (far - near)
    let X = vector_float4(scaleX, 0, 0, 0)
    let Y = vector_float4(0, scaleY, 0, 0)
    let Z = vector_float4(0, 0, scaleZ, -1)
    let W = vector_float4(0, 0, scaleW, 0)
    return matrix_float4x4(columns:(X, Y, Z, W))
}
複製程式碼

最後兩個變換是從clip space裁剪空間normalized device coordinates (NDC)規格化裝置座標,還有從NDCscreen space螢幕空間.這兩步是由Metal框架為我們處理的.

下一步,回到createBuffers()函式,讓我們修改modelViewProjectionMatrix,我們之前為了適應modelMatrix來設定的:

let aspect = Float(drawableSize.width / drawableSize.height)
let projMatrix = projectionMatrix(1, far: 100, aspect: aspect, fovy: 1.1)
let modelViewProjectionMatrix = matrix_multiply(projMatrix, matrix_multiply(viewMatrix(), modelMatrix()))
複製程式碼

drawRect(:)中我們需要設定裁剪模式,正面模式,來避免出現奇怪的現象比如立方體透明瞭:

command_encoder.setFrontFacingWinding(.CounterClockwise)
command_encoder.setCullMode(.Back)
複製程式碼

在playground主頁面中,看看新產生的影象:

chapter09_6.png

這就是我們一直想看到的最終版3D立方體! 還要做一件事來讓它更真實:讓它旋轉起來.首先,讓我們建立一個全域性變數命名為rotation,我們想要隨著時間流逝來重新整理它:

var rotation: Float = 0
複製程式碼

下一步,從createBuffers()函式中取出矩陣,並建立一個新的命名為update().下面是我們每幀更新rotation來建立平滑滾動效果的地方:

func update() {
    let scaled = scalingMatrix(0.5)
    rotation += 1 / 100 * Float(M_PI) / 4
    let rotatedY = rotationMatrix(rotation, float3(0, 1, 0))
    let rotatedX = rotationMatrix(Float(M_PI) / 4, float3(1, 0, 0))
    let modelMatrix = matrix_multiply(matrix_multiply(rotatedX, rotatedY), scaled)
    let cameraPosition = vector_float3(0, 0, -3)
    let viewMatrix = translationMatrix(cameraPosition)
    let aspect = Float(drawableSize.width / drawableSize.height)
    let projMatrix = projectionMatrix(0, far: 10, aspect: aspect, fovy: 1)
    let modelViewProjectionMatrix = matrix_multiply(projMatrix, matrix_multiply(viewMatrix, modelMatrix))
    let bufferPointer = uniform_buffer.contents()
    var uniforms = Uniforms(modelViewProjectionMatrix: modelViewProjectionMatrix)
    memcpy(bufferPointer, &uniforms, sizeof(Uniforms))
}
複製程式碼

drawRect(:)中呼叫update函式:

update()
複製程式碼

在playground主頁面中,看看新產生的影象:

chapter09_7.gif

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

下次見!

相關文章