A fork of bevy to implement some features for forestia
Go to file
robtfm 75d92f4434 fix distinct directional lights per view (#19147)
# 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());
    }
}
```
2025-05-30 23:53:51 +02:00
.cargo Fix typos in config_fast_builds.toml (#16025) 2024-10-20 16:50:40 +00:00
.github Add no_std Library Example (#18333) 2025-03-18 00:45:25 +00:00
assets Add bindless support back to ExtendedMaterial. (#18025) 2025-04-10 01:19:33 +02:00
benches Fix spawn tracking for spawn commands (#19351) 2025-05-30 23:53:51 +02:00
crates fix distinct directional lights per view (#19147) 2025-05-30 23:53:51 +02:00
docs Revert "Allow partial support for bevy_log in no_std (#18782)" (#18816) 2025-04-14 23:41:29 +02:00
docs-rs Relationship(…Target) html trait tag (#18140) 2025-03-04 08:05:16 +00:00
docs-template Fix Formatting of Optimisation Table (#18375) 2025-03-18 00:01:45 +00:00
errors Upgrade to Rust Edition 2024 (#17967) 2025-02-24 03:54:47 +00:00
examples Fix occlusion culling not respecting device limits (#18974) 2025-05-30 22:59:09 +02:00
src Add no_std support to bevy (#17955) 2025-03-07 03:39:46 +00:00
tests Rename bevy_platform_support to bevy_platform (#18813) 2025-04-14 22:45:27 +02:00
tests-integration/simple-ecs-test Internalize BevyManifest logic. Switch to RwLock (#18263) 2025-03-12 00:46:01 +00:00
tools Added support for .wesl files to the regex pattern for examples (#19178) 2025-05-30 22:59:10 +02:00
.gitattributes Enforce linux-style line endings for .rs and .toml (#3197) 2021-11-26 21:05:35 +00:00
.gitignore Harden proc macro path resolution and add integration tests. (#17330) 2025-02-09 19:45:45 +00:00
Cargo.toml Make sure that serde_json::Map::into_values exists (#19229) 2025-05-30 22:59:10 +02:00
clippy.toml Enable nonstandard_macro_braces and enforce [] for children! (#17974) 2025-02-22 01:54:49 +00:00
CODE_OF_CONDUCT.md Update CODE_OF_CONDUCT.md 2020-08-19 20:25:58 +01:00
CONTRIBUTING.md Reworded the CONTRIBUTING.md doc (#16849) 2024-12-17 19:18:34 +00:00
CREDITS.md Fix typos CREDITS.md (#17899) 2025-02-17 09:30:04 +00:00
deny.toml Ignore unmaintained security advisory about paste for now (#18209) 2025-03-09 20:17:29 +00:00
LICENSE-APACHE Let the project page support GitHub's new ability to display open source licenses (#4966) 2022-06-08 17:55:57 +00:00
LICENSE-MIT Let the project page support GitHub's new ability to display open source licenses (#4966) 2022-06-08 17:55:57 +00:00
README.md Update Contributor's Guide link in README.md (#16592) 2024-12-02 15:18:19 +00:00
rustfmt.toml Upgrade to Rust Edition 2024 (#17967) 2025-02-24 03:54:47 +00:00
typos.toml Bump typos to 1.29.7 (#17902) 2025-02-17 20:41:25 +00:00

Bevy

License Crates.io Downloads Docs CI Discord

What is Bevy?

Bevy is a refreshingly simple data-driven game engine built in Rust. It is free and open-source forever!

WARNING

Bevy is still in the early stages of development. Important features are missing. Documentation is sparse. A new version of Bevy containing breaking changes to the API is released approximately once every 3 months. We provide migration guides, but we can't guarantee migrations will always be easy. Use only if you are willing to work in this environment.

MSRV: Bevy relies heavily on improvements in the Rust language and compiler. As a result, the Minimum Supported Rust Version (MSRV) is generally close to "the latest stable release" of Rust.

Design Goals

  • Capable: Offer a complete 2D and 3D feature set
  • Simple: Easy for newbies to pick up, but infinitely flexible for power users
  • Data Focused: Data-oriented architecture using the Entity Component System paradigm
  • Modular: Use only what you need. Replace what you don't like
  • Fast: App logic should run quickly, and when possible, in parallel
  • Productive: Changes should compile quickly ... waiting isn't fun

About

  • Features: A quick overview of Bevy's features.
  • News: A development blog that covers our progress, plans and shiny new features.

Docs

Community

Before contributing or participating in discussions with the community, you should familiarize yourself with our Code of Conduct.

  • Discord: Bevy's official discord server.
  • Reddit: Bevy's official subreddit.
  • GitHub Discussions: The best place for questions about Bevy, answered right here!
  • Bevy Assets: A collection of awesome Bevy projects, tools, plugins and learning materials.

Contributing

If you'd like to help build Bevy, check out the Contributor's Guide. For simple problems, feel free to open an issue or PR and tackle it yourself!

For more complex architecture decisions and experimental mad science, please open an RFC (Request For Comments) so we can brainstorm together effectively!

Getting Started

We recommend checking out the Quick Start Guide for a brief introduction.

Follow the Setup guide to ensure your development environment is set up correctly. Once set up, you can quickly try out the examples by cloning this repo and running the following commands:

# Switch to the correct version (latest release, default is main development branch)
git checkout latest
# Runs the "breakout" example
cargo run --example breakout

To draw a window with standard functionality enabled, use:

use bevy::prelude::*;

fn main(){
  App::new()
    .add_plugins(DefaultPlugins)
    .run();
}

Fast Compiles

Bevy can be built just fine using default configuration on stable Rust. However for really fast iterative compiles, you should enable the "fast compiles" setup by following the instructions here.

Bevy Cargo Features

This list outlines the different cargo features supported by Bevy. These allow you to customize the Bevy feature set for your use-case.

Thanks

Bevy is the result of the hard work of many people. A huge thanks to all Bevy contributors, the many open source projects that have come before us, the Rust gamedev ecosystem, and the many libraries we build on.

A huge thanks to Bevy's generous sponsors. Bevy will always be free and open source, but it isn't free to make. Please consider sponsoring our work if you like what we're building.

This project is tested with BrowserStack.

License

Bevy is free, open source and permissively licensed! Except where noted (below and/or in individual files), all code in this repository is dual-licensed under either:

at your option. This means you can select the license you prefer! This dual-licensing approach is the de-facto standard in the Rust ecosystem and there are very good reasons to include both.

Some of the engine's code carries additional copyright notices and license terms due to their external origins. These are generally BSD-like, but exact details vary by crate: If the README of a crate contains a 'License' header (or similar), the additional copyright notices and license terms applicable to that crate will be listed. The above licensing requirement still applies to contributions to those crates, and sections of those crates will carry those license terms. The license field of each crate will also reflect this. For example, bevy_mikktspace has code under the Zlib license (as well as a copyright notice when choosing the MIT license).

The assets included in this repository (for our examples) typically fall under different open licenses. These will not be included in your game (unless copied in by you), and they are not distributed in the published bevy crates. See CREDITS.md for the details of the licenses of those files.

Your contributions

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.