Metal框架詳細解析(三十二) —— Metal渲染管道教程(一)

版本記錄

版本號 時間
V1.0 2018.10.12 星期五

前言

很多做視頻和圖像的,相信對這個框架都不是很陌生,它渲染高級3D圖形,并使用GPU執(zhí)行數(shù)據(jù)并行計算。接下來的幾篇我們就詳細的解析這個框架。感興趣的看下面幾篇文章。
1. Metal框架詳細解析(一)—— 基本概覽
2. Metal框架詳細解析(二) —— 器件和命令(一)
3. Metal框架詳細解析(三) —— 渲染簡單的2D三角形(一)
4. Metal框架詳細解析(四) —— 關(guān)于GPU Family 4(一)
5. Metal框架詳細解析(五) —— 關(guān)于GPU Family 4之關(guān)于Imageblocks(二)
6. Metal框架詳細解析(六) —— 關(guān)于GPU Family 4之關(guān)于Tile Shading(三)
7. Metal框架詳細解析(七) —— 關(guān)于GPU Family 4之關(guān)于光柵順序組(四)
8. Metal框架詳細解析(八) —— 關(guān)于GPU Family 4之關(guān)于增強的MSAA和Imageblock采樣覆蓋控制(五)
9. Metal框架詳細解析(九) —— 關(guān)于GPU Family 4之關(guān)于線程組共享(六)
10. Metal框架詳細解析(十) —— 基本組件(一)
11. Metal框架詳細解析(十一) —— 基本組件之器件選擇 - 圖形渲染的器件選擇(二)
12. Metal框架詳細解析(十二) —— 基本組件之器件選擇 - 計算處理的設(shè)備選擇(三)
13. Metal框架詳細解析(十三) —— 計算處理(一)
14. Metal框架詳細解析(十四) —— 計算處理之你好,計算(二)
15. Metal框架詳細解析(十五) —— 計算處理之關(guān)于線程和線程組(三)
16. Metal框架詳細解析(十六) —— 計算處理之計算線程組和網(wǎng)格大小(四)
17. Metal框架詳細解析(十七) —— 工具、分析和調(diào)試(一)
18. Metal框架詳細解析(十八) —— 工具、分析和調(diào)試之Metal GPU Capture(二)
19. Metal框架詳細解析(十九) —— 工具、分析和調(diào)試之GPU活動監(jiān)視器(三)
20. Metal框架詳細解析(二十) —— 工具、分析和調(diào)試之關(guān)于Metal著色語言文件名擴展名、使用Metal的命令行工具構(gòu)建庫和標記Metal對象和命令(四)
21. Metal框架詳細解析(二十一) —— 基本課程之基本緩沖區(qū)(一)
22. Metal框架詳細解析(二十二) —— 基本課程之基本紋理(二)
23. Metal框架詳細解析(二十三) —— 基本課程之CPU和GPU同步(三)
24. Metal框架詳細解析(二十四) —— 基本課程之參數(shù)緩沖 - 基本參數(shù)緩沖(四)
25. Metal框架詳細解析(二十五) —— 基本課程之參數(shù)緩沖 - 帶有數(shù)組和資源堆的參數(shù)緩沖區(qū)(五)
26. Metal框架詳細解析(二十六) —— 基本課程之參數(shù)緩沖 - 具有GPU編碼的參數(shù)緩沖區(qū)(六)
27. Metal框架詳細解析(二十七) —— 高級技術(shù)之圖層選擇的反射(一)
28. Metal框架詳細解析(二十八) —— 高級技術(shù)之使用專用函數(shù)的LOD(一)
29. Metal框架詳細解析(二十九) —— 高級技術(shù)之具有參數(shù)緩沖區(qū)的動態(tài)地形(一)
30. Metal框架詳細解析(三十) —— 延遲照明(一)
31. Metal框架詳細解析(三十一) —— 在視圖中混合Metal和OpenGL渲染(一)

開始

首先看一下寫作環(huán)境

Swift 4, iOS 11, Xcode 9

