原文链接
此文章为了练习我那蹩脚的英文
Note: Metal 程序不能在模拟器中运行; 需要一个 Apple A7 设备.。
Getting Started
配置Metal需要7个步骤:
- 创建 MTLDevice
- 创建 CAMetalLayer
- 创建顶点缓冲区
- 创建顶点着色器
- 创建一个片段着色器
- 创建一个渲染管道
- 创建一个命令队列
创建MTLDevice
首先必获取一个MTLDevice的实例。
可以把MTLDevice想象成连接到GPU的桥梁。创建的所有Metal对象都使用这个MTLDevice。
在controller中引用头文件:
import Metal
Note: 如果出现编译错误,确定你工程中设置的target指向支持Metal的iOS设备。目前Metal还不支持模拟器。
在ViewController中创建一个成员变量:
var device: MTLDevice!
由于在viewDidLoad()
方法中初始化这个变量,所以需要生命为可选类型。为了方便,使用了隐式解包可选类型。
最后在viewDidLoad()
中添加初始化方法:
device = MTLCreateSystemDefaultDevice()
这个方法返回一个MTLDevice的实例。
创建 CAMetalLayer
在iOS中,任何可以看到的东西都基于一个CALayer。CALayer的子类都有自己的作用。
如果想使用Metal在屏幕上绘制一些东西,必须使用CAMetalLayer。
声明一个metallayer :
var metalLayer: CAMetalLayer!
然后添加到viewDidLoad()
方法中:
metalLayer = CAMetalLayer() //1
metalLayer.device = device //2
metalLayer.pixelFormat = .bgra8Unorm //3
metalLayer.framebufferOnly = true //4
metalLayer.frame = view.layer.frame //5
view.layer.addSublayer(metalLayer) //6
1 创建一个CAMetalLayer
- 必须指定一个MTLDevice,将之前创建的device赋给它
- 指定像素格式为bgra8Unorm,“为Blue, Green, Red和Alpha提供8字节,按照这个顺序 — 标准值在0和1之间”,这是CAMetalLayer仅有的两种格式其中之一。
- 由于性能原因,苹果建议将framebufferOnly设置为true,除非你需要在这个layer做纹理采样,或者在可绘制layer上开启计算内核。(Apple encourages you to set framebufferOnly to true for performance reasons unless you need to sample from the textures generated for this layer, or if you need to enable compute kernels on the layer drawable texture)大多数情况,不用这样做。
- 相对于父视图设置layer的位置和大小
- 最终,将layer添加到视图中
创建顶点缓冲区
在Metal中的一切都是三角形,即便是再复杂的3D图形,都可以分解成一系列的三角形。
Metal中,默认的坐标系为归一化坐标系,看起来是一个中心为(0,0,0.5)的2x2x1的立方体。
如果假设Z=0,那么(-1,-1,0)为左下角,(0,0,0)为中心,(1,1,0)为右上角。例子中的三角形的三个点:
1.png我们要为这个三角形创建一个缓冲区,接着添加一个属性:
let vertexData: [Float] = [
0.0,1.0,0.0,
-1.0,-1.0,0.0,
1.0,-1.0,0.0]
这将在CPU中创建一个浮点型数组,接下来要使用MTLBuffer来将其发送到GPU上。
另一个属性:
var vertexBuffer: MTLBuffer!
然后再viewDidLoad()
中添加如下方法:
let dataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0]) // 1
vertexBuffer = device.makeBuffer(bytes:vertexData, length: dataSize, options:[]) //2
- 通过数组中第一个元素的大小乘以数组元素的个数来获取vertexData在内存中所占字节数。
- 使用
makeBuffer(bytes:length:options:)
在GPU上创建一个缓冲区。将空座位默认配置。
创建一个定点着色器
之前创建的顶点将成为接下来要写的vertex shader的一个小程序的输入。顶点着色器就像是在GPU上运行的用Metal Shading Language语言(类似C++语言)编写的小程序。
每个定点都会调用一次定点着色器,其作用则是获取定点的信息,比如位置、颜色、坐标系并且返回一个隐式修改位置和可能的其他数据。
为了简便,顶点着色器将会返回与传入时一样的位置:
接下来创建一个名为Shaders.metal的文件。
Note: 在Metal中可以在一个文件中创建多个顶点着色器。 也可为多个着色器创建不同的文件,因为Metal会将工程中所有的Metail文件都加载一次。
在Shaders.metal文件中添加如下代码:
vertex float4 basic_vertex( //1
const device packed_float3* vertex_array[[buffer(0)]], //2
unsigned int vid [ [ vertex_id ] ]) { //3
return float4(vertex_array[vid], 1.0); //4
)
- 所有的定点着色器命名必须以vertex开头,函数必须返回最终的顶点位置。然后外部将通过名字来访问着色器。
- 第一个参数为指向packed_float3数组的指针—即每个定点的位置。[[ … ]]语法用来声明指定的附加信息,比如资源位置,着色器输入和内置变量。这里使用[[buffer(0)]]来指示这个参数将会由Metal代码发送到定点着色器中第一个数据缓冲区填充。
- 定点着色器将持有一个vertex_id属性,这意味着它将会被填充在顶点数组中指定的顶点的索引。
- 在这里返回基于顶点id的顶点数组内的位置,还将向量转化为float4,最终之为1.0。
创建片段着色器
创建顶点着色器之后,为屏幕中的每一个片段(像素)调用另一个着色器:片段着色器。片段着色器通过内插来自顶点着色器的输出值来获取其输入值。比如三角形底部的两个顶点之间的片段:
3.png该片段的输入值将是底部两个顶点的输出值的50/50的混合。
片段着色器的工作是返回每个片段的最终颜色。为了简便,每个片段都将设置为白色。添加如下代码在Shaders.metal文件中:
fragment half4 basic_fragment() { //1
return half4(1.0) //2
}
- 所有的片段着色器命名必须以fragment开头。方法必须返回片段的最终颜色。返回类型为half4,需要注意half4比float4更加内存高效,因为只占用很少的GPU内存。
- 这里返回(1,1,1,1)白色
创建渲染管道
现在已经创建了片段着色器,需要将他们结合在一起生成一个render pipeline。
Metal非常炫酷于着色器都使用了预编译,渲染管道配置在你第一次设置之后就会编译。这使得一切都很高效。
首先在viewController.swift中添加属性:
var pipelineState: MTLRenderPipelineState!
之后在viewDidLoad()
中添加如下方法:
// 1
let defaultLibrary = device.newDefaultLibrary()!
let fragmentProgram
= defaultLibrary.makeFunction(name: "basic_fragment")
let vertexProgram
= defaultLibrary.makeFunction(name: "basic_vertex")
// 2
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.vertexFunction = vertexProgram
pipelineStateDescriptor.fragmentFunction = fragmentProgram
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
// 3
pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
- 可以通过调用
device.newDefaultLibrary()!
获取MTLLibrary对象来访问项目中任何的预编译着色器。然后可以使用名字来查找着色器 - 配置渲染管道。这包含了你想使用的着色器和颜色附件的像素格式—即正在渲染的输出缓冲区,CAMetaLayer本身。
- 最后,将管道配置编译成管道状态来高效使用。
创建命令队列
最后一步为创建MTLCommandQueue。把这想象成一个命令列表来告诉GPU怎样执行。添加一个新的属性:
var commandQueue: MTLCommandQueue!
接着在viewDidLoad()中添加:
commandQueue = device.makeCommandQueue()
以上,一次性配置代码结束!
绘制三角形
绘制三角形需要5步:
- 创建显示链接
- 创建渲染描述符
- 创建命令缓冲区
- 创建渲染命令编码器
- 提交命令缓冲区
创建显示链接
现在需要一个方法,每当屏幕刷新时重新绘制。在iOS中,可以使用CADisplayLink类。添加一个属性:
var timer: CADisplayLink!
在viewDidLoad()
中初始化:
timer = CADisplayLink(target: self, #selector(ViewController.gameloop))
timer.add(to: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)
没当屏幕刷新时,都会调用gameloop()
方法。
最后添加以下方法:
func render() {
// TODO
}
func gameloop() {
autoreleasepool {
self.render()
}
}
创建渲染描述符
第二步则是创建MTLRenderPassDescriptor,这个对象用来配置哪个纹理正在渲染,清晰的颜色是什么,以及一些其他的配置。
在render()
方法中添加如下方法:
guard let drawable = metalLayer?.nextDrawable() else { return }
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 104.0/255.0, blue: 5.0/255.0, alpha: 1.0)
首先用之前创建的layer来调用nextDrawable()
方法,它会返回需要绘制的纹理,使其显示在屏幕上。
接着配置渲染描述符,使用将loadAction设置为清除,这意味着“在绘制之前,将纹理设置为透明色”,然后将清除颜色设置为绿色。
创建命令缓冲区
下一步则是创建命令缓冲区。把这想象成渲染命令的命令列表。在提交命令缓冲区前,不会执行任何操作。
在render()
方法中添加如下代码:
let commandBuffer = commandQueue.makeCommandBuffer()
一个命令缓冲区可以包含一个或多个渲染命令。
创建渲染命令编码器
创建渲染命令,需要使用一个叫做渲染命令编码器的帮助对象。在render()
中添加如下代码:
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
renderEncoder.setRenderPipelineState(pipelineState)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, at: 0)
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
renderEncoder.endEncoding()
在这里使用之前创建的管道和顶点缓冲区来创建一个命令编码器。
这里最核心的部分为
drawPrimitives(type:vertexStart:vertexCount:instanceCount:)
方法。这里告诉GPU基于顶点缓冲区来绘制一组三角形。每个三角形由三个顶点组成,从顶点缓冲区的第0个开始,这里总共有1个三角形。
最后调用endEncoding()
方法
提交命令缓冲区
最后一步则是提交命令缓冲区。在render()
方法中添加如下代码:
commandBuffer.present(drawable)
commandBuffer.commit()
第一行代码需要确保在绘制完成后立即显示新纹理。接着提交事务将任务发送到GPU上。
最终的效果:
如果程序崩溃, 确保是在真机上运行,具有 A7 或者更好的芯片 ( iPhone 5S, iPhone 6, iPhone 6 Plus, iPad Air, or iPad mini (2nd generation) )。
网友评论