Metal 练习:第一篇入门2D


Metal 练习:第一篇

在 iOS 8, Apple 发布了自己的3D图形GPU加速器:Metal。
Metal与OpenGL ES相似,都是一套底层的API来与3D图形硬件进行交互。不同的是Metal不是跨平台,从这一篇开始,我们将会介绍 Metal API。 将会学习Metal中一些重要的类,如device,command queue等等。

* Metal应用不能跑在 iOS的模拟器上,需要一台真机 A7芯片及以上

Metal vs. OpenGL ES

OpenGL ES被设计在跨平台上使用,那意味着你可以用C++来写OpenGL ES ,然后修改很少的代码就可以跑在其它的平台上,如安卓。 Apple认为OpenGL ES的跨平台支持做的非常好,但是还没有充分利用Apple的硬件的优势,所以Apple要采用一种全新的方式将软件与硬件结合更完美。 所以就有了Metal。可以提供10倍的绘制速度于OpenGL ES。参考 WWDC2014 keynote

编码

代码主要有两个文件 ViewController.swift 和 Shaders.metal, 将这两个文件拉到一个新建的工程可以直接运行。 编码的步骤及注释如下

// ViewController.swift

/// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// 步骤:
/// 在开始渲染前有几个必要的步骤创建Metal
/// 1. MTLDevice
/// 2. CAMetalLayer
/// 3. Vertex Buffer
/// 4. Vertex Shader
/// 5. Fragment Shader
/// 6. Render Pipeline
/// 7. Command Queue
/// 开始渲染
/// 1. Create a Display Link
/// 2. Create a Render Pass Descriptor
/// 3. Create a Command Buffer
/// 4. Create a Render Command Encoder
/// 5. Commit your Command Buffer
/// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

import UIKit
import Metal

class ViewController: UIViewController {

    /// direct connect to the GPU, other Metal Objects(like command queues, buffers and textures) you need using this MTLDevie
    var device: MTLDevice!
    /// a special subclass of CALayer for Metal
    var metalLayer: CAMetalLayer!
    
    var vertexBuffer: MTLBuffer!
    
    var pipelineState: MTLRenderPipelineState!
    /// 命令的有序列表,让GPU执行
    var commandQueue: MTLCommandQueue!
    
    /// -------------------------------------------------------------
    var timer: CADisplayLink!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 1. create MTLDevice
        device = MTLCreateSystemDefaultDevice()
        
        // 2. create CAMetalLayer
        metalLayer = CAMetalLayer()
        // 2.1 给layer指定device
        metalLayer.device = device
        // 2.2 8 bytes 以这个顺序 blue, green, red, alpha
        metalLayer.pixelFormat = .bgra8Unorm
        // 2.3 推荐使用 true
        metalLayer.framebufferOnly = true
        // 2.4 设置图层的frame
        metalLayer.frame = view.layer.frame
        // 2.5 添加到图层上
        view.layer.addSublayer(metalLayer)
        
        // 3. 创建 vertex buffer
        let vertexData: [Float] = [
            0.0, 1.0, 0,
            -1.0, -1.0, 0.0,
            1.0, -1.0, 0.0
        ]
        // 3.1 以 bytes 形式获得 vertexData 的大小, 首元素的大小 * count
        let dataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0])
        // 3.2 用下面的方法在GPU上创建一个新的buffer, 将 vertexData 数据从CPU绑定到buffer上
        vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: [])
        
        // 4. 创建 vertex shader, 见 Shaders.metal 文件
        
        // 5. 创建 fragment shader,见 Shaders.metal 文件
        
        // 6. 创建 render pipeline
        // 6.1 包含在工程内的预编译 `shaders` 都可以通过 `MTLLibrary` 访问,然后根据名字查找
        let defaultLibrary = device.makeDefaultLibrary()!
        let fragmentProgram = defaultLibrary.makeFunction(name: "basic_fragment")
        let vertexProgram = defaultLibrary.makeFunction(name: "basic_vertex")
        // 6.2 设置 render pipeline 配置
        let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
        pipelineStateDescriptor.vertexFunction = vertexProgram
        pipelineStateDescriptor.fragmentFunction = fragmentProgram
        pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
        // 6.3 绑定到 pipeline state
        pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
        
        // 7. 创建 command queue
        commandQueue = device.makeCommandQueue()!
        
        /// -------------------------------------------------------------
        // Render 1. 创建一个Display Link,一个定时器与屏幕刷新率同步
        timer = CADisplayLink(target: self, selector: #selector(gameloop))
        timer.add(to: RunLoop.main, forMode: .default)
        
        // Render 2. 创建 MTLRenderPassDescriptor,配置texture渲染
        // 见 render() 方法
        
        // Render 3. 创建 command buffer
        // 见 render() 方法
        
        // Render 4. 创建 render command encoder
        // 见 render() 方法
        
        // Render 5. 提交 command buffer
        // 见 render() 方法
    }
}

extension ViewController {
    
    func render() {
        // Render 2.
        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: 55.0 / 255.0,
                                                                            alpha: 1.0)
        
        // Render 3. 想象成一个 render commmands 队列,直到提交时才会执行
        let commandBuffer = commandQueue.makeCommandBuffer()!
        
        // Render 4. 创建 render Encoder,指定了管线和vertex buffer
        let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)!
        renderEncoder.setRenderPipelineState(pipelineState)
        renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
        renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
        renderEncoder.endEncoding()
        
        // Render 5.
        commandBuffer.present(drawable)
        commandBuffer.commit()
    }
    
    @objc func gameloop() {
        autoreleasepool {
            self.render()
        }
    }
}

// 创建方法: File -> New -> File -> Metal File 
// Shaders.metal

#include 
using namespace metal;

// 4. 创建 vertex shader
vertex float4 basic_vertex( // vertex shader 要以关键字 `vertex` 开头,必须有一个返回值(float4:包含4个float值的向量)
    const device packed_float3* vertex_array[[buffer(0)]], // packed_float3:包含3个float向量,[[...]]语义定义一个属性,[[buffer(0)]]表明从Metal代码中传递来的数据第一个缓冲区将填充这个参数
                           unsigned int vid [[vertex_id]]) { // 特定的`vertex_id`属性,将用顶点数组中指定索引位置填充
    return float4(vertex_array[vid], 1.0); // 根据`vid`找到顶点数据且转换为float4并返回
}

// 5. 创建 fragment shader
fragment half4 basic_fragment() { // fragment shader 要以关键字 `fragment` 开头,要返回fragment的color, half4:RGBA四分量的颜色值
    return half4(1.0); // 此处返回的全1,是白色
}

参考及更多资料

  • 原文:Metal Tutorial: Getting Started
  • Apple’s Metal for Developers page
  • Apple’s Metal Programming Guide
  • Apple’s Metal Shading Language Guide
  • WWDC2014 For Metal