MetalKit Introduction

这是MetalKit和Swift多部分系列的第2部分,阅读可能会有帮助 part 1. 对于第2部分,我们将继续使用第1部分中的代码进行开发. Our goal is to create a rotating cube. 在接下来的部分,我们将介绍照明和输入检测. 为此,我们需要创建一些新类来充当对象,并修改第1部分中的一些现有代码. 如果你对其他的MetalKit和Swift开发感兴趣,可以看看我朋友的 channel.

Node.swift

First, we shall create a file called Node which will act as our base object class. 节点可以有子节点,并且能够呈现所有的子节点.

import MetalKit

class Node {
    var childern: [Node] = []
    
    func add(child: Node) {
        childern.append(child)
    }
    
    func render(commandEncoder: MTLRenderCommandEncoder, deltaTime: Float) {
        childern.forEach{ $0.render(commandEncoder: commandEncoder, deltaTime: deltaTime)}
    }
}

Primitive.swift

Second, we will need to create a file named Primitive which will handle MTLBuffers and MTLStates. Primitives 将用于创建对象内部的场景吗. We will create a Cube object later.

import MetalKit

class Primitive: Node {
    // Buffers
    var vertexBuffer: MTLBuffer!
    var indexBuffer: MTLBuffer!
    // BufferData
    var vertices: [Vertex]!
    var indices: [UInt16]!
    // States
    var renderPipelineState: MTLRenderPipelineState!
    var depthStencilState: MTLDepthStencilState!
    // Constraints
    var modelConstraints = ModelConstraints()
    // ...

var depthStencilState: MTLDepthStencilState

深度和模板状态对象,用于指定渲染通道中使用的深度、模板配置和操作. MTLRenderCommandEncoder使用一个MTLDepthStencilState对象来设置渲染通道的深度和模板状态.

Now we will create the init method of the Primitive. Similar to the Renderer 在第1部分中,我们将使用该设备来帮助我们创建所需的大多数对象.

init(withDevice device: MTLDevice) {
        super.init()
        buildVertices()
        buildBuffers(device: device)
        buildPipelineState(device: device)
        buildDepthStencil(device: device)
}

Build Vertices 将创建顶点和索引数组的对象. 它将从继承的对象重写 Primitive.

public func buildVertices() {
    
}

Build Buffers we will use the method makeBuffer. 这将使用三个参数:字节、长度和选项.

makeBuffer(bytes:length:options:)

分配一个给定长度的新缓冲区,并通过复制现有数据来初始化它的内容.

private func buildBuffers(device: MTLDevice) {
        vertexBuffer = device.makeBuffer(bytes: vertices,
                                         length: MemoryLayout.stride * vertices.count,
                                         options: [])
        indexBuffer = device.makeBuffer(bytes: indices,
                                        length: MemoryLayout.stride * indices.count,
                                        options: [])
}

Build Pipeline State 将设置顶点和碎片着色函数. We will also define a VertextDescriptor, 这将帮助金属顶点着色函数, to better understand what we want to pass to it.

private func buildPipelineState(device: MTLDevice) {
        let library = device.makeDefaultLibrary()
        // Retrieve the shader functions
        let vertexFunction = library?.makeFunction(name: "basic_vertex_function")
        let fragmentFunction = library?.makeFunction(name: "basic_fragment_function")
        
        // Create the renderPiplineDescriptor
        let renderPipelineDescriptor = MTLRenderPipelineDescriptor()
        renderPipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
        renderPipelineDescriptor.vertexFunction = vertexFunction
        renderPipelineDescriptor.fragmentFunction = fragmentFunction
        renderPipelineDescriptor.depthAttachmentPixelFormat = .depth32Float
        
        // Create the VertexDescriptor
        let vertexDescriptor = MTLVertexDescriptor()
        vertexDescriptor.attributes[0].bufferIndex = 0
        vertexDescriptor.attributes[0].format = .float3
        vertexDescriptor.attributes[0].offset = 0
        
        vertexDescriptor.attributes[1].bufferIndex = 0
        vertexDescriptor.attributes[1].format = .float4
        vertexDescriptor.attributes[1].offset = MemoryLayout.size
        
        vertexDescriptor.layouts[0].stride = MemoryLayout.stride
        
        // Create the PipelineState
        renderPipelineDescriptor.vertexDescriptor = vertexDescriptor
        
        do {
            renderPipelineState = try device.makeRenderPipelineState(描述符:renderPipelineDescriptor)
        } catch {
            print(error.localizedDescription)
        }
}

MTLVertexDescriptor

描述顶点数据如何组织并映射到顶点函数的对象.

Build Depth Stencil will create an MTLDepthStencilDescriptor. Which we will use to create the Depth Stencil.

private func buildDepthStencil(设备:MTLDevice) {
        let depthStencilDescriptor = MTLDepthStencilDescriptor()
        depthStencilDescriptor.isDepthWriteEnabled = true
        depthStencilDescriptor.depthCompareFunction = .less
        depthStencilState = device.makeDepthStencilState(描述符:depthStencilDescriptor)
}

Finally, we have the Render function which deals with the Command Encoder. This is used inside the Renderer’s draw 函数,将被场景中的每个节点调用. 我们将把大多数内建的值传递给 Command Encoder.

覆盖func render(commandEncoder: MTLRenderCommandEncoder, deltaTime: Float) {
        commandEncoder.setRenderPipelineState(renderPipelineState)
        super.render(commandEncoder: commandEncoder, deltaTime: deltaTime)
        commandEncoder.setVertexBuffer(vertexBuffer,
                                       offset: 0,
                                       index: 0)
        commandEncoder.setDepthStencilState(depthStencilState)
        commandEncoder.setVertexBytes(&modelConstraints, length: MemoryLayout.stride, index: 1)
        commandEncoder.drawIndexedPrimitives(type: .triangle,
                                             indexCount: indices.count,
                                             indexType: .uint16,
                                             indexBuffer: indexBuffer,
                                             indexBufferOffset: 0)
}

Types.swift

This is where we will create our 模型约束,场景约束和可约束. Also, we will move the Vertex struct we made from part 1.

import MetalKit

struct Vertex {
    var position: float3
    var color: float4
}

struct ModelConstraints {
    var modelMatrix = matrix_identity_float4x4
}

struct SceneConstraints {
    var projectionMatrix = matrix_identity_float4x4
}

protocol Constraintable {
    func scale(axis: float3)
    
