Shaders Advanced / Deferred Gbuffer Raymarching

Back to examples View in GitHub

Writes a raymarched signed distance field directly into the deferred gbuffer, so Bevy's standard deferred PBR lighting shades it as if it were a mesh. This example assumes prior familiarity with raymarching and is intended to demonstrate integration between a full-screen pass and the deferred renderer.

use bevy::{
    camera_controller::free_camera::{FreeCamera, FreeCameraPlugin},
    core_pipeline::{
        core_3d::CORE_3D_DEPTH_FORMAT,
        deferred::{
            copy_lighting_id::copy_deferred_lighting_id, node::late_deferred_prepass,
            DEFERRED_LIGHTING_PASS_ID_FORMAT, DEFERRED_PREPASS_FORMAT,
        },
        prepass::{DeferredPrepass, DepthPrepass, ViewPrepassTextures},
        Core3d, Core3dSystems, FullscreenShader,
    },
    pbr::{
        per_view_shadow_pass, shared_shadow_pass, DefaultOpaqueRendererMethod, ShadowView,
        ViewLightEntities, LATE_SHADOW_PASS,
    },
    prelude::*,
    render::{
        globals::{GlobalsBuffer, GlobalsUniform},
        render_resource::{binding_types::uniform_buffer, *},
        renderer::{RenderContext, ViewQuery},
        view::{ViewDepthStencilTexture, ViewUniform, ViewUniformOffset, ViewUniforms},
        RenderApp, RenderStartup,
    },
};

fn main() {
    App::new()
        // Render everything through the deferred pipeline
        .insert_resource(DefaultOpaqueRendererMethod::deferred())
        .add_plugins((DefaultPlugins, DeferredRaymarchPlugin, FreeCameraPlugin))
        .add_systems(Startup, setup)
        .run();
}

const SHADER_ASSET_PATH: &str = "shaders/deferred_raymarch.wesl";

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(4.0, 3.0, 6.0).looking_at(Vec3::new(0.0, 0.2, 0.0), Vec3::Y),
        // Deferred rendering requires MSAA to be off.
        Msaa::Off,
        DepthPrepass,
        DeferredPrepass,
        AmbientLight {
            brightness: 200.0,
            ..default()
        },
        FreeCamera::default(),
    ));

    // A ground plane that catches the SDF's shadow
    commands.spawn((
        Mesh3d(meshes.add(Plane3d::default().mesh().size(20.0, 20.0))),
        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
        Transform::from_xyz(0.0, -1.5, 0.0),
    ));

    // A "regular" mesh cube
    commands.spawn((
        Mesh3d(meshes.add(Cuboid::from_length(1.2))),
        MeshMaterial3d(materials.add(StandardMaterial {
            base_color: Color::srgb(0.2, 0.4, 0.9),
            perceptual_roughness: 0.4,
            ..default()
        })),
        Transform::from_xyz(2.2, -0.9, 0.5),
    ));

    commands.spawn((
        DirectionalLight {
            illuminance: 8_000.0,
            shadow_maps_enabled: true,
            ..default()
        },
        Transform::from_xyz(4.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));
}

struct DeferredRaymarchPlugin;

impl Plugin for DeferredRaymarchPlugin {
    fn build(&self, app: &mut App) {
        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
            return;
        };

        render_app
            .add_systems(RenderStartup, init_raymarch_pipelines)
            .add_systems(
                Core3d,
                (
                    // The gbuffer write must run after the mesh deferred prepass has
                    // filled the gbuffer, but *before* `copy_deferred_lighting_id`
                    // bakes the lighting-pass ids into the depth routing texture the
                    // lighting pass reads
                    raymarch_gbuffer_pass
                        .in_set(Core3dSystems::Prepass)
                        .after(late_deferred_prepass)
                        .before(copy_deferred_lighting_id),
                    // Write the SDF into the shadow maps after the mesh shadow passes
                    // have drawn, so it casts shadows like any other caster
                    raymarch_directional_shadow_pass
                        .after(per_view_shadow_pass::<LATE_SHADOW_PASS>)
                        .before(Core3dSystems::MainPass),
                    raymarch_shared_shadow_pass
                        .after(shared_shadow_pass::<LATE_SHADOW_PASS>)
                        .before(Core3dSystems::MainPass),
                ),
            );
    }
}

#[derive(Resource)]
struct RaymarchGBufferPipeline {
    layout: BindGroupLayoutDescriptor,
    pipeline_id: CachedRenderPipelineId,
}

#[derive(Resource)]
struct RaymarchShadowPipeline {
    layout: BindGroupLayoutDescriptor,
    pipeline_id: CachedRenderPipelineId,
}

