Render Passes

Date:     Updated:

카테고리:

태그:

Main Reference
- Metal by Tutorials - 4th edition


지금까지는 하나의 render pass만을 사용했는데, 다른 말로 하면 하나의 render command encoder만을 사용했다는 의미이다. 조금만 더 복잡한 작업을 수행하려고 하면 offscreen texture에 렌더해야하는 상황이 자주 발생하며, offscreen render의 결과를 뒤에 등장하는 pass에서 사용하게 된다.

1

Render command encoder를 세팅하기 위해서는 render pass descriptor가 필요하다. 기존 프로젝트에서는 MTKView.currentRenderPassDescriptor 를 사용했는데, offscreen texture에 렌더링 하기 위해서는 직접 descriptor를 정의해야 한다.

  • Render Pass : Frame Buffer(where) + Pipeline State (how)
protocol RenderPass {
    var label: String { get }
    var descriptor: MTLRenderPassDescriptor? { get set }
    mutating func resize(view: MTKView, size: CGSize)
    func draw(
        commandBuffer: MTLCommandBuffer,
        scene: GameScene,
        uniforms: Uniforms,
        params: Params
    )
}

extension RenderPass {
    static func buildDepthStencilState() -> MTLDepthStencilState? {
        let descriptor = MTLDepthStencilDescriptor()
        descriptor.depthCompareFunction = .less
        descriptor.isDepthWriteEnabled = true
        return Renderer.device.makeDepthStencilState(
            descriptor: descriptor)
    }
}
  • Offscreen Texture를 사용할 경우 window 크기에 맞춰주는 resize 함수가 필요
  • mtkView(size) 호출할 때 각 Pass의 resize 함수를 호출
    • 사실 resize가 필요 없는 Pass들도 존재(ex. shadow pass)
    • 이런 경우 구현부를 비워두면 됨
  • DepthStencil State는 RenderPass에서 생성하는 구조..
    • 다수의 renderPass가 default PSO를 사용하기 때문에 효율적임
enum PipelineStates {
    static func createPSO(descriptor: MTLRenderPipelineDescriptor)
    -> MTLRenderPipelineState {
    let pipelineState: MTLRenderPipelineState
    do {
      pipelineState =
      try Renderer.device.makeRenderPipelineState(
        descriptor: descriptor)
    } catch {
      fatalError(error.localizedDescription)
    }
    return pipelineState
    }

    static func createForwardPSO(colorPixelFormat: MTLPixelFormat) -> MTLRenderPipelineState {
    let vertexFunction = Renderer.library?.makeFunction(name: "vertex_main")
    let fragmentFunction = Renderer.library?.makeFunction(name: "fragment_main")
    let pipelineDescriptor = MTLRenderPipelineDescriptor()
    pipelineDescriptor.vertexFunction = vertexFunction
    pipelineDescriptor.fragmentFunction = fragmentFunction
    pipelineDescriptor.colorAttachments[0].pixelFormat = colorPixelFormat
    pipelineDescriptor.depthAttachmentPixelFormat = .depth32Float
    pipelineDescriptor.vertexDescriptor =
      MTLVertexDescriptor.defaultLayout
    return createPSO(descriptor: pipelineDescriptor)
    }

    static func createObjectIdPSO() -> MTLRenderPipelineState {
        let pipelineDescriptor = MTLRenderPipelineDescriptor()
        let vertexFunction =
        Renderer.library?.makeFunction(name: "vertex_main")
        let fragmentFunction =
        Renderer.library?.makeFunction(name: "fragment_objectId")
        pipelineDescriptor.vertexFunction = vertexFunction
        pipelineDescriptor.fragmentFunction = fragmentFunction
        pipelineDescriptor.colorAttachments[0].pixelFormat = .r32Uint
        pipelineDescriptor.depthAttachmentPixelFormat = .invalid
        pipelineDescriptor.vertexDescriptor =
        MTLVertexDescriptor.defaultLayout
        return Self.createPSO(descriptor: pipelineDescriptor)
    }
}
  • Pipeline State는 case가 없는 enum으로 관리 (static func로만 이루어져 있음)
  • PipelineState.Create…PSO() 처럼 namespace로 깔끔하게 사용 가능
  • class나 struct 처럼 PipelineState 객체를 생성할 수 없음
  • IS Engine 구조보다 훨씬 깔끔하다..
  • colorAttachment(+ depth)의 pixelFormat을 Descriptor에서 지정하는데 renderTexture의 pixelFormat과 동일해야 함
  • Factory 패턴!
