定个小目标
最近刚看完一本书Ray Tracing in One Weekend,书里是通过软件渲染的形式(纯 CPU 计算)实现光线追踪,打算在 macOS 上用 Metal API 调用 GPU 实现一下。
看看本节最终效果
画三角形
老早之前我就写过一篇我当时觉得好玩的东西,用 Metal 画一个三角形(Swift 函数式风格),其实当时的写法相当 naive,不过里面相对有价值的内容就是 之前的实现。现在首先来新建一个项目,顺便把之前 抄过来,接下来在此基础上重构。
ViewController 实现 MTKViewDelegate
抄完之后第一步就是实现 draw(in view: MTKView)
import Cocoa
import MetalKit
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
(view as? MetalView)?.delegate = self
}
override var representedObject: Any? {
didSet {}
}
}
extension ViewController: MTKViewDelegate {
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}
func draw(in view: MTKView) {
if let v = view as? MetalView {
v.render()
}
}
}
protocol 开道
我们定义一下 PathTracer 协议
import Metal
protocol PathTracer {
var device: (any MTLDevice)? { get }
var queue: (any MTLCommandQueue)? { get }
var pipeline: (any MTLRenderPipelineState)? { get }
func render()
}
让 MetalView 实现 PathTracer 协议
class MetalView: MTKView {
required init(coder: NSCoder) {
super.init(coder: coder)
device = MTLCreateSystemDefaultDevice()
_queue = device?.makeCommandQueue()
createPipeline()
}
private var _queue: (any MTLCommandQueue)?
private var _pipeline: (any MTLRenderPipelineState)?
}
extension MetalView: PathTracer {
var queue: (any MTLCommandQueue)? { _queue }
var pipeline: (any MTLRenderPipelineState)? { _pipeline }
}
接着来改造一下原先版本的 render 函数
extension MetalView {
private func createPipeline() {
if let device, let library = device.makeDefaultLibrary() {
let renderPipelineDesc = MTLRenderPipelineDescriptor()
renderPipelineDesc.vertexFunction = library.makeFunction(name: "vertexFn")
renderPipelineDesc.fragmentFunction = library.makeFunction(name: "fragmentFn")
renderPipelineDesc.colorAttachments[0].pixelFormat = .bgra8Unorm
_pipeline = try? device.makeRenderPipelineState(descriptor: renderPipelineDesc)
}
}
func render() {
guard let device = device else { fatalError("Failed to find default device.") }
let vertexData: [Float] = [
0.0, 0.5,
-0.5, -0.5,
0.5, -0.5,
]
let dataSize = vertexData.count * MemoryLayout<Float>.size
let vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: [])
let renderPassDesc = MTLRenderPassDescriptor()
if let currentDrawable {
renderPassDesc.colorAttachments[0].texture = currentDrawable.texture
renderPassDesc.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0, alpha: 1.0)
renderPassDesc.colorAttachments[0].loadAction = .clear
guard let queue else { fatalError("Failed to make command queue.") }
let commandBuffer = queue.makeCommandBuffer()
guard let commandBuffer else { fatalError("Failed to make command buffer.") }
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesc)
guard let encoder = encoder else { fatalError("Failed to make render command encoder.") }
if let pipeline {
encoder.setRenderPipelineState(pipeline)
encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
encoder.endEncoding()
commandBuffer.present(currentDrawable)
commandBuffer.commit()
}
}
}
}
由于我们把 vertexData 调整了一下,所以别忘了把 shader 也调整一下,主要是调整顶点
#include <metal_stdlib>
using namespace metal;
struct VertexIn {
float2 position;
};
struct Vertex {
float4 position [[position]];
};
vertex Vertex vertexFn(constant VertexIn *vertices [[buffer(0)]], uint vid [[vertex_id]]) {
return Vertex { float4(vertices[vid].position, 0, 1) };
}
fragment float4 fragmentFn(Vertex vert [[stage_in]]) {
return float4(1, 0, 0, 1);
}
来运行一下看看当前的结果
其实这个坐标是这样的
画直角三角形
根据前面的坐标图,我们把这个三角形改成直角三角形,其实就是把顶点坐标改成左上、右上、左下三个点,所以顶点很明显就是
let vertexData: [Float] = [
-1, 1,
-1, -1,
1, 1
]
于是就会显示出这样子的结果
拼一个矩形
现在如果放两个直角三角形拼起来,就能画出我们这节最终想要的一个“战场”
extension MetalView: PathTracer {
// ...
func render() {
// ...
let vertexData: [Float] = [
-1, 1,
-1, -1,
1, 1,
1, 1,
-1, -1,
1, -1,
]
// ...
if let currentDrawable {
// ...
if let pipeline {
// ...
encoder.drawPrimitives(
type: .triangle,
vertexStart: 0,
vertexCount: 6,
instanceCount: 2
)
// ...
}
}
}
}
最终我们就完成了开头提到的本节最终效果
暂时先到这,我们说白了现在就是搞了一张画布,后续的渲染工作基本上只需要调整 Fragment Shader 的代码就可以了。