Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Custom Passes

Custom rendering hooks into the graph through two State methods. One runs at startup. The other runs every frame.

configure_render_graph()

This is the setup hook. It runs once when the renderer is constructed. The game adds passes, declares textures, and changes the pipeline shape.

#![allow(unused)]
fn main() {
fn configure_render_graph(
    &mut self,
    graph: &mut RenderGraph<World>,
    device: &wgpu::Device,
    surface_format: wgpu::TextureFormat,
    resources: RenderResources,
) {
    // Add custom textures
    let my_texture = graph.add_color_texture("my_effect")
        .format(wgpu::TextureFormat::Rgba16Float)
        .size(1920, 1080)
        .clear_color(wgpu::Color::BLACK)
        .transient();

    // Add custom passes
    let my_pass = MyCustomPass::new(device);
    graph.add_pass(
        Box::new(my_pass),
        &[("input", resources.scene_color), ("output", my_texture)],
    );
}
}

RenderResources

RenderResources is the bundle of ResourceIds the engine already declared. Anything custom usually reads from scene_color and writes to compute_output or swapchain.

FieldDescription
scene_colorHDR color buffer (Rgba16Float)
depthMain depth buffer (Depth32Float)
compute_outputPost-processed output before swapchain blit
swapchainFinal swapchain output
view_normalsView-space normals
ssao_raw / ssaoRaw and blurred SSAO
ssgi_raw / ssgiRaw and blurred SSGI
ssr_raw / ssrRaw and blurred SSR
surface_width / surface_heightCurrent window dimensions in pixels

update_render_graph()

The per-frame hook. Use it for runtime state flips, not for adding passes (use the setup hook for that).

#![allow(unused)]
fn main() {
fn update_render_graph(&mut self, graph: &mut RenderGraph<World>, world: &World) {
    if self.bloom_changed {
        let _ = graph.set_pass_enabled("bloom_pass", self.bloom_enabled);
        self.bloom_changed = false;
    }
}
}

Adding Built-in Passes

The built-in pass types are reusable. A custom graph can pick and choose which built-ins to include.

#![allow(unused)]
fn main() {
use nightshade::render::passes;

fn configure_render_graph(
    &mut self,
    graph: &mut RenderGraph<World>,
    device: &wgpu::Device,
    surface_format: wgpu::TextureFormat,
    resources: RenderResources,
) {
    let bloom_texture = graph.add_color_texture("bloom")
        .format(wgpu::TextureFormat::Rgba16Float)
        .size(960, 540)
        .clear_color(wgpu::Color::BLACK)
        .transient();

    // Bloom
    let bloom_pass = passes::BloomPass::new(device, 1920, 1080);
    graph.add_pass(
        Box::new(bloom_pass),
        &[("hdr", resources.scene_color), ("bloom", bloom_texture)],
    );

    // SSAO
    let ssao_pass = passes::SsaoPass::new(device, 1920, 1080);
    graph.add_pass(
        Box::new(ssao_pass),
        &[
            ("depth", resources.depth),
            ("normals", resources.view_normals),
            ("ssao_raw", resources.ssao_raw),
        ],
    );

    let ssao_blur_pass = passes::SsaoBlurPass::new(device, 1920, 1080);
    graph.add_pass(
        Box::new(ssao_blur_pass),
        &[("ssao_raw", resources.ssao_raw), ("ssao", resources.ssao)],
    );

    // Final compositing
    let postprocess_pass = passes::PostProcessPass::new(device, surface_format, 0.3);
    graph.add_pass(
        Box::new(postprocess_pass),
        &[
            ("hdr", resources.scene_color),
            ("bloom", bloom_texture),
            ("ssao", resources.ssao),
            ("output", resources.compute_output),
        ],
    );

    // Blit to swapchain
    let blit_pass = passes::BlitPass::new(device, surface_format);
    graph.add_pass(
        Box::new(blit_pass),
        &[("input", resources.compute_output), ("output", resources.swapchain)],
    );
}
}

PassBuilder Fluent API

pass() is an alternative to add_pass() that reads more like an expression. The builder adds the pass to the graph when it goes out of scope.

#![allow(unused)]
fn main() {
graph.pass(Box::new(bloom_pass))
    .read("hdr", resources.scene_color)
    .write("bloom", bloom_texture);

graph.pass(Box::new(postprocess_pass))
    .read("hdr", resources.scene_color)
    .read("bloom", bloom_texture)
    .read("ssao", resources.ssao)
    .write("output", resources.swapchain);
}

Conditional Passes

There are two ways to make a pass conditional. The first is to leave it out of the graph entirely at setup time.

#![allow(unused)]
fn main() {
fn configure_render_graph(
    &mut self,
    graph: &mut RenderGraph<World>,
    device: &wgpu::Device,
    surface_format: wgpu::TextureFormat,
    resources: RenderResources,
) {
    if self.ssao_enabled {
        let ssao_pass = passes::SsaoPass::new(device, 1920, 1080);
        graph.add_pass(
            Box::new(ssao_pass),
            &[
                ("depth", resources.depth),
                ("normals", resources.view_normals),
                ("ssao_raw", resources.ssao_raw),
            ],
        );
    }
}
}

The second is to add the pass unconditionally and toggle it at runtime.

#![allow(unused)]
fn main() {
fn update_render_graph(&mut self, graph: &mut RenderGraph<World>, _world: &World) {
    let _ = graph.set_pass_enabled("ssao_pass", self.ssao_enabled);
}
}

The toggle path is the right choice when the setting changes during play. The conditional-construction path is the right choice when the decision is fixed at startup, because it skips the per-frame check entirely.

Default Pipeline

If the game does not override configure_render_graph(), the default implementation adds three passes:

  1. BloomPass runs HDR bloom at half resolution.
  2. PostProcessPass does tonemapping and compositing.
  3. BlitPass copies to the swapchain.

The engine always adds the core passes (clear, sky, shadow, mesh, skinned mesh, grass, grid, lines, selection) regardless of what the custom configuration does. Overriding configure_render_graph() only changes the post-process tail.