extension RenderPass {
	// ...

    static func makeTexture(
        size: CGSize,
        pixelFormat: MTLPixelFormat,
        label: String,
        storageMode: MTLStorageMode = .private,
        usage: MTLTextureUsage = [.shaderRead, .renderTarget]
    ) -> MTLTexture? {
        let width = Int(size.width)
        let height = Int(size.height)
        guard width > 0 && height > 0 else { return nil }
        let textureDesc =
        MTLTextureDescriptor.texture2DDescriptor(
            pixelFormat: pixelFormat,
            width: width,
            height: height,
            mipmapped: false)
        textureDesc.storageMode = storageMode
        textureDesc.usage = usage
        guard let texture =
                Renderer.device.makeTexture(descriptor: textureDesc) else {
            fatalError("Failed to create texture")
        }
        texture.label = label
        return texture
    }
}

struct ObjectIdRenderPass: RenderPass {
    let label = "Object ID Render Pass"
    var descriptor: MTLRenderPassDescriptor?
    var pipelineState: MTLRenderPipelineState

    var idTexture: MTLTexture?

    mutating func resize(view: MTKView, size: CGSize) {
        idTexture = Self.makeTexture(
            size: size,
            pixelFormat: .r32Uint,
            label: "ID Texture")
    }

    func draw(
        commandBuffer: MTLCommandBuffer,
        scene: GameScene,
        uniforms: Uniforms,
        params: Params
    ) {
        guard let descriptor = descriptor else {
            return
        }
        descriptor.colorAttachments[0].texture = idTexture
        guard let renderEncoder =
                commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)
        else { return }

        renderEncoder.label = label
        renderEncoder.setRenderPipelineState(pipelineState)
        for model in scene.models {
            model.render(
                encoder: renderEncoder,
                uniforms: uniforms,
                params: params)
        }
        renderEncoder.endEncoding()
    }

    init() {
        pipelineState = PipelineStates.createObjectIdPSO()
        descriptor = MTLRenderPassDescriptor()
    }

}
  • IS Engine에서는 Texture class가 device도 들고있는데, 굳이 들고 있을 필요가 있을까..?
  • Texture의 경우 resize 함수에서 rebuild되도록
    • 만약 size가 변경되어도, 이전에 사용하던 MTLTexture 리소스는 자동으로 해제되고
    • 새로운 MTLTexture 생성
  • 참고로 맨 처음 mtkView window가 초기 size로 켜질 때 resize로 간주하고 호출됨

  • 그리고 renderEncoder를 renderPass에서 생성하고 endEncoding하는 구조..
    • 괜찮아 보이는데 이런 식이면 triple buffering같은 기법을 사용하기는 힘들듯
class Renderer: NSObject {
  static var device: MTLDevice!
  static var commandQueue: MTLCommandQueue!
  static var library: MTLLibrary!

  var uniforms = Uniforms()
  var params = Params()

  var forwardRenderPass: ForwardRenderPass
  var objectIdRenderPass: ObjectIdRenderPass

  init(metalView: MTKView) {
    guard
      let device = MTLCreateSystemDefaultDevice(),
      let commandQueue = device.makeCommandQueue() else {
        fatalError("GPU not available")
    }
    Self.device = device
    Self.commandQueue = commandQueue
    metalView.device = device

    // create the shader function library
    let library = device.makeDefaultLibrary()
    Self.library = library

    forwardRenderPass = ForwardRenderPass(view: metalView)
    objectIdRenderPass = ObjectIdRenderPass()

    super.init()
    metalView.clearColor = MTLClearColor(
      red: 0.93,
      green: 0.97,
      blue: 1.0,
      alpha: 1.0)
    metalView.depthStencilPixelFormat = .depth32Float
    mtkView(
      metalView,
      drawableSizeWillChange: metalView.drawableSize)

    // set the device's scale factor
#if os(macOS)
    params.scaleFactor = Float(NSScreen.main?.backingScaleFactor ?? 1)
#elseif os(iOS)
    params.scaleFactor = Float(UIScreen.main.scale)
#endif

  }

