譯者注:本文是Raywenderlich上《Metal by Tutorials》免費章節的翻譯,是原書第3章.原書第 3 章完成了一個顯示立方體的 app,相比其餘教程介紹了不少 GPU 硬件部分基礎知識.
官網原文地址Metal Rendering Pipeline Tutorialc++
版本 Swift 4,iOS 11, Xcode 9程序員
本文是咱們書《Metal by Tutorials》中第 3 章的節選。這本書會帶你進入 Metal 圖形編程---Metal 是蘋果的 GPU 編程框架。你將會用 Metal 構建你本身的遊戲引擎,建立 3D 場景及構建你本身的 3D 遊戲。但願你喜歡!算法
在本教程中,你將深刻了解渲染管線,並建立一個 Metal app 來渲染出一個紅色立方體。在這個過程當中,你會了解到全部相關的硬件芯片基本知識,他們負責接收 3D 物體並將其變成屏幕上顯示的像素。編程
全部的計算機都有一個Central Processing Unit (CPU),它操做並管理着電腦上的資源。計算機也都有一個Graphics Processing Unit (GPU)。swift
GPU 是一個特殊的硬件,它能夠很是快速地處理圖像,視頻和海量的數據。這被稱做throughput(吞吐量)。吞吐量是指在單位時間內處理的數據量。數組
CPU 則沒法很是快速處理大量數據,但它能夠很是快的處理不少序列任務(一個接一個的)。處理一個任務所需的時間叫作latency(延遲)。緩存
最理想的配置就是低延遲高吞吐量。低延遲用於 CPU 執行串行隊列任務, 就不會致使系統變慢或無響應;高吞吐量容許 GPU 異步渲染視頻或遊戲無需阻塞 CPU。由於 GPU 有高度並行性的架構,專門用於作一些重複的任務,只需少許或無數據傳遞,因此它能夠處理大量數據。架構
下面的圖表顯示了 CPU 和 GPU 之間的主要差別。app
CPU 有大容量緩存及少許算術邏輯單元Arithmetic Logic Unit (ALU) 核心。CPU 上的低延遲緩存是用於快速訪問臨時資源。GPU 沒有那麼大的緩存,但有更多的 ALU 核心,它們只進行計算無需保存中間結果到內存中。框架
同時,CPU 只有幾個核心,而 GPU 有上百個甚至上千個核心。有了更多的核心,GPU 能夠將問題分割成許多小部分,每一個部分並行運行在單獨的核心上,這樣隱藏了延遲。處理完成後,各部分的結果被組合起來,並將最終結果返回給 CPU。可是,核心數並非唯一的關鍵因素!
GPU 核心除了通過精簡以外,還有一些特殊的電路用來處理幾何體,通常叫作shader cores(着色器核心)。這些着色器核心負責處理你在屏幕上看到的各類漂亮顏色。GPU 一次寫入一整幀來填滿整個渲染窗口。而後繼續處理下一幀以維持一個合理的幀率。
CPU 則繼續傳遞指令給 GPU 使其保持忙碌狀態,但有時候,可能 CPU 會中止發送指令,或者 GPU 中止處理接收到的指令。爲了不阻塞,CPU 上的 Metal 會在命令緩衝區排列多個命令,並按順序傳遞新指令,這樣下一幀就不用等待 GPU 完成第一幀了。這樣,無論 CPU,GPU 誰先完成工做,都會有更多工做等待完成。
圖形管線的 GPU 部分在它接收到全部指令和資源時就會啓動。
你已經用 Playgrounds 學過了 Metal。Playgrounds 很是適合於測試學習新的概念。同時學會如何創建一個完整的 Metal 工程也是很重要的。由於 iOS 模擬器不支持 Metal,你須要使用 macOS app.
注意:本教程的項目文件中也包含了 iOS target。
使用Cocoa App模板建立一個新的 macOS app。
命名爲Pipeline並勾選Use Storyboards。其餘不勾選。
打開Main.storyboard並選中View Controller Scene的View
。
在右側檢查器中,將 view 從NSView
改成MTKView
。
這樣就將主視圖做爲了 MetalKit View。
打開ViewController.swift。在文件的頂部,導入MetalKit framework:
import MetalKit
複製代碼
而後,在viewDidLoad()
中添加下面代碼:
guard let metalView = view as? MTKView else {
fatalError("metal view not set up in storyboard")
}
複製代碼
如今你能夠選擇。你能夠繼承MTKView
並在 storyboard 中使用這個視圖。這樣,子類的draw(_:)
將會每幀被調用,你就能夠將代碼寫在該方法裏面。可是,本教程中,你將創建一個Renderer
類並遵照MTKViewDelegate
協議,並設置Renderer
爲MTKView
的代理。MTKView
每幀都會調用代理方法,你須要把必須的繪製代碼寫在這裏。
注意:若是你之前用的是其餘 API,你可能會想要尋找遊戲循環構造。你也能夠選擇擴展
CAMetalLayer
而不是建立MTKView
。你還能夠用CADisplayLink
來計時;可是蘋果引入了MetalKit
並使用協議來更方便地管理遊戲循環。
建立一個新的 Swift 文件命名爲Renderer.swift,並用下面代碼替換其中內容:
import MetalKit
class Renderer: NSObject {
init(metalView: MTKView) {
super.init()
}
}
extension Renderer: MTKViewDelegate {
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
func draw(in view: MTKView) {
print("draw")
}
}
複製代碼
這裏你建立一個構造器並讓Renderer
遵照了MTKViewDelegate
,實現MTKView
的兩個代理方法:
mtkView(_:drawableSizeWillChange:)
:獲取每次窗口尺寸改變。這容許你更新渲染座標系統。draw(in:)
:每幀調用。 在ViewController.swift,添加一個屬性來持有 renderer:var renderer: Renderer?
複製代碼
在viewDidLoad()
的末尾,初始化 renderer:
renderer = Renderer(metalView: metalView)
複製代碼
首先,你須要創建 Metal 環境。 Metal 相比 OpenGL 的巨大優點就是,你能夠預先實例化一些對象,而沒必要每幀都建立一次。下面的圖表列出了你能夠在 app 一開始就建立的對象。
MTLDevice
:軟件對 GPU 硬件的引用。MTLCommandQueue
:負責建立及組織每幀所需的MTLCommandBuffers
.MTLLibrary
:包含了從頂點着色器和片斷着色器轉換獲得的代碼。MTLRenderPipelineState
:設置繪製信息,好比使用哪一個着色器函數,哪一個深度和顏色設置,及如何讀取頂點數據。MTLBuffer
:以一種格式持有數據,如頂點信息,方便你將其發送到 GPU。通常狀況下,在你的 app 中只有一個MTLDevice
, 一個MTLCommandQueue
及一個MTLLibrary
對象。通常會有若干個MTLRenderPipelineState
對象來定義不一樣的管線狀態,還有若干個MTLBuffer
來保存數據。
在你使用這些對象前,你須要初始化他們。在Renderer
中添加下列屬性:
static var device: MTLDevice!
static var commandQueue: MTLCommandQueue!
var mesh: MTKMesh!
var vertexBuffer: MTLBuffer!
var pipelineState: MTLRenderPipelineState!
複製代碼
這些屬性是用來引用不一樣對象的。方便起見,他們如今都是隱式解包的,可是你能夠在完成初始化後改變他們。你沒必要引用MTLLibrary
,因此須要建立它。
下一步,在init(metalView:)
的super.init()
前面添加代碼:
guard let device = MTLCreateSystemDefaultDevice() else {
fatalError("GPU not available")
}
metalView.device = device
Renderer.commandQueue = device.makeCommandQueue()!
複製代碼
這裏初始化了 GPU 並建立了命令隊列。你使用了類屬性來保存 device 和命令隊列以確保只有一份存在。有些狀況下,你可能須要不止一個,可是大部分狀況下,一個就夠了。
最後,在super.init()
以後,添加下面代碼:
metalView.clearColor = MTLClearColor(red: 1.0, green: 1.0,
blue: 0.8, alpha: 1.0)
metalView.delegate = self
複製代碼
這裏設置metalView.clearColor
爲一種奶油色。同時也將Renderer
設置爲metalView
的代理,這樣它就會調用MTKViewDelegate
的繪製方法。
構建並運行 app 以確保全部事情已經完成並起做用了。若是正常的話,你將看到一個灰色的窗口。在調試控制檯中,你將會看到單詞"draw"不斷重複出現。用這個來檢驗你的 app 是否每幀都在調用draw(in:)
方法。
你看不到
metalView
的奶油色由於你沒有請求 GPU 來作任何繪製操做。
一個專門的類來建立 3D 圖元網格是頗有用的。在本教程中,你將建立一個類來建立 3D 形狀圖元,並向其添加立方體。
建立一個新的 Swift 文件命名爲Primitive.swift,並用下面代碼替換默認代碼:
import MetalKit
class Primitive {
class func makeCube(device: MTLDevice, size: Float) -> MDLMesh {
let allocator = MTKMeshBufferAllocator(device: device)
let mesh = MDLMesh(boxWithExtent: [size, size, size],
segments: [1, 1, 1],
inwardNormals: false, geometryType: .triangles,
allocator: allocator)
return mesh
}
}
複製代碼
這個類方法返回一個立方體。
在Renderer.swift中,在init(metalView:)
,在調用super.init()
以前,先創建網格:
let mdlMesh = Primitive.makeCube(device: device, size: 1)
do {
mesh = try MTKMesh(mesh: mdlMesh, device: device)
} catch let error {
print(error.localizedDescription)
}
複製代碼
而後,建立MTLBuffer
來盛放將發送到 GPU 的頂點數據。
vertexBuffer = mesh.vertexBuffers[0].buffer
複製代碼
這會將數據放在一個MTLBuffer
中。如今你須要創建管線狀態,以讓 GPU 知道如何渲染數據。
首先,建立MTLLibrary
並確保頂點和片斷着色器函數可用。
繼續在super.init()
以前添加:
let library = device.makeDefaultLibrary()
let vertexFunction = library?.makeFunction(name: "vertex_main")
let fragmentFunction = library?.makeFunction(name: "fragment_main")
複製代碼
你將會在本教程的稍後部分建立這些着色器。與 OpenGL 着色器不一樣,這些着色器會在你編譯項目時被編譯好,這無疑比運行中編譯更有效率。結果被儲存在 library 中。
如今,建立管線狀態:
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.vertexDescriptor = MTKMetalVertexDescriptorFromModelIO(mdlMesh.vertexDescriptor)
pipelineDescriptor.colorAttachments[0].pixelFormat = metalView.colorPixelFormat
do {
pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
} catch let error {
fatalError(error.localizedDescription)
}
複製代碼
這裏爲 GPU 建立了一個可能的狀態。GPU 須要在開始管理頂點以前,就知道它的完整狀態。你爲 GPU 設置兩個着色器函數,並設置要寫入紋理的像素格式。
同時設置了管線的頂點描述符。它決定了 GPU 如何翻譯處理你在網格數據MTLBuffer
傳遞過去的頂點數據。
若是你須要調用不一樣的頂點或片斷函數,或使用不一樣的數據佈局,那麼你就須要多個管線狀態。建立管線狀態是至關花費時間的,這就是爲何你須要儘早建立,可是在不一樣幀間切換管線狀態是很是快速和高效的。
初始化是完整的,你的項目即將編譯。可是,當你嘗試運行它時,你會遇到一個錯誤,由於你尚未建立着色器函數。
在Renderer.swift中,替換draw(in:)
中的print
語句:
guard let descriptor = view.currentRenderPassDescriptor,
let commandBuffer = Renderer.commandQueue.makeCommandBuffer(),
let renderEncoder =
commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else {
return
}
// drawing code goes here
renderEncoder.endEncoding()
guard let drawable = view.currentDrawable else {
return
}
commandBuffer.present(drawable)
commandBuffer.commit()
複製代碼
這裏建立了渲染命令編碼器,並將視圖的可繪製紋理髮送到 GPU。
在 CPU 上,要給 GPU 準備數據,你須要把數據和管線狀態給 GPU。而後你須要發起繪製調用(draw call)。
仍是在draw(in:)
中,替換註釋:
// drawing code goes here
複製代碼
爲下面代碼:
renderEncoder.setRenderPipelineState(pipelineState)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
for submesh in mesh.submeshes {
renderEncoder.drawIndexedPrimitives(type: .triangle,
indexCount: submesh.indexCount,
indexType: submesh.indexType,
indexBuffer: submesh.indexBuffer.buffer,
indexBufferOffset: submesh.indexBuffer.offset)
}
複製代碼
當你在draw(in:)
的末尾提交命令緩衝時,就指示了 GPU 數據和管線都準備好了,GPU 能夠接管過去了。
終於到了審查 GPU 管線的時候了!在下面圖表中,你能夠看到管線的狀態。
圖形管線在多個階段都接收頂點,同時頂點會在多個空間座標系中進行變換。
作爲一個 Metal 程序員,你只須要考慮頂點和片斷處理階段,由於只有這兩個階段是可編程控制的。在教程後面,你會寫一個頂點着色器和一個片斷着色器。其餘的非可編程管線階段,如Vertex Fetch(頂點獲取),Primitive Assembly(圖元組裝)和Rasterization(光柵化),GPU 有專門設計的硬件單元來處理這些階段。
下一步,你將逐個瞭解這些階段。
該階段的名稱在不一樣圖形 API 中不一樣。例如,DirectX中叫作Input Assembling。
要開始渲染 3D 內容,你首先須要一個 scene。一個 scene 場景包含不少模型,模型中有頂點組成的網格。最簡單的模型就是立方體,它有 6 個面(12 個三角形)。
你使用頂點描述符來定義頂點的屬性讀取方式,如位置,紋理座標,法線和顏色。你也能夠選擇不使用頂點描述符,只將一組MTLBuffer
頂點發送過去。可是,若是你這樣作,就必須提早知道頂點緩衝是如何組織的。
當 GPU 獲取頂點緩衝時,MTLRenderCommandEncoder
的繪製調用告訴 GPU 緩衝是否有索引。若是緩衝沒有索引,GPU 就假設緩衝是個數組,按順序一次取一個元素。
這些索引很是重要,由於頂點是被緩存起來以供重用的。例如,一個立方體有 12 個三角形和 8 個頂點。若是你不使用索引,你必須爲每一個三角形指定頂點並將 36 個頂點發送到 GPU。這個聽起來可能不太多,可是在一個擁有上千個頂點的模型中,頂點緩存是很是重要的!
另外還有一個給已着色頂點用的第二緩衝,這樣被屢次訪問的頂點也只需着色一次。已着色頂點是指已經應用了顏色的頂點。可是這些是在下一階段才發生的。
一個特殊的硬件單元叫作調度器Scheduler將頂點和他們的屬性發送到Vertex Processing(頂點處理) 階段。
在這個階段,頂點是被單獨處理的。你須要寫代碼來計算逐頂點的光照和顏色。更重要的是,你要將頂點座標,通過不一樣座標空間的轉換,來肯定在最終幀緩衝中的位置。
如今是時候來看看在硬件層面上到底發生了什麼吧。來看一眼現代的 AMD GPU 的架構:
從上到下,GPU 擁有:36 個CU共有 2304 個着色器核心 shader core。這個數目和你的四核心 CPU 相比,差別巨大!
對移動設備來講,事情有點不一樣。下面這張圖,展現了最近幾年 iOS 設備上的 GPU 結構。PowerVR GPU 取消了SE和CU,使用了Unified Shading Cluster (USC)。這個特製的 GPU 有 6 個USC,每一個USC又有 32 個核心,總共 192 個核心。
注意:iPhoneX 上的最新的移動 GPU 是蘋果徹底自主設計的。不幸的是,蘋果並無公開它的 GPU 硬件特性。
那麼你能用這麼多核心作什麼呢?由於這些核心是專門用於頂點和片斷着色的,顯然這些核心能夠並行工做,因此頂點和片斷的處理能夠更快速。固然還有一些規則。在一個 CU 內,你只能處理頂點或片斷,不能同時處理二者。好消息是有 36 個 CU!另外一個規則就是每一個 SE 只能處理一個着色函數。有四個 SE 可讓你更加靈活的組合工做。例如,你能夠一次性,同時在一個 SE 上運行一個片斷着色器,在第二個 SE 上運行第二個片斷着色器。或者你能夠將你的頂點着色器從片斷着色器中分離出來,讓他們在不一樣的 SE 上並行運行。
如今,是時候來看看頂點處理的過程了!你即將要寫的頂點着色器vertex shader應該是最小化的,並封裝了大部分必要的頂點着色器語法。
用Metal File模板來建立一個新文件,命名爲Shaders.metal。而後,將下面代碼添加在文件末尾:
// 1
struct VertexIn {
float4 position [[ attribute(0) ]];
};
// 2
vertex float4 vertex_main(const VertexIn vertexIn [[ stage_in ]]) {
return vertexIn.position;
}
複製代碼
代碼含義:
VertexIn
來描述頂點屬性,以匹配先前建立的頂點描述符。在本例中,只有一個position
。vertex_main
,它接收VertexIn
結構體,並以float4
格式返回頂點位置。記住,頂點在頂點緩衝中是有索引的。頂點着色器經過[[ stage_in ]]
屬性拿到當前索引,並解包這個索引對應的VertexIn
結構體緩存。
計算單元可以處理(一次)大批量的頂點,數量取決於着色器核心的最大值。該批處理能夠完整利用 CU 高速緩存,所以能夠根據須要重用頂點。該批處理會讓 CU 保持繁忙狀態直處處理完成,可是其餘的 CU 會變成可用狀態以處理下一批次。
頂點處理一旦完成,高速緩存就會被清理,爲下一批次頂點作好準備。此時,頂點已經被排序過,分組過了,準備被髮送到下一階段了。
回顧一下,CPU 將一個從模型的網格中建立的頂點緩衝發送給 GPU。用一個頂點描述符來配置頂點緩衝,以此告訴 GPU 頂點數據是什麼結構的。在 GPU 上,你建立一個結構體來包裝頂點屬性。頂點着色器經過函數參數接收這個結構體,並經過[[ stage_in ]]
修飾詞,知道了position
是從 CPU 經過頂點緩衝中的[[ attribute(0) ]]
位置傳遞過來。而後,頂點着色器處理全部的頂點並經過float4
返回他們的位置。
一個特殊的硬件單元叫作分配器Distributer,將分組過的頂點數據塊發送到下一個Primitive Assembly(圖元組裝) 階段。
前一階段將頂點分組成數據塊發送到本階段。須要注意的是,同一個幾何體形狀(圖元primitive)的頂點老是會在同一個塊中。這就意味着,一個頂點的點,或兩個頂點的線,或者三個頂點的三角形,老是會在同一個塊中,所以,不再須要讀取第二個數據塊了。
與此同時,CPU 還在派發繪製調用draw call命令時,發送了頂點的鏈接信息過來,好比這樣:
renderEncoder.drawIndexedPrimitives(type: .triangle,
indexCount: submesh.indexCount,
indexType: submesh.indexType,
indexBuffer: submesh.indexBuffer.buffer,
indexBufferOffset: 0)
複製代碼
繪製函數的第一個參數包含了最重要的頂點鏈接信息。在本例中,它告訴 GPU 利用拿到的頂點緩衝繪製三角形。
Metal API 提供了五種基礎形狀:
[[point_size]]
來指定點的尺寸。其實還有另外一種基礎形狀(圖元)叫作patch,可是它須要特殊處理,不能被用在帶有索引的繪製調用函數中。
管線指定了頂點的旋轉方向。若是旋轉方向是逆時針的,那麼三角形頂點的順序就是逆時針的面,就是正面。不然,這個面就是背面,能夠被剔除,由於咱們看不到他們的顏色和光照。
當被其餘圖元遮擋時,該圖元將會被剔除,可是,若是他們只是部分在屏幕外,他們將會被裁剪。
爲了效率,你應當指定旋轉方向並啓用背面剔除。
此時,圖元已經從頂點被徹底組裝好了,並將進入到光柵化器。
當前,有兩種不一樣的渲染技術:光線追蹤ray tracing和光柵化rasterization,固然有時候也會一塊兒使用。它們差別很是大,各有優勢和缺點。
當渲染內容是靜態的,距離較遠的時候,光線追蹤效果更好;當內容很是靠近鏡頭且不斷移動時,光柵化效果更好。
使用光線追蹤時,從屏幕上的每個點,發射一條射線到場景中,看看是否和場景中的物體有交點。若是有,將屏幕上像素的顏色改爲距離屏幕最近的物體的顏色。
光柵化是另外一種工做方式:從場景中的每個物體,發射射線到屏幕上,看看哪些像素被該物體覆蓋了。深度信息也會像光線追蹤同樣被保留,因此,當有更近的物體出現時,會更新屏幕上像素的顏色。
此時,上一階段中發過來的鏈接後的頂點,會根據 X 和 Y 座標被呈如今二維網格上。這一步就是三角形設置triangle setup。
這裏,光柵化器須要計算任意兩個頂點間線段的斜率。當三個頂點間的三個斜率都已知後,三角形就能夠同這三條邊構成。
下一步的處理叫作掃瞄轉換scan conversion,逐行掃瞄屏幕尋找交點,肯定哪一部分是可見的,哪一部分是不可見的。要繪製屏幕上的點,只需它們的頂點和斜率就夠了。掃瞄算法肯定是否線段上的全部點或三角形內的全部點都是可見的,若是是可見的,就全都會被填充上顏色。
對移動設備來講,光柵化能夠充分利用 PowerVR GPU 的tiled架構優點,能夠並行光柵化一個 32x32 的圖塊網格。這樣一來,32 就是分配給圖塊的屏幕像素的數量,該尺寸完美匹配了 USC 的核心數量。
若是一個物體躲在另外一個物體後面會怎樣?光柵化器如何決定哪一個物體要被渲染呢?這個隱藏表面的移除問題能夠被解決,方法是經過使用儲存的深度信息(提早 Z 測試)來決定任意一個點是否在場景中另外一些點的前面。
在光柵化完成後,三個另外的硬件單元接管了任務:
此時,調度器Scheduler單元再次將任務調度給着色器核心,可是這一次,光柵化後的片斷被髮送到Fragment Processing(片斷處理) 階段。
是時候快速複習一下管線知識了。
前一階段的圖元處理是序列進行的,由於只有一個Primitive Assembly(圖元組裝) 單元,及一個Rasterization(光柵化) 單元。然而,一旦片斷到達了調度器Scheduler單元,工做就能夠被分叉forked(分割)成許多小的部分,每一部分被分配到可用的着色器核心上。
上百個甚至上千個核心如今在並行處理。當工做完成後,結果就會被接合joined(合併)並再次發送到內存中。
片斷處理階段是另外一個可編程控制階段。你將建立一個片斷着色函數來接收頂點函數輸出的光照,紋理座標,深度和顏色信息。
片斷着色器的輸出是該片斷的顏色。每個片斷都會爲幀緩衝中的最終像素顏色作出貢獻。每一個片斷的全部的屬性是插值獲得的。
例如,要渲染一個三角形,頂點函數會處理三個頂點,顏色分別爲紅,綠和藍。正如圖表顯示的那樣,組成三角形的每一個片斷都是三種顏色插值獲得的。線性插值就是簡單地根據兩個端點的距離和顏色平均一下獲得的。若是一個端點是紅色的,另外一個端點是綠色的,那麼線段的中間點的顏色就是黃色的。依此類推。
插值方程的參數化形式以下,其中參數p是顏色份量的百分比(或從 0 到 1 的範圍):
newColor = p * oldColor1 + (1 - p) * oldColor2
複製代碼
顏色是很容易可視化的,可是全部其餘頂點函數的輸出也是相似的插值方式來獲得各個片斷。
注意:若是你不想一個頂點的輸出被插值,就將屬性
[[ flat ]]
添加到它的定義裏。
在Shader.Metal中,在文件末尾添加片斷函數:
fragment float4 fragment_main() {
return float4(1, 0, 0, 1);
}
複製代碼
這多是最簡單的片斷函數了。你返回了插值顏色float4
。全部組成立方體的的片斷都會是紅色的。
GPU 接收片斷並進行了一系列的後置處理測試:
一旦片斷已經被處理成像素,分配器Distributer單元將他們發送到色彩寫入Color Writing單元。這個單元負責將最終顏色寫入到一個特殊的內存位置叫作framebuffer(幀緩衝)。從這裏,視圖獲得了每一幀刷新時的帶有顏色的像素。可是,顏色被寫入幀緩衝是否意味着已經同時顯示在屏幕上了呢?
一個叫作double-buffering(雙重緩衝) 的技術用來解決這個問題。當第一個緩衝顯示在屏幕上時,第二個在後臺更新。而後,兩個緩衝被交換,第二個緩衝被顯示在屏幕上,第一個被更新,一直循環下去。
喲!這裏要了解好多硬件信息啊。然而,你編寫的代碼用在每一個 Metal 渲染器上,你就應該學會認識渲染的過程,儘管只是剛剛開始查看蘋果的示例代碼。
構建並運行 app,你的 app 將會渲染出一個紅色的立方體。
你會注意到,立方體並非正方形。記住,Metal 使用了 標準化設備座標Normalized Device Coordinates (NDC),x軸取值範圍是-1 到 1。重設你的窗口尺寸,立方體將會保持與窗口成比例的大小。Metal 就是用於華麗的圖形及快速又平滑的動畫。下一步,你將讓你的立方體在屏幕上,上下來回移動。爲了實現這個效果,你須要一個每幀更新的計時器,立方體的位置將依賴於這個計時器。你將在頂點函數中更新頂點的位置,這樣就會將計時器數據發送到 GPU。
在Renderer
的上面,添加計時器屬性:
var timer: Float = 0
複製代碼
在draw(in:)
中,在下面代碼前面:
renderEncoder.setRenderPipelineState(pipelineState)
複製代碼
添加
// 1
timer += 0.05
var currentTime = sin(timer)
// 2
renderEncoder.setVertexBytes(¤tTime,
length: MemoryLayout<Float>.stride,
index: 1)
複製代碼
sin()
是很好的一個方法。setVertexBytes(_:length:index:)
是創建MTLBuffer
的另外一種方法。這裏,你設置currentTime
爲緩衝參數表中的索引 1 中。在Shaders.metal中,用下面代碼替換頂點函數:
vertex float4 vertex_main(const VertexIn vertexIn [[ stage_in ]],
constant float &timer [[ buffer(1) ]]) {
float4 position = vertexIn.position;
position.y += timer;
return position;
}
複製代碼
這裏,你的頂點函數 從 buffer 1 中接收了 float 格式的 timer。將 timer 的值加到 y 上,並返回新的位置。
構建並運行 app,如今你就獲得了運動起來的立方體!
只加了幾行代碼,你學會了管線是如何工做的而且還添加了一點動畫效果。
若是你想要查看本教程完成後的項目,你能夠下載本教程資料,在final文件夾找到。
若是你喜歡在本教程所學到的東西,何不嘗試一下咱們的新書Metal by Tutorials呢?
這本書將會帶你瞭解用 Metal 實現低級別的圖形編程。當你學習該書時,你將會學到不少製做一個遊戲引擎的基礎知識,並逐步將其組裝成你本身的引擎。
當你的遊戲引擎完成時,你將可以組成 3D 場景並編碼出本身的簡單版 3D 遊戲。由於你將從無到有構建你的 3D 遊戲引擎,因此你將可以自定義屏幕上顯示的任何內容。
可是除了技術上的定義處,Metal 仍是使用 GPU 並行處理能力來可視化數據或解決數值難題的最理想方式。因此也被用於機器學習,圖像/視頻處理或者像本書中所寫,圖形渲染。
本書是那些想要學習 3D 圖形或想要深刻理解遊戲引擎工做原理的,中級 Swift 開發者最好的學習資源。
若是你對本教程還有什麼問題或意見,請在下面留言討論!