本系列文章是對 metalkit.org 上面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
]
複製程式碼
為了理解這些索引是如何被儲存的,讓我們看下面這幅圖:
對於前方的面(矩形)來說,我們用到的頂點儲存在vertex buffer頂點緩衝器
的0到3號位.稍後我們將新增另外4個頂點.前面是由兩個三角形構成.我們先用頂點0,1和2繪製一個三角形,然後用頂點2,3和0再繪製一個三角形.請注意,正如期待的那樣,有兩個頂點被重用了.還要注意的是繪製是以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主頁面中,看看新產生的影像:
現在我們知道如何繪製一個矩形了,讓我們看看如何繪製更多矩形!
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()
中註釋掉rotation
和translation
呼叫,只保留縮放倍數為0.5.你將很可能看到一個像這樣的影像:
呃,但是仍然是一個矩形!是的,因為我們仍沒有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主頁面中,看看新產生的影像:
呃...立方體看上去差不多了,可是有些地方沒了.下一步變換,畫素將從世界空間
到攝像機空間
.我們在螢幕上看到的所有東西都是被一個虛擬攝像機觀察到的,它通過帶有near最近和far最遠平面的frustum平頭截體(金字塔形)來限制觀察(攝像機)空間:
回到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)規格化裝置座標
,還有從NDC
到screen 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主頁面中,看看新產生的影像:
這就是我們一直想看到的最終版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主頁面中,看看新產生的影像:
原始碼source code 已釋出在Github上.
下次見!