本系列文章是對 metalkit.org 上面MetalKit內容的全面翻譯和學習.c++
上一節我說咱們將學習Metal shading language
.在學以前,咱們先作一些代碼清理和重構.從下載前一節的源代碼 source code開始.咱們將從重構render() 函數開始.因此讓咱們取出vertex buffer和render pipeline state,並建立3個新的函數放進去,這樣咱們的舊函數就減小到這樣:github
var vertex_buffer: MTLBuffer!
var rps: MTLRenderPipelineState! = nil
func render() {
device = MTLCreateSystemDefaultDevice()
createBuffer()
registerShaders()
sendToGPU()
}
複製代碼
咱們先對createBuffer() 函數作一些改變.回憶上一節vertex data是Float
類型的數組,像這樣:swift
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:post
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() 函數.咱們無需改變舊代碼,只須要將它移動到新的地方:ui
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上.
下次見!