bevy_pbr2: Add support for not casting/receiving shadows (#2726)

# Objective

Allow marking meshes as not casting / receiving shadows.

## Solution

- Added `NotShadowCaster` and `NotShadowReceiver` zero-sized type components.
- Extract these components into `bool`s in `ExtractedMesh`
- Only generate `DrawShadowMesh` `Drawable`s for meshes _without_ `NotShadowCaster`
- Add a `u32` bit `flags` member to `MeshUniform` with one flag indicating whether the mesh is a shadow receiver
- If a mesh does _not_ have the `NotShadowReceiver` component, then it is a shadow receiver, and so the bit in the `MeshUniform` is set, otherwise it is not set.
- Added an example illustrating the functionality.

NOTE: I wanted to have the default state of a mesh as being a shadow caster and shadow receiver, hence the `Not*` components. However, I am on the fence about this. I don't want to have a negative performance impact, nor have people wondering why their custom meshes don't have shadows because they forgot to add `ShadowCaster` and `ShadowReceiver` components, but I also really don't like the double negatives the `Not*` approach incurs. What do you think?

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
Robert Swain 2021-08-25 19:44:20 +00:00
parent f368bf7fc7
commit f4aa3284a8
6 changed files with 286 additions and 35 deletions

View file

@ -199,6 +199,10 @@ path = "examples/3d/render_to_texture.rs"
name = "shadow_biases_pipelined"
path = "examples/3d/shadow_biases_pipelined.rs"
[[example]]
name = "shadow_caster_receiver_pipelined"
path = "examples/3d/shadow_caster_receiver_pipelined.rs"
[[example]]
name = "spawner"
path = "examples/3d/spawner.rs"

View file

@ -0,0 +1,182 @@
use bevy::{
ecs::prelude::*,
input::Input,
math::{EulerRot, Mat4, Vec3},
pbr2::{
DirectionalLight, DirectionalLightBundle, NotShadowCaster, NotShadowReceiver, PbrBundle,
PointLight, PointLightBundle, StandardMaterial,
},
prelude::{App, Assets, Handle, KeyCode, Transform},
render2::{
camera::{OrthographicProjection, PerspectiveCameraBundle},
color::Color,
mesh::{shape, Mesh},
},
PipelinedDefaultPlugins,
};
fn main() {
println!(
"Controls:
C - toggle shadow casters (i.e. casters become not, and not casters become casters)
R - toggle shadow receivers (i.e. receivers become not, and not receivers become receivers)
L - switch between directional and point lights"
);
App::new()
.add_plugins(PipelinedDefaultPlugins)
.add_startup_system(setup)
.add_system(toggle_light)
.add_system(toggle_shadows)
.run();
}
/// set up a 3D scene to test shadow biases and perspective projections
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let spawn_plane_depth = 500.0f32;
let spawn_height = 2.0;
let sphere_radius = 0.25;
let white_handle = materials.add(StandardMaterial {
base_color: Color::WHITE,
perceptual_roughness: 1.0,
..Default::default()
});
let sphere_handle = meshes.add(Mesh::from(shape::Icosphere {
radius: sphere_radius,
..Default::default()
}));
// sphere - initially a caster
commands.spawn_bundle(PbrBundle {
mesh: sphere_handle.clone(),
material: materials.add(Color::RED.into()),
transform: Transform::from_xyz(-1.0, spawn_height, 0.0),
..Default::default()
});
// sphere - initially not a caster
commands
.spawn_bundle(PbrBundle {
mesh: sphere_handle,
material: materials.add(Color::BLUE.into()),
transform: Transform::from_xyz(1.0, spawn_height, 0.0),
..Default::default()
})
.insert(NotShadowCaster);
// floating plane - initially not a shadow receiver and not a caster
commands
.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Plane { size: 20.0 })),
material: materials.add(Color::GREEN.into()),
transform: Transform::from_xyz(0.0, 1.0, -10.0),
..Default::default()
})
.insert_bundle((NotShadowCaster, NotShadowReceiver));
// lower ground plane - initially a shadow receiver
commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Plane { size: 20.0 })),
material: white_handle,
..Default::default()
});
println!("Using DirectionalLight");
commands.spawn_bundle(PointLightBundle {
transform: Transform::from_xyz(5.0, 5.0, 0.0),
point_light: PointLight {
intensity: 0.0,
range: spawn_plane_depth,
color: Color::WHITE,
..Default::default()
},
..Default::default()
});
let theta = std::f32::consts::FRAC_PI_4;
let light_transform = Mat4::from_euler(EulerRot::ZYX, 0.0, std::f32::consts::FRAC_PI_2, -theta);
commands.spawn_bundle(DirectionalLightBundle {
directional_light: DirectionalLight {
illuminance: 100000.0,
shadow_projection: OrthographicProjection {
left: -10.0,
right: 10.0,
bottom: -10.0,
top: 10.0,
near: -50.0,
far: 50.0,
..Default::default()
},
..Default::default()
},
transform: Transform::from_matrix(light_transform),
..Default::default()
});
// camera
commands.spawn_bundle(PerspectiveCameraBundle {
transform: Transform::from_xyz(-5.0, 5.0, 5.0)
.looking_at(Vec3::new(-1.0, 1.0, 0.0), Vec3::Y),
..Default::default()
});
}
fn toggle_light(
input: Res<Input<KeyCode>>,
mut point_lights: Query<&mut PointLight>,
mut directional_lights: Query<&mut DirectionalLight>,
) {
if input.just_pressed(KeyCode::L) {
for mut light in point_lights.iter_mut() {
light.intensity = if light.intensity == 0.0 {
println!("Using PointLight");
100000000.0
} else {
0.0
};
}
for mut light in directional_lights.iter_mut() {
light.illuminance = if light.illuminance == 0.0 {
println!("Using DirectionalLight");
100000.0
} else {
0.0
};
}
}
}
fn toggle_shadows(
mut commands: Commands,
input: Res<Input<KeyCode>>,
queries: QuerySet<(
Query<Entity, (With<Handle<Mesh>>, With<NotShadowCaster>)>,
Query<Entity, (With<Handle<Mesh>>, With<NotShadowReceiver>)>,
Query<Entity, (With<Handle<Mesh>>, Without<NotShadowCaster>)>,
Query<Entity, (With<Handle<Mesh>>, Without<NotShadowReceiver>)>,
)>,
) {
if input.just_pressed(KeyCode::C) {
println!("Toggling casters");
for entity in queries.q0().iter() {
commands.entity(entity).remove::<NotShadowCaster>();
}
for entity in queries.q2().iter() {
commands.entity(entity).insert(NotShadowCaster);
}
}
if input.just_pressed(KeyCode::R) {
println!("Toggling receivers");
for entity in queries.q1().iter() {
commands.entity(entity).remove::<NotShadowReceiver>();
}
for entity in queries.q3().iter() {
commands.entity(entity).insert(NotShadowReceiver);
}
}
}