fn init_raymarch_pipelines(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    fullscreen_shader: Res<FullscreenShader>,
    pipeline_cache: Res<PipelineCache>,
) {
    let layout = BindGroupLayoutDescriptor::new(
        "raymarch_bind_group_layout",
        &BindGroupLayoutEntries::sequential(
            ShaderStages::FRAGMENT,
            (
                uniform_buffer::<ViewUniform>(true),
                uniform_buffer::<GlobalsUniform>(false),
            ),
        ),
    );

    let shader = asset_server.load::<Shader>(SHADER_ASSET_PATH);
    let vertex_state = fullscreen_shader.to_vertex_state();

    // Writing depth lets the SDF sort against meshes
    let depth_stencil = DepthStencilState {
        format: CORE_3D_DEPTH_FORMAT,
        depth_write_enabled: Some(true),
        depth_compare: Some(CompareFunction::GreaterEqual),
        stencil: StencilState::default(),
        bias: DepthBiasState::default(),
    };

    let gbuffer_pipeline_id = pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor {
        label: Some("raymarch_gbuffer_pipeline".into()),
        layout: vec![layout.clone()],
        vertex: vertex_state.clone(),
        fragment: Some(FragmentState {
            shader: shader.clone(),
            entry_point: Some("fragment".into()),
            targets: vec![
                Some(ColorTargetState {
                    format: DEFERRED_PREPASS_FORMAT,
                    blend: None,
                    write_mask: ColorWrites::ALL,
                }),
                Some(ColorTargetState {
                    format: DEFERRED_LIGHTING_PASS_ID_FORMAT,
                    blend: None,
                    write_mask: ColorWrites::ALL,
                }),
            ],
            ..default()
        }),
        depth_stencil: Some(depth_stencil.clone()),
        ..default()
    });

    let shadow_pipeline_id = pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor {
        label: Some("raymarch_shadow_pipeline".into()),
        layout: vec![layout.clone()],
        vertex: vertex_state,
        fragment: Some(FragmentState {
            shader,
            entry_point: Some("fragment_shadow".into()),
            targets: vec![],
            ..default()
        }),
        depth_stencil: Some(depth_stencil),
        ..default()
    });

    commands.insert_resource(RaymarchGBufferPipeline {
        layout: layout.clone(),
        pipeline_id: gbuffer_pipeline_id,
    });
    commands.insert_resource(RaymarchShadowPipeline {
        layout,
        pipeline_id: shadow_pipeline_id,
    });
}

fn raymarch_bind_group(
    ctx: &RenderContext,
    pipeline_cache: &PipelineCache,
    layout: &BindGroupLayoutDescriptor,
    view_uniforms: &ViewUniforms,
    globals: &GlobalsBuffer,
) -> Option<BindGroup> {
    let view_binding = view_uniforms.uniforms.binding()?;
    let globals_binding = globals.buffer.binding()?;
    Some(ctx.render_device().create_bind_group(
        "raymarch_bind_group",
        &pipeline_cache.get_bind_group_layout(layout),
        &BindGroupEntries::sequential((view_binding, globals_binding)),
    ))
}

fn raymarch_gbuffer_pass(
    view: ViewQuery<(
        &ViewUniformOffset,
        &ViewDepthStencilTexture,
        &ViewPrepassTextures,
    )>,
    pipeline: Option<Res<RaymarchGBufferPipeline>>,
    pipeline_cache: Res<PipelineCache>,
    view_uniforms: Res<ViewUniforms>,
    globals: Res<GlobalsBuffer>,
    mut ctx: RenderContext,
) {
    let Some(pipeline) = pipeline else {
        return;
    };
    let Some(render_pipeline) = pipeline_cache.get_render_pipeline(pipeline.pipeline_id) else {
        return;
    };

    let (view_uniform_offset, view_depth, view_prepass_textures) = view.into_inner();

    let (Some(deferred), Some(lighting_pass_id)) = (
        &view_prepass_textures.deferred,
        &view_prepass_textures.deferred_lighting_pass_id,
    ) else {
        return;
    };

    let Some(bind_group) = raymarch_bind_group(
        &ctx,
        &pipeline_cache,
        &pipeline.layout,
        &view_uniforms,
        &globals,
    ) else {
        return;
    };

    {
        // We load rather than clear because we only want to overwrite the pixels which the deferred mesh
        // prepass didn't write
        let mut pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
            label: Some("raymarch_gbuffer_pass"),
            color_attachments: &[
                Some(deferred.get_attachment()),
                Some(lighting_pass_id.get_attachment()),
            ],
            depth_stencil_attachment: Some(view_depth.get_attachment(StoreOp::Store)),
            timestamp_writes: None,
            occlusion_query_set: None,
            multiview_mask: None,
        });

        pass.set_render_pipeline(render_pipeline);
        pass.set_bind_group(0, &bind_group, &[view_uniform_offset.offset]);
        pass.draw(0..3, 0..1);
    }

    // The deferred lighting pass reconstructs world position from the prepass depth
    // texture, not the depth attachment we just wrote, so we have to do a copy.
    if let Some(prepass_depth) = &view_prepass_textures.depth {
        ctx.command_encoder().copy_texture_to_texture(
            view_depth.texture().as_image_copy(),
            prepass_depth.texture.texture.as_image_copy(),
            view_prepass_textures.size,
        );
    }
}