在本教程中,您將深入了解渲染管道并創(chuàng)建一個呈現(xiàn)紅色立方體的Metal應(yīng)用程序。 在此過程中,您將發(fā)現(xiàn)所有負責拍攝3D物體并將其轉(zhuǎn)換為您在屏幕上看到的華麗像素的硬件芯片。


The GPU and the CPU - GPU和CPU

所有計算機都有一個中央處理單元(CPU),用于驅(qū)動操作并管理計算機上的資源。他們還有一個圖形處理單元(Graphics Processing Unit (GPU))

GPU是一種專用硬件組件,可以非常快速地處理圖像,視頻和大量數(shù)據(jù)。這稱為吞吐量(throughput)。吞吐量通過在特定時間單位中處理的數(shù)據(jù)量來度量。

另一方面,CPU無法快速處理大量數(shù)據(jù),但它可以非常快速地處理許多順序任務(wù)(一個接一個)。處理任務(wù)所需的時間稱為延遲(latency)

理想的設(shè)置包括低延遲和高吞吐量。低延遲允許串行執(zhí)行排隊任務(wù),因此CPU可以執(zhí)行命令,而不會使系統(tǒng)變慢或無響應(yīng);高吞吐量讓GPU可以異步渲染視頻和游戲而不會拖延CPU。由于GPU具有高度并行化的架構(gòu),專門用于重復(fù)執(zhí)行相同的任務(wù),并且很少或沒有數(shù)據(jù)傳輸,因此能夠處理更大量的數(shù)據(jù)。

下圖顯示了CPU和GPU之間的主要差異。

CPU具有大容量高速緩存和少量算術(shù)邏輯單元(Arithmetic Logic Unit - ALU)內(nèi)核。 CPU上的低延遲高速緩存用于快速訪問臨時資源。 GPU沒有太多的高速緩沖存儲器,并且有更多ALU核心的空間,它們只進行計算而不將部分結(jié)果保存到存儲器中。

此外,CPU通常只有少數(shù)內(nèi)核,而GPU有數(shù)百甚至數(shù)千個內(nèi)核。通過更多核心,GPU可以將問題分解為許多較小的部分,每個部分并行運行在單獨的核心上,從而隱藏延遲。在處理結(jié)束時,將部分結(jié)果合并,并將最終結(jié)果返回給CPU。但核心并不是唯一重要的事情!

除了精簡之外,GPU內(nèi)核還具有用于處理幾何結(jié)構(gòu)的特殊電路,通常稱為著色器內(nèi)核(shader cores)。這些著色器核心負責您在屏幕上看到的美麗色彩。 GPU一次寫入整個幀以適合整個渲染窗口。然后,它將繼續(xù)盡快渲染下一幀以保持良好的幀速率。

CPU繼續(xù)向GPU發(fā)出命令以使其保持忙碌,但在某些時候,CPU將完成發(fā)送命令或GPU將完成處理它收到的命令。 為了避免停止,CPU上的Metal會在命令緩沖區(qū)中排隊多個命令,并按順序為下一幀發(fā)出新命令,而不必等待GPU完成第一幀。 這樣,無論誰先完成工作,都會有更多的工作要做。

一旦接收到所有命令和資源,圖形管道的GPU部分就會啟動。


The Metal Project - Metal工程

您一直在使用Playgrounds來了解MetalPlaygrounds非常適合測試和學習新概念。 了解如何設(shè)置完整的Metal項目非常重要。 由于iOS模擬器不支持Metal,因此您將使用macOS應(yīng)用程序。

注意:本教程的挑戰(zhàn)項目的項目文件還包括iOS target

使用Cocoa App模板創(chuàng)建一個新的macOS應(yīng)用程序。

將項目命名為Pipeline并選中Use Storyboards。 不選中其余選項。

打開Main.storyboard并在View Controller Scene下選擇View

Identity檢查器中,將視圖從NSView更改為MTKView

這會將主視圖設(shè)置為MetalKit View

打開ViewController.swift。 在文件的頂部,導(dǎo)入MetalKit框架:

import MetalKit

然后,將此代碼添加到viewDidLoad()

guard let metalView = view as? MTKView else {
  fatalError("metal view not set up in storyboard")
}

你現(xiàn)在有了選擇。 您可以將MTKView子類化并在sb中使用此視圖。 在這種情況下,每個幀都會調(diào)用子類的draw(_ :),并將繪圖代碼放在該方法中。 但是,在本教程中,您將設(shè)置符合MTKViewDelegateRenderer類,并將Renderer設(shè)置為MTKView的委托。 MTKView每幀調(diào)用一個委托方法,這是你放置必要的繪圖代碼的地方。

注意:如果您來自不同的API世界,您可能正在尋找游戲循環(huán)結(jié)構(gòu)。 您可以選擇擴展CAMetalLayer而不是創(chuàng)建MTKView。 然后,您可以使用CADisplayLink進行計時;但Apple推出的MetalKit協(xié)議可以更輕松地管理游戲循環(huán)。


The Renderer Class - Renderer類

創(chuàng)建一個名為Renderer.swift的新Swift文件,并使用以下代碼替換其內(nèi)容:

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")
  }
}

在這里,您創(chuàng)建一個初始化程序,并使用兩個MTKView委托方法使Renderer遵守MTKViewDelegate

  • mtkView(_:drawableSizeWillChange :):每次窗口大小改變時調(diào)用。 這允許您更新渲染坐標系。
  • draw(in :):每幀調(diào)用一次。

ViewController.swift中,添加一個屬性來保存renderer

var renderer: Renderer?

viewDidLoad()的末尾,初始化renderer

renderer = Renderer(metalView: metalView)

Initialization - 初始化

首先,您需要設(shè)置Metal環(huán)境。

