hello-metal.4 动画

这个系列是我用来学习 Metal API 的笔记,我的最终目的是希望实现一个基于 Metal 的游戏引擎。

目前系列有:

hello-metal.1 看到了绿色
hello-metal.2 第一个三角形
hello-metal.3 四边形
hello-metal.4 动画
hello-metal.5 材质贴图

点击查看上一篇 hello-metal.3 四边形

在上一篇已经完成了四边形的绘制,这一篇我们来实现一个简单的动画效果。

动起来

现在我们需要增加一个结构体,用来保存画面的偏移,这样每次画面更新的时候,我们都可以使用偏移来控制顶点的坐标,达到动画的效果。

增加存储数据的结构体

在 Renderer 中增加一个结构体,用来保存动画的值,增加一个 Float 类型的变量,保存每帧时间。

1
2
3
4
5
struct Constants {
var animateBy: Float = 0
}
var constants = Constants()
var time: Float = 0

计算每帧移动的距离

在 draw 函数中,我们使用画面的最佳刷新率作为累加值。

1
2
3
4
time += 1 / Float(view.preferredFramesPerSecond)

let animateBy = abs(sin(time) / 2 + 0.5)
constants.animateBy = animateBy

发送数据到 GPU

然后我们将结构体放进 GPU 中,MTLCommandEncoder 提供了 setVertexBytes 函数来保存数据。

1
2
3
commandEncoder?.setVertexBytes(&constants,
length: MemoryLayout<Constants>.stride,
index: 1)

我们为这个数据设置一个索引值 1,这样我们就可以在着色器代码中访问了。

修改着色器

1
2
3
4
5
6
7
8
9
10
11
struct Constants {
float animateBy;
};

vertex float4 vertex_shader(const device packed_float3 *vertices [[ buffer(0) ]],
constant Constants &constants [[ buffer(1) ]],
uint vertexId [[ vertex_id ]]) {
float4 position = float4(vertices[vertexId], 1);
position.x += constants.animateBy;
return position;
}

我们只需要在着色器代码中增加一个 struct,保持相同的内存布局,然后在函数参数中使用 constant 修饰结构体和buffer。

constconstant 的不同在于,constant 是地址空间,const 是类型限定符。

现在我们再跑一下,就可以看到一个动画效果了。

完整代码

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
//
// Renderer.swift
// HelloMetal
//
// Created by lxz on 2022/4/4.
//

import MetalKit

enum 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?

struct Constants {
var animateBy: Float = 0
}
var constants = Constants()
var time: Float = 0

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
}

time += 1 / Float(view.preferredFramesPerSecond)

let animateBy = abs(sin(time) / 2 + 0.5)
constants.animateBy = animateBy

let commandBuffer = commandQueue.makeCommandBuffer()
let commandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: view.currentRenderPassDescriptor!)

commandEncoder?.setRenderPipelineState(pipelineState)
commandEncoder?.setVertexBuffer(vertexBuffer,
offset: 0,
index: 0)

commandEncoder?.setVertexBytes(&constants,
length: MemoryLayout<Constants>.stride,
index: 1)

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
20
21
22
23
24
25
//
// Shader.metal
// HelloMetal
//
// Created by lxz on 2022/4/4.
//

#include <metal_stdlib>
using namespace metal;

struct Constants {
float animateBy;
};

vertex float4 vertex_shader(const device packed_float3 *vertices [[ buffer(0) ]],
constant Constants &constants [[ buffer(1) ]],
uint vertexId [[ vertex_id ]]) {
float4 position = float4(vertices[vertexId], 1);
position.x += constants.animateBy;
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
//
// ViewController.swift
// HelloMetal
//
// Created by lxz on 2022/4/4.
//

import UIKit
import MetalKit

class 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
}
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!