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

版本记录

版本号 时间
V1.0 2018.10.12 星期五

前言

很多做视频和图像的,相信对这个框架都不是很陌生,它渲染高级3D图形,并使用GPU执行数据并行计算。接下来的几篇我们就详细的解析这个框架。感兴趣的看下面几篇文章。
1. Metal框架详细解析(一)—— 基本概览
2. Metal框架详细解析(二) —— 器件和命令(一)
3. Metal框架详细解析(三) —— 渲染简单的2D三角形(一)
4. Metal框架详细解析(四) —— 关于GPU Family 4(一)
5. Metal框架详细解析(五) —— 关于GPU Family 4之关于Imageblocks(二)
6. Metal框架详细解析(六) —— 关于GPU Family 4之关于Tile Shading(三)
7. Metal框架详细解析(七) —— 关于GPU Family 4之关于光栅顺序组(四)
8. Metal框架详细解析(八) —— 关于GPU Family 4之关于增强的MSAA和Imageblock采样覆盖控制(五)
9. Metal框架详细解析(九) —— 关于GPU Family 4之关于线程组共享(六)
10. Metal框架详细解析(十) —— 基本组件(一)
11. Metal框架详细解析(十一) —— 基本组件之器件选择 - 图形渲染的器件选择(二)
12. Metal框架详细解析(十二) —— 基本组件之器件选择 - 计算处理的设备选择(三)
13. Metal框架详细解析(十三) —— 计算处理(一)
14. Metal框架详细解析(十四) —— 计算处理之你好,计算(二)
15. Metal框架详细解析(十五) —— 计算处理之关于线程和线程组(三)
16. Metal框架详细解析(十六) —— 计算处理之计算线程组和网格大小(四)
17. Metal框架详细解析(十七) —— 工具、分析和调试(一)
18. Metal框架详细解析(十八) —— 工具、分析和调试之Metal GPU Capture(二)
19. Metal框架详细解析(十九) —— 工具、分析和调试之GPU活动监视器(三)
20. Metal框架详细解析(二十) —— 工具、分析和调试之关于Metal着色语言文件名扩展名、使用Metal的命令行工具构建库和标记Metal对象和命令(四)
21. Metal框架详细解析(二十一) —— 基本课程之基本缓冲区(一)
22. Metal框架详细解析(二十二) —— 基本课程之基本纹理(二)
23. Metal框架详细解析(二十三) —— 基本课程之CPU和GPU同步(三)
24. Metal框架详细解析(二十四) —— 基本课程之参数缓冲 - 基本参数缓冲(四)
25. Metal框架详细解析(二十五) —— 基本课程之参数缓冲 - 带有数组和资源堆的参数缓冲区(五)
26. Metal框架详细解析(二十六) —— 基本课程之参数缓冲 - 具有GPU编码的参数缓冲区(六)
27. Metal框架详细解析(二十七) —— 高级技术之图层选择的反射(一)
28. Metal框架详细解析(二十八) —— 高级技术之使用专用函数的LOD(一)
29. Metal框架详细解析(二十九) —— 高级技术之具有参数缓冲区的动态地形(一)
30. Metal框架详细解析(三十) —— 延迟照明(一)
31. Metal框架详细解析(三十一) —— 在视图中混合Metal和OpenGL渲染(一)

开始

首先看一下写作环境

Swift 4, iOS 11, Xcode 9

在本教程中,您将深入了解渲染管道并创建一个呈现红色立方体的Metal应用程序。 在此过程中,您将发现所有负责拍摄3D物体并将其转换为您在屏幕上看到的华丽像素的硬件芯片。


The GPU and the CPU - GPU和CPU

所有计算机都有一个中央处理单元(CPU),用于驱动操作并管理计算机上的资源。他们还有一个图形处理单元(Graphics Processing Unit (GPU))

GPU是一种专用硬件组件,可以非常快速地处理图像,视频和大量数据。这称为吞吐量(throughput)。吞吐量通过在特定时间单位中处理的数据量来度量。

另一方面,CPU无法快速处理大量数据,但它可以非常快速地处理许多顺序任务(一个接一个)。处理任务所需的时间称为延迟(latency)

理想的设置包括低延迟和高吞吐量。低延迟允许串行执行排队任务,因此CPU可以执行命令,而不会使系统变慢或无响应;高吞吐量让GPU可以异步渲染视频和游戏而不会拖延CPU。由于GPU具有高度并行化的架构,专门用于重复执行相同的任务,并且很少或没有数据传输,因此能够处理更大量的数据。

下图显示了CPU和GPU之间的主要差异。

CPU具有大容量高速缓存和少量算术逻辑单元(Arithmetic Logic Unit - ALU)内核。 CPU上的低延迟高速缓存用于快速访问临时资源。 GPU没有太多的高速缓冲存储器,并且有更多ALU核心的空间,它们只进行计算而不将部分结果保存到存储器中。

此外,CPU通常只有少数内核,而GPU有数百甚至数千个内核。通过更多核心,GPU可以将问题分解为许多较小的部分,每个部分并行运行在单独的核心上,从而隐藏延迟。在处理结束时,将部分结果合并,并将最终结果返回给CPU。但核心并不是唯一重要的事情!

除了精简之外,GPU内核还具有用于处理几何结构的特殊电路,通常称为着色器内核(shader cores)。这些着色器核心负责您在屏幕上看到的美丽色彩。 GPU一次写入整个帧以适合整个渲染窗口。然后,它将继续尽快渲染下一帧以保持良好的帧速率。

