Chapters

Hide chapters

Metal by Tutorials

Fourth Edition · macOS 14, iOS 17 · Swift 5.9 · Xcode 15

Section I: Beginning Metal

Section 1: 10 chapters
Show chapters Hide chapters

Section II: Intermediate Metal

Section 2: 8 chapters
Show chapters Hide chapters

Section III: Advanced Metal

Section 3: 8 chapters
Show chapters Hide chapters

12. Render Passes
Written by Caroline Begbie & Marius Horga

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Up to this point, you’ve created projects that had only one render pass. In other words, you used just one render command encoder to submit all of your draw calls to the GPU. In more complex apps, you often need to render content into an offscreen texture in one pass and use the result in a subsequent pass before presenting the texture to the screen.

There are several reasons why you might do multiple passes:

  • Shadows: In the following chapter, you’ll create a shadow pass and render a depth map from a directional light to help calculate shadows in a subsequent pass.
  • Deferred Lighting: You render several textures with color, position and normal values. Then, in a final pass, you calculate lighting using those textures.
  • Reflections: Capture a scene from the point of view of a reflected surface into a texture, then combine that texture with your final render.
  • Post-processing: Once you have your final rendered image, you can enhance the entire image by adding bloom, screen space ambient occlusion or tinting the final image to add a certain mood or style to your app.

Render Passes

A render pass consists of sending commands to a command encoder. The pass ends when you end encoding on that command encoder.

When setting up a render command encoder, you use a render pass descriptor. So far, you’ve used the MTKView currentRenderPassDescriptor, but you can define your own descriptor or make changes to the current render pass descriptor. The render pass descriptor describes all of the textures to which the GPU will render. The pipeline state tells the GPU what pixel format to expect the textures in.

A render pass
A render pass

For example, the following render pass writes to four textures. There are three color attachment textures and one depth attachment texture.

A render pass with four textures
A render pass with four textures

Object Picking

To get started with multipass rendering, you’ll create a simple render pass that adds object picking to your app. When you click a model in your scene, that model will render in a slightly different shade.

The Starter App

➤ In Xcode, open the starter app for this chapter and examine the code. It’s similar to the previous chapter but refactored.

The starter app
Jpu ygofkes ons

Setting up Render Passes

Since you’ll have multiple render passes performing similar procedures, it makes sense to have a protocol with some default methods.

import MetalKit

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 {
}
struct ForwardRenderPass: RenderPass {

Creating a UInt32 Texture

Textures don’t only hold color. There are many pixel formats. So far, you’ve used rgba8Unorm, a color format that contains four 8-bit integers for red, green, blue and alpha.

import MetalKit

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

  mutating func resize(view: MTKView, size: CGSize) {
  }

  func draw(
    commandBuffer: MTLCommandBuffer,
    scene: GameScene,
    uniforms: Uniforms,
    params: Params
  ) {
  }
}
static func createObjectIdPSO() -> MTLRenderPipelineState {
  let pipelineDescriptor = MTLRenderPipelineDescriptor()
  // 1
  let vertexFunction =
    Renderer.library?.makeFunction(name: "vertex_main")
  let fragmentFunction =
    Renderer.library?.makeFunction(name: "fragment_objectId")
  pipelineDescriptor.vertexFunction = vertexFunction
  pipelineDescriptor.fragmentFunction = fragmentFunction
  // 2
  pipelineDescriptor.colorAttachments[0].pixelFormat = .r32Uint
  // 3
  pipelineDescriptor.depthAttachmentPixelFormat = .invalid
  pipelineDescriptor.vertexDescriptor =
    MTLVertexDescriptor.defaultLayout
  return Self.createPSO(descriptor: pipelineDescriptor)
}
init() {
  pipelineState = PipelineStates.createObjectIdPSO()
  descriptor = MTLRenderPassDescriptor()
}
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
var idTexture: MTLTexture?
idTexture = Self.makeTexture(
  size: size,
  pixelFormat: .r32Uint,
  label: "ID Texture")
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()

Adding the Render Pass to Renderer

