Re: 0x00. 从零开始的光线追踪实现-画布

213 阅读3分钟

定个小目标

最近刚看完一本书Ray Tracing in One Weekend,书里是通过软件渲染的形式(纯 CPU 计算)实现光线追踪,打算在 macOS 上用 Metal API 调用 GPU 实现一下。

看看本节最终效果

image.png

画三角形

老早之前我就写过一篇我当时觉得好玩的东西,用 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);
}

来运行一下看看当前的结果

image.png

其实这个坐标是这样的

fig-02-ndc.svg

画直角三角形

根据前面的坐标图,我们把这个三角形改成直角三角形,其实就是把顶点坐标改成左上、右上、左下三个点,所以顶点很明显就是

let vertexData: [Float] = [
  -1,  1,
  -1, -1,
   1,  1
]

于是就会显示出这样子的结果

image.png

拼一个矩形

现在如果放两个直角三角形拼起来,就能画出我们这节最终想要的一个“战场”

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
        )
        // ...
      }
    }
  }
}

最终我们就完成了开头提到的本节最终效果

image.png


暂时先到这,我们说白了现在就是搞了一张画布,后续的渲染工作基本上只需要调整 Fragment Shader 的代码就可以了。