本系列文章是對 metalkit.org 上面MetalKit內容的全面翻譯和學習.git
MetalKit系統文章目錄github
我打賭不少讀者很相信MetalKit
系列,因此今天我重回這個系列,咱們將學習如何在Metal
中繪製3D內容.讓咱們繼續咱們在playground中的工做,繼續本系列的第8部分 part 8.swift
在本章結束時,咱們將渲染一個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:post
var index_buffer: MTLBuffer!
複製代碼
下一步,咱們須要在createBuffers()
函數中把index_data
賦值給index buffer
:學習
index_buffer = device!.newBufferWithBytes(index_data, length: sizeof(UInt16) * index_data.count , options: [])
複製代碼
最後,在drawRect(:)
函數中咱們須要將drawPrimitives
調用:ui
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上.
下次見!