Skip to main content

deferred_raymarch/
deferred_raymarch.rs

1//! Writes a raymarched signed distance field directly into the deferred gbuffer, so
2//! Bevy's standard deferred PBR lighting shades it as if it were a mesh. This example assumes
3//! prior familiarity with raymarching and is intended to demonstrate integration between a full-screen
4//! pass and the deferred renderer.
5
6use bevy::{
7    camera_controller::free_camera::{FreeCamera, FreeCameraPlugin},
8    core_pipeline::{
9        core_3d::CORE_3D_DEPTH_FORMAT,
10        deferred::{
11            copy_lighting_id::copy_deferred_lighting_id, node::late_deferred_prepass,
12            DEFERRED_LIGHTING_PASS_ID_FORMAT, DEFERRED_PREPASS_FORMAT,
13        },
14        prepass::{DeferredPrepass, DepthPrepass, ViewPrepassTextures},
15        Core3d, Core3dSystems, FullscreenShader,
16    },
17    pbr::{
18        per_view_shadow_pass, shared_shadow_pass, DefaultOpaqueRendererMethod, ShadowView,
19        ViewLightEntities, LATE_SHADOW_PASS,
20    },
21    prelude::*,
22    render::{
23        globals::{GlobalsBuffer, GlobalsUniform},
24        render_resource::{binding_types::uniform_buffer, *},
25        renderer::{RenderContext, ViewQuery},
26        view::{ViewDepthStencilTexture, ViewUniform, ViewUniformOffset, ViewUniforms},
27        RenderApp, RenderStartup,
28    },
29};
30
31fn main() {
32    App::new()
33        // Render everything through the deferred pipeline
34        .insert_resource(DefaultOpaqueRendererMethod::deferred())
35        .add_plugins((DefaultPlugins, DeferredRaymarchPlugin, FreeCameraPlugin))
36        .add_systems(Startup, setup)
37        .run();
38}
39
40const SHADER_ASSET_PATH: &str = "shaders/deferred_raymarch.wesl";
41
42fn setup(
43    mut commands: Commands,
44    mut meshes: ResMut<Assets<Mesh>>,
45    mut materials: ResMut<Assets<StandardMaterial>>,
46) {
47    commands.spawn((
48        Camera3d::default(),
49        Transform::from_xyz(4.0, 3.0, 6.0).looking_at(Vec3::new(0.0, 0.2, 0.0), Vec3::Y),
50        // Deferred rendering requires MSAA to be off.
51        Msaa::Off,
52        DepthPrepass,
53        DeferredPrepass,
54        AmbientLight {
55            brightness: 200.0,
56            ..default()
57        },
58        FreeCamera::default(),
59    ));
60
61    // A ground plane that catches the SDF's shadow
62    commands.spawn((
63        Mesh3d(meshes.add(Plane3d::default().mesh().size(20.0, 20.0))),
64        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
65        Transform::from_xyz(0.0, -1.5, 0.0),
66    ));
67
68    // A "regular" mesh cube
69    commands.spawn((
70        Mesh3d(meshes.add(Cuboid::from_length(1.2))),
71        MeshMaterial3d(materials.add(StandardMaterial {
72            base_color: Color::srgb(0.2, 0.4, 0.9),
73            perceptual_roughness: 0.4,
74            ..default()
75        })),
76        Transform::from_xyz(2.2, -0.9, 0.5),
77    ));
78
79    commands.spawn((
80        DirectionalLight {
81            illuminance: 8_000.0,
82            shadow_maps_enabled: true,
83            ..default()
84        },
85        Transform::from_xyz(4.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y),
86    ));
87}
88
89struct DeferredRaymarchPlugin;
90
91impl Plugin for DeferredRaymarchPlugin {
92    fn build(&self, app: &mut App) {
93        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
94            return;
95        };
96
97        render_app
98            .add_systems(RenderStartup, init_raymarch_pipelines)
99            .add_systems(
100                Core3d,
101                (
102                    // The gbuffer write must run after the mesh deferred prepass has
103                    // filled the gbuffer, but *before* `copy_deferred_lighting_id`
104                    // bakes the lighting-pass ids into the depth routing texture the
105                    // lighting pass reads
106                    raymarch_gbuffer_pass
107                        .in_set(Core3dSystems::Prepass)
108                        .after(late_deferred_prepass)
109                        .before(copy_deferred_lighting_id),
110                    // Write the SDF into the shadow maps after the mesh shadow passes
111                    // have drawn, so it casts shadows like any other caster
112                    raymarch_directional_shadow_pass
113                        .after(per_view_shadow_pass::<LATE_SHADOW_PASS>)
114                        .before(Core3dSystems::MainPass),
115                    raymarch_shared_shadow_pass
116                        .after(shared_shadow_pass::<LATE_SHADOW_PASS>)
117                        .before(Core3dSystems::MainPass),
118                ),
119            );
120    }
121}
122
123#[derive(Resource)]
124struct RaymarchGBufferPipeline {
125    layout: BindGroupLayoutDescriptor,
126    pipeline_id: CachedRenderPipelineId,
127}
128
129#[derive(Resource)]
130struct RaymarchShadowPipeline {
131    layout: BindGroupLayoutDescriptor,
132    pipeline_id: CachedRenderPipelineId,
133}
134
135fn init_raymarch_pipelines(
136    mut commands: Commands,
137    asset_server: Res<AssetServer>,
138    fullscreen_shader: Res<FullscreenShader>,
139    pipeline_cache: Res<PipelineCache>,
140) {
141    let layout = BindGroupLayoutDescriptor::new(
142        "raymarch_bind_group_layout",
143        &BindGroupLayoutEntries::sequential(
144            ShaderStages::FRAGMENT,
145            (
146                uniform_buffer::<ViewUniform>(true),
147                uniform_buffer::<GlobalsUniform>(false),
148            ),
149        ),
150    );
151
152    let shader = asset_server.load::<Shader>(SHADER_ASSET_PATH);
153    let vertex_state = fullscreen_shader.to_vertex_state();
154
155    // Writing depth lets the SDF sort against meshes
156    let depth_stencil = DepthStencilState {
157        format: CORE_3D_DEPTH_FORMAT,
158        depth_write_enabled: Some(true),
159        depth_compare: Some(CompareFunction::GreaterEqual),
160        stencil: StencilState::default(),
161        bias: DepthBiasState::default(),
162    };
163
164    let gbuffer_pipeline_id = pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor {
165        label: Some("raymarch_gbuffer_pipeline".into()),
166        layout: vec![layout.clone()],
167        vertex: vertex_state.clone(),
168        fragment: Some(FragmentState {
169            shader: shader.clone(),
170            entry_point: Some("fragment".into()),
171            targets: vec![
172                Some(ColorTargetState {
173                    format: DEFERRED_PREPASS_FORMAT,
174                    blend: None,
175                    write_mask: ColorWrites::ALL,
176                }),
177                Some(ColorTargetState {
178                    format: DEFERRED_LIGHTING_PASS_ID_FORMAT,
179                    blend: None,
180                    write_mask: ColorWrites::ALL,
181                }),
182            ],
183            ..default()
184        }),
185        depth_stencil: Some(depth_stencil.clone()),
186        ..default()
187    });
188
189    let shadow_pipeline_id = pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor {
190        label: Some("raymarch_shadow_pipeline".into()),
191        layout: vec![layout.clone()],
192        vertex: vertex_state,
193        fragment: Some(FragmentState {
194            shader,
195            entry_point: Some("fragment_shadow".into()),
196            targets: vec![],
197            ..default()
198        }),
199        depth_stencil: Some(depth_stencil),
200        ..default()
201    });
202
203    commands.insert_resource(RaymarchGBufferPipeline {
204        layout: layout.clone(),
205        pipeline_id: gbuffer_pipeline_id,
206    });
207    commands.insert_resource(RaymarchShadowPipeline {
208        layout,
209        pipeline_id: shadow_pipeline_id,
210    });
211}
212
213fn raymarch_bind_group(
214    ctx: &RenderContext,
215    pipeline_cache: &PipelineCache,
216    layout: &BindGroupLayoutDescriptor,
217    view_uniforms: &ViewUniforms,
218    globals: &GlobalsBuffer,
219) -> Option<BindGroup> {
220    let view_binding = view_uniforms.uniforms.binding()?;
221    let globals_binding = globals.buffer.binding()?;
222    Some(ctx.render_device().create_bind_group(
223        "raymarch_bind_group",
224        &pipeline_cache.get_bind_group_layout(layout),
225        &BindGroupEntries::sequential((view_binding, globals_binding)),
226    ))
227}
228
229fn raymarch_gbuffer_pass(
230    view: ViewQuery<(
231        &ViewUniformOffset,
232        &ViewDepthStencilTexture,
233        &ViewPrepassTextures,
234    )>,
235    pipeline: Option<Res<RaymarchGBufferPipeline>>,
236    pipeline_cache: Res<PipelineCache>,
237    view_uniforms: Res<ViewUniforms>,
238    globals: Res<GlobalsBuffer>,
239    mut ctx: RenderContext,
240) {
241    let Some(pipeline) = pipeline else {
242        return;
243    };
244    let Some(render_pipeline) = pipeline_cache.get_render_pipeline(pipeline.pipeline_id) else {
245        return;
246    };
247
248    let (view_uniform_offset, view_depth, view_prepass_textures) = view.into_inner();
249
250    let (Some(deferred), Some(lighting_pass_id)) = (
251        &view_prepass_textures.deferred,
252        &view_prepass_textures.deferred_lighting_pass_id,
253    ) else {
254        return;
255    };
256
257    let Some(bind_group) = raymarch_bind_group(
258        &ctx,
259        &pipeline_cache,
260        &pipeline.layout,
261        &view_uniforms,
262        &globals,
263    ) else {
264        return;
265    };
266
267    {
268        // We load rather than clear because we only want to overwrite the pixels which the deferred mesh
269        // prepass didn't write
270        let mut pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
271            label: Some("raymarch_gbuffer_pass"),
272            color_attachments: &[
273                Some(deferred.get_attachment()),
274                Some(lighting_pass_id.get_attachment()),
275            ],
276            depth_stencil_attachment: Some(view_depth.get_attachment(StoreOp::Store)),
277            timestamp_writes: None,
278            occlusion_query_set: None,
279            multiview_mask: None,
280        });
281
282        pass.set_render_pipeline(render_pipeline);
283        pass.set_bind_group(0, &bind_group, &[view_uniform_offset.offset]);
284        pass.draw(0..3, 0..1);
285    }
286
287    // The deferred lighting pass reconstructs world position from the prepass depth
288    // texture, not the depth attachment we just wrote, so we have to do a copy.
289    if let Some(prepass_depth) = &view_prepass_textures.depth {
290        ctx.command_encoder().copy_texture_to_texture(
291            view_depth.texture().as_image_copy(),
292            prepass_depth.texture.texture.as_image_copy(),
293            view_prepass_textures.size,
294        );
295    }
296}
297
298fn raymarch_directional_shadow_pass(
299    view: ViewQuery<&ViewLightEntities>,
300    shadow_views: Query<(&ShadowView, &ViewUniformOffset)>,
301    pipeline: Option<Res<RaymarchShadowPipeline>>,
302    pipeline_cache: Res<PipelineCache>,
303    view_uniforms: Res<ViewUniforms>,
304    globals: Res<GlobalsBuffer>,
305    mut ctx: RenderContext,
306) {
307    let Some(pipeline) = pipeline else {
308        return;
309    };
310    let Some(render_pipeline) = pipeline_cache.get_render_pipeline(pipeline.pipeline_id) else {
311        return;
312    };
313
314    let view_lights = view.into_inner();
315    for light_entity in view_lights.lights.iter().copied() {
316        let Ok((shadow_view, view_uniform_offset)) = shadow_views.get(light_entity) else {
317            continue;
318        };
319        draw_raymarch_shadow(
320            &mut ctx,
321            &pipeline_cache,
322            &pipeline.layout,
323            &view_uniforms,
324            &globals,
325            render_pipeline,
326            shadow_view,
327            view_uniform_offset,
328        );
329    }
330}
331
332fn raymarch_shared_shadow_pass(
333    view: ViewQuery<(&ShadowView, &ViewUniformOffset)>,
334    pipeline: Option<Res<RaymarchShadowPipeline>>,
335    pipeline_cache: Res<PipelineCache>,
336    view_uniforms: Res<ViewUniforms>,
337    globals: Res<GlobalsBuffer>,
338    mut ctx: RenderContext,
339) {
340    let Some(pipeline) = pipeline else {
341        return;
342    };
343    let Some(render_pipeline) = pipeline_cache.get_render_pipeline(pipeline.pipeline_id) else {
344        return;
345    };
346
347    let (shadow_view, view_uniform_offset) = view.into_inner();
348    draw_raymarch_shadow(
349        &mut ctx,
350        &pipeline_cache,
351        &pipeline.layout,
352        &view_uniforms,
353        &globals,
354        render_pipeline,
355        shadow_view,
356        view_uniform_offset,
357    );
358}
359
360fn draw_raymarch_shadow(
361    ctx: &mut RenderContext,
362    pipeline_cache: &PipelineCache,
363    layout: &BindGroupLayoutDescriptor,
364    view_uniforms: &ViewUniforms,
365    globals: &GlobalsBuffer,
366    render_pipeline: &RenderPipeline,
367    shadow_view: &ShadowView,
368    view_uniform_offset: &ViewUniformOffset,
369) {
370    let Some(bind_group) = raymarch_bind_group(ctx, pipeline_cache, layout, view_uniforms, globals)
371    else {
372        return;
373    };
374
375    let mut pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
376        label: Some("raymarch_shadow_pass"),
377        color_attachments: &[],
378        depth_stencil_attachment: Some(shadow_view.depth_attachment.get_attachment(StoreOp::Store)),
379        timestamp_writes: None,
380        occlusion_query_set: None,
381        multiview_mask: None,
382    });
383
384    pass.set_render_pipeline(render_pipeline);
385    pass.set_bind_group(0, &bind_group, &[view_uniform_offset.offset]);
386    pass.draw(0..3, 0..1);
387}