Skip to content

Commit

Permalink
light renderlayers (#10742)
Browse files Browse the repository at this point in the history
# 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.
  • Loading branch information
robtfm authored Dec 12, 2023
1 parent 55402bd commit 67d92e9
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 24 deletions.
68 changes: 51 additions & 17 deletions crates/bevy_pbr/src/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,7 @@ pub(crate) struct PointLightAssignmentData {
range: f32,
shadows_enabled: bool,
spot_light_angle: Option<f32>,
render_layers: RenderLayers,
}

impl PointLightAssignmentData {
Expand Down Expand Up @@ -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<Vec<PointLightAssignmentData>>,
mut cluster_aabb_spheres: Local<Vec<Option<Sphere>>>,
mut max_point_lights_warning_emitted: Local<bool>,
Expand All @@ -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(),
}
},
),
);
Expand All @@ -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(),
}
},
),
);
Expand Down Expand Up @@ -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| {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -1520,6 +1549,11 @@ pub(crate) fn assign_lights_to_clusters(

let mut update_from_light_intersections = |visible_lights: &mut Vec<Entity>| {
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
Expand Down
8 changes: 7 additions & 1 deletion crates/bevy_pbr/src/render/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -48,6 +48,7 @@ pub struct ExtractedDirectionalLight {
shadow_normal_bias: f32,
cascade_shadow_config: CascadeShadowConfig,
cascades: HashMap<Entity, Vec<Cascade>>,
render_layers: RenderLayers,
}

#[derive(Copy, Clone, ShaderType, Default, Debug)]
Expand Down Expand Up @@ -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!
Expand Down Expand Up @@ -315,6 +317,7 @@ pub fn extract_lights(
&CascadeShadowConfig,
&GlobalTransform,
&ViewVisibility,
Option<&RenderLayers>,
),
Without<SpotLight>,
>,
Expand Down Expand Up @@ -430,6 +433,7 @@ pub fn extract_lights(
cascade_config,
transform,
view_visibility,
maybe_layers,
) in &directional_lights
{
if !view_visibility.get() {
Expand All @@ -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,
));
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_pbr/src/render/mesh_view_types.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions crates/bevy_pbr/src/render/pbr_functions.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 4 additions & 1 deletion crates/bevy_render/src/view/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ pub struct ViewUniform {
frustum: [Vec4; 6],
color_grading: ColorGrading,
mip_bias: f32,
render_layers: u32,
}

#[derive(Resource, Default)]
Expand Down Expand Up @@ -357,6 +358,7 @@ pub fn prepare_view_uniforms(
Option<&Frustum>,
Option<&TemporalJitter>,
Option<&MipBias>,
Option<&RenderLayers>,
)>,
) {
let view_iter = views.iter();
Expand All @@ -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;
Expand Down Expand Up @@ -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(),
}),
};

Expand Down
1 change: 1 addition & 0 deletions crates/bevy_render/src/view/view.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ struct View {
frustum: array<vec4<f32>, 6>,
color_grading: ColorGrading,
mip_bias: f32,
render_layers: u32,
};
5 changes: 5 additions & 0 deletions crates/bevy_render/src/view/visibility/render_layers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
15 changes: 10 additions & 5 deletions examples/3d/render_to_texture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 67d92e9

Please sign in to comment.