    func translate(direction: float3)
    
    func rotate(angle: Float, axis: float3)
}

我们将使用Constraints来修改场景中对象的值. 我们将需要创建用于缩放、平移和旋转的数学函数.

Math.swift

import MetalKit

extension Float {
    var rads: Float {
        return (self / 180) * Float.pi
    }
}

extension matrix_float4x4 {
    
    init(degreesFov: Float, aspectRatio: Float, nearZ: Float, farZ: Float) {
        let fov = degreesFov.rads
        
        let y = 1 / tan(fov * 0.5)
        let x = y / aspectRatio
        let z1 = farZ / (nearZ - farZ)
        let w = (z1 * nearZ)
        
        columns = (
            float4(x,0,0,0),
            float4(0,y,0,0),
            float4(0,0,z1,-1),
            float4(0,0,w,0)
        )
    }
    
    mutating func scale(axis: float3) {
        var result = matrix_identity_float4x4
        
        let x,y,z :Float
        (x,y,z) = (axis.x,axis.y,axis.z)
        
        result.columns = (
            float4(x,0,0,0),
            float4(0,y,0,0),
            float4(0,0,z,0),
            float4(0,0,0,1)
        )
        
        self = matrix_multiply(self, result)
    }
    
    改变func rotate(角度:Float,轴:float3) {
        var result = matrix_identity_float4x4
        
        let x,y,z :Float
        (x,y,z) = (axis.x,axis.y,axis.z)
        let c: Float = cos(angle)
        let s: Float = sin(angle)
        
        let mc: Float = (1 - c)
        
        let r1c1 = x * x * mc + c
        let r2c1 = x * y * mc + z * s
        let r3c1 = x * z * mc - y * s
        let r4c1: Float = 0.0
        
        let r1c2 = y * x * mc - z * s
        let r2c2 = y * y * mc + c
        let r3c2 = y * z * mc + x * s
        let r4c2: Float = 0.0
        
        let r1c3 = z * x * mc + y * s
        let r2c3 = z * y * mc - x * s
        let r3c3 = z * z * mc + c
        let r4c3: Float = 0.0
        
        let r1c4: Float = 0.0
        let r2c4: Float = 0.0
        let r3c4: Float = 0.0
        let r4c4: Float = 1.0
        
        result.columns = (
            float4(r1c1,r2c1,r3c1,r4c1),
            float4(r1c2,r2c2,r3c2,r4c2),
            float4(r1c3,r2c3,r3c3,r4c3),
            float4(r1c4,r2c4,r3c4,r4c4)
        )
        
        self = matrix_multiply(self, result)
    }
    