  static func buildDepthStencilState() -> MTLDepthStencilState? {
    let descriptor = MTLDepthStencilDescriptor()
    descriptor.depthCompareFunction = .less
    descriptor.isDepthWriteEnabled = true
    return Renderer.device.makeDepthStencilState(
      descriptor: descriptor)
  }
}

extension Renderer {
  func mtkView(
    _ view: MTKView,
    drawableSizeWillChange size: CGSize
  ) {
    forwardRenderPass.resize(view: view, size: size)
    objectIdRenderPass.resize(view: view, size: size)

  }

  func updateUniforms(scene: GameScene) {
    uniforms.viewMatrix = scene.camera.viewMatrix
    uniforms.projectionMatrix = scene.camera.projectionMatrix
    params.lightCount = Int32(scene.lighting.lights.count)
    params.cameraPosition = scene.camera.position
  }

  func draw(scene: GameScene, in view: MTKView) {
    guard
      let commandBuffer = Self.commandQueue.makeCommandBuffer(),
      let descriptor = view.currentRenderPassDescriptor else {
        return
    }

    updateUniforms(scene: scene)

	objectIdRenderPass.draw(
		commandBuffer: commandBuffer,
		scene: scene,
		uniforms: uniforms,
		params: params)

    forwardRenderPass.descriptor = descriptor
    forwardRenderPass.draw(
      commandBuffer: commandBuffer,
      scene: scene,
      uniforms: uniforms,
      params: params)

    guard let drawable = view.currentDrawable else {
      return
    }
    commandBuffer.present(drawable)
    commandBuffer.commit()
  }
}

  • Renderer 클래스에 ObjectidRenderPass 추가해주면 됨
  • 이때 어떤 함수들은 class 구현부에 있고, 어떤 함수들은 extension으로 빼둠
    • 보통 가독성을 위해 protocol 단위로 extension으로 나누기도 함.
    • renderer의 경우 mtkView의 MTKViewDelegate protocol을 위한 함수들이 빠져있음
struct GameScene {
  static var objectId: Int32 = 1
  lazy var train: Model = {
    createModel(name: "train.usdz")
  }()
  lazy var treefir1: Model = {
    createModel(name: "treefir.usdz")
  }()
  lazy var treefir2: Model = {
    createModel(name: "treefir.usdz")
  }()
  lazy var treefir3: Model = {
    createModel(name: "treefir.usdz")
  }()

    func createModel(name: String) -> Model {
    let model = Model(name: name, objectId: Self.objectId)
    Self.objectId += 1
    return model
  }

  // ...
}
  • object ID는 gameScene에서 관리하니까 훨씬 편하네..

1

GPU Capture로 idTexture를 찍어보면 잘못된 결과가 나오는데, Depth Testing을 하지 않아서 그럼. 사실 loadAction의 clear 설정까지 반드시 해줄 필요는 없지만, 추후 디버깅이나 후처리를 위해 해주는게 좋음

Reading the Object ID Texture

Option 1 (Read the texture on the CPU)

  • extract the object ID using the touch location as the coordinates.
  • synchronization issues

Option 2

  • Keep the texture on the GPU and do the test there
fragment float4 fragment_main() {
	// ...

	if (!is_null_texture(idTexture)) {
	    uint2 coord = uint2(
	                        params.touchX * params.scaleFactor,
	                        params.touchY * params.scaleFactor);
	    uint objectID = idTexture.read(coord).r;
	    if (params.objectId != 0 && objectID == params.objectId) {
	        material.baseColor = float3(0.9, 0.5, 0);
	    }
	}
}



맨 위로 이동하기

Metal 카테고리 내 다른 글 보러가기

댓글 남기기