# Objective
after #15156 it seems like using distinct directional lights on
different views is broken (and will probably break spotlights too). fix
them
## Solution
the reason is a bit hairy so with an example:
- camera 0 on layer 0
- camera 1 on layer 1
- dir light 0 on layer 0 (2 cascades)
- dir light 1 on layer 1 (2 cascades)
in render/lights.rs:
- outside of any view loop,
- we count the total number of shadow casting directional light cascades
(4) and assign an incrementing `depth_texture_base_index` for each (0-1
for one light, 2-3 for the other, depending on iteration order) (line
1034)
- allocate a texture array for the total number of cascades plus
spotlight maps (4) (line 1106)
- in the view loop, for directional lights we
- skip lights that don't intersect on renderlayers (line 1440)
- assign an incrementing texture layer to each light/cascade starting
from 0 (resets to 0 per view) (assigning 0 and 1 each time for the 2
cascades of the intersecting light) (line 1509, init at 1421)
then in the rendergraph:
- camera 0 renders the shadow map for light 0 to texture indices 0 and 1
- camera 0 renders using shadows from the `depth_texture_base_index`
(maybe 0-1, maybe 2-3 depending on the iteration order)
- camera 1 renders the shadow map for light 1 to texture indices 0 and 1
- camera 0 renders using shadows from the `depth_texture_base_index`
(maybe 0-1, maybe 2-3 depending on the iteration order)
issues:
- one of the views uses empty shadow maps (bug)
- we allocated a texture layer per cascade per light, even though not
all lights are used on all views (just inefficient)
- I think we're allocating texture layers even for lights with
`shadows_enabled: false` (just inefficient)
solution:
- calculate upfront the view with the largest number of directional
cascades
- allocate this many layers (plus layers for spotlights) in the texture
array
- keep using texture layers 0..n in the per-view loop, but build
GpuLights.gpu_directional_lights within the loop too so it refers to the
same layers we render to
nice side effects:
- we can now use `max_texture_array_layers / MAX_CASCADES_PER_LIGHT`
shadow-casting directional lights per view, rather than overall.
- we can remove the `GpuDirectionalLight::skip` field, since the gpu
lights struct is constructed per view
a simpler approach would be to keep everything the same, and just
increment the texture layer index in the view loop even for
non-intersecting lights. this pr reduces the total shadowmap vram used
as well and isn't *much* extra complexity. but if we want something less
risky/intrusive for 16.1 that would be the way.
## Testing
i edited the split screen example to put separate lights on layer 1 and
layer 2, and put the plane and fox on both layers (using lots of
unrelated code for render layer propagation from #17575).
without the fix the directional shadows will only render on one of the
top 2 views even though there are directional lights on both layers.
```rs
//! Renders two cameras to the same window to accomplish "split screen".
use std::f32::consts::PI;
use bevy::{
pbr::CascadeShadowConfigBuilder, prelude::*, render:📷:Viewport, window::WindowResized,
};
use bevy_render::view::RenderLayers;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(HierarchyPropagatePlugin::<RenderLayers>::default())
.add_systems(Startup, setup)
.add_systems(Update, (set_camera_viewports, button_system))
.run();
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let all_layers = RenderLayers::layer(1).with(2).with(3).with(4);
// plane
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(100.0, 100.0))),
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
all_layers.clone()
));
commands.spawn((
SceneRoot(
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")),
),
Propagate(all_layers.clone()),
));
// Light
commands.spawn((
Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
DirectionalLight {
shadows_enabled: true,
..default()
},
CascadeShadowConfigBuilder {
num_cascades: if cfg!(all(
feature = "webgl2",
target_arch = "wasm32",
not(feature = "webgpu")
)) {
// Limited to 1 cascade in WebGL
1
} else {
2
},
first_cascade_far_bound: 200.0,
maximum_distance: 280.0,
..default()
}
.build(),
RenderLayers::layer(1),
));
commands.spawn((
Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
DirectionalLight {
shadows_enabled: true,
..default()
},
CascadeShadowConfigBuilder {
num_cascades: if cfg!(all(
feature = "webgl2",
target_arch = "wasm32",
not(feature = "webgpu")
)) {
// Limited to 1 cascade in WebGL
1
} else {
2
},
first_cascade_far_bound: 200.0,
maximum_distance: 280.0,
..default()
}
.build(),
RenderLayers::layer(2),
));
// Cameras and their dedicated UI
for (index, (camera_name, camera_pos)) in [
("Player 1", Vec3::new(0.0, 200.0, -150.0)),
("Player 2", Vec3::new(150.0, 150., 50.0)),
("Player 3", Vec3::new(100.0, 150., -150.0)),
("Player 4", Vec3::new(-100.0, 80., 150.0)),
]
.iter()
.enumerate()
{
let camera = commands
.spawn((
Camera3d::default(),
Transform::from_translation(*camera_pos).looking_at(Vec3::ZERO, Vec3::Y),
Camera {
// Renders cameras with different priorities to prevent ambiguities
order: index as isize,
..default()
},
CameraPosition {
pos: UVec2::new((index % 2) as u32, (index / 2) as u32),
},
RenderLayers::layer(index+1)
))
.id();
// Set up UI
commands
.spawn((
UiTargetCamera(camera),
Node {
width: Val::Percent(100.),
height: Val::Percent(100.),
..default()
},
))
.with_children(|parent| {
parent.spawn((
Text::new(*camera_name),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.),
left: Val::Px(12.),
..default()
},
));
buttons_panel(parent);
});
}
fn buttons_panel(parent: &mut ChildSpawnerCommands) {
parent
.spawn(Node {
position_type: PositionType::Absolute,
width: Val::Percent(100.),
height: Val::Percent(100.),
display: Display::Flex,
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(20.)),
..default()
})
.with_children(|parent| {
rotate_button(parent, "<", Direction::Left);
rotate_button(parent, ">", Direction::Right);
});
}
fn rotate_button(parent: &mut ChildSpawnerCommands, caption: &str, direction: Direction) {
parent
.spawn((
RotateCamera(direction),
Button,
Node {
width: Val::Px(40.),
height: Val::Px(40.),
border: UiRect::all(Val::Px(2.)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BorderColor(Color::WHITE),
BackgroundColor(Color::srgb(0.25, 0.25, 0.25)),
))
.with_children(|parent| {
parent.spawn(Text::new(caption));
});
}
}
#[derive(Component)]
struct CameraPosition {
pos: UVec2,
}
#[derive(Component)]
struct RotateCamera(Direction);
enum Direction {
Left,
Right,
}
fn set_camera_viewports(
windows: Query<&Window>,
mut resize_events: EventReader<WindowResized>,
mut query: Query<(&CameraPosition, &mut Camera)>,
) {
// We need to dynamically resize the camera's viewports whenever the window size changes
// so then each camera always takes up half the screen.
// A resize_event is sent when the window is first created, allowing us to reuse this system for initial setup.
for resize_event in resize_events.read() {
let window = windows.get(resize_event.window).unwrap();
let size = window.physical_size() / 2;
for (camera_position, mut camera) in &mut query {
camera.viewport = Some(Viewport {
physical_position: camera_position.pos * size,
physical_size: size,
..default()
});
}
}
}
fn button_system(
interaction_query: Query<
(&Interaction, &ComputedNodeTarget, &RotateCamera),
(Changed<Interaction>, With<Button>),
>,
mut camera_query: Query<&mut Transform, With<Camera>>,
) {
for (interaction, computed_target, RotateCamera(direction)) in &interaction_query {
if let Interaction::Pressed = *interaction {
// Since TargetCamera propagates to the children, we can use it to find
// which side of the screen the button is on.
if let Some(mut camera_transform) = computed_target
.camera()
.and_then(|camera| camera_query.get_mut(camera).ok())
{
let angle = match direction {
Direction::Left => -0.1,
Direction::Right => 0.1,
};
camera_transform.rotate_around(Vec3::ZERO, Quat::from_axis_angle(Vec3::Y, angle));
}
}
}
}
use std::marker::PhantomData;
use bevy::{
app::{App, Plugin, Update},
ecs::query::QueryFilter,
prelude::{
Changed, Children, Commands, Component, Entity, Local, Query,
RemovedComponents, SystemSet, With, Without,
},
};
/// Causes the inner component to be added to this entity and all children.
/// A child with a Propagate<C> component of it's own will override propagation from
/// that point in the tree
#[derive(Component, Clone, PartialEq)]
pub struct Propagate<C: Component + Clone + PartialEq>(pub C);
/// Internal struct for managing propagation
#[derive(Component, Clone, PartialEq)]
pub struct Inherited<C: Component + Clone + PartialEq>(pub C);
/// Stops the output component being added to this entity.
/// Children will still inherit the component from this entity or its parents
#[derive(Component, Default)]
pub struct PropagateOver<C: Component + Clone + PartialEq>(PhantomData<fn() -> C>);
/// Stops the propagation at this entity. Children will not inherit the component.
#[derive(Component, Default)]
pub struct PropagateStop<C: Component + Clone + PartialEq>(PhantomData<fn() -> C>);
pub struct HierarchyPropagatePlugin<C: Component + Clone + PartialEq, F: QueryFilter = ()> {
_p: PhantomData<fn() -> (C, F)>,
}
impl<C: Component + Clone + PartialEq, F: QueryFilter> Default for HierarchyPropagatePlugin<C, F> {
fn default() -> Self {
Self {
_p: Default::default(),
}
}
}
#[derive(SystemSet, Clone, PartialEq, PartialOrd, Ord)]
pub struct PropagateSet<C: Component + Clone + PartialEq> {
_p: PhantomData<fn() -> C>,
}
impl<C: Component + Clone + PartialEq> std::fmt::Debug for PropagateSet<C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PropagateSet")
.field("_p", &self._p)
.finish()
}
}
impl<C: Component + Clone + PartialEq> Eq for PropagateSet<C> {}
impl<C: Component + Clone + PartialEq> std:#️⃣:Hash for PropagateSet<C> {
fn hash<H: std:#️⃣:Hasher>(&self, state: &mut H) {
self._p.hash(state);
}
}
impl<C: Component + Clone + PartialEq> Default for PropagateSet<C> {
fn default() -> Self {
Self {
_p: Default::default(),
}
}
}
impl<C: Component + Clone + PartialEq, F: QueryFilter + 'static> Plugin
for HierarchyPropagatePlugin<C, F>
{
fn build(&self, app: &mut App) {
app.add_systems(
Update,
(
update_source::<C, F>,
update_stopped::<C, F>,
update_reparented::<C, F>,
propagate_inherited::<C, F>,
propagate_output::<C, F>,
)
.chain()
.in_set(PropagateSet::<C>::default()),
);
}
}
pub fn update_source<C: Component + Clone + PartialEq, F: QueryFilter>(
mut commands: Commands,
changed: Query<(Entity, &Propagate<C>), (Changed<Propagate<C>>, Without<PropagateStop<C>>)>,
mut removed: RemovedComponents<Propagate<C>>,
) {
for (entity, source) in &changed {
commands
.entity(entity)
.try_insert(Inherited(source.0.clone()));
}
for removed in removed.read() {
if let Ok(mut commands) = commands.get_entity(removed) {
commands.remove::<(Inherited<C>, C)>();
}
}
}
pub fn update_stopped<C: Component + Clone + PartialEq, F: QueryFilter>(
mut commands: Commands,
q: Query<Entity, (With<Inherited<C>>, F, With<PropagateStop<C>>)>,
) {
for entity in q.iter() {
let mut cmds = commands.entity(entity);
cmds.remove::<Inherited<C>>();
}
}
pub fn update_reparented<C: Component + Clone + PartialEq, F: QueryFilter>(
mut commands: Commands,
moved: Query<
(Entity, &ChildOf, Option<&Inherited<C>>),
(
Changed<ChildOf>,
Without<Propagate<C>>,
Without<PropagateStop<C>>,
F,
),
>,
parents: Query<&Inherited<C>>,
) {
for (entity, parent, maybe_inherited) in &moved {
if let Ok(inherited) = parents.get(parent.parent()) {
commands.entity(entity).try_insert(inherited.clone());
} else if maybe_inherited.is_some() {
commands.entity(entity).remove::<(Inherited<C>, C)>();
}
}
}
pub fn propagate_inherited<C: Component + Clone + PartialEq, F: QueryFilter>(
mut commands: Commands,
changed: Query<
(&Inherited<C>, &Children),
(Changed<Inherited<C>>, Without<PropagateStop<C>>, F),
>,
recurse: Query<
(Option<&Children>, Option<&Inherited<C>>),
(Without<Propagate<C>>, Without<PropagateStop<C>>, F),
>,
mut to_process: Local<Vec<(Entity, Option<Inherited<C>>)>>,
mut removed: RemovedComponents<Inherited<C>>,
) {
// gather changed
for (inherited, children) in &changed {
to_process.extend(
children
.iter()
.map(|child| (child, Some(inherited.clone()))),
);
}
// and removed
for entity in removed.read() {
if let Ok((Some(children), _)) = recurse.get(entity) {
to_process.extend(children.iter().map(|child| (child, None)))
}
}
// propagate
while let Some((entity, maybe_inherited)) = (*to_process).pop() {
let Ok((maybe_children, maybe_current)) = recurse.get(entity) else {
continue;
};
if maybe_current == maybe_inherited.as_ref() {
continue;
}
if let Some(children) = maybe_children {
to_process.extend(
children
.iter()
.map(|child| (child, maybe_inherited.clone())),
);
}
if let Some(inherited) = maybe_inherited {
commands.entity(entity).try_insert(inherited.clone());
} else {
commands.entity(entity).remove::<(Inherited<C>, C)>();
}
}
}
pub fn propagate_output<C: Component + Clone + PartialEq, F: QueryFilter>(
mut commands: Commands,
changed: Query<
(Entity, &Inherited<C>, Option<&C>),
(Changed<Inherited<C>>, Without<PropagateOver<C>>, F),
>,
) {
for (entity, inherited, maybe_current) in &changed {
if maybe_current.is_some_and(|c| &inherited.0 == c) {
continue;
}
commands.entity(entity).try_insert(inherited.0.clone());
}
}
```