View file

@ -107,6 +107,7 @@ Example | File | Description
`pbr` | [`3d/pbr.rs`](./3d/pbr.rs) | Demonstrates use of Physically Based Rendering (PBR) properties
`pbr_pipelined` | [`3d/pbr_pipelined.rs`](./3d/pbr_pipelined.rs) | Demonstrates use of Physically Based Rendering (PBR) properties
`render_to_texture` | [`3d/render_to_texture.rs`](./3d/render_to_texture.rs) | Shows how to render to texture
`shadow_caster_receiver_pipelined` | [`3d/shadow_caster_receiver_pipelined.rs`](./3d/shadow_caster_receiver_pipelined.rs) | Demonstrates how to prevent meshes from casting/receiving shadows in a 3d scene
`shadow_biases_pipelined` | [`3d/shadow_biases_pipelined.rs`](./3d/shadow_biases_pipelined.rs) | Demonstrates how shadow biases affect shadows in a 3d scene
`spawner` | [`3d/spawner.rs`](./3d/spawner.rs) | Renders a large number of cubes with changing position and material
`texture` | [`3d/texture.rs`](./3d/texture.rs) | Shows configuration of texture materials

View file

@ -151,3 +151,8 @@ impl Default for AmbientLight {
}
}
}
/// Add this component to make a `Mesh` not cast shadows
pub struct NotShadowCaster;
/// Add this component to make a `Mesh` not receive shadows
pub struct NotShadowReceiver;

View file

