学习3D的API, 比如OpenGL, DirectX, 做的第一个渲染总会是三角形. 所以, 我们也来看下如何用Metal来渲染一个三角形.

扩展MyMetalView

我们给上次帖子中的MyMetalView增加几个成员函数待用.

class MyMetalView: MTKView {
    var vertexData: [Float]!
    var vertexBuffer: MTLBuffer!

    var renderPipelineState: MTLRenderPipelineState!

    var cmdQueue: MTLCommandQueue!
}

准备顶点

我们在屏幕中间画一个三角形, 三个顶点的数据放在vertexData里面.

并且生成了一个MTLBuffer, vertexBuffer.

public func initVertex() {
    vertexData = [-0.7, -0.7, 0.0, 1.0,
    0.7, -0.7, 0.0, 1.0,
    0.0, 0.7, 0.0, 1.0]

    let vertexDataSize = vertexData.count * MemoryLayout<Float>.size

    vertexBuffer = (self.device?.makeBuffer(bytes: vertexData, length: vertexDataSize, options: []))!
}

Metal Shader

我们需要写一个最基本的shader来渲染.

#include <metal_stdlib>

using namespace metal;

struct Vertex {
    float4 postion [[position]];
};

vertex Vertex vertex_func(constant Vertex *vertices [[buffer(0)]],
                          uint vid [[vertex_id]] ){
    return vertices[vid];
}

fragment float4 fragment_func(Vertex vert [[stage_in]]) {
    return float4(0.7, 1, 1, 1);
}

我们在后面再详细讨论Metal Shader.

public func initShader() {
    let shaderStr = """ ... """
    do {
        let library = try self.device?.makeLibrary(source: shaderStr, options: nil)

        let vertex_func = library?.makeFunction(name: "vertex_func")
        let fragment_func = library?.makeFunction(name: "fragment_func")

        let renderPipelineDescriptor = MTLRenderPipelineDescriptor()

        renderPipelineDescriptor.vertexFunction = vertex_func
        renderPipelineDescriptor.fragmentFunction = fragment_func

        renderPipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormat.bgra8Unorm

        renderPipelineState = try self.device?.makeRenderPipelineState(descriptor: renderPipelineDescriptor)
    }
    catch let e {
        print("\(e)")
        fatalError()
    }
}

为了便于编辑shader内容, 我们将shader放入变量shaderStr中. 通过上面代码就获得了一个带有我们自定义的MTLRenderPipelineState.

渲染

接下来渲染.

public override func draw(_ dirtyRect: NSRect) {
    let renderPassDescriptor = self.currentRenderPassDescriptor!
    let drawable = self.currentDrawable
        
    let bgColor = MTLClearColor(red: 0.3, green: 0.4, blue: 0.5, alpha: 1)

    renderPassDescriptor.colorAttachments[0].clearColor = bgColor

    let cmdBuffer = cmdQueue.makeCommandBuffer()!
        
    let cmdEncoder = cmdBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)!

    cmdEncoder.setRenderPipelineState(self.renderPipelineState!)
    cmdEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
    cmdEncoder.drawPrimitives(type: MTLPrimitiveType.triangle, vertexStart: 0, vertexCount: 3)
    cmdEncoder.endEncoding()
        
    cmdBuffer.present(drawable!)
    cmdBuffer.commit()
}

一点感慨

在写这个三角形的demo的时候, 走了一点弯路, 感觉写对了, 三角形却怎么都渲染不出来. 翻来覆去的比较代码, 没有一点头绪.

这时我把MyMetalView搬到一个空白的Xcode工程中进行调试, 点了一下"Capture GPU frame", 马上打开了一片新天地. 在GPU运行堆栈, 看到Geometry的顶点数据异常. 然后反过来看顶点数据初始化的时候, vertexData 声明成了 initVertex的局部变量. 这样导致渲染的时候, vertexData就变成了未定义的值了. 顺利解决问题.

想到学习OpenGL的时候, 一旦渲染出错, 真是一筹莫展. Metal借助Xcode这种一点即用的GPU调试功能, 一定会对我们掌握3D渲染大有裨益.

同样代码也都托管在github上. https://github.com/young40/LearnMetal . 欢迎star, 感谢!