From f4aa3284a8bc4be46583ab0a7efe6727c926854e Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Wed, 25 Aug 2021 19:44:20 +0000 Subject: [PATCH] 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 --- Cargo.toml | 4 + .../3d/shadow_caster_receiver_pipelined.rs | 182 ++++++++++++++++++ examples/README.md | 1 + pipelined/bevy_pbr2/src/light.rs | 5 + pipelined/bevy_pbr2/src/render/mod.rs | 72 +++++-- pipelined/bevy_pbr2/src/render/pbr.wgsl | 57 ++++-- 6 files changed, 286 insertions(+), 35 deletions(-) create mode 100644 examples/3d/shadow_caster_receiver_pipelined.rs diff --git a/Cargo.toml b/Cargo.toml index d38ff3375f..0fccee6aac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/examples/3d/shadow_caster_receiver_pipelined.rs b/examples/3d/shadow_caster_receiver_pipelined.rs new file mode 100644 index 0000000000..c3a68d0378 --- /dev/null +++ b/examples/3d/shadow_caster_receiver_pipelined.rs @@ -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>, + mut materials: ResMut>, +) { + 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>, + 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>, + queries: QuerySet<( + Query>, With)>, + Query>, With)>, + Query>, Without)>, + Query>, Without)>, + )>, +) { + if input.just_pressed(KeyCode::C) { + println!("Toggling casters"); + for entity in queries.q0().iter() { + commands.entity(entity).remove::(); + } + 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::(); + } + for entity in queries.q3().iter() { + commands.entity(entity).insert(NotShadowReceiver); + } + } +} diff --git a/examples/README.md b/examples/README.md index 40e82fb112..c0343a9016 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/pipelined/bevy_pbr2/src/light.rs b/pipelined/bevy_pbr2/src/light.rs index 571a9048bc..dab8eee43a 100644 --- a/pipelined/bevy_pbr2/src/light.rs +++ b/pipelined/bevy_pbr2/src/light.rs @@ -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; diff --git a/pipelined/bevy_pbr2/src/render/mod.rs b/pipelined/bevy_pbr2/src/render/mod.rs index 31e160d835..915657d60d 100644 --- a/pipelined/bevy_pbr2/src/render/mod.rs +++ b/pipelined/bevy_pbr2/src/render/mod.rs @@ -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, transform_binding_offset: u32, material_handle: Handle, + casts_shadows: bool, + receives_shadows: bool, } pub struct ExtractedMeshes { @@ -385,10 +387,23 @@ pub fn extract_meshes( meshes: Res>, materials: Res>, images: Res>, - query: Query<(&GlobalTransform, &Handle, &Handle)>, + query: Query<( + &GlobalTransform, + &Handle, + &Handle, + 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, } +#[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, + transform_uniforms: DynamicUniformVec, material_bind_groups: FrameSlabMap, mesh_transform_bind_group: FrameSlabMap, mesh_transform_bind_group_key: Option>, @@ -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 + }); + } } } } diff --git a/pipelined/bevy_pbr2/src/render/pbr.wgsl b/pipelined/bevy_pbr2/src/render/pbr.wgsl index 6e866689fa..81be4e8241 100644 --- a/pipelined/bevy_pbr2/src/render/pbr.wgsl +++ b/pipelined/bevy_pbr2/src/render/pbr.wgsl @@ -4,14 +4,19 @@ struct View { view_proj: mat4x4; world_position: vec3; }; -[[group(0), binding(0)]] -var view: View; [[block]] struct Mesh { - transform: mat4x4; + model: mat4x4; + // '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(vertex.position, 1.0); + let world_position = mesh.model * vec4(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(mesh.transform.x.xyz, mesh.transform.y.xyz, mesh.transform.z.xyz) * vertex.normal; + out.world_normal = mat3x3(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; // projection: mat4x4; @@ -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 { var output_color: vec4 = 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 = material.emissive; - if ((material.flags & FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) { + if ((material.flags & STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) { emissive = vec4(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 { 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 { // 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 { 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; }