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.
This commit is contained in:
robtfm 2023-12-12 19:45:37 +00:00 committed by GitHub
parent 55402bdf2e
commit 67d92e9b85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 86 additions and 24 deletions

View file

@ -1154,6 +1154,7 @@ pub(crate) struct PointLightAssignmentData {
range: f32,
shadows_enabled: bool,
spot_light_angle: Option<f32>,
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<Vec<PointLightAssignmentData>>,
mut cluster_aabb_spheres: Local<Vec<Option<Sphere>>>,
mut max_point_lights_warning_emitted: Local<bool>,
@ -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<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

View file

@ -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<Entity, Vec<Cascade>>,
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<SpotLight>,
>,
@ -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;

View file

@ -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;

View file

@ -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) {

View file

@ -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(),
}),
};

View file

@ -21,4 +21,5 @@ struct View {
frustum: array<vec4<f32>, 6>,
color_grading: ColorGrading,
mip_bias: f32,
render_layers: u32,
};

View file

@ -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)]

View file

@ -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 {