diff --git a/Cargo.toml b/Cargo.toml index d7041ff66dcc7..46874858d2d00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1261,6 +1261,17 @@ description = "Illustrates various lighting options in a simple scene" category = "3D Rendering" wasm = true +[[example]] +name = "light_render_layers" +path = "examples/3d/light_render_layers.rs" +doc-scrape-examples = true + +[package.metadata.example.light_render_layers] +name = "Light Render Layers" +description = "Demonstrates using render layers to control which directional and point lights affect meshes" +category = "3D Rendering" +wasm = true + [[example]] name = "lines" path = "examples/3d/lines.rs" diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl b/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl index 8d8a22b943ea6..1e680c56caa32 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl @@ -92,6 +92,7 @@ struct VertexOutput { ddy_uv: vec2, world_tangent: vec4, mesh_flags: u32, + render_layers: u32, cluster_id: u32, material_bind_group_slot: u32, #ifdef PREPASS_FRAGMENT @@ -172,6 +173,7 @@ fn resolve_vertex_output(frag_coord: vec4) -> VertexOutput { ddy_uv, world_tangent, instance_uniform.flags, + instance_uniform.render_layers, instance_id ^ meshlet_id, instance_uniform.material_and_lightmap_bind_group_slot & 0xffffu, #ifdef PREPASS_FRAGMENT diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index d1cb7e27c853b..f2219a16dfcbf 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -84,6 +84,7 @@ pub struct ExtractedPointLight { pub shadow_normal_bias: f32, pub shadow_map_near_z: f32, pub spot_light_angles: Option<(f32, f32)>, + pub render_layers: RenderLayers, pub volumetric: bool, pub soft_shadows_enabled: bool, /// whether this point light contributes diffuse light to lightmapped meshes @@ -129,6 +130,28 @@ bitflags::bitflags! { } } +/// The first bit in light flags that stores packed render layer membership. +const LIGHT_RENDER_LAYERS_SHIFT: u32 = 6; +/// The bitmask used to store packed render layer membership in light flags. +const LIGHT_RENDER_LAYERS_MASK: u32 = (1 << (u32::BITS - LIGHT_RENDER_LAYERS_SHIFT)) - 1; + +/// Packs render layers into light flags. +/// +/// We reserve the lower bits for light feature flags. If layers outside the +/// packed range are used, we fall back to `all bits set` to preserve behavior. +fn render_layers_to_light_mask(render_layers: &RenderLayers) -> u32 { + let bits = render_layers.bits(); + let low_bits = bits.first().copied().unwrap_or_default(); + let unsupported_bits = (low_bits >> (u32::BITS - LIGHT_RENDER_LAYERS_SHIFT)) != 0 + || bits.iter().skip(1).any(|&extra_bits| extra_bits != 0); + + if unsupported_bits { + LIGHT_RENDER_LAYERS_MASK + } else { + (low_bits as u32) & LIGHT_RENDER_LAYERS_MASK + } +} + #[derive(Copy, Clone, ShaderType, Default, Debug)] pub struct GpuDirectionalCascade { clip_from_world: Mat4, @@ -300,6 +323,7 @@ pub fn extract_lights( &GlobalTransform, &ViewVisibility, &CubemapFrusta, + Option<&RenderLayers>, Option<&VolumetricLight>, ), Or<( @@ -308,6 +332,7 @@ pub fn extract_lights( Changed, Changed, Changed, + Changed, Changed, )>, >, @@ -322,6 +347,7 @@ pub fn extract_lights( &GlobalTransform, &ViewVisibility, &Frustum, + Option<&RenderLayers>, Option<&VolumetricLight>, ), Or<( @@ -330,6 +356,7 @@ pub fn extract_lights( Changed, Changed, Changed, + Changed, Changed, )>, >, @@ -413,6 +440,7 @@ pub fn extract_lights( transform, view_visibility, frusta, + maybe_layers, volumetric_light, ) in point_lights.iter() { @@ -462,6 +490,7 @@ pub fn extract_lights( * core::f32::consts::SQRT_2, shadow_map_near_z: point_light.shadow_map_near_z, spot_light_angles: None, + render_layers: maybe_layers.unwrap_or_default().clone(), volumetric: volumetric_light.is_some(), affects_lightmapped_mesh_diffuse: point_light.affects_lightmapped_mesh_diffuse, #[cfg(feature = "experimental_pbr_pcss")] @@ -490,6 +519,7 @@ pub fn extract_lights( transform, view_visibility, frustum, + maybe_layers, volumetric_light, ) in spot_lights.iter() { @@ -538,6 +568,7 @@ pub fn extract_lights( * core::f32::consts::SQRT_2, shadow_map_near_z: spot_light.shadow_map_near_z, spot_light_angles: Some((spot_light.inner_angle, spot_light.outer_angle)), + render_layers: maybe_layers.unwrap_or_default().clone(), volumetric: volumetric_light.is_some(), affects_lightmapped_mesh_diffuse: spot_light.affects_lightmapped_mesh_diffuse, #[cfg(feature = "experimental_pbr_pcss")] @@ -1005,6 +1036,8 @@ pub fn prepare_lights( for (index, &(entity, _, light, _)) in point_lights.iter().enumerate() { let mut flags = PointLightFlags::NONE; + let render_layers_mask = render_layers_to_light_mask(&light.render_layers); + flags |= PointLightFlags::from_bits_retain(render_layers_mask << LIGHT_RENDER_LAYERS_SHIFT); // Lights are sorted, shadow enabled lights are first if light.shadow_maps_enabled @@ -1298,6 +1331,9 @@ pub fn prepare_lights( num_directional_lights_for_this_view += 1; let mut flags = DirectionalLightFlags::NONE; + let render_layers_mask = render_layers_to_light_mask(&light.render_layers); + flags |= + DirectionalLightFlags::from_bits_retain(render_layers_mask << LIGHT_RENDER_LAYERS_SHIFT); // Lights are sorted, volumetric and shadow enabled lights are first if light.volumetric @@ -1904,12 +1940,27 @@ pub(crate) struct SpecializeShadowsSystemParam<'w, 's> { render_lightmaps: Res<'w, RenderLightmaps>, view_lights: Query<'w, 's, (Entity, &'static ViewLightEntities), With>, view_light_entities: Query<'w, 's, (&'static LightEntity, &'static ExtractedView)>, - point_light_entities: - Query<'w, 's, &'static RenderCubemapVisibleEntities, With>, - directional_light_entities: - Query<'w, 's, &'static RenderCascadesVisibleEntities, With>, - spot_light_entities: - Query<'w, 's, &'static RenderVisibleMeshEntities, With>, + point_light_entities: Query< + 'w, + 's, + (&'static RenderCubemapVisibleEntities, &'static ExtractedPointLight), + With, + >, + directional_light_entities: Query< + 'w, + 's, + ( + &'static RenderCascadesVisibleEntities, + &'static ExtractedDirectionalLight, + ), + With, + >, + spot_light_entities: Query< + 'w, + 's, + (&'static RenderVisibleMeshEntities, &'static ExtractedPointLight), + With, + >, light_key_cache: Res<'w, LightKeyCache>, specialized_shadow_material_pipeline_cache: ResMut<'w, SpecializedShadowMaterialPipelineCache>, pending_shadow_queues: ResMut<'w, PendingShadowQueues>, @@ -1963,28 +2014,41 @@ pub(crate) fn specialize_shadows( continue; }; - let visible_entities = match light_entity { + let (visible_entities, light_render_layers) = match light_entity { LightEntity::Directional { light_entity, cascade_index, - } => directional_light_entities - .get(*light_entity) - .expect("Failed to get directional light visible entities") - .entities - .get(&entity) - .expect("Failed to get directional light visible entities for view") - .get(*cascade_index) - .expect("Failed to get directional light visible entities for cascade"), + } => { + let (visible_entities, light) = directional_light_entities + .get(*light_entity) + .expect("Failed to get directional light visible entities"); + ( + visible_entities + .entities + .get(&entity) + .expect("Failed to get directional light visible entities for view") + .get(*cascade_index) + .expect( + "Failed to get directional light visible entities for cascade", + ), + &light.render_layers, + ) + } LightEntity::Point { light_entity, face_index, - } => point_light_entities - .get(*light_entity) - .expect("Failed to get point light visible entities") - .get(*face_index), - LightEntity::Spot { light_entity } => spot_light_entities - .get(*light_entity) - .expect("Failed to get spot light visible entities"), + } => { + let (visible_entities, light) = point_light_entities + .get(*light_entity) + .expect("Failed to get point light visible entities"); + (visible_entities.get(*face_index), &light.render_layers) + } + LightEntity::Spot { light_entity } => { + let (visible_entities, light) = spot_light_entities + .get(*light_entity) + .expect("Failed to get spot light visible entities"); + (visible_entities, &light.render_layers) + } }; let mut maybe_specialized_shadow_material_pipeline_cache = @@ -2071,6 +2135,10 @@ pub(crate) fn specialize_shadows( { continue; } + let mesh_layers = mesh_instance.render_layers.as_ref().unwrap_or_default(); + if !light_render_layers.intersects(mesh_layers) { + continue; + } let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id()) else { continue; }; @@ -2178,12 +2246,14 @@ pub fn queue_shadows( mesh_allocator: Res, view_lights: Query<(Entity, &ViewLightEntities, Option<&RenderLayers>), With>, view_light_entities: Query<(&LightEntity, &ExtractedView)>, - point_light_entities: Query<&RenderCubemapVisibleEntities, With>, + point_light_entities: + Query<(&RenderCubemapVisibleEntities, &ExtractedPointLight), With>, directional_light_entities: Query< - &RenderCascadesVisibleEntities, + (&RenderCascadesVisibleEntities, &ExtractedDirectionalLight), With, >, - spot_light_entities: Query<&RenderVisibleMeshEntities, With>, + spot_light_entities: + Query<(&RenderVisibleMeshEntities, &ExtractedPointLight), With>, specialized_material_pipeline_cache: Res, mut pending_shadow_queues: ResMut, dirty_specializations: Res, @@ -2214,28 +2284,41 @@ pub fn queue_shadows( "View pending shadow queues should have been created in `specialize_shadows`", ); - let visible_entities = match light_entity { + let (visible_entities, light_render_layers) = match light_entity { LightEntity::Directional { light_entity, cascade_index, - } => directional_light_entities - .get(*light_entity) - .expect("Failed to get directional light visible entities") - .entities - .get(&entity) - .expect("Failed to get directional light visible entities for view") - .get(*cascade_index) - .expect("Failed to get directional light visible entities for cascade"), + } => { + let (visible_entities, light) = directional_light_entities + .get(*light_entity) + .expect("Failed to get directional light visible entities"); + ( + visible_entities + .entities + .get(&entity) + .expect("Failed to get directional light visible entities for view") + .get(*cascade_index) + .expect( + "Failed to get directional light visible entities for cascade", + ), + &light.render_layers, + ) + } LightEntity::Point { light_entity, face_index, - } => point_light_entities - .get(*light_entity) - .expect("Failed to get point light visible entities") - .get(*face_index), - LightEntity::Spot { light_entity } => spot_light_entities - .get(*light_entity) - .expect("Failed to get spot light visible entities"), + } => { + let (visible_entities, light) = point_light_entities + .get(*light_entity) + .expect("Failed to get point light visible entities"); + (visible_entities.get(*face_index), &light.render_layers) + } + LightEntity::Spot { light_entity } => { + let (visible_entities, light) = spot_light_entities + .get(*light_entity) + .expect("Failed to get spot light visible entities"); + (visible_entities, &light.render_layers) + } }; // First, remove meshes that need to be respecialized, and those that were removed, from the bins. @@ -2277,7 +2360,9 @@ pub fn queue_shadows( let mesh_layers = mesh_instance.render_layers.as_ref().unwrap_or_default(); let camera_layers = camera_layers.unwrap_or_default(); - if !camera_layers.intersects(mesh_layers) { + if !camera_layers.intersects(mesh_layers) + || !light_render_layers.intersects(mesh_layers) + { continue; } diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index f9d09332f0440..64c91864a6d75 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -492,6 +492,8 @@ pub struct MeshUniform { pub local_from_world_transpose_a: [Vec4; 2], pub local_from_world_transpose_b: f32, pub flags: u32, + /// Packed render layer mask used for per-mesh light filtering in shaders. + pub render_layers: u32, // Four 16-bit unsigned normalized UV values packed into a `UVec2`: // // <--- MSB LSB ---> @@ -547,6 +549,8 @@ pub struct MeshInputUniform { pub lightmap_uv_rect: UVec2, /// Various [`MeshFlags`]. pub flags: u32, + /// Packed render layer mask used for per-mesh light filtering in shaders. + pub render_layers: u32, /// The index of this mesh's [`MeshInputUniform`] in the previous frame's /// buffer, if applicable. /// @@ -591,6 +595,8 @@ pub struct MeshInputUniform { /// /// If the mesh has no morph targets, this is `u32::MAX`. pub morph_descriptor_index: u32, + /// Padding to preserve 16-byte alignment for POD casts. + pub pad: [u32; 3], } impl_atomic_pod!(MeshInputUniform, MeshInputUniformBlob); @@ -629,6 +635,7 @@ impl MeshUniform { current_skin_index: Option, morph_descriptor_index: Option, tag: Option, + render_layers: Option<&RenderLayers>, ) -> Self { let (local_from_world_transpose_a, local_from_world_transpose_b) = mesh_transforms.world_from_local.inverse_transpose_3x3(); @@ -644,6 +651,7 @@ impl MeshUniform { local_from_world_transpose_a, local_from_world_transpose_b, flags: mesh_transforms.flags, + render_layers: render_layers_to_shader_mask(render_layers), first_vertex_index, current_skin_index: current_skin_index.unwrap_or(u32::MAX), material_and_lightmap_bind_group_slot: u32::from(material_bind_group_slot) @@ -657,6 +665,27 @@ impl MeshUniform { } } +/// Number of render layer bits available for per-mesh/per-light shader filtering. +/// +/// Keep this in sync with the light-side packing in `light.rs` and the shader +/// constants in `mesh_view_types.wgsl`. +const SHADER_RENDER_LAYER_MASK_BITS: u32 = 26; +const SHADER_RENDER_LAYER_MASK: u32 = (1 << SHADER_RENDER_LAYER_MASK_BITS) - 1; + +fn render_layers_to_shader_mask(render_layers: Option<&RenderLayers>) -> u32 { + let render_layers = render_layers.unwrap_or_default(); + let bits = render_layers.bits(); + let low_bits = bits.first().copied().unwrap_or_default(); + let unsupported_bits = (low_bits >> SHADER_RENDER_LAYER_MASK_BITS) != 0 + || bits.iter().skip(1).any(|&extra_bits| extra_bits != 0); + + if unsupported_bits { + SHADER_RENDER_LAYER_MASK + } else { + (low_bits as u32) & SHADER_RENDER_LAYER_MASK + } +} + // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_types.wgsl! bitflags::bitflags! { /// Various flags and tightly-packed values on a mesh. @@ -1394,6 +1423,7 @@ impl RenderMeshInstanceGpuBuilder { world_from_local: self.world_from_local.to_transpose(), lightmap_uv_rect: self.lightmap_uv_rect, flags: self.mesh_flags.bits(), + render_layers: render_layers_to_shader_mask(self.render_layers.as_ref()), previous_input_index: u32::MAX, timestamp: timestamp.0, first_vertex_index, @@ -1409,6 +1439,7 @@ impl RenderMeshInstanceGpuBuilder { ) | ((lightmap_slot as u32) << 16), tag: self.shared.tag, morph_descriptor_index, + pad: [0; 3], }; let world_from_local = &self.world_from_local; @@ -2503,6 +2534,7 @@ impl GetBatchData for MeshPipeline { current_skin_index, morph_descriptor_index, Some(mesh_instance.tag()), + mesh_instance.render_layers.as_ref(), ), mesh_instance.should_batch().then_some(( MeshBatchSetCompareData { @@ -2584,6 +2616,7 @@ impl GetFullBatchData for MeshPipeline { current_skin_index, morph_descriptor_index, Some(mesh_instance.tag()), + mesh_instance.render_layers.as_ref(), )) } diff --git a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl index ace75cd53c65e..50bdd3e577f71 100644 --- a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl +++ b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl @@ -369,6 +369,7 @@ fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { output[mesh_output_index].local_from_world_transpose_a = local_from_world_transpose_a; output[mesh_output_index].local_from_world_transpose_b = local_from_world_transpose_b; output[mesh_output_index].flags = current_input[input_index].flags; + output[mesh_output_index].render_layers = current_input[input_index].render_layers; output[mesh_output_index].lightmap_uv_rect = current_input[input_index].lightmap_uv_rect; output[mesh_output_index].first_vertex_index = current_input[input_index].first_vertex_index; output[mesh_output_index].current_skin_index = current_input[input_index].current_skin_index; diff --git a/crates/bevy_pbr/src/render/mesh_types.wgsl b/crates/bevy_pbr/src/render/mesh_types.wgsl index 993185d5b35f9..1c1cfa6146772 100644 --- a/crates/bevy_pbr/src/render/mesh_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_types.wgsl @@ -14,6 +14,8 @@ struct Mesh { local_from_world_transpose_b: f32, // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32, + // Packed render layer mask used for per-mesh light filtering. + render_layers: u32, lightmap_uv_rect: vec2, // The index of the mesh's first vertex in the vertex buffer. first_vertex_index: u32, diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index bacde26fb99f8..736afd66207b7 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -23,6 +23,10 @@ const POINT_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 1u << 2u; const POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 1u << 3u; const POINT_LIGHT_FLAGS_CONTACT_SHADOWS_ENABLED_BIT: u32 = 1u << 4u; const POINT_LIGHT_FLAGS_SPOT_LIGHT_BIT: u32 = 1u << 5u; +// Packed render layer bits in point and directional light `flags`. +// Keep in sync with `LIGHT_RENDER_LAYERS_SHIFT` in `render/light.rs`. +const LIGHT_RENDER_LAYERS_SHIFT: u32 = 6u; +const LIGHT_RENDER_LAYERS_MASK: u32 = (1u << (32u - LIGHT_RENDER_LAYERS_SHIFT)) - 1u; struct DirectionalCascade { clip_from_world: mat4x4, diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index b09131c7be1eb..3c49aaadada45 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -42,8 +42,10 @@ fn pbr_input_from_vertex_output( #ifdef MESHLET_MESH_MATERIAL_PASS pbr_input.flags = in.mesh_flags; + pbr_input.render_layers = in.render_layers; #else pbr_input.flags = mesh[in.instance_index].flags; + pbr_input.render_layers = mesh[in.instance_index].render_layers; #endif pbr_input.is_orthographic = view.clip_from_view[3].w == 1.0; diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 8a08c7cdb0292..ebc1867bcae68 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -63,6 +63,15 @@ const DITHER_THRESHOLD_MAP: vec4 = vec4( 0x050d070f ); +fn light_layer_mask_from_flags(flags: u32) -> u32 { + return (flags >> mesh_view_types::LIGHT_RENDER_LAYERS_SHIFT) & + mesh_view_types::LIGHT_RENDER_LAYERS_MASK; +} + +fn mesh_and_light_layers_intersect(mesh_layers: u32, light_flags: u32) -> bool { + return (mesh_layers & light_layer_mask_from_flags(light_flags)) != 0u; +} + // Processes a visibility range dither value and discards the fragment if // needed. // @@ -459,6 +468,11 @@ fn apply_pbr_lighting( i < clusterable_object_index_ranges.first_spot_light_index_offset; i = i + 1u) { let light_id = clustering::get_clusterable_object_id(i); + if !mesh_and_light_layers_intersect( + in.render_layers, + view_bindings::clustered_lights.data[light_id].flags) { + continue; + } // If we're lightmapped, disable diffuse contribution from the light if // requested, to avoid double-counting light. @@ -515,6 +529,11 @@ fn apply_pbr_lighting( i < clusterable_object_index_ranges.first_reflection_probe_index_offset; i = i + 1u) { let light_id = clustering::get_clusterable_object_id(i); + if !mesh_and_light_layers_intersect( + in.render_layers, + view_bindings::clustered_lights.data[light_id].flags) { + continue; + } // If we're lightmapped, disable diffuse contribution from the light if // requested, to avoid double-counting light. @@ -585,6 +604,9 @@ fn apply_pbr_lighting( // check if this light should be skipped, which occurs if this light does not intersect with the view // note point and spot lights aren't skippable, as the relevant lights are filtered in `assign_lights_to_clusters` let light = &view_bindings::lights.directional_lights[i]; + if !mesh_and_light_layers_intersect(in.render_layers, (*light).flags) { + continue; + } // If we're lightmapped, disable diffuse contribution from the light if // requested, to avoid double-counting light. @@ -851,6 +873,7 @@ fn apply_fog( fragment_world_position: vec3, view_world_position: vec3, frag_coord_xy: vec2, + render_layers: u32, ) -> vec4 { let view_to_world = fragment_world_position.xyz - view_world_position.xyz; @@ -874,6 +897,9 @@ fn apply_fog( let n_directional_lights = view_bindings::lights.n_directional_lights; for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) { let light = view_bindings::lights.directional_lights[i]; + if !mesh_and_light_layers_intersect(render_layers, light.flags) { + continue; + } let scattering_contribution = pow( max( dot(view_to_world_normalized, light.direction_to_light), @@ -974,6 +1000,7 @@ fn main_pass_post_lighting_processing( pbr_input.world_position.xyz, view_bindings::view.world_position.xyz, pbr_input.frag_coord.xy, + pbr_input.render_layers, ); } #endif // DISTANCE_FOG diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index b8b51c577ecef..08d477ff9376a 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -119,6 +119,7 @@ struct PbrInput { anisotropy_B: vec3, is_orthographic: bool, flags: u32, + render_layers: u32, }; // Creates a PbrInput with default values @@ -146,6 +147,7 @@ fn pbr_input_new() -> PbrInput { pbr_input.lightmap_light = vec3(0.0); pbr_input.flags = 0u; + pbr_input.render_layers = 1u; return pbr_input; } diff --git a/crates/bevy_render/src/occlusion_culling/mesh_preprocess_types.wgsl b/crates/bevy_render/src/occlusion_culling/mesh_preprocess_types.wgsl index b8b6349500824..a7dd8abae2f08 100644 --- a/crates/bevy_render/src/occlusion_culling/mesh_preprocess_types.wgsl +++ b/crates/bevy_render/src/occlusion_culling/mesh_preprocess_types.wgsl @@ -10,6 +10,8 @@ struct MeshInput { lightmap_uv_rect: vec2, // Various flags. flags: u32, + // Packed render layer mask used for per-mesh light filtering in shaders. + render_layers: u32, previous_input_index: u32, first_vertex_index: u32, first_index_index: u32, @@ -26,6 +28,8 @@ struct MeshInput { // // If the mesh has no morph targets, this is `u32::MAX`. morph_descriptor_index: u32, + // Explicitly match the Rust-side `MeshInputUniform` trailing padding. + pad: array, } // The `wgpu` indirect parameters structure. This is a union of two structures. diff --git a/examples/3d/light_render_layers.rs b/examples/3d/light_render_layers.rs new file mode 100644 index 0000000000000..94abcec91f16a --- /dev/null +++ b/examples/3d/light_render_layers.rs @@ -0,0 +1,243 @@ +//! Demonstrates how render layers can control which lights affect which meshes. +//! +//! ## Controls +//! +//! | Key Binding | Action | +//! |:------------|:---------------------------------------| +//! | `1` | Toggle directional light on layer `1` | +//! | `2` | Toggle directional light on layer `0` | +//! | `3` | Toggle point light on layer `1` | +//! | `4` | Toggle point light on layer `0` | + +use bevy::{camera::visibility::RenderLayers, prelude::*}; + +const DIRECTIONAL_LAYER_1_ILLUMINANCE: f32 = 10_000.0; +const DIRECTIONAL_LAYER_0_ILLUMINANCE: f32 = 20_000.0; +const POINT_LAYER_1_INTENSITY: f32 = 3_500_000.0; +const POINT_LAYER_0_INTENSITY: f32 = 4_500_000.0; +const POINT_LIGHT_RANGE: f32 = 45.0; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .insert_resource(GlobalAmbientLight { + brightness: 0.0, + ..default() + }) + .init_resource::() + .add_systems(Startup, setup) + .add_systems(Update, (toggle_lights, update_help_text)) + .run(); +} + +#[derive(Resource)] +struct LightStates { + directional_layer_1: bool, + directional_layer_0: bool, + point_layer_1: bool, + point_layer_0: bool, +} + +impl Default for LightStates { + fn default() -> Self { + Self { + directional_layer_1: true, + directional_layer_0: true, + point_layer_1: true, + point_layer_0: true, + } + } +} + +#[derive(Component)] +struct DirectionalLayer1Light; + +#[derive(Component)] +struct DirectionalLayer0Light; + +#[derive(Component)] +struct PointLayer1Light; + +#[derive(Component)] +struct PointLayer0Light; + +#[derive(Component)] +struct ExampleText; + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 4.0, 12.0).looking_at(Vec3::ZERO, Vec3::Y), + RenderLayers::from_layers(&[0, 1]), + )); + + // Ground plane for extra lighting context. + commands.spawn(( + Mesh3d(meshes.add(Plane3d::default().mesh().size(14.0, 14.0))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: Color::srgb(0.08, 0.08, 0.08), + perceptual_roughness: 1.0, + ..default() + })), + )); + + // Object 1: on layers 0 and 1. + commands.spawn(( + Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))), + MeshMaterial3d(materials.add(Color::srgb(0.8, 0.2, 0.2))), + Transform::from_xyz(-2.5, 1.0, 0.0), + RenderLayers::from_layers(&[0, 1]), + )); + + // Object 2: only on layer 0. + commands.spawn(( + Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))), + MeshMaterial3d(materials.add(Color::srgb(0.2, 0.2, 0.8))), + Transform::from_xyz(2.5, 1.0, 0.0), + RenderLayers::layer(0), + )); + + // Directional light A: layer 1 only. + commands.spawn(( + DirectionalLight { + color: Color::srgb(1.0, 0.82, 0.75), + illuminance: DIRECTIONAL_LAYER_1_ILLUMINANCE, + ..default() + }, + Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, -1.0, -1.0, 0.0)), + RenderLayers::layer(1), + DirectionalLayer1Light, + )); + + // Directional light B: layer 0 only. + commands.spawn(( + DirectionalLight { + color: Color::srgb(0.75, 0.8, 1.0), + illuminance: DIRECTIONAL_LAYER_0_ILLUMINANCE, + ..default() + }, + Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, -0.6, 0.8, 0.0)), + RenderLayers::layer(0), + DirectionalLayer0Light, + )); + + // Point light C: layer 1 only. + commands.spawn(( + PointLight { + color: Color::srgb(1.0, 0.45, 0.1), + intensity: POINT_LAYER_1_INTENSITY, + range: POINT_LIGHT_RANGE, + ..default() + }, + Transform::from_xyz(-4.0, 3.0, 3.0), + RenderLayers::layer(1), + PointLayer1Light, + )); + + // Point light D: layer 0 only. + commands.spawn(( + PointLight { + color: Color::srgb(0.1, 0.55, 1.0), + intensity: POINT_LAYER_0_INTENSITY, + range: POINT_LIGHT_RANGE, + ..default() + }, + Transform::from_xyz(4.0, 3.0, 3.0), + RenderLayers::layer(0), + PointLayer0Light, + )); + + commands.spawn(( + Text::default(), + Node { + position_type: PositionType::Absolute, + top: px(12), + left: px(12), + padding: UiRect::all(px(8)), + ..default() + }, + BackgroundColor(Color::BLACK.with_alpha(0.7)), + ExampleText, + )); +} + +fn toggle_lights( + key_input: Res>, + mut light_states: ResMut, + mut directional_layer_1: Single< + &mut DirectionalLight, + (With, Without), + >, + mut directional_layer_0: Single< + &mut DirectionalLight, + (With, Without), + >, + mut point_layer_1: + Single<&mut PointLight, (With, Without)>, + mut point_layer_0: + Single<&mut PointLight, (With, Without)>, +) { + if key_input.just_pressed(KeyCode::Digit1) { + light_states.directional_layer_1 = !light_states.directional_layer_1; + } + if key_input.just_pressed(KeyCode::Digit2) { + light_states.directional_layer_0 = !light_states.directional_layer_0; + } + if key_input.just_pressed(KeyCode::Digit3) { + light_states.point_layer_1 = !light_states.point_layer_1; + } + if key_input.just_pressed(KeyCode::Digit4) { + light_states.point_layer_0 = !light_states.point_layer_0; + } + + directional_layer_1.illuminance = if light_states.directional_layer_1 { + DIRECTIONAL_LAYER_1_ILLUMINANCE + } else { + 0.0 + }; + directional_layer_0.illuminance = if light_states.directional_layer_0 { + DIRECTIONAL_LAYER_0_ILLUMINANCE + } else { + 0.0 + }; + point_layer_1.intensity = if light_states.point_layer_1 { + POINT_LAYER_1_INTENSITY + } else { + 0.0 + }; + point_layer_0.intensity = if light_states.point_layer_0 { + POINT_LAYER_0_INTENSITY + } else { + 0.0 + }; +} + +fn update_help_text(mut text: Single<&mut Text, With>, light_states: Res) { + fn status(enabled: bool) -> &'static str { + if enabled { + "ON" + } else { + "OFF" + } + } + + text.clear(); + text.push_str(&format!( + "Light Render Layers\n\ +Left cube: layers [0, 1]\n\ +Right cube: layer [0]\n\ +\n\ +1 - Directional light on layer 1 [{}]\n\ +2 - Directional light on layer 0 [{}]\n\ +3 - Point light on layer 1 [{}]\n\ +4 - Point light on layer 0 [{}]", + status(light_states.directional_layer_1), + status(light_states.directional_layer_0), + status(light_states.point_layer_1), + status(light_states.point_layer_0), + )); +} diff --git a/examples/README.md b/examples/README.md index d52964858c231..9a4aef9d57353 100644 --- a/examples/README.md +++ b/examples/README.md @@ -167,6 +167,7 @@ Example | Description [Generate Custom Mesh](../examples/3d/generate_custom_mesh.rs) | Simple showcase of how to generate a custom mesh with a custom texture [Irradiance Volumes](../examples/3d/irradiance_volumes.rs) | Demonstrates irradiance volumes [Light Probe Blending](../examples/3d/light_probe_blending.rs) | Demonstrates blending between multiple reflection probes +[Light Render Layers](../examples/3d/light_render_layers.rs) | Demonstrates using render layers to control which directional and point lights affect meshes [Light Textures](../examples/3d/light_textures.rs) | Demonstrates light textures [Lighting](../examples/3d/lighting.rs) | Illustrates various lighting options in a simple scene [Lightmaps](../examples/3d/lightmaps.rs) | Rendering a scene with baked lightmaps