译者注:本文是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 的架构:
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 就是用于华丽的图形及快速又平滑的动画。下一步,你将让你的立方体在屏幕上,上下来回移动。为了实现这个效果,你须要一个每帧更新的计时器,立方体的位置将依赖于这个计时器。你将在顶点函数中更新顶点的位置,这样就会将计时器数据发送到 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 开发者最好的学习资源。
若是你对本教程还有什么问题或意见,请在下面留言讨论!