    mutating func translate(direction: float3) {
        var result = matrix_identity_float4x4
        
        let x,y,z :Float
        (x,y,z) = (direction.x,direction.y,direction.z)
        
        result.columns = (
            float4(1,0,0,0),
            float4(0,1,0,0),
            float4(0,0,1,0),
            float4(x,y,z,1)
        )
        
        self = matrix_multiply(self, result)
    }
}

现在我们已经添加了type并实现了Math函数. We can extend our Primitive object by adding the following lines.

extension Primitive: Constraintable {
    func scale(axis: float3) {
        modelConstraints.modelMatrix.scale(axis: axis)
    }
    
    func translate(direction: float3) {
        modelConstraints.modelMatrix.translate(direction: direction)
    }
    
    func rotate(angle: Float, axis: float3) {
        modelConstraints.modelMatrix.rotate(angle: angle, axis: axis)
    }
}

Scene.swift

Now we will create a Scene object that will contain Primitive objects.

import MetalKit

class Scene: Node {
    var device: MTLDevice!
    var sceneConstraints = SceneConstraints()
    var objects: [Primitive] = []
    
    init(device: MTLDevice) {
        self.device = device
        super.init()
        sceneConstraints.projectionMatrix = matrix_float4x4(degreesFov: 45, aspectRatio: 1, nearZ: 0.1, farZ: 100)
    }
    
    覆盖func render(commandEncoder: MTLRenderCommandEncoder, deltaTime: Float) {
        commandEncoder.setVertexBytes(&sceneConstraints, length: MemoryLayout.stride, index: 2)
        super.render(commandEncoder: commandEncoder, deltaTime: deltaTime)
    }
}

Cube.swift

Now we will create our first Primitive object. 这很简单,因为我们只需要定义一个函数, buildVertices.

import MetalKit

class Cube: Primitive {
    
    override func buildVertices() {
        vertices = [
            顶点(position: float3(-1,1), color: float4(1,0,0,1)),
            顶点(位置:float3(-1,-1,1),颜色:float4(0,1,0,1)),
            顶点(position: float3(1,1,1), color: float4(0,0,1,1)),
            顶点(position: float3(1,-1,1), color: float4(1,0,1,1)),
            顶点(位置:float3(-1,1,-1),颜色:float4(1,1,0,1)),
            顶点(position: float3(1,1,-1), color: float4(0,1,1,1)),
            顶点(位置:float3(-1,-1,-1),颜色:float4(0.5,0.5,0,1)),
            顶点(position: float3(1,-1,-1),颜色:float4(1,0,0).5,1))
        ]
        indices = [
            0,1,2,  2,1,3, //Front
            5,2,3,  5,3,7,
            0,2,4,  2,5,4,
            0,1,4,  4,1,6,
            5,4,6,  5,6,7,
            3,1,6,  3,6,7
            
        ]
    }
}

CubeScene.swift

import MetalKit

