Metal 初窥

渲染

顶点 (vertice, vertex)

一个顶点就是两个或多个的线, 曲线, 集合图形的焦点.

一个 3D 渲染器将会通过 model loader 代码去阅读这些顶点, 这些代码解析了顶点的列表. 然后渲染器会传递这些顶点到 GPU, shader (着色器, 流处理器) 方法会去处理这些顶点, 去创建最终的图片或者纹理, 然后会重新传递到 CPU, 以便在屏幕上展示.

渲染通道 (Rendering Pipeline) 是一个传送到 CPU 的命令表. 渲染通道包含了可编程和不可编程的方法, 前者, 一般被认为是顶点方法 (vertex functions)碎片方法 (fragment functions), 是你可以手动影响你最终你看见的渲染的模型的方法.

每个静态图像都被认为是一个 frame (帧), 图片出现的速率叫做帧率.

Metal 处理序列

屏幕快照 2018-09-10 下午9.14.30

MetalKit

MetalKit 有一个自己的视图 — MTKView
MTKView 在 MacOS 上是 NSView 的子类,在 iOS 上是 UIView 的子类。

检测是否有合适的 GPU:

1
2
3
guard let device = device else {
fatalError("GPU is not supported")
}

创建一个 MTKView:

1
2
3
let frame = CGRect(x: 75/2.0, y: 96, width: 300, height: 300)
let view = MTKView(frame: frame, device: device)
view.clearColor = MTLClearColor(red: 1, green: 1, blue: 0.8, alpha: 1)

MTKClearColor 代表了一个 RGBA 色值。色值被存储在 clearColor 中, 用这个值设置 MTKView。

创建一个 MTLCommandQueue 对象:

1
2
3
guard let commandQueue = device.makeCommandQueue() else { 
fatalError("Could not create a command queue")
}

DeviceCommand 需要在一开始就设置, 设置一次, 各处使用.

The Model - Load a model

Model I/O 是一个整合 Metal 和 SceneKit 的框架. 它的主要目的是加载 3D 模型, 设置 data buffer 以便渲染.

1
2
3
4
5
6
7
// 1. 创建一个网格数据内存管理器
let allocator = MTKMeshBufferAllocator(device: device)
// 2. Model I/O 创建一个有着特定大小的球体
// 并且返回一个 MDLMesh 对象, buffers 中有着所有的顶点信息
let mdlMesh = MDLMesh(sphereWithExtent: [0.75, 0.75, 0.75], segments: [100, 100], inwardNormals: false, geometryType: .triangles, allocator: allocator)
// 3. 为了让 Metal 可以使用这些网格, 从一个 Model I/O mesh 转换成一个 MetalKit mesh
let mesh = try! MTKMesh(mesh: mdlMesh, device: device)

Shader Functions

Shader Functions 是跑在 GPU 上的一小段代码, 用 Metal Shading Language (着色语言) 来写它们, 是一段 C++ 的代码子集.

通常情况下, 你会为 shader functions 创建一个独立的文件, 后缀为 .metal

The vertex function is where you usually manipulate vertex positions and the fragment function is where you specify the pixel color.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let shader = """
#include <metal_stdlib> \n
using namespace metal;

struct VertexIn {
float4 position [[ attribute(0) ]];
};

vertex float4 vertex_main(const VertexIn vertex_in [[ stage_in ]]) {
return vertex_in.position;
}

fragment float4 fragment_main() {
return float4(1, 0, 0, 1);
}
"""

// 创建一个包含 shader 中两个方法的 Metal 库
let library = try device.makeLibrary(source: shader, options: nil)
let vertexFunction = library.makeFunction(name: "vertex_main")
let fragmentFunction = library.makeFunction(name: "fragment_main")

The compiler will check that these functions exist and make them available to a pipeline descriptor (描述符号).

The Pipeline State - Setup the pipeline

在 Metal 中, 你为 GPU 设置一个通道状态, 通过这是这种状态, 你告诉 GPU, 除非状态改变, 什么都不会改变, 并且 GPU 可以运行更高效。通道状态包含了所有 CPU 需要的信息, 例如, 它需要用到的像素格式化和它是否需要渲染深度, 此外, 它也持有你创建的顶点和碎片方法。

然而, 你不直接创建通道状态, 而是通过创建一个 descriptor 来创建它。descriptor 持有了所有通道需要知道的东西, 并且, 为了那些特殊的渲染场景, 你仅仅是改变那些必要的属性即可:

1
2
3
4
5
6
7
8
9
10
// 创建一个 pipeline descriptor
let descriptor = MTLRenderPipelineDescriptor()
// 设置像素格式化
// .bgra8Unorm: Ordinary format with four 8-bit normalized unsigned integer components in BGRA order.
descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
descriptor.vertexFunction = vertexFunction
descriptor.fragmentFunction = fragmentFunction
// MTKMetalVertexDescriptorFromModelIO(_:)
// 返回一个部分被转换的 Metal 顶点描述符号
descriptor.vertexDescriptor = MTKMetalVertexDescriptorFromModelIO(mesh.vertexDescriptor)

vertex descriptor 描述了那些顶点如何在 Metal buffer 中被布局的。
当 Model I/O 加载球体网格的时候, 会自动创建顶点描述符号。
通过 descriptor 创建 pipeline state:

1
let pipelineState = try device.makeRenderPipelineState(descriptor: descriptor)

Creating a pipeline state takes valuable processing time, so all of the above should be a one-time setup. In a real app, you might create several pipeline states to call different shading functions or use different vertex layouts.

Rendering

MTKView 有一个代理方法, 可以运行每一帧。

MTKView provides a render pass descriptor and a drawable.

1
2
3
4
5
6
7
8

guard let commandBuffer = commandQueue.makeCommandBuffer(),

let descriptor = view.currentRenderPassDescriptor,

let renderEncoder =
commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)
else { fatalError() }