From 67d92e9b855fd76d6be5ba8c5c19e2d570842b78 Mon Sep 17 00:00:00 2001 From: robtfm <50659922+robtfm@users.noreply.github.com> Date: Tue, 12 Dec 2023 19:45:37 +0000 Subject: [PATCH] light renderlayers (#10742) # Objective add `RenderLayers` awareness to lights. lights default to `RenderLayers::layer(0)`, and must intersect the camera entity's `RenderLayers` in order to affect the camera's output. note that lights already use renderlayers to filter meshes for shadow casting. this adds filtering lights per view based on intersection of camera layers and light layers. fixes #3462 ## Solution PointLights and SpotLights are assigned to individual views in `assign_lights_to_clusters`, so we simply cull the lights which don't match the view layers in that function. DirectionalLights are global, so we - add the light layers to the `DirectionalLight` struct - add the view layers to the `ViewUniform` struct - check for intersection before processing the light in `apply_pbr_lighting` potential issue: when mesh/light layers are smaller than the view layers weird results can occur. e.g: camera = layers 1+2 light = layers 1 mesh = layers 2 the mesh does not cast shadows wrt the light as (1 & 2) == 0. the light affects the view as (1+2 & 1) != 0. the view renders the mesh as (1+2 & 2) != 0. so the mesh is rendered and lit, but does not cast a shadow. this could be fixed (so that the light would not affect the mesh in that view) by adding the light layers to the point and spot light structs, but i think the setup is pretty unusual, and space is at a premium in those structs (adding 4 bytes more would reduce the webgl point+spot light max count to 240 from 256). I think typical usage is for cameras to have a single layer, and meshes/lights to maybe have multiple layers to render to e.g. minimaps as well as primary views. if there is a good use case for the above setup and we should support it, please let me know. --- ## Migration Guide Lights no longer affect all `RenderLayers` by default, now like cameras and meshes they default to `RenderLayers::layer(0)`. To recover the previous behaviour and have all lights affect all views, add a `RenderLayers::all()` component to the light entity. --- crates/bevy_pbr/src/light.rs | 68 ++++++++++++++----- crates/bevy_pbr/src/render/light.rs | 8 ++- .../bevy_pbr/src/render/mesh_view_types.wgsl | 1 + crates/bevy_pbr/src/render/pbr_functions.wgsl | 7 ++ crates/bevy_render/src/view/mod.rs | 5 +- crates/bevy_render/src/view/view.wgsl | 1 + .../src/view/visibility/render_layers.rs | 5 ++ examples/3d/render_to_texture.rs | 15 ++-- 8 files changed, 86 insertions(+), 24 deletions(-) diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 3eaea9dbb2b9a..179f7cf646703 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -1154,6 +1154,7 @@ pub(crate) struct PointLightAssignmentData { range: f32, shadows_enabled: bool, spot_light_angle: Option, + render_layers: RenderLayers, } impl PointLightAssignmentData { @@ -1194,10 +1195,23 @@ pub(crate) fn assign_lights_to_clusters( &Frustum, &ClusterConfig, &mut Clusters, + Option<&RenderLayers>, Option<&mut VisiblePointLights>, )>, - point_lights_query: Query<(Entity, &GlobalTransform, &PointLight, &ViewVisibility)>, - spot_lights_query: Query<(Entity, &GlobalTransform, &SpotLight, &ViewVisibility)>, + point_lights_query: Query<( + Entity, + &GlobalTransform, + &PointLight, + Option<&RenderLayers>, + &ViewVisibility, + )>, + spot_lights_query: Query<( + Entity, + &GlobalTransform, + &SpotLight, + Option<&RenderLayers>, + &ViewVisibility, + )>, mut lights: Local>, mut cluster_aabb_spheres: Local>>, mut max_point_lights_warning_emitted: Local, @@ -1215,12 +1229,15 @@ pub(crate) fn assign_lights_to_clusters( .iter() .filter(|(.., visibility)| visibility.get()) .map( - |(entity, transform, point_light, _visibility)| PointLightAssignmentData { - entity, - transform: GlobalTransform::from_translation(transform.translation()), - shadows_enabled: point_light.shadows_enabled, - range: point_light.range, - spot_light_angle: None, + |(entity, transform, point_light, maybe_layers, _visibility)| { + PointLightAssignmentData { + entity, + transform: GlobalTransform::from_translation(transform.translation()), + shadows_enabled: point_light.shadows_enabled, + range: point_light.range, + spot_light_angle: None, + render_layers: maybe_layers.copied().unwrap_or_default(), + } }, ), ); @@ -1229,12 +1246,15 @@ pub(crate) fn assign_lights_to_clusters( .iter() .filter(|(.., visibility)| visibility.get()) .map( - |(entity, transform, spot_light, _visibility)| PointLightAssignmentData { - entity, - transform: *transform, - shadows_enabled: spot_light.shadows_enabled, - range: spot_light.range, - spot_light_angle: Some(spot_light.outer_angle), + |(entity, transform, spot_light, maybe_layers, _visibility)| { + PointLightAssignmentData { + entity, + transform: *transform, + shadows_enabled: spot_light.shadows_enabled, + range: spot_light.range, + spot_light_angle: Some(spot_light.outer_angle), + render_layers: maybe_layers.copied().unwrap_or_default(), + } }, ), ); @@ -1264,7 +1284,7 @@ pub(crate) fn assign_lights_to_clusters( // check each light against each view's frustum, keep only those that affect at least one of our views let frusta: Vec<_> = views .iter() - .map(|(_, _, _, frustum, _, _, _)| *frustum) + .map(|(_, _, _, frustum, _, _, _, _)| *frustum) .collect(); let mut lights_in_view_count = 0; lights.retain(|light| { @@ -1296,9 +1316,18 @@ pub(crate) fn assign_lights_to_clusters( lights.truncate(MAX_UNIFORM_BUFFER_POINT_LIGHTS); } - for (view_entity, camera_transform, camera, frustum, config, clusters, mut visible_lights) in - &mut views + for ( + view_entity, + camera_transform, + camera, + frustum, + config, + clusters, + maybe_layers, + mut visible_lights, + ) in &mut views { + let view_layers = maybe_layers.copied().unwrap_or_default(); let clusters = clusters.into_inner(); if matches!(config, ClusterConfig::None) { @@ -1520,6 +1549,11 @@ pub(crate) fn assign_lights_to_clusters( let mut update_from_light_intersections = |visible_lights: &mut Vec| { for light in &lights { + // check if the light layers overlap the view layers + if !view_layers.intersects(&light.render_layers) { + continue; + } + let light_sphere = light.sphere(); // Check if the light is within the view frustum diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index d7eff2ede461e..581ff0480e534 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -11,7 +11,7 @@ use bevy_render::{ render_resource::*, renderer::{RenderContext, RenderDevice, RenderQueue}, texture::*, - view::{ExtractedView, ViewVisibility, VisibleEntities}, + view::{ExtractedView, RenderLayers, ViewVisibility, VisibleEntities}, Extract, }; use bevy_transform::{components::GlobalTransform, prelude::Transform}; @@ -48,6 +48,7 @@ pub struct ExtractedDirectionalLight { shadow_normal_bias: f32, cascade_shadow_config: CascadeShadowConfig, cascades: HashMap>, + render_layers: RenderLayers, } #[derive(Copy, Clone, ShaderType, Default, Debug)] @@ -169,6 +170,7 @@ pub struct GpuDirectionalLight { num_cascades: u32, cascades_overlap_proportion: f32, depth_texture_base_index: u32, + render_layers: u32, } // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! @@ -315,6 +317,7 @@ pub fn extract_lights( &CascadeShadowConfig, &GlobalTransform, &ViewVisibility, + Option<&RenderLayers>, ), Without, >, @@ -430,6 +433,7 @@ pub fn extract_lights( cascade_config, transform, view_visibility, + maybe_layers, ) in &directional_lights { if !view_visibility.get() { @@ -449,6 +453,7 @@ pub fn extract_lights( shadow_normal_bias: directional_light.shadow_normal_bias * std::f32::consts::SQRT_2, cascade_shadow_config: cascade_config.clone(), cascades: cascades.cascades.clone(), + render_layers: maybe_layers.copied().unwrap_or_default(), }, render_visible_entities, )); @@ -883,6 +888,7 @@ pub fn prepare_lights( num_cascades: num_cascades as u32, cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion, depth_texture_base_index: num_directional_cascades_enabled as u32, + render_layers: light.render_layers.bits(), }; if index < directional_shadow_enabled_count { num_directional_cascades_enabled += num_cascades; diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index f115d49d7898a..3062ad671a77f 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -33,6 +33,7 @@ struct DirectionalLight { num_cascades: u32, cascades_overlap_proportion: f32, depth_texture_base_index: u32, + render_layers: u32, }; const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 9b4668f3e75ec..66d815db7ec4a 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -267,6 +267,13 @@ fn apply_pbr_lighting( // directional lights (direct) let n_directional_lights = view_bindings::lights.n_directional_lights; for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) { + // check the directional light render layers intersect the view render layers + // note this is not necessary for point and spot lights, as the relevant lights are filtered in `assign_lights_to_clusters` + let light = &view_bindings::lights.directional_lights[i]; + if ((*light).render_layers & view_bindings::view.render_layers) == 0u { + continue; + } + var shadow: f32 = 1.0; if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u && (view_bindings::lights.directional_lights[i].flags & mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 73d8bf9c24e80..7a0b928d7cced 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -172,6 +172,7 @@ pub struct ViewUniform { frustum: [Vec4; 6], color_grading: ColorGrading, mip_bias: f32, + render_layers: u32, } #[derive(Resource, Default)] @@ -357,6 +358,7 @@ pub fn prepare_view_uniforms( Option<&Frustum>, Option<&TemporalJitter>, Option<&MipBias>, + Option<&RenderLayers>, )>, ) { let view_iter = views.iter(); @@ -368,7 +370,7 @@ pub fn prepare_view_uniforms( else { return; }; - for (entity, camera, frustum, temporal_jitter, mip_bias) in &views { + for (entity, camera, frustum, temporal_jitter, mip_bias, maybe_layers) in &views { let viewport = camera.viewport.as_vec4(); let unjittered_projection = camera.projection; let mut projection = unjittered_projection; @@ -408,6 +410,7 @@ pub fn prepare_view_uniforms( frustum, color_grading: camera.color_grading, mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0, + render_layers: maybe_layers.copied().unwrap_or_default().bits(), }), }; diff --git a/crates/bevy_render/src/view/view.wgsl b/crates/bevy_render/src/view/view.wgsl index caa2b3a122f3e..a48fb19f56382 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -21,4 +21,5 @@ struct View { frustum: array, 6>, color_grading: ColorGrading, mip_bias: f32, + render_layers: u32, }; diff --git a/crates/bevy_render/src/view/visibility/render_layers.rs b/crates/bevy_render/src/view/visibility/render_layers.rs index 811d29698dec1..2a0e5da7d89fc 100644 --- a/crates/bevy_render/src/view/visibility/render_layers.rs +++ b/crates/bevy_render/src/view/visibility/render_layers.rs @@ -110,6 +110,11 @@ impl RenderLayers { pub fn intersects(&self, other: &RenderLayers) -> bool { (self.0 & other.0) > 0 } + + /// get the bitmask representation of the contained layers + pub fn bits(&self) -> u32 { + self.0 + } } #[cfg(test)] diff --git a/examples/3d/render_to_texture.rs b/examples/3d/render_to_texture.rs index 80e04261bac8a..a319e7d6e4e03 100644 --- a/examples/3d/render_to_texture.rs +++ b/examples/3d/render_to_texture.rs @@ -87,11 +87,16 @@ fn setup( )); // Light - // NOTE: Currently lights are shared between passes - see https://github.com/bevyengine/bevy/issues/3462 - commands.spawn(PointLightBundle { - transform: Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)), - ..default() - }); + // NOTE: we add the light to all layers so it affects both the rendered-to-texture cube, and the cube on which we display the texture + // Setting the layer to RenderLayers::layer(0) would cause the main view to be lit, but the rendered-to-texture cube to be unlit. + // Setting the layer to RenderLayers::layer(1) would cause the rendered-to-texture cube to be lit, but the main view to be unlit. + commands.spawn(( + PointLightBundle { + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)), + ..default() + }, + RenderLayers::all(), + )); commands.spawn(( Camera3dBundle {