class CubeScene: Scene {
    override init(device: MTLDevice) {
        super.init(device: device)
        // Create the Cube
        let c = Cube(withDevice: device)
        objects.append(c)
        // Move the Cube away from the camera
        c.translate(direction: float3(0,0,-6))
        // Add the Cube to the Scene
        add(child: c)
    }
    
    覆盖func render(commandEncoder: MTLRenderCommandEncoder, deltaTime: Float) {
        // Rotate the objects in the Scene
        objects.forEach{ $0.旋转(角度:deltaTime,轴:float3(1,1,0))}
        super.render(commandEncoder: commandEncoder, deltaTime: deltaTime)
    }
}

Updating Renderer.swift

Now we can update the Renderer’s variables and remove some unneeded code!

class Renderer: NSObject {
    var commandQueue: MTLCommandQueue!
    var scenes: [Scene] = []
    // ...

Next, we will only need these methods for the init

init(device: MTLDevice) {
        super.init()
        createCommandQueue(device: device)
        scenes.append(CubeScene(device: device))
}
    
    func createCommandQueue(device: MTLDevice) {
        commandQueue = device.makeCommandQueue()
}

Finally, we will change the draw function by replacing the following lines

// ...
commandEncoder?.setRenderPipelineState(renderPipelineState)
// Pass in the vertexBuffer into index 0
commandEncoder?.setVertexBuffer(vertexBuffer,偏移量:0,索引:0)
// Draw primitive at vertexStart 0
commandEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertices.count)
// ...

With

// ...
let deltaTime = 1 / Float(view.preferredFramesPerSecond)
        
scenes.forEach{ $0.render(commandEncoder: commandEncoder!,
                               deltaTime: deltaTime) }
// ...

Updating MetalView.swift

Inside MetalView, we will simply add one line. depthStencilPixelFormat = .depth32Float

required init(coder: NSCoder) {
        super.init(coder: coder)
        //确保我们在一个可以运行金属的设备上!
        守护let defaultDevice = MTLCreateSystemDefaultDevice() else {
            fatalError("Device loading error")
        }
        device = defaultDevice
        depthStencilPixelFormat = .depth32Float // Add this line!
        colorPixelFormat = .bgra8Unorm
        // Our clear color, can be set to any color
        clearColor = MTLClearColor(red: 0.1, green: 0.57, blue: 0.25, alpha: 1)
        createRenderer(device: defaultDevice)
}

Updating Shaders.metal

The first update we will do to the Shaders is to add what is known as Vertex Attributes.

Vertex Attributes

顶点函数可以通过索引缓冲区来读取每个顶点的输入
顶点函数使用顶点和实例id. In addition, per-vertex inputs can also be
通过使用[[stage_in]]属性声明它们,作为参数传递给顶点函数.
(Page 66)
struct VertexIn {
    float3 position [[ attribute(0) ]];
    float4 color [[ attribute(1) ]];
};

接下来我们将为我们在Swift中创建的约束添加两个简单的结构体.

struct ModelConstraints {
    float4x4 modelMatrix;
};

struct SceneConstraints {
    float4x4 projectionMatrix;
};

Finally, we will update the basic_vertex_function to use the scene and model constraints.

顶点VertexOut basic_vertex_function(VertexIn vIn [[stage_in]]),
                                       constant ModelConstraints &modelConstants [[ buffer(1) ]],
                                       constant SceneConstraints &sceneConstants [[ buffer(2) ]]) {
    VertexOut vOut;
    vOut.位置= sceneConstants.projectionMatrix * modelConstants.modelMatrix * float4(vIn.position,1);
    vOut.color = vIn.color;
    return vOut;
}

希望当你运行它时,你能看到一个旋转的立方体! 如果您有任何问题或想查看完整的源代码,请检查这 GitHub page!

Rotating Cube

斯威夫特开发人员与激情把咖啡变成代码. 目前使用MetalKit, ARKit和SceneKit深入研究Objective-C和Swift. Feel free to follow me on GitLab or join my open source agile development team oneleif.

Contact