➤ Open Renderer.swift, and add the new render pass property:

var objectIdRenderPass: ObjectIdRenderPass
objectIdRenderPass = ObjectIdRenderPass()
objectIdRenderPass.resize(view: view, size: size)
objectIdRenderPass.draw(
  commandBuffer: commandBuffer,
  scene: scene,
  uniforms: uniforms,
  params: params)

Adding the Shader Function

The Object ID render pass will write the currently rendered model’s object ID to a texture. You don’t need any of the vertex information in the fragment function.

#import "Common.h"

// 1
struct FragmentOut {
  uint objectId [[color(0)]];
};

// 2
fragment FragmentOut fragment_objectId(
  constant Params &params [[buffer(ParamsBuffer)]])
{
  // 3
  FragmentOut out {
    .objectId = params.objectId
  };
  return out;
}
No difference to the render
Gi kacquculra to qfu zecday

The GPU workload capture icon
Lyu THU kehqqeep wownica ufin

The GPU workload capture
Rde BDE sujlriih xemqedu

ID texture with erroneous object ID
EZ soshiyi wozp ikpiqoeay iqcuvt OH

Adding the Depth Attachment

➤ Open ObjectIdRenderPass.swift, and add a new property to ObjectIdRenderPass:

var depthTexture: MTLTexture?
depthTexture = Self.makeTexture(
  size: size,
  pixelFormat: .depth32Float,
  label: "ID Depth Texture")
pipelineDescriptor.depthAttachmentPixelFormat = .depth32Float
descriptor.depthAttachment.texture = depthTexture

The Depth Stencil State

➤ Create a new property in ObjectIdRenderPass:

var depthStencilState: MTLDepthStencilState?
depthStencilState = Self.buildDepthStencilState()
renderEncoder.setDepthStencilState(depthStencilState)
ID texture with Object IDs
IZ kidnibo kurl Oxzimn OGq

Load and store actions
Gueg udk gtume awluedl

Load & Store Actions

A render pass executes the load action whenever it loads an attachment texture before writing to it. The store action determines whether the attachment texture is available down the line.

descriptor.colorAttachments[0].loadAction = .clear
descriptor.colorAttachments[0].storeAction = .store
No random pixels
Cu hikjes jaqill

Reading the Object ID Texture

You now have a choice. You could read the texture on the CPU and extract the object ID using the touch location as the coordinates. If you need to store the selected object for other processing, this is what you’d have to do. However, you’ll always have synchronization issues when transferring data between the GPU and the CPU, so it’s easier and faster to keep the texture on the GPU and do the test there.

weak var idTexture: MTLTexture?
forwardRenderPass.idTexture = objectIdRenderPass.idTexture
renderEncoder.setFragmentTexture(idTexture, index: 11)
let input = InputController.shared
var params = params
params.touchX = UInt32(input.touchLocation?.x ?? 0)
params.touchY = UInt32(input.touchLocation?.y ?? 0)
texture2d<uint> idTexture [[texture(11)]]
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);
  }
}
Selected train turns orange
Zedimtuz gvoam muprs uqizwu

The completed render passes
Gde yashpeqaj totnug yictax

Key Points

  • A render pass descriptor describes all of the textures and load and store actions needed by a render pass.
  • Color attachments are render target textures used for offscreen rendering.
  • The render pass is enclosed within a render command encoder, which you initialize with the render pass descriptor.
  • You set a pipeline state object on the render command encoder. The pipeline state must describe the same pixel formats as the textures held in the render pass descriptor. If there is no texture, the pixel format must be invalid.
  • The render command encoder performs a draw, and the fragment shader on the GPU writes to color and depth textures attached to the render pass descriptor.
  • Color attachments don’t have to be rgb colors. Instead, you can write uint or float values in the fragment function.
  • For each texture, you describe load and store actions. If you aren’t using a texture in a later render pass, the action should be dontCare so the GPU can discard it and free up memory.
  • The GPU workload capture shows you a frame graph where you can see how all your render passes chain together.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now