CPU继续向GPU发出命令以使其保持忙碌,但在某些时候,CPU将完成发送命令或GPU将完成处理它收到的命令。 为了避免停止,CPU上的Metal会在命令缓冲区中排队多个命令,并按顺序为下一帧发出新命令,而不必等待GPU完成第一帧。 这样,无论谁先完成工作,都会有更多的工作要做。

一旦接收到所有命令和资源,图形管道的GPU部分就会启动。


The Metal Project - Metal工程

您一直在使用Playgrounds来了解MetalPlaygrounds非常适合测试和学习新概念。 了解如何设置完整的Metal项目非常重要。 由于iOS模拟器不支持Metal,因此您将使用macOS应用程序。

注意:本教程的挑战项目的项目文件还包括iOS target

使用Cocoa App模板创建一个新的macOS应用程序。

将项目命名为Pipeline并选中Use Storyboards。 不选中其余选项。

打开Main.storyboard并在View Controller Scene下选择View

Identity检查器中,将视图从NSView更改为MTKView

这会将主视图设置为MetalKit View

打开ViewController.swift。 在文件的顶部,导入MetalKit框架:

import MetalKit

然后,将此代码添加到viewDidLoad()

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

你现在有了选择。 您可以将MTKView子类化并在sb中使用此视图。 在这种情况下,每个帧都会调用子类的draw(_ :),并将绘图代码放在该方法中。 但是,在本教程中,您将设置符合MTKViewDelegateRenderer类,并将Renderer设置为MTKView的委托。 MTKView每帧调用一个委托方法,这是你放置必要的绘图代码的地方。

注意:如果您来自不同的API世界,您可能正在寻找游戏循环结构。 您可以选择扩展CAMetalLayer而不是创建MTKView。 然后,您可以使用CADisplayLink进行计时;但Apple推出的MetalKit协议可以更轻松地管理游戏循环。


The Renderer Class - Renderer类

创建一个名为Renderer.swift的新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")
  }
}

在这里,您创建一个初始化程序,并使用两个MTKView委托方法使Renderer遵守MTKViewDelegate

  • mtkView(_:drawableSizeWillChange :):每次窗口大小改变时调用。 这允许您更新渲染坐标系。
  • draw(in :):每帧调用一次。

ViewController.swift中,添加一个属性来保存renderer

var renderer: Renderer?

viewDidLoad()的末尾,初始化renderer

renderer = Renderer(metalView: metalView)

Initialization - 初始化

首先,您需要设置Metal环境。

MetalOpenGL具有一个主要优势,因为您可以预先实例化某些对象,而不是在每个帧中创建它们。 下图显示了您可以在应用程序开头创建的一些对象。

  • `MTLDevice``:GPU硬件设备的软件引用。
  • MTLCommandQueue:负责每帧创建和组织MTLCommandBuffers
  • MTLLibrary:包含顶点和片段着色器函数的源代码。
  • MTLRenderPipelineState:设置绘图的信息,例如要使用的着色器函数,要使用的深度和颜色设置以及如何读取顶点数据。
  • MTLBuffer:以可以发送到GPU的形式保存数据,例如顶点信息。

通常,您的应用程序中将有一个MTLDevice,一个MTLCommandQueue和一个MTLLibrary对象。 您还将拥有几个MTLRenderPipelineState对象,这些对象将定义各种管道状态,以及几个用于保存数据的MTLBuffers

但是,在使用这些对象之前,需要初始化它们。 将这些属性添加到Renderer

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

这些是保持对不同对象的引用所需的属性。 为方便起见,它们目前都是隐式解包的选项,但您可以在完成初始化后更改它。 此外,您不需要保留对MTLLibrary的引用,因此无需创建它。

接下来,在super.init()之前将此代码添加到init(metalView :)

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

这会初始化GPU并创建command queue。 您正在使用设备和命令队列的类属性来确保每个属性中只有一个存在。 在极少数情况下,您可能需要不止一个 - 但在大多数应用程序中,一个就够了。

最后,在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绘图方法。

Build并运行应用程序以确保一切都已设置并正常运行。 如果一切顺利,你应该看到一个普通的灰色窗口。 在调试控制台中,您将反复看到“draw”一词。 使用此选项可验证您的应用是否正在为每个帧调用draw(in :)

注意:您不会看到metalView的奶油色,因为您还没有要求GPU进行任何绘图。


Set Up the Data - 设置数据

构建3D基元网格的类总是有用的。 在本教程中,您将设置一个用于创建3D形状基元的类,并且您将为其添加一个立方体。

创建一个名为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 :)中,在调用super.init()之前,设置网格mesh

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

然后,设置包含您将发送到GPU的顶点数据的MTLBuffer

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着色器不同,这些是在编译项目时编译的,这比动态编译更有效。 结果存储在库中。

现在,创建管道状态:

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将写入的纹理的像素格式。

您还可以设置管道的顶点描述符(vertex descriptor)。 这就是GPU将如何解释您将在网格数据MTLBuffer中呈现的顶点数据的方式。

如果需要调用不同的顶点或片段函数,或使用不同的数据布局,那么您将需要更多的管道状态。 创建管道状态相对耗时,这就是为什么你提前做到这一点的原因,但在帧期间切换管道状态是快速和有效的。

初始化完成,您的项目将编译。 但是,如果您尝试运行它,则会出现错误,因为您尚未设置着色器函数。


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

这将设置渲染命令编码器(render command encoder)并将视图的可绘制纹理呈现给GPU。


Drawing - 绘制

在CPU方面,要准备GPU,您需要为其提供数据和管道状态。 然后,您需要发出绘制调用。

仍在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可以接管。

后记

本篇主要讲述了Metal渲染管道教程,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容