fn raymarch_directional_shadow_pass(
    view: ViewQuery<&ViewLightEntities>,
    shadow_views: Query<(&ShadowView, &ViewUniformOffset)>,
    pipeline: Option<Res<RaymarchShadowPipeline>>,
    pipeline_cache: Res<PipelineCache>,
    view_uniforms: Res<ViewUniforms>,
    globals: Res<GlobalsBuffer>,
    mut ctx: RenderContext,
) {
    let Some(pipeline) = pipeline else {
        return;
    };
    let Some(render_pipeline) = pipeline_cache.get_render_pipeline(pipeline.pipeline_id) else {
        return;
    };

    let view_lights = view.into_inner();
    for light_entity in view_lights.lights.iter().copied() {
        let Ok((shadow_view, view_uniform_offset)) = shadow_views.get(light_entity) else {
            continue;
        };
        draw_raymarch_shadow(
            &mut ctx,
            &pipeline_cache,
            &pipeline.layout,
            &view_uniforms,
            &globals,
            render_pipeline,
            shadow_view,
            view_uniform_offset,
        );
    }
}

fn raymarch_shared_shadow_pass(
    view: ViewQuery<(&ShadowView, &ViewUniformOffset)>,
    pipeline: Option<Res<RaymarchShadowPipeline>>,
    pipeline_cache: Res<PipelineCache>,
    view_uniforms: Res<ViewUniforms>,
    globals: Res<GlobalsBuffer>,
    mut ctx: RenderContext,
) {
    let Some(pipeline) = pipeline else {
        return;
    };
    let Some(render_pipeline) = pipeline_cache.get_render_pipeline(pipeline.pipeline_id) else {
        return;
    };

    let (shadow_view, view_uniform_offset) = view.into_inner();
    draw_raymarch_shadow(
        &mut ctx,
        &pipeline_cache,
        &pipeline.layout,
        &view_uniforms,
        &globals,
        render_pipeline,
        shadow_view,
        view_uniform_offset,
    );
}

fn draw_raymarch_shadow(
    ctx: &mut RenderContext,
    pipeline_cache: &PipelineCache,
    layout: &BindGroupLayoutDescriptor,
    view_uniforms: &ViewUniforms,
    globals: &GlobalsBuffer,
    render_pipeline: &RenderPipeline,
    shadow_view: &ShadowView,
    view_uniform_offset: &ViewUniformOffset,
) {
    let Some(bind_group) = raymarch_bind_group(ctx, pipeline_cache, layout, view_uniforms, globals)
    else {
        return;
    };

    let mut pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
        label: Some("raymarch_shadow_pass"),
        color_attachments: &[],
        depth_stencil_attachment: Some(shadow_view.depth_attachment.get_attachment(StoreOp::Store)),
        timestamp_writes: None,
        occlusion_query_set: None,
        multiview_mask: None,
    });

    pass.set_render_pipeline(render_pipeline);
    pass.set_bind_group(0, &bind_group, &[view_uniform_offset.offset]);
    pass.draw(0..3, 0..1);
}
//! Raymarches a signed distance field and packs it into the deferred gbuffer exactly as
//! a `StandardMaterial` would (via `deferred_gbuffer_from_pbr_input`), so Bevy's deferred
//! PBR lighting shades it like any other geometry. This example assumes familiarity with
//! raymarching.

import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput;
import bevy_pbr::render::mesh_view_bindings::view;
import bevy_pbr::render::view_transformations::{
    uv_to_ndc,
    position_ndc_to_view,
    position_ndc_to_world,
    direction_view_to_world,
    position_world_to_view,
    view_z_to_depth_ndc,
};
import bevy_pbr::deferred::functions::deferred_gbuffer_from_pbr_input;
import bevy_pbr::render::pbr_types::{pbr_input_new, STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE};
import bevy_pbr::render::mesh_types::MESH_FLAGS_SHADOW_RECEIVER_BIT;
import bevy_render::globals::Globals;

@group(0) @binding(1) var<uniform> globals: Globals;

const FAR: f32 = 100.0;
const SURFACE_EPSILON: f32 = 0.001;
const MAX_STEPS: u32 = 160u;
const STEP_SCALE: f32 = 0.5;

