mirror of
https://github.com/bevyengine/bevy
synced 2024-11-26 06:30:19 +00:00
a15d152635
# 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](ec1465559f/crates/bevy_pbr/src/render/pbr.wgsl (L168)
). - The deferred lighting pass unpacks the `PbrInput` and [feeds it into the pbr() function](ec1465559f/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl (L65)
), then outputs the shaded color data. - There is now a resource [DefaultOpaqueRendererMethod](ec1465559f/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()](ec1465559f/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](ec1465559f/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](ec1465559f/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](b79182d2a3/crates/bevy_pbr/src/prepass/prepass_io.wgsl (L95)
) attachment. The custom deferred lighting pass plugin can then set [its corresponding depth](ec1465559f/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl (L37)
). Then with the lighting pass using [CompareFunction::Equal](ec1465559f/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 <nickolas.g.russell@gmail.com>
428 lines
13 KiB
Rust
428 lines
13 KiB
Rust
//! This example compares Forward, Forward + Prepass, and Deferred rendering.
|
|
|
|
use std::f32::consts::*;
|
|
|
|
use bevy::{
|
|
core_pipeline::{
|
|
fxaa::Fxaa,
|
|
prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass},
|
|
},
|
|
pbr::NotShadowReceiver,
|
|
pbr::{CascadeShadowConfigBuilder, DirectionalLightShadowMap},
|
|
pbr::{DefaultOpaqueRendererMethod, NotShadowCaster, OpaqueRendererMethod},
|
|
prelude::*,
|
|
render::render_resource::TextureFormat,
|
|
};
|
|
|
|
fn main() {
|
|
App::new()
|
|
.insert_resource(Msaa::Off)
|
|
.insert_resource(DefaultOpaqueRendererMethod::deferred())
|
|
.insert_resource(AmbientLight {
|
|
color: Color::WHITE,
|
|
brightness: 1.0 / 5.0f32,
|
|
})
|
|
.insert_resource(DirectionalLightShadowMap { size: 4096 })
|
|
.add_plugins(DefaultPlugins)
|
|
.insert_resource(Normal(None))
|
|
.insert_resource(Pause(true))
|
|
.add_systems(Startup, (setup, setup_parallax))
|
|
.add_systems(
|
|
Update,
|
|
(animate_light_direction, switch_mode, spin, update_normal),
|
|
)
|
|
.run();
|
|
}
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
asset_server: Res<AssetServer>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
) {
|
|
commands.spawn((
|
|
Camera3dBundle {
|
|
camera: Camera {
|
|
// Deferred both supports both hdr: true and hdr: false
|
|
hdr: false,
|
|
..default()
|
|
},
|
|
transform: Transform::from_xyz(0.7, 0.7, 1.0)
|
|
.looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
|
|
..default()
|
|
},
|
|
FogSettings {
|
|
color: Color::rgba(0.05, 0.05, 0.05, 1.0),
|
|
falloff: FogFalloff::Linear {
|
|
start: 1.0,
|
|
end: 8.0,
|
|
},
|
|
..default()
|
|
},
|
|
EnvironmentMapLight {
|
|
diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
|
|
specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
|
|
},
|
|
DepthPrepass,
|
|
MotionVectorPrepass,
|
|
DeferredPrepass,
|
|
Fxaa::default(),
|
|
));
|
|
|
|
commands.spawn(DirectionalLightBundle {
|
|
directional_light: DirectionalLight {
|
|
shadows_enabled: true,
|
|
..default()
|
|
},
|
|
cascade_shadow_config: CascadeShadowConfigBuilder {
|
|
num_cascades: 3,
|
|
maximum_distance: 10.0,
|
|
..default()
|
|
}
|
|
.into(),
|
|
transform: Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 0.0, -FRAC_PI_4)),
|
|
..default()
|
|
});
|
|
|
|
// FlightHelmet
|
|
let helmet_scene = asset_server.load("models/FlightHelmet/FlightHelmet.gltf#Scene0");
|
|
|
|
commands.spawn(SceneBundle {
|
|
scene: helmet_scene.clone(),
|
|
..default()
|
|
});
|
|
commands.spawn(SceneBundle {
|
|
scene: helmet_scene,
|
|
transform: Transform::from_xyz(-3.0, 0.0, -3.0),
|
|
..default()
|
|
});
|
|
|
|
let mut forward_mat: StandardMaterial = Color::rgb(0.1, 0.2, 0.1).into();
|
|
forward_mat.opaque_render_method = OpaqueRendererMethod::Forward;
|
|
let forward_mat_h = materials.add(forward_mat);
|
|
|
|
// Plane
|
|
commands.spawn(PbrBundle {
|
|
mesh: meshes.add(shape::Plane::from_size(50.0).into()),
|
|
material: forward_mat_h.clone(),
|
|
..default()
|
|
});
|
|
|
|
let cube_h = meshes.add(Mesh::from(shape::Cube { size: 0.1 }));
|
|
let sphere_h = meshes.add(Mesh::from(shape::UVSphere {
|
|
radius: 0.125,
|
|
sectors: 128,
|
|
stacks: 128,
|
|
}));
|
|
|
|
// Cubes
|
|
commands.spawn(PbrBundle {
|
|
mesh: cube_h.clone(),
|
|
material: forward_mat_h.clone(),
|
|
transform: Transform::from_xyz(-0.3, 0.5, -0.2),
|
|
..default()
|
|
});
|
|
commands.spawn(PbrBundle {
|
|
mesh: cube_h,
|
|
material: forward_mat_h,
|
|
transform: Transform::from_xyz(0.2, 0.5, 0.2),
|
|
..default()
|
|
});
|
|
|
|
let sphere_color = Color::rgb(10.0, 4.0, 1.0);
|
|
let sphere_pos = Transform::from_xyz(0.4, 0.5, -0.8);
|
|
// Emissive sphere
|
|
let mut unlit_mat: StandardMaterial = sphere_color.into();
|
|
unlit_mat.unlit = true;
|
|
commands.spawn((
|
|
PbrBundle {
|
|
mesh: sphere_h.clone(),
|
|
material: materials.add(unlit_mat),
|
|
transform: sphere_pos,
|
|
..default()
|
|
},
|
|
NotShadowCaster,
|
|
));
|
|
// Light
|
|
commands.spawn(PointLightBundle {
|
|
point_light: PointLight {
|
|
intensity: 1.0,
|
|
radius: 0.125,
|
|
shadows_enabled: true,
|
|
color: sphere_color,
|
|
..default()
|
|
},
|
|
transform: sphere_pos,
|
|
..default()
|
|
});
|
|
|
|
// Spheres
|
|
for i in 0..6 {
|
|
let j = i % 3;
|
|
let s_val = if i < 3 { 0.0 } else { 0.2 };
|
|
let material = if j == 0 {
|
|
materials.add(StandardMaterial {
|
|
base_color: Color::rgb(s_val, s_val, 1.0),
|
|
perceptual_roughness: 0.089,
|
|
metallic: 0.0,
|
|
..default()
|
|
})
|
|
} else if j == 1 {
|
|
materials.add(StandardMaterial {
|
|
base_color: Color::rgb(s_val, 1.0, s_val),
|
|
perceptual_roughness: 0.089,
|
|
metallic: 0.0,
|
|
..default()
|
|
})
|
|
} else {
|
|
materials.add(StandardMaterial {
|
|
base_color: Color::rgb(1.0, s_val, s_val),
|
|
perceptual_roughness: 0.089,
|
|
metallic: 0.0,
|
|
..default()
|
|
})
|
|
};
|
|
commands.spawn(PbrBundle {
|
|
mesh: sphere_h.clone(),
|
|
material,
|
|
transform: Transform::from_xyz(
|
|
j as f32 * 0.25 + if i < 3 { -0.15 } else { 0.15 } - 0.4,
|
|
0.125,
|
|
-j as f32 * 0.25 + if i < 3 { -0.15 } else { 0.15 } + 0.4,
|
|
),
|
|
..default()
|
|
});
|
|
}
|
|
|
|
// sky
|
|
commands.spawn((
|
|
PbrBundle {
|
|
mesh: meshes.add(Mesh::from(shape::Box::default())),
|
|
material: materials.add(StandardMaterial {
|
|
base_color: Color::hex("888888").unwrap(),
|
|
unlit: true,
|
|
cull_mode: None,
|
|
..default()
|
|
}),
|
|
transform: Transform::from_scale(Vec3::splat(1_000_000.0)),
|
|
..default()
|
|
},
|
|
NotShadowCaster,
|
|
NotShadowReceiver,
|
|
));
|
|
|
|
// Example instructions
|
|
commands.spawn(
|
|
TextBundle::from_section(
|
|
"",
|
|
TextStyle {
|
|
font_size: 18.0,
|
|
color: Color::WHITE,
|
|
..default()
|
|
},
|
|
)
|
|
.with_style(Style {
|
|
position_type: PositionType::Absolute,
|
|
top: Val::Px(10.0),
|
|
left: Val::Px(10.0),
|
|
..default()
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct Pause(bool);
|
|
|
|
fn animate_light_direction(
|
|
time: Res<Time>,
|
|
mut query: Query<&mut Transform, With<DirectionalLight>>,
|
|
pause: Res<Pause>,
|
|
) {
|
|
if pause.0 {
|
|
return;
|
|
}
|
|
for mut transform in &mut query {
|
|
transform.rotate_y(time.delta_seconds() * PI / 5.0);
|
|
}
|
|
}
|
|
|
|
fn setup_parallax(
|
|
mut commands: Commands,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut normal: ResMut<Normal>,
|
|
asset_server: Res<AssetServer>,
|
|
) {
|
|
// The normal map. Note that to generate it in the GIMP image editor, you should
|
|
// open the depth map, and do Filters → Generic → Normal Map
|
|
// You should enable the "flip X" checkbox.
|
|
let normal_handle = asset_server.load("textures/parallax_example/cube_normal.png");
|
|
normal.0 = Some(normal_handle);
|
|
|
|
let mut cube: Mesh = shape::Cube { size: 0.15 }.into();
|
|
|
|
// NOTE: for normal maps and depth maps to work, the mesh
|
|
// needs tangents generated.
|
|
cube.generate_tangents().unwrap();
|
|
|
|
let parallax_material = materials.add(StandardMaterial {
|
|
perceptual_roughness: 0.4,
|
|
base_color_texture: Some(asset_server.load("textures/parallax_example/cube_color.png")),
|
|
normal_map_texture: normal.0.clone(),
|
|
// The depth map is a greyscale texture where black is the highest level and
|
|
// white the lowest.
|
|
depth_map: Some(asset_server.load("textures/parallax_example/cube_depth.png")),
|
|
parallax_depth_scale: 0.09,
|
|
parallax_mapping_method: ParallaxMappingMethod::Relief { max_steps: 4 },
|
|
max_parallax_layer_count: 5.0f32.exp2(),
|
|
..default()
|
|
});
|
|
commands.spawn((
|
|
PbrBundle {
|
|
mesh: meshes.add(cube),
|
|
material: parallax_material,
|
|
transform: Transform::from_xyz(0.4, 0.2, -0.8),
|
|
..default()
|
|
},
|
|
Spin { speed: 0.3 },
|
|
));
|
|
}
|
|
|
|
/// Store handle of the normal to later modify its format in [`update_normal`].
|
|
#[derive(Resource)]
|
|
struct Normal(Option<Handle<Image>>);
|
|
|
|
// See `examples/3d/parallax_mapping.rs` example for reasoning
|
|
fn update_normal(
|
|
mut already_ran: Local<bool>,
|
|
mut images: ResMut<Assets<Image>>,
|
|
normal: Res<Normal>,
|
|
) {
|
|
if *already_ran {
|
|
return;
|
|
}
|
|
if let Some(normal) = normal.0.as_ref() {
|
|
if let Some(image) = images.get_mut(normal) {
|
|
image.texture_descriptor.format = TextureFormat::Rgba8Unorm;
|
|
*already_ran = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct Spin {
|
|
speed: f32,
|
|
}
|
|
|
|
fn spin(time: Res<Time>, mut query: Query<(&mut Transform, &Spin)>, pause: Res<Pause>) {
|
|
if pause.0 {
|
|
return;
|
|
}
|
|
for (mut transform, spin) in query.iter_mut() {
|
|
transform.rotate_local_y(spin.speed * time.delta_seconds());
|
|
transform.rotate_local_x(spin.speed * time.delta_seconds());
|
|
transform.rotate_local_z(-spin.speed * time.delta_seconds());
|
|
}
|
|
}
|
|
|
|
#[derive(Resource, Default)]
|
|
enum DefaultRenderMode {
|
|
#[default]
|
|
Deferred,
|
|
Forward,
|
|
ForwardPrepass,
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn switch_mode(
|
|
mut text: Query<&mut Text>,
|
|
mut commands: Commands,
|
|
keys: Res<Input<KeyCode>>,
|
|
mut default_opaque_renderer_method: ResMut<DefaultOpaqueRendererMethod>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
cameras: Query<Entity, With<Camera>>,
|
|
mut pause: ResMut<Pause>,
|
|
mut hide_ui: Local<bool>,
|
|
mut mode: Local<DefaultRenderMode>,
|
|
) {
|
|
let mut text = text.single_mut();
|
|
let text = &mut text.sections[0].value;
|
|
|
|
text.clear();
|
|
|
|
if keys.just_pressed(KeyCode::Space) {
|
|
pause.0 = !pause.0;
|
|
}
|
|
|
|
if keys.just_pressed(KeyCode::Key1) {
|
|
*mode = DefaultRenderMode::Deferred;
|
|
default_opaque_renderer_method.set_to_deferred();
|
|
println!("DefaultOpaqueRendererMethod: Deferred");
|
|
for _ in materials.iter_mut() {}
|
|
for camera in &cameras {
|
|
commands.entity(camera).remove::<NormalPrepass>();
|
|
commands.entity(camera).insert(DepthPrepass);
|
|
commands.entity(camera).insert(MotionVectorPrepass);
|
|
commands.entity(camera).insert(DeferredPrepass);
|
|
}
|
|
}
|
|
if keys.just_pressed(KeyCode::Key2) {
|
|
*mode = DefaultRenderMode::Forward;
|
|
default_opaque_renderer_method.set_to_forward();
|
|
println!("DefaultOpaqueRendererMethod: Forward");
|
|
for _ in materials.iter_mut() {}
|
|
for camera in &cameras {
|
|
commands.entity(camera).remove::<NormalPrepass>();
|
|
commands.entity(camera).remove::<DepthPrepass>();
|
|
commands.entity(camera).remove::<MotionVectorPrepass>();
|
|
commands.entity(camera).remove::<DeferredPrepass>();
|
|
}
|
|
}
|
|
if keys.just_pressed(KeyCode::Key3) {
|
|
*mode = DefaultRenderMode::ForwardPrepass;
|
|
default_opaque_renderer_method.set_to_forward();
|
|
println!("DefaultOpaqueRendererMethod: Forward + Prepass");
|
|
for _ in materials.iter_mut() {}
|
|
for camera in &cameras {
|
|
commands.entity(camera).insert(NormalPrepass);
|
|
commands.entity(camera).insert(DepthPrepass);
|
|
commands.entity(camera).insert(MotionVectorPrepass);
|
|
commands.entity(camera).remove::<DeferredPrepass>();
|
|
}
|
|
}
|
|
|
|
if keys.just_pressed(KeyCode::H) {
|
|
*hide_ui = !*hide_ui;
|
|
}
|
|
|
|
if !*hide_ui {
|
|
text.push_str("(H) Hide UI\n");
|
|
text.push_str("(Space) Play/Pause\n\n");
|
|
text.push_str("Rendering Method:\n");
|
|
|
|
text.push_str(&format!(
|
|
"(1) {} Deferred\n",
|
|
if let DefaultRenderMode::Deferred = *mode {
|
|
">"
|
|
} else {
|
|
""
|
|
}
|
|
));
|
|
text.push_str(&format!(
|
|
"(2) {} Forward\n",
|
|
if let DefaultRenderMode::Forward = *mode {
|
|
">"
|
|
} else {
|
|
""
|
|
}
|
|
));
|
|
text.push_str(&format!(
|
|
"(3) {} Forward + Prepass\n",
|
|
if let DefaultRenderMode::ForwardPrepass = *mode {
|
|
">"
|
|
} else {
|
|
""
|
|
}
|
|
));
|
|
}
|
|
}
|