本系列文章是對 metalkit.org 上面MetalKit內容的全面翻譯和學習.
上一節我說我們將學習Metal shading language
.在學之前,我們先做一些程式碼清理和重構.從下載前一節的原始碼 source code開始.我們將從重構render() 函式開始.所以讓我們取出vertex buffer和render pipeline state,並建立3個新的函式放進去,這樣我們的舊函式就減少到這樣:
var vertex_buffer: MTLBuffer!
var rps: MTLRenderPipelineState! = nil
func render() {
device = MTLCreateSystemDefaultDevice()
createBuffer()
registerShaders()
sendToGPU()
}
複製程式碼
我們先對createBuffer() 函式做一些改變.回憶上一節vertex data是Float
型別的陣列,像這樣:
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成員,這樣我們就可以在CPU
和GPU
之間來回傳遞資料:
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;
}
複製程式碼
如果你執行程式,你看到一個更漂亮的彩色三角形:
你也許會奇怪,為什麼我們只傳遞給三個頂點對應顏色,但頂點之間的顏色卻是漸變的?要理解這些,就必須先理解兩種著色器的不同及它們在圖形管線中角色的不同.讓我們看看任一個著色器的語法(這裡先頂點著色器作例子):
vertex Vertex vertex_func(constant Vertex *vertices [[buffer(0)]], uint vid [[vertex_id]])
複製程式碼
第一個關鍵詞,是函式限定符只能使用vertex, fragment和kernel.下一個關鍵詞是返回值型別.接下來是帶有圓括號引數的函式name名稱.Metal shading language
限定了指標的使用,必須用device,threadgroup或constant修飾符來宣告,這些修飾符指定了函式變數或引數分配到的記憶體區域.[[...]] 語法是用來宣告屬性,例如資源位置,著色器輸入,以及在著色器與CPU之間來回傳遞的內建變數.
Metal
使用 [[ buffer(index)]] 屬性來標識出位置,讓device
和constant 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上.
下次見!