// Swap this for any SDF you like, the gbuffer integration below is independent of the
// shape.
const GYROID_SCALE: f32 = 5.0;

fn gyroid_field(p: vec3<f32>) -> f32 {
    let q = p * GYROID_SCALE + globals.time * 0.6;
    return dot(sin(q), cos(q.yzx));
}

fn map(p: vec3<f32>) -> f32 {
    let shell = (abs(gyroid_field(p)) - 0.14) / GYROID_SCALE;
    let ball = length(p) - 1.3;
    return max(ball, shell);
}

fn sdf_normal(p: vec3<f32>) -> vec3<f32> {
    let e = vec2<f32>(1.0, -1.0) * 0.0005;
    return normalize(
        e.xyy * map(p + e.xyy) +
        e.yyx * map(p + e.yyx) +
        e.yxy * map(p + e.yxy) +
        e.xxx * map(p + e.xxx)
    );
}

fn raymarch(ray_origin: vec3<f32>, ray_dir: vec3<f32>) -> f32 {
    var t = 0.0;
    for (var i = 0u; i < MAX_STEPS; i++) {
        let d = map(ray_origin + ray_dir * t);
        if d < SURFACE_EPSILON {
            return t;
        }
        t += d * STEP_SCALE;
        if t > FAR {
            break;
        }
    }
    return FAR;
}

struct Ray {
    origin: vec3<f32>,
    dir: vec3<f32>,
}

fn ray_for_uv(uv: vec2<f32>) -> Ray {
    let ndc = uv_to_ndc(uv);
    var ray: Ray;
    // Check ortho projection
    if view.clip_from_view[3].w == 1.0 {
        ray.origin = position_ndc_to_world(vec3<f32>(ndc, 1.0));
        ray.dir = normalize(direction_view_to_world(vec3<f32>(0.0, 0.0, -1.0)));
    } else {
        ray.origin = view.world_position;
        ray.dir = normalize(direction_view_to_world(position_ndc_to_view(vec3<f32>(ndc, 1.0))));
    }
    return ray;
}

fn depth_for_world_pos(world_pos: vec3<f32>) -> f32 {
    return view_z_to_depth_ndc(position_world_to_view(world_pos).z);
}

/// Our render pass has two color targets, matching the deferred prepass:
///   location 0: the packed gbuffer (Rgba32Uint)
///   location 1: the deferred lighting pass id (R8Uint)
struct GBufferOutput {
    @location(0) deferred: vec4<u32>,
    @location(1) deferred_lighting_pass_id: u32,
    @builtin(frag_depth) depth: f32,
}

@fragment
fn fragment(in: FullscreenVertexOutput) -> GBufferOutput {
    let ray = ray_for_uv(in.uv);
    let t = raymarch(ray.origin, ray.dir);

    // discard so we leave the mesh gbuffer intact when we miss
    if t >= FAR {
        discard;
    }

    let world_pos = ray.origin + ray.dir * t;
    let normal = sdf_normal(world_pos);

    // Fill in a PbrInput as a StandardMaterial fragment would
    var pbr_input = pbr_input_new();
    pbr_input.frag_coord = vec4<f32>(in.position.xy, depth_for_world_pos(world_pos), 1.0);
    pbr_input.world_position = vec4<f32>(world_pos, 1.0);
    pbr_input.world_normal = normal;
    pbr_input.N = normal;
    pbr_input.V = normalize(view.world_position - world_pos);

    // Per-pixel base color
    let field = gyroid_field(world_pos);
    let color = 0.5 + 0.5 * cos(
        6.2831 * (field * 0.4 + globals.time * 0.05) + vec3<f32>(0.0, 0.8, 1.6)
    );
    pbr_input.material.base_color = vec4<f32>(color, 1.0);
    pbr_input.material.metallic = 0.7;
    pbr_input.material.perceptual_roughness = 0.25;
    pbr_input.material.flags = STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE;
    // Let the surface receive shadows cast by other geometry.
    pbr_input.flags = MESH_FLAGS_SHADOW_RECEIVER_BIT;

    var out: GBufferOutput;
    out.deferred = deferred_gbuffer_from_pbr_input(pbr_input);
    // The lighting pass reads this per-pixel id to choose which lighting shader runs.
    //  1 is Bevy's built-in PBR deferred shader.
    out.deferred_lighting_pass_id = 1u;
    out.depth = pbr_input.frag_coord.z;
    return out;
}

// When rendering into a light's shadow map we only need depth
@fragment
fn fragment_shadow(in: FullscreenVertexOutput) -> @builtin(frag_depth) f32 {
    let ray = ray_for_uv(in.uv);
    let t = raymarch(ray.origin, ray.dir);
    if t >= FAR {
        discard;
    }
    return depth_for_world_pos(ray.origin + ray.dir * t);
}