Skip to content

Commit a15d152

Browse files
DGriffin91nickrart
andauthored
Deferred Renderer (#9258)
# Objective - Add a [Deferred Renderer](https://en.wikipedia.org/wiki/Deferred_shading) to Bevy. - This allows subsequent passes to access per pixel material information before/during shading. - Accessing this per pixel material information is needed for some features, like GI. It also makes other features (ex. Decals) simpler to implement and/or improves their capability. There are multiple approaches to accomplishing this. The deferred shading approach works well given the limitations of WebGPU and WebGL2. Motivation: [I'm working on a GI solution for Bevy](https://youtu.be/eH1AkL-mwhI) # Solution - The deferred renderer is implemented with a prepass and a deferred lighting pass. - The prepass renders opaque objects into the Gbuffer attachment (`Rgba32Uint`). The PBR shader generates a `PbrInput` in mostly the same way as the forward implementation and then [packs it into the Gbuffer](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/render/pbr.wgsl#L168). - The deferred lighting pass unpacks the `PbrInput` and [feeds it into the pbr() function](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl#L65), then outputs the shaded color data. - There is now a resource [DefaultOpaqueRendererMethod](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/material.rs#L599) that can be used to set the default render method for opaque materials. If materials return `None` from [opaque_render_method()](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/material.rs#L131) the `DefaultOpaqueRendererMethod` will be used. Otherwise, custom materials can also explicitly choose to only support Deferred or Forward by returning the respective [OpaqueRendererMethod](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/material.rs#L603) - Deferred materials can be used seamlessly along with both opaque and transparent forward rendered materials in the same scene. The [deferred rendering example](https://github.com/DGriffin91/bevy/blob/deferred/examples/3d/deferred_rendering.rs) does this. - The deferred renderer does not support MSAA. If any deferred materials are used, MSAA must be disabled. Both TAA and FXAA are supported. - Deferred rendering supports WebGL2/WebGPU. ## Custom deferred materials - Custom materials can support both deferred and forward at the same time. The [StandardMaterial](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/render/pbr.wgsl#L166) does this. So does [this example](https://github.com/DGriffin91/bevy_glowy_orb_tutorial/blob/deferred/assets/shaders/glowy.wgsl#L56). - Custom deferred materials that require PBR lighting can create a `PbrInput`, write it to the deferred GBuffer and let it be rendered by the `PBRDeferredLightingPlugin`. - Custom deferred materials that require custom lighting have two options: 1. Use the base_color channel of the `PbrInput` combined with the `STANDARD_MATERIAL_FLAGS_UNLIT_BIT` flag. [Example.](https://github.com/DGriffin91/bevy_glowy_orb_tutorial/blob/deferred/assets/shaders/glowy.wgsl#L56) (If the unlit bit is set, the base_color is stored as RGB9E5 for extra precision) 2. A Custom Deferred Lighting pass can be created, either overriding the default, or running in addition. The a depth buffer is used to limit rendering to only the required fragments for each deferred lighting pass. Materials can set their respective depth id via the [deferred_lighting_pass_id](https://github.com/DGriffin91/bevy/blob/b79182d2a32cac28c4213c2457a53ac2cc885332/crates/bevy_pbr/src/prepass/prepass_io.wgsl#L95) attachment. The custom deferred lighting pass plugin can then set [its corresponding depth](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl#L37). Then with the lighting pass using [CompareFunction::Equal](https://github.com/DGriffin91/bevy/blob/ec1465559f2c82001830e908fc02ff1d7c2efe51/crates/bevy_pbr/src/deferred/mod.rs#L335), only the fragments with a depth that equal the corresponding depth written in the material will be rendered. Custom deferred lighting plugins can also be created to render the StandardMaterial. The default deferred lighting plugin can be bypassed with `DefaultPlugins.set(PBRDeferredLightingPlugin { bypass: true })` --------- Co-authored-by: nickrart <[email protected]>
1 parent c8fd390 commit a15d152

40 files changed

+2959
-485
lines changed

Cargo.toml

+10
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,16 @@ description = "Illustrates bloom configuration using HDR and emissive materials"
611611
category = "3D Rendering"
612612
wasm = true
613613

614+
[[example]]
615+
name = "deferred_rendering"
616+
path = "examples/3d/deferred_rendering.rs"
617+
618+
[package.metadata.example.deferred_rendering]
619+
name = "Deferred Rendering"
620+
description = "Renders meshes with both forward and deferred pipelines"
621+
category = "3D Rendering"
622+
wasm = true
623+
614624
[[example]]
615625
name = "load_gltf"
616626
path = "examples/3d/load_gltf.rs"

assets/shaders/array_texture.wgsl

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#import bevy_pbr::mesh_vertex_output MeshVertexOutput
22
#import bevy_pbr::mesh_view_bindings view
3-
#import bevy_pbr::pbr_types STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT
3+
#import bevy_pbr::pbr_types STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT, PbrInput, pbr_input_new
44
#import bevy_core_pipeline::tonemapping tone_mapping
55
#import bevy_pbr::pbr_functions as fns
66

@@ -16,7 +16,7 @@ fn fragment(
1616

1717
// Prepare a 'processed' StandardMaterial by sampling all textures to resolve
1818
// the material members
19-
var pbr_input: fns::PbrInput = fns::pbr_input_new();
19+
var pbr_input: PbrInput = pbr_input_new();
2020

2121
pbr_input.material.base_color = textureSample(my_array_texture, my_array_texture_sampler, mesh.uv, layer);
2222
#ifdef VERTEX_COLORS

crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs

+18-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
22
clear_color::{ClearColor, ClearColorConfig},
33
core_3d::{Camera3d, Opaque3d},
4-
prepass::{DepthPrepass, MotionVectorPrepass, NormalPrepass},
4+
prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass},
55
skybox::{SkyboxBindGroup, SkyboxPipelineId},
66
};
77
use bevy_ecs::{prelude::*, query::QueryItem};
@@ -34,6 +34,7 @@ impl ViewNode for MainOpaquePass3dNode {
3434
Option<&'static DepthPrepass>,
3535
Option<&'static NormalPrepass>,
3636
Option<&'static MotionVectorPrepass>,
37+
Option<&'static DeferredPrepass>,
3738
Option<&'static SkyboxPipelineId>,
3839
Option<&'static SkyboxBindGroup>,
3940
&'static ViewUniformOffset,
@@ -53,12 +54,24 @@ impl ViewNode for MainOpaquePass3dNode {
5354
depth_prepass,
5455
normal_prepass,
5556
motion_vector_prepass,
57+
deferred_prepass,
5658
skybox_pipeline,
5759
skybox_bind_group,
5860
view_uniform_offset,
5961
): QueryItem<Self::ViewQuery>,
6062
world: &World,
6163
) -> Result<(), NodeRunError> {
64+
let load = if deferred_prepass.is_none() {
65+
match camera_3d.clear_color {
66+
ClearColorConfig::Default => LoadOp::Clear(world.resource::<ClearColor>().0.into()),
67+
ClearColorConfig::Custom(color) => LoadOp::Clear(color.into()),
68+
ClearColorConfig::None => LoadOp::Load,
69+
}
70+
} else {
71+
// If the deferred lighting pass has run, don't clear again in this pass.
72+
LoadOp::Load
73+
};
74+
6275
// Run the opaque pass, sorted front-to-back
6376
// NOTE: Scoped to drop the mutable borrow of render_context
6477
#[cfg(feature = "trace")]
@@ -69,23 +82,17 @@ impl ViewNode for MainOpaquePass3dNode {
6982
label: Some("main_opaque_pass_3d"),
7083
// NOTE: The opaque pass loads the color
7184
// buffer as well as writing to it.
72-
color_attachments: &[Some(target.get_color_attachment(Operations {
73-
load: match camera_3d.clear_color {
74-
ClearColorConfig::Default => {
75-
LoadOp::Clear(world.resource::<ClearColor>().0.into())
76-
}
77-
ClearColorConfig::Custom(color) => LoadOp::Clear(color.into()),
78-
ClearColorConfig::None => LoadOp::Load,
79-
},
80-
store: true,
81-
}))],
85+
color_attachments: &[Some(
86+
target.get_color_attachment(Operations { load, store: true }),
87+
)],
8288
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
8389
view: &depth.view,
8490
// NOTE: The opaque main pass loads the depth buffer and possibly overwrites it
8591
depth_ops: Some(Operations {
8692
load: if depth_prepass.is_some()
8793
|| normal_prepass.is_some()
8894
|| motion_vector_prepass.is_some()
95+
|| deferred_prepass.is_some()
8996
{
9097
// if any prepass runs, it will generate a depth buffer so we should use it,
9198
// even if only the normal_prepass is used.

crates/bevy_core_pipeline/src/core_3d/mod.rs

+116-10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ pub mod graph {
1010
pub mod node {
1111
pub const MSAA_WRITEBACK: &str = "msaa_writeback";
1212
pub const PREPASS: &str = "prepass";
13+
pub const DEFERRED_PREPASS: &str = "deferred_prepass";
14+
pub const COPY_DEFERRED_LIGHTING_ID: &str = "copy_deferred_lighting_id";
15+
pub const END_PREPASSES: &str = "end_prepasses";
1316
pub const START_MAIN_PASS: &str = "start_main_pass";
1417
pub const MAIN_OPAQUE_PASS: &str = "main_opaque_pass";
1518
pub const MAIN_TRANSPARENT_PASS: &str = "main_transparent_pass";
@@ -24,13 +27,16 @@ pub mod graph {
2427
}
2528
pub const CORE_3D: &str = graph::NAME;
2629

30+
// PERF: vulkan docs recommend using 24 bit depth for better performance
31+
pub const CORE_3D_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float;
32+
2733
use std::{cmp::Reverse, ops::Range};
2834

2935
pub use camera_3d::*;
3036
pub use main_opaque_pass_3d_node::*;
3137
pub use main_transparent_pass_3d_node::*;
3238

33-
use bevy_app::{App, Plugin};
39+
use bevy_app::{App, Plugin, PostUpdate};
3440
use bevy_ecs::prelude::*;
3541
use bevy_render::{
3642
camera::{Camera, ExtractedCamera},
@@ -50,12 +56,17 @@ use bevy_render::{
5056
view::ViewDepthTexture,
5157
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
5258
};
53-
use bevy_utils::{nonmax::NonMaxU32, FloatOrd, HashMap};
59+
use bevy_utils::{nonmax::NonMaxU32, tracing::warn, FloatOrd, HashMap};
5460

5561
use crate::{
62+
deferred::{
63+
copy_lighting_id::CopyDeferredLightingIdNode, node::DeferredGBufferPrepassNode,
64+
AlphaMask3dDeferred, Opaque3dDeferred, DEFERRED_LIGHTING_PASS_ID_FORMAT,
65+
DEFERRED_PREPASS_FORMAT,
66+
},
5667
prepass::{
57-
node::PrepassNode, AlphaMask3dPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass,
58-
Opaque3dPrepass, ViewPrepassTextures, DEPTH_PREPASS_FORMAT, MOTION_VECTOR_PREPASS_FORMAT,
68+
node::PrepassNode, AlphaMask3dPrepass, DeferredPrepass, DepthPrepass, MotionVectorPrepass,
69+
NormalPrepass, Opaque3dPrepass, ViewPrepassTextures, MOTION_VECTOR_PREPASS_FORMAT,
5970
NORMAL_PREPASS_FORMAT,
6071
},
6172
skybox::SkyboxPlugin,
@@ -69,7 +80,8 @@ impl Plugin for Core3dPlugin {
6980
fn build(&self, app: &mut App) {
7081
app.register_type::<Camera3d>()
7182
.register_type::<Camera3dDepthLoadOp>()
72-
.add_plugins((SkyboxPlugin, ExtractComponentPlugin::<Camera3d>::default()));
83+
.add_plugins((SkyboxPlugin, ExtractComponentPlugin::<Camera3d>::default()))
84+
.add_systems(PostUpdate, check_msaa);
7385

7486
let render_app = match app.get_sub_app_mut(RenderApp) {
7587
Ok(render_app) => render_app,
@@ -82,6 +94,8 @@ impl Plugin for Core3dPlugin {
8294
.init_resource::<DrawFunctions<Transparent3d>>()
8395
.init_resource::<DrawFunctions<Opaque3dPrepass>>()
8496
.init_resource::<DrawFunctions<AlphaMask3dPrepass>>()
97+
.init_resource::<DrawFunctions<Opaque3dDeferred>>()
98+
.init_resource::<DrawFunctions<AlphaMask3dDeferred>>()
8599
.add_systems(ExtractSchedule, extract_core_3d_camera_phases)
86100
.add_systems(ExtractSchedule, extract_camera_prepass_phase)
87101
.add_systems(
@@ -92,6 +106,8 @@ impl Plugin for Core3dPlugin {
92106
sort_phase_system::<Transparent3d>.in_set(RenderSet::PhaseSort),
93107
sort_phase_system::<Opaque3dPrepass>.in_set(RenderSet::PhaseSort),
94108
sort_phase_system::<AlphaMask3dPrepass>.in_set(RenderSet::PhaseSort),
109+
sort_phase_system::<Opaque3dDeferred>.in_set(RenderSet::PhaseSort),
110+
sort_phase_system::<AlphaMask3dDeferred>.in_set(RenderSet::PhaseSort),
95111
prepare_core_3d_depth_textures.in_set(RenderSet::PrepareResources),
96112
prepare_prepass_textures.in_set(RenderSet::PrepareResources),
97113
),
@@ -101,6 +117,15 @@ impl Plugin for Core3dPlugin {
101117
render_app
102118
.add_render_sub_graph(CORE_3D)
103119
.add_render_graph_node::<ViewNodeRunner<PrepassNode>>(CORE_3D, PREPASS)
120+
.add_render_graph_node::<ViewNodeRunner<DeferredGBufferPrepassNode>>(
121+
CORE_3D,
122+
DEFERRED_PREPASS,
123+
)
124+
.add_render_graph_node::<ViewNodeRunner<CopyDeferredLightingIdNode>>(
125+
CORE_3D,
126+
COPY_DEFERRED_LIGHTING_ID,
127+
)
128+
.add_render_graph_node::<EmptyNode>(CORE_3D, END_PREPASSES)
104129
.add_render_graph_node::<EmptyNode>(CORE_3D, START_MAIN_PASS)
105130
.add_render_graph_node::<ViewNodeRunner<MainOpaquePass3dNode>>(
106131
CORE_3D,
@@ -118,6 +143,9 @@ impl Plugin for Core3dPlugin {
118143
CORE_3D,
119144
&[
120145
PREPASS,
146+
DEFERRED_PREPASS,
147+
COPY_DEFERRED_LIGHTING_ID,
148+
END_PREPASSES,
121149
START_MAIN_PASS,
122150
MAIN_OPAQUE_PASS,
123151
MAIN_TRANSPARENT_PASS,
@@ -341,12 +369,14 @@ pub fn extract_camera_prepass_phase(
341369
Option<&DepthPrepass>,
342370
Option<&NormalPrepass>,
343371
Option<&MotionVectorPrepass>,
372+
Option<&DeferredPrepass>,
344373
),
345374
With<Camera3d>,
346375
>,
347376
>,
348377
) {
349-
for (entity, camera, depth_prepass, normal_prepass, motion_vector_prepass) in cameras_3d.iter()
378+
for (entity, camera, depth_prepass, normal_prepass, motion_vector_prepass, deferred_prepass) in
379+
cameras_3d.iter()
350380
{
351381
if camera.is_active {
352382
let mut entity = commands.get_or_spawn(entity);
@@ -361,6 +391,13 @@ pub fn extract_camera_prepass_phase(
361391
));
362392
}
363393

394+
if deferred_prepass.is_some() {
395+
entity.insert((
396+
RenderPhase::<Opaque3dDeferred>::default(),
397+
RenderPhase::<AlphaMask3dDeferred>::default(),
398+
));
399+
}
400+
364401
if depth_prepass.is_some() {
365402
entity.insert(DepthPrepass);
366403
}
@@ -370,6 +407,9 @@ pub fn extract_camera_prepass_phase(
370407
if motion_vector_prepass.is_some() {
371408
entity.insert(MotionVectorPrepass);
372409
}
410+
if deferred_prepass.is_some() {
411+
entity.insert(DeferredPrepass);
412+
}
373413
}
374414
}
375415
}
@@ -428,8 +468,7 @@ pub fn prepare_core_3d_depth_textures(
428468
mip_level_count: 1,
429469
sample_count: msaa.samples(),
430470
dimension: TextureDimension::D2,
431-
// PERF: vulkan docs recommend using 24 bit depth for better performance
432-
format: TextureFormat::Depth32Float,
471+
format: CORE_3D_DEPTH_FORMAT,
433472
usage,
434473
view_formats: &[],
435474
};
@@ -445,6 +484,22 @@ pub fn prepare_core_3d_depth_textures(
445484
}
446485
}
447486

487+
// Disable MSAA and warn if using deferred rendering
488+
pub fn check_msaa(
489+
mut msaa: ResMut<Msaa>,
490+
deferred_views: Query<Entity, (With<Camera>, With<DeferredPrepass>)>,
491+
) {
492+
if !deferred_views.is_empty() {
493+
match *msaa {
494+
Msaa::Off => (),
495+
_ => {
496+
warn!("MSAA is incompatible with deferred rendering and has been disabled.");
497+
*msaa = Msaa::Off;
498+
}
499+
};
500+
}
501+
}
502+
448503
// Prepares the textures used by the prepass
449504
pub fn prepare_prepass_textures(
450505
mut commands: Commands,
@@ -458,6 +513,7 @@ pub fn prepare_prepass_textures(
458513
Option<&DepthPrepass>,
459514
Option<&NormalPrepass>,
460515
Option<&MotionVectorPrepass>,
516+
Option<&DeferredPrepass>,
461517
),
462518
(
463519
With<RenderPhase<Opaque3dPrepass>>,
@@ -467,8 +523,12 @@ pub fn prepare_prepass_textures(
467523
) {
468524
let mut depth_textures = HashMap::default();
469525
let mut normal_textures = HashMap::default();
526+
let mut deferred_textures = HashMap::default();
527+
let mut deferred_lighting_id_textures = HashMap::default();
470528
let mut motion_vectors_textures = HashMap::default();
471-
for (entity, camera, depth_prepass, normal_prepass, motion_vector_prepass) in &views_3d {
529+
for (entity, camera, depth_prepass, normal_prepass, motion_vector_prepass, deferred_prepass) in
530+
&views_3d
531+
{
472532
let Some(physical_target_size) = camera.physical_target_size else {
473533
continue;
474534
};
@@ -489,7 +549,7 @@ pub fn prepare_prepass_textures(
489549
mip_level_count: 1,
490550
sample_count: msaa.samples(),
491551
dimension: TextureDimension::D2,
492-
format: DEPTH_PREPASS_FORMAT,
552+
format: CORE_3D_DEPTH_FORMAT,
493553
usage: TextureUsages::COPY_DST
494554
| TextureUsages::RENDER_ATTACHMENT
495555
| TextureUsages::TEXTURE_BINDING,
@@ -544,10 +604,56 @@ pub fn prepare_prepass_textures(
544604
.clone()
545605
});
546606

607+
let cached_deferred_texture = deferred_prepass.is_some().then(|| {
608+
deferred_textures
609+
.entry(camera.target.clone())
610+
.or_insert_with(|| {
611+
texture_cache.get(
612+
&render_device,
613+
TextureDescriptor {
614+
label: Some("prepass_deferred_texture"),
615+
size,
616+
mip_level_count: 1,
617+
sample_count: 1,
618+
dimension: TextureDimension::D2,
619+
format: DEFERRED_PREPASS_FORMAT,
620+
usage: TextureUsages::RENDER_ATTACHMENT
621+
| TextureUsages::TEXTURE_BINDING,
622+
view_formats: &[],
623+
},
624+
)
625+
})
626+
.clone()
627+
});
628+
629+
let deferred_lighting_pass_id_texture = deferred_prepass.is_some().then(|| {
630+
deferred_lighting_id_textures
631+
.entry(camera.target.clone())
632+
.or_insert_with(|| {
633+
texture_cache.get(
634+
&render_device,
635+
TextureDescriptor {
636+
label: Some("deferred_lighting_pass_id_texture"),
637+
size,
638+
mip_level_count: 1,
639+
sample_count: 1,
640+
dimension: TextureDimension::D2,
641+
format: DEFERRED_LIGHTING_PASS_ID_FORMAT,
642+
usage: TextureUsages::RENDER_ATTACHMENT
643+
| TextureUsages::TEXTURE_BINDING,
644+
view_formats: &[],
645+
},
646+
)
647+
})
648+
.clone()
649+
});
650+
547651
commands.entity(entity).insert(ViewPrepassTextures {
548652
depth: cached_depth_texture,
549653
normal: cached_normals_texture,
550654
motion_vectors: cached_motion_vectors_texture,
655+
deferred: cached_deferred_texture,
656+
deferred_lighting_pass_id: deferred_lighting_pass_id_texture,
551657
size,
552658
});
553659
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#import bevy_pbr::utils
2+
#import bevy_core_pipeline::fullscreen_vertex_shader FullscreenVertexOutput
3+
4+
@group(0) @binding(0)
5+
var material_id_texture: texture_2d<u32>;
6+
7+
struct FragmentOutput {
8+
@builtin(frag_depth) frag_depth: f32,
9+
10+
}
11+
12+
@fragment
13+
fn fragment(in: FullscreenVertexOutput) -> FragmentOutput {
14+
var out: FragmentOutput;
15+
// Depth is stored as unorm, so we are dividing the u8 by 255.0 here.
16+
out.frag_depth = f32(textureLoad(material_id_texture, vec2<i32>(in.position.xy), 0).x) / 255.0;
17+
return out;
18+
}
19+

0 commit comments

Comments
 (0)