MetalOpenGL具有一個主要優(yōu)勢,因為您可以預(yù)先實例化某些對象,而不是在每個幀中創(chuàng)建它們。 下圖顯示了您可以在應(yīng)用程序開頭創(chuàng)建的一些對象。

  • `MTLDevice``:GPU硬件設(shè)備的軟件引用。
  • MTLCommandQueue:負責每幀創(chuàng)建和組織MTLCommandBuffers
  • MTLLibrary:包含頂點和片段著色器函數(shù)的源代碼。
  • MTLRenderPipelineState:設(shè)置繪圖的信息,例如要使用的著色器函數(shù),要使用的深度和顏色設(shè)置以及如何讀取頂點數(shù)據(jù)。
  • MTLBuffer:以可以發(fā)送到GPU的形式保存數(shù)據(jù),例如頂點信息。

通常,您的應(yīng)用程序中將有一個MTLDevice,一個MTLCommandQueue和一個MTLLibrary對象。 您還將擁有幾個MTLRenderPipelineState對象,這些對象將定義各種管道狀態(tài),以及幾個用于保存數(shù)據(jù)的MTLBuffers

但是,在使用這些對象之前,需要初始化它們。 將這些屬性添加到Renderer

static var device: MTLDevice!
static var commandQueue: MTLCommandQueue!
var mesh: MTKMesh!
var vertexBuffer: MTLBuffer!
var pipelineState: MTLRenderPipelineState!

這些是保持對不同對象的引用所需的屬性。 為方便起見,它們目前都是隱式解包的選項,但您可以在完成初始化后更改它。 此外,您不需要保留對MTLLibrary的引用,因此無需創(chuàng)建它。

接下來,在super.init()之前將此代碼添加到init(metalView :)

guard let device = MTLCreateSystemDefaultDevice() else {
  fatalError("GPU not available")
}
metalView.device = device
Renderer.commandQueue = device.makeCommandQueue()!

這會初始化GPU并創(chuàng)建command queue。 您正在使用設(shè)備和命令隊列的類屬性來確保每個屬性中只有一個存在。 在極少數(shù)情況下,您可能需要不止一個 - 但在大多數(shù)應(yīng)用程序中,一個就夠了。

最后,在super.init()之后,添加以下代碼:

metalView.clearColor = MTLClearColor(red: 1.0, green: 1.0,
                                     blue: 0.8, alpha: 1.0)
metalView.delegate = self

這將metalView.clearColor設(shè)置為奶油色。 它還將Renderer設(shè)置為metalView的代理,以便它調(diào)用MTKViewDelegate繪圖方法。

Build并運行應(yīng)用程序以確保一切都已設(shè)置并正常運行。 如果一切順利,你應(yīng)該看到一個普通的灰色窗口。 在調(diào)試控制臺中,您將反復(fù)看到“draw”一詞。 使用此選項可驗證您的應(yīng)用是否正在為每個幀調(diào)用draw(in :)

注意:您不會看到metalView的奶油色,因為您還沒有要求GPU進行任何繪圖。


Set Up the Data - 設(shè)置數(shù)據(jù)

構(gòu)建3D基元網(wǎng)格的類總是有用的。 在本教程中,您將設(shè)置一個用于創(chuàng)建3D形狀基元的類,并且您將為其添加一個立方體。

創(chuàng)建一個名為Primitive.swift的新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 :)中,在調(diào)用super.init()之前,設(shè)置網(wǎng)格mesh

let mdlMesh = Primitive.makeCube(device: device, size: 1)
do {
  mesh = try MTKMesh(mesh: mdlMesh, device: device)
} catch let error {
  print(error.localizedDescription)
}

然后,設(shè)置包含您將發(fā)送到GPU的頂點數(shù)據(jù)的MTLBuffer

vertexBuffer = mesh.vertexBuffers[0].buffer

這將數(shù)據(jù)放入MTLBuffer中。 現(xiàn)在,您需要設(shè)置管道狀態(tài),以便GPU知道如何呈現(xiàn)數(shù)據(jù)。

首先,設(shè)置MTLLibrary并確保存在頂點和片段著色器函數(shù)。

繼續(xù)在super.init()之前添加代碼:

let library = device.makeDefaultLibrary()
let vertexFunction = library?.makeFunction(name: "vertex_main")
let fragmentFunction = library?.makeFunction(name: "fragment_main")

您將在本教程后面創(chuàng)建這些著色器函數(shù)。 與OpenGL著色器不同,這些是在編譯項目時編譯的,這比動態(tài)編譯更有效。 結(jié)果存儲在庫中。

現(xiàn)在,創(chuàng)建管道狀態(tài):

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設(shè)置了潛在的狀態(tài)。 在開始管理頂點之前,GPU需要知道其完整狀態(tài)。 您可以設(shè)置GPU將調(diào)用的兩個著色器函數(shù),還可以設(shè)置GPU將寫入的紋理的像素格式。

您還可以設(shè)置管道的頂點描述符(vertex descriptor)。 這就是GPU將如何解釋您將在網(wǎng)格數(shù)據(jù)MTLBuffer中呈現(xiàn)的頂點數(shù)據(jù)的方式。

如果需要調(diào)用不同的頂點或片段函數(shù),或使用不同的數(shù)據(jù)布局,那么您將需要更多的管道狀態(tài)。 創(chuàng)建管道狀態(tài)相對耗時,這就是為什么你提前做到這一點的原因,但在幀期間切換管道狀態(tài)是快速和有效的。

初始化完成,您的項目將編譯。 但是,如果您嘗試運行它,則會出現(xiàn)錯誤,因為您尚未設(shè)置著色器函數(shù)。


Render Frames - 渲染幀

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()

這將設(shè)置渲染命令編碼器(render command encoder)并將視圖的可繪制紋理呈現(xiàn)給GPU。


Drawing - 繪制

在CPU方面,要準備GPU,您需要為其提供數(shù)據(jù)和管道狀態(tài)。 然后,您需要發(fā)出繪制調(diào)用。

仍在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:)結(jié)束,提交命令緩沖區(qū)時,這向GPU指示數(shù)據(jù)和管道都已設(shè)置并且GPU可以接管。

后記

本篇主要講述了Metal渲染管道教程,感興趣的給個贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容