这个系列是我用来学习 Metal API 的笔记,我的最终目的是希望实现一个基于 Metal 的游戏引擎。
目前系列有:
hello-metal.1 看到了绿色
hello-metal.2 第一个三角形
hello-metal.3 四边形
hello-metal.4 动画
hello-metal.5 材质贴图
上一篇文章其实已经把基础内容讲完了,我们已经把绘制的流程走通了,这篇文章会说一下如何绘制一个四边形。
重构代码 首先我们需要稍微重构一下代码,现在的代码都耦合在一个函数里,这对我们非常的不好,我们抽出来一个 Renderer 的文件,专门存放绘制代码。
首先基于 NSObject 派生出 Renderer 类,将各种成员变量都转移到这里,并在在构造函数接受最重要的 MTLDevice 对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import MetalKitenum Colors { static let wenderlichGreen = MTLClearColor (red: 0.0 , green: 0.4 , blue: 0.21 , alpha: 1.0 ) }class Renderer : NSObject { let device: MTLDevice let commandQueue: MTLCommandQueue var vertices: [Float ] = [ 0 , 1 , 0 , - 1 , - 1 , 0 , 1 , - 1 , 0 ] var pipelineState: MTLRenderPipelineState ? var vertexBuffer: MTLBuffer ? init (device : MTLDevice ) { self .device = device commandQueue = device.makeCommandQueue()! super .init () buildModel() buildPipelineState() } private func buildModel () { vertexBuffer = device.makeBuffer(bytes: vertices, length: vertices.count * MemoryLayout <Float >.size, options: []) } private func buildPipelineState () { let library = device.makeDefaultLibrary() let vertexFunction = library? .makeFunction(name: "vertex_shader" ) let fragmentFunction = library? .makeFunction(name: "fragment_shader" ) let pipelineDescriptor = MTLRenderPipelineDescriptor () pipelineDescriptor.vertexFunction = vertexFunction pipelineDescriptor.fragmentFunction = fragmentFunction pipelineDescriptor.colorAttachments[0 ].pixelFormat = .bgra8Unorm do { pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor) } catch let error as NSError { print ("error: \(error.localizedDescription) " ) } } }
我们把所有的代码都转移到新的类中,只需要在 ViewController 中保留 Renderer 对象就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import UIKitimport MetalKitclass ViewController : UIViewController { var metalView: MTKView { return view as! MTKView } var renderer: Renderer ! override func viewDidLoad () { super .viewDidLoad() metalView.device = MTLCreateSystemDefaultDevice () metalView.clearColor = Colors .wenderlichGreen renderer = Renderer (device: metalView.device! ) } }
metalView 支持 delegate,我们可以让 Renderer 派生自 MTKViewDelegate,就可以将原本绘制三角形部分的代码转移到 draw 函数中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 extension Renderer : MTKViewDelegate { func mtkView (_ view : MTKView , drawableSizeWillChange size : CGSize ) { } func draw (in view : MTKView ) { let commandBuffer = commandQueue.makeCommandBuffer() let commandEncoder = commandBuffer? .makeRenderCommandEncoder(descriptor: view.currentRenderPassDescriptor! ) commandEncoder? .setRenderPipelineState(pipelineState! ) commandEncoder? .setVertexBuffer(vertexBuffer, offset: 0 , index: 0 ) commandEncoder? .drawPrimitives(type: .triangle, vertexStart: 0 , vertexCount: vertices.count) commandEncoder? .endEncoding() commandBuffer? .present(view.currentDrawable! ) commandBuffer? .commit() } }
回到 ViewController.swift 中,我们需要在 viewDidLoad 函数中将 metalView 的 delegate 设置为 renderer。
1 metalView.delegate = renderer
swift 提供一个 guard 的语法,可以方便的进行一些检查,可以把 draw 里一定存在的变量统一检查,如果任何一个不存在,都会运行到 else 中。
1 2 3 4 5 6 guard let drawable = view.currentDrawable, let pipelineState = pipelineState, let descriptor = view.currentRenderPassDescriptorelse { return }
顶点索引 在上一篇讲过,顶点数组需要处理成顶点缓冲区,如果我们准备画一个四边形,正常来说我们需要准备两份三角形的坐标,但是!注意了,但是啊!由于两个三角形的斜边其实是同一条,那么意味着两个三角形的两个顶点,其实坐标是一样的,他们是重复的,如果我们完整的发送了全部顶点信息,虽然可以正常使用,但是数据量比较大的时候,就会浪费更多的资源。
一个模型通常由上万个三角形组成,每两个三角形都会浪费两个顶点,那么浪费掉的空间就不能忽略了,所以 Metal、OpenGL 这些 API 都提供了顶点索引功能,我们只需要发送顶点的坐标,然后使用顶点索引将点联系起来,这样就不用发送重复的顶点信息,只需要告诉 GPU,嘿老兄,这个点又被我用了,麻烦你读取一下吧。
我们将顶点数组换成这样的数据:
1 2 3 4 5 6 var vertices: [Float ] = [ - 1 , 1 , 0 , - 1 , - 1 , 0 , 1 , - 1 , 0 , 1 , 1 , 0 , ]
我们创建一个 indices,用来保存顶点索引,同样是三位一组,一共两组,使用了顶点数组中的三组坐标信息。
1 2 3 4 let indices: [UInt16 ] = [ 0 , 1 , 2 , 2 , 3 , 0 ]
同样的,我们还需要使用一个 MTLBuffer 保存顶点索引缓存。
1 var indexBuffer: MTLBuffer ?
在 buildModel 函数里初始化顶点索引缓存。
1 2 3 indexBuffer = device.makeBuffer(bytes: indices, length: indices.count * MemoryLayout <UInt16 >.size, options: [])
绘制四边形 我们已经有顶点缓冲了,也有顶点索引缓冲,可以去试一下了。
在 guard 中赋值一下,保证它不为空。
1 2 3 4 5 6 7 guard let drawable = view.currentDrawable, let pipelineState = pipelineState, let indexBuffer = indexBuffer, let descriptor = view.currentRenderPassDescriptorelse { return }
接下来就是比较重要的部分了,我们要如何使用顶点索引呢?
MTLCommandEncoder 有一个 drawIndexedPrimitives 函数,可以接受顶点索引缓冲。
1 2 3 4 5 commandEncoder? .drawIndexedPrimitives(type: .triangle, indexCount: indices.count, indexType: .uint16, indexBuffer: indexBuffer, indexBufferOffset: 0 )
让我们运行一下吧!
非常棒,我们看到整个屏幕都是红色了。
完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 import MetalKitenum Colors { static let wenderlichGreen = MTLClearColor (red: 0.0 , green: 0.4 , blue: 0.21 , alpha: 1.0 ) }class Renderer : NSObject { let device: MTLDevice let commandQueue: MTLCommandQueue var vertices: [Float ] = [ - 1 , 1 , 0 , - 1 , - 1 , 0 , 1 , - 1 , 0 , 1 , 1 , 0 , ] let indices: [UInt16 ] = [ 0 , 1 , 2 , 2 , 3 , 0 ] var pipelineState: MTLRenderPipelineState ? var vertexBuffer: MTLBuffer ? var indexBuffer: MTLBuffer ? init (device : MTLDevice ) { self .device = device commandQueue = device.makeCommandQueue()! super .init () buildModel() buildPipelineState() } private func buildModel () { vertexBuffer = device.makeBuffer(bytes: vertices, length: vertices.count * MemoryLayout <Float >.size, options: []) indexBuffer = device.makeBuffer(bytes: indices, length: indices.count * MemoryLayout <UInt16 >.size, options: []) } private func buildPipelineState () { let library = device.makeDefaultLibrary() let vertexFunction = library? .makeFunction(name: "vertex_shader" ) let fragmentFunction = library? .makeFunction(name: "fragment_shader" ) let pipelineDescriptor = MTLRenderPipelineDescriptor () pipelineDescriptor.vertexFunction = vertexFunction pipelineDescriptor.fragmentFunction = fragmentFunction pipelineDescriptor.colorAttachments[0 ].pixelFormat = .bgra8Unorm do { pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor) } catch let error as NSError { print ("error: \(error.localizedDescription) " ) } } }extension Renderer : MTKViewDelegate { func mtkView (_ view : MTKView , drawableSizeWillChange size : CGSize ) { } func draw (in view : MTKView ) { guard let drawable = view.currentDrawable, let pipelineState = pipelineState, let indexBuffer = indexBuffer, let descriptor = view.currentRenderPassDescriptor else { return } let commandBuffer = commandQueue.makeCommandBuffer() let commandEncoder = commandBuffer? .makeRenderCommandEncoder(descriptor: view.currentRenderPassDescriptor! ) commandEncoder? .setRenderPipelineState(pipelineState) commandEncoder? .setVertexBuffer(vertexBuffer, offset: 0 , index: 0 ) commandEncoder? .drawIndexedPrimitives(type: .triangle, indexCount: indices.count, indexType: .uint16, indexBuffer: indexBuffer, indexBufferOffset: 0 ) commandEncoder? .endEncoding() commandBuffer? .present(view.currentDrawable! ) commandBuffer? .commit() } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <metal_stdlib> using namespace metal;vertex float4 vertex_shader (const device packed_float3 *vertices [[ buffer(0 ) ]], uint vertexId [[ vertex_id ]]) { float4 position = float4 (vertices[vertexId], 1 ); return position; }fragment half4 fragment_shader () { return half4 (1 , 0 , 0 , 1 ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import UIKitimport MetalKitclass ViewController : UIViewController { var metalView: MTKView { return view as! MTKView } var renderer: Renderer ! override func viewDidLoad () { super .viewDidLoad() metalView.device = MTLCreateSystemDefaultDevice () metalView.clearColor = Colors .wenderlichGreen renderer = Renderer (device: metalView.device! ) metalView.delegate = renderer } }