@ -1,7 +1,7 @@
mod light;
pub use light::*;
use crate::{StandardMaterial, StandardMaterialUniformData};
use crate::{NotShadowCaster, NotShadowReceiver, StandardMaterial, StandardMaterialUniformData};
use bevy_asset::{Assets, Handle};
use bevy_core_pipeline::Transparent3dPhase;
use bevy_ecs::{prelude::*, system::SystemState};
@ -120,11 +120,11 @@ impl FromWorld for PbrShaders {
let mesh_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStage::VERTEX,
visibility: ShaderStage::VERTEX | ShaderStage::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: BufferSize::new(Mat4::std140_size_static() as u64),
min_binding_size: BufferSize::new(80),
},
count: None,
}],
@ -374,6 +374,8 @@ struct ExtractedMesh {
mesh: Handle<Mesh>,
transform_binding_offset: u32,
material_handle: Handle<StandardMaterial>,
casts_shadows: bool,
receives_shadows: bool,
}
pub struct ExtractedMeshes {
@ -385,10 +387,23 @@ pub fn extract_meshes(
meshes: Res<Assets<Mesh>>,
materials: Res<Assets<StandardMaterial>>,
images: Res<Assets<Image>>,
query: Query<(&GlobalTransform, &Handle<Mesh>, &Handle<StandardMaterial>)>,
query: Query<(
&GlobalTransform,
&Handle<Mesh>,
&Handle<StandardMaterial>,
Option<&NotShadowCaster>,
Option<&NotShadowReceiver>,
)>,
) {
let mut extracted_meshes = Vec::new();
for (transform, mesh_handle, material_handle) in query.iter() {
for (
transform,
mesh_handle,
material_handle,
maybe_not_shadow_caster,
maybe_not_shadow_receiver,
) in query.iter()
{
if !meshes.contains(mesh_handle) {
continue;
}
@ -419,6 +434,10 @@ pub fn extract_meshes(
mesh: mesh_handle.clone_weak(),
transform_binding_offset: 0,
material_handle: material_handle.clone_weak(),
// NOTE: Double-negative is so that meshes cast and receive shadows by default
// Not not shadow caster means that this mesh is a shadow caster
casts_shadows: maybe_not_shadow_caster.is_none(),
receives_shadows: maybe_not_shadow_receiver.is_none(),
});
} else {
continue;
@ -435,9 +454,25 @@ struct MeshDrawInfo {
material_bind_group_key: FrameSlabMapKey<BufferId, BindGroup>,
}
#[derive(Debug, AsStd140)]
pub struct MeshUniform {
model: Mat4,
flags: u32,
}
// NOTE: These must match the bit flags in bevy_pbr2/src/render/pbr.wgsl!
bitflags::bitflags! {
#[repr(transparent)]
struct MeshFlags: u32 {
const SHADOW_RECEIVER = (1 << 0);
const NONE = 0;
const UNINITIALIZED = 0xFFFF;
}
}
#[derive(Default)]
pub struct MeshMeta {
transform_uniforms: DynamicUniformVec<Mat4>,
transform_uniforms: DynamicUniformVec<MeshUniform>,
material_bind_groups: FrameSlabMap<BufferId, BindGroup>,
mesh_transform_bind_group: FrameSlabMap<BufferId, BindGroup>,
mesh_transform_bind_group_key: Option<FrameSlabMapKey<BufferId, BindGroup>>,
@ -453,8 +488,15 @@ pub fn prepare_meshes(
.transform_uniforms
.reserve_and_clear(extracted_meshes.meshes.len(), &render_device);
for extracted_mesh in extracted_meshes.meshes.iter_mut() {
extracted_mesh.transform_binding_offset =
mesh_meta.transform_uniforms.push(extracted_mesh.transform);
let flags = if extracted_mesh.receives_shadows {
MeshFlags::SHADOW_RECEIVER
} else {
MeshFlags::NONE
};
extracted_mesh.transform_binding_offset = mesh_meta.transform_uniforms.push(MeshUniform {
model: extracted_mesh.transform,
flags: flags.bits,
});
}
mesh_meta
@ -694,12 +736,14 @@ pub fn queue_meshes(
for view_light_entity in view_lights.lights.iter().copied() {
let mut shadow_phase = view_light_shadow_phases.get_mut(view_light_entity).unwrap();
// TODO: this should only queue up meshes that are actually visible by each "light view"
for i in 0..extracted_meshes.meshes.len() {
shadow_phase.add(Drawable {
draw_function: draw_shadow_mesh,
draw_key: i,
sort_key: 0, // TODO: sort back-to-front
})
for (i, mesh) in extracted_meshes.meshes.iter().enumerate() {
if mesh.casts_shadows {
shadow_phase.add(Drawable {
draw_function: draw_shadow_mesh,
draw_key: i,
sort_key: 0, // TODO: sort back-to-front
});
}
}
}
}

View file

@ -4,14 +4,19 @@ struct View {
view_proj: mat4x4<f32>;
world_position: vec3<f32>;
};
[[group(0), binding(0)]]
var view: View;
[[block]]
struct Mesh {
transform: mat4x4<f32>;
model: mat4x4<f32>;
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32;
};
let MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 1u;
[[group(0), binding(0)]]
var view: View;
[[group(1), binding(0)]]
var mesh: Mesh;
@ -30,7 +35,7 @@ struct VertexOutput {
[[stage(vertex)]]
fn vertex(vertex: Vertex) -> VertexOutput {
let world_position = mesh.transform * vec4<f32>(vertex.position, 1.0);
let world_position = mesh.model * vec4<f32>(vertex.position, 1.0);
var out: VertexOutput;
out.uv = vertex.uv;
@ -38,7 +43,7 @@ fn vertex(vertex: Vertex) -> VertexOutput {
out.clip_position = view.view_proj * world_position;
// FIXME: The inverse transpose of the model matrix should be used to correctly handle scaling
// of normals
out.world_normal = mat3x3<f32>(mesh.transform.x.xyz, mesh.transform.y.xyz, mesh.transform.z.xyz) * vertex.normal;
out.world_normal = mat3x3<f32>(mesh.model.x.xyz, mesh.model.y.xyz, mesh.model.z.xyz) * vertex.normal;
return out;
}
@ -83,10 +88,17 @@ struct StandardMaterial {
perceptual_roughness: f32;
metallic: f32;
reflectance: f32;
// 'flags' is a bit field indicating various option. uint is 32 bits so we have up to 32 options.
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32;
};
let STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT: u32 = 1u;
let STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT: u32 = 2u;
let STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT: u32 = 4u;
let STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT: u32 = 8u;
let STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT: u32 = 16u;
let STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 32u;
struct PointLight {
color: vec4<f32>;
// projection: mat4x4<f32>;
@ -118,13 +130,6 @@ struct Lights {
n_directional_lights: u32;
};
let FLAGS_BASE_COLOR_TEXTURE_BIT: u32 = 1u;
let FLAGS_EMISSIVE_TEXTURE_BIT: u32 = 2u;
let FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT: u32 = 4u;
let FLAGS_OCCLUSION_TEXTURE_BIT: u32 = 8u;
let FLAGS_DOUBLE_SIDED_BIT: u32 = 16u;
let FLAGS_UNLIT_BIT: u32 = 32u;
[[group(0), binding(1)]]
var lights: Lights;
@ -463,22 +468,22 @@ struct FragmentInput {
[[stage(fragment)]]
fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
var output_color: vec4<f32> = material.base_color;
if ((material.flags & FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) {
if ((material.flags & STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) {
output_color = output_color * textureSample(base_color_texture, base_color_sampler, in.uv);
}
// // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit
if ((material.flags & FLAGS_UNLIT_BIT) == 0u) {
if ((material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) {
// TODO use .a for exposure compensation in HDR
var emissive: vec4<f32> = material.emissive;
if ((material.flags & FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) {
if ((material.flags & STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) {
emissive = vec4<f32>(emissive.rgb * textureSample(emissive_texture, emissive_sampler, in.uv).rgb, 1.0);
}
// calculate non-linear roughness from linear perceptualRoughness
var metallic: f32 = material.metallic;
var perceptual_roughness: f32 = material.perceptual_roughness;
if ((material.flags & FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) {
if ((material.flags & STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) {
let metallic_roughness = textureSample(metallic_roughness_texture, metallic_roughness_sampler, in.uv);
// Sampling from GLTF standard channels for now
metallic = metallic * metallic_roughness.b;
@ -487,7 +492,7 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
let roughness = perceptualRoughnessToRoughness(perceptual_roughness);
var occlusion: f32 = 1.0;
if ((material.flags & FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) {
if ((material.flags & STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) {
occlusion = textureSample(occlusion_texture, occlusion_sampler, in.uv).r;
}
@ -500,7 +505,7 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
// vec3 B = cross(N, T) * v_WorldTangent.w;
// # endif
if ((material.flags & FLAGS_DOUBLE_SIDED_BIT) != 0u) {
if ((material.flags & STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u) {
if (!in.is_front) {
N = -N;
}
@ -543,13 +548,23 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
let n_directional_lights = i32(lights.n_directional_lights);
for (var i: i32 = 0; i < n_point_lights; i = i + 1) {
let light = lights.point_lights[i];
let shadow = fetch_point_shadow(i, in.world_position, in.world_normal);
var shadow: f32;
if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u) {
shadow = fetch_point_shadow(i, in.world_position, in.world_normal);
} else {
shadow = 1.0;
}
let light_contrib = point_light(in.world_position.xyz, light, roughness, NdotV, N, V, R, F0, diffuse_color);
light_accum = light_accum + light_contrib * shadow;
}
for (var i: i32 = 0; i < n_directional_lights; i = i + 1) {
let light = lights.directional_lights[i];
let shadow = fetch_directional_shadow(i, in.world_position, in.world_normal);
var shadow: f32;
if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u) {
shadow = fetch_directional_shadow(i, in.world_position, in.world_normal);
} else {
shadow = 1.0;
}
let light_contrib = directional_light(light, roughness, NdotV, N, V, R, F0, diffuse_color);
light_accum = light_accum + light_contrib * shadow;
}