Skip to content

Commit

Permalink
Add light occlusion (#20)
Browse files Browse the repository at this point in the history
* Adds LightOccluder2d component

* Adds SDF Pass

* Use fixed size uniform buffer in WebGL2

Dynamic arrays aren't supported in WebGL2, so we have to fall back to
a fixed size array in a uniform binding.

Co-Authored-By: Miguel Albernaz <[email protected]>

* Almost setup ray marching? :)

Co-Authored-By: Miguel Albernaz <[email protected]>

* Kill bevy_pbr

PBR is for 3D stuff. It's nice to not need it.

We'll want to clean up our view conversion definitions in the future.

Co-Authored-By: Miguel Albernaz <[email protected]>

* Add isolated light map pass

The resulting texture isn't used yet, but it's being written to.

Co-Authored-By: Miguel Albernaz <[email protected]>

* Use light map

Now we're using our light map, blended with our main texture.

Co-Authored-By: Miguel Albernaz <[email protected]>

* Fix ambient light

Co-Authored-By: Miguel Albernaz <[email protected]>

* Check occluder visibility

Co-Authored-By: Miguel Albernaz <[email protected]>

* Use separate node for SDF passo

Not strictly necessary, but I do like having an explicit render graph,
especially if it's something someone else ever wanted to tweak.

Co-Authored-By: Miguel Albernaz <[email protected]>

* Use enum for light occluder shapes

This will allow us to sanely expand our API as we introduce more
occluder shapes.

Co-Authored-By: Miguel Albernaz <[email protected]>

* Pull out light map node

A refactor, again getting us to a world of a proper render graph.

Co-Authored-By: Miguel Albernaz <[email protected]>

* Move sdf shader to sdf module

Co-Authored-By: Miguel Albernaz <[email protected]>

* Use SDF specific component for texture

Nice to split this one up, rather than all the textures living on one
component.

Co-Authored-By: Miguel Albernaz <[email protected]>

* Add separate light map texture component

Now we can completely isolate our light map and sdf textures.

Co-Authored-By: Miguel Albernaz <[email protected]>

* Hoist types shader

This shader isn't specific to any one pipeline, so let's pull it up.

Co-Authored-By: Miguel Albernaz <[email protected]>

* Use shared shader types

Co-Authored-By: Miguel Albernaz <[email protected]>

* Remove lighting settings

No blur yet. Will add this back later.

Co-Authored-By: Miguel Albernaz <[email protected]>

* Fix comment

Co-Authored-By: Miguel Albernaz <[email protected]>

* Pull view transformations to separate shader

Co-Authored-By: Miguel Albernaz <[email protected]>

* Add occluders to changelog

Co-Authored-By: Miguel Albernaz <[email protected]>

* Add light occlusion example

Co-Authored-By: Miguel Albernaz <[email protected]>

* Fix occluder field order

These were wrong!

* Add more occluders to occlusion example

* Use texture sample level

Missed this when porting things over.

* Fix ambient light

* Split up shape from light occluder

This will allow us to encode more information into an occluder, such as
information on how shadows should be cast.

* Allow shadows to be toggled on a per light basis

* Update docs

* Remove future goals

---------

Co-authored-by: Miguel Albernaz <[email protected]>
  • Loading branch information
jgayfer and malbernaz authored Aug 6, 2024
1 parent ea834d7 commit 30db46a
Show file tree
Hide file tree
Showing 30 changed files with 1,065 additions and 208 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Light occlusion with hard shadows (#20).
- Added `LightOccluder2d` component and `LightOccluder2dBundle`.
- Added `cast_shadows` field to `PointLight2d` (defaults to false).

## [0.2.2] - 2024-07-18

### Fixed
Expand Down
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ bevy = { version = "0.14", default-features = false, features = [
"bevy_render",
"bevy_core_pipeline",
"bevy_winit",
"x11"
"x11",
] }
smallvec = "1.13"

Expand Down Expand Up @@ -49,3 +49,7 @@ path = "examples/multiple.rs"
[[example]]
name = "dungeon"
path = "examples/dungeon.rs"

[[example]]
name = "occlusion"
path = "examples/occlusion.rs"
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Designed to be simple to use, yet expressive enough to fit a variety of needs.

- Component driven design
- Configurable [point lights](https://docs.rs/bevy_light_2d/0.2.2/bevy_light_2d/light/struct.PointLight2d.html)
- Light occlusion
- Dynamic shadows
- Camera specific [ambient light](https://docs.rs/bevy_light_2d/0.2.2/bevy_light_2d/light/struct.AmbientLight2d.html)
- Single camera rendering

Expand Down Expand Up @@ -71,11 +73,6 @@ of drop in options available.
My goal with this crate is to fill that void, prioritizing ease of use and
general application over depth of features.

## Future goals

- Light occluders + shadows
- Sprite lights

## Bevy compatibility

| bevy | bevy_light_2d |
Expand Down
1 change: 1 addition & 0 deletions examples/dungeon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ fn spawn_candles(mut commands: Commands, spritesheet: Res<CandleSpritesheet>) {
color: Color::Srgba(YELLOW),
intensity: 25.0,
falloff: 4.0,
..default()
},
..default()
},))
Expand Down
1 change: 1 addition & 0 deletions examples/multiple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ fn setup(mut commands: Commands) {
radius: 50.,
intensity: 5.0,
falloff: 5.0,
..default()
},
transform: Transform::from_xyz(25., 50., 0.),
..default()
Expand Down
85 changes: 85 additions & 0 deletions examples/occlusion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use bevy::prelude::*;
use bevy_light_2d::prelude::*;

fn main() {
App::new()
.add_plugins((DefaultPlugins, Light2dPlugin))
.add_systems(Startup, setup)
.add_systems(Update, move_lights)
.run();
}

fn setup(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());

commands.spawn(PointLight2dBundle {
point_light: PointLight2d {
intensity: 20.0,
radius: 1000.0,
falloff: 10.0,
cast_shadows: true,
..default()
},
transform: Transform {
translation: Vec3::new(0.0, 100.0, 0.0),
..default()
},
..default()
});

commands.spawn(LightOccluder2dBundle {
light_occluder: LightOccluder2d {
shape: LightOccluder2dShape::Rectangle {
half_size: Vec2::splat(25.0),
},
},
transform: Transform::from_xyz(-400.0, -50.0, 0.0),
..default()
});

commands.spawn(LightOccluder2dBundle {
light_occluder: LightOccluder2d {
shape: LightOccluder2dShape::Rectangle {
half_size: Vec2::splat(25.0),
},
},
transform: Transform::from_xyz(-200.0, -50.0, 0.0),
..default()
});

commands.spawn(LightOccluder2dBundle {
light_occluder: LightOccluder2d {
shape: LightOccluder2dShape::Rectangle {
half_size: Vec2::splat(25.0),
},
},
transform: Transform::from_xyz(0.0, -50.0, 0.0),
..default()
});

commands.spawn(LightOccluder2dBundle {
light_occluder: LightOccluder2d {
shape: LightOccluder2dShape::Rectangle {
half_size: Vec2::splat(25.0),
},
},
transform: Transform::from_xyz(200.0, -50.0, 0.0),
..default()
});

commands.spawn(LightOccluder2dBundle {
light_occluder: LightOccluder2d {
shape: LightOccluder2dShape::Rectangle {
half_size: Vec2::splat(25.0),
},
},
transform: Transform::from_xyz(400.0, -50.0, 0.0),
..default()
});
}

fn move_lights(mut query: Query<&mut Transform, With<PointLight2d>>, time: Res<Time>) {
for mut light_transform in &mut query {
light_transform.translation.x = time.elapsed_seconds().sin() * 500.
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
#![doc = include_str!("../README.md")]

pub mod light;
pub mod occluder;
pub mod plugin;
mod render;

/// A module which exports commonly used dependencies.
pub mod prelude {
pub use crate::light::{AmbientLight2d, PointLight2d, PointLight2dBundle};
pub use crate::occluder::{LightOccluder2d, LightOccluder2dBundle, LightOccluder2dShape};
pub use crate::plugin::Light2dPlugin;
}
3 changes: 3 additions & 0 deletions src/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ pub struct PointLight2d {
/// How quickly illumination from the light should deteriorate over distance.
/// A higher falloff value will result in less illumination at the light's maximum radius.
pub falloff: f32,
/// Whether the light should cast shadows.
pub cast_shadows: bool,
}

impl Default for PointLight2d {
Expand All @@ -42,6 +44,7 @@ impl Default for PointLight2d {
intensity: 1.0,
radius: 0.5,
falloff: 0.0,
cast_shadows: false,
}
}
}
Expand Down
51 changes: 51 additions & 0 deletions src/occluder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//! A module which contains occluder components.
use bevy::{
ecs::{bundle::Bundle, component::Component},
math::Vec2,
render::view::{InheritedVisibility, ViewVisibility, Visibility},
transform::components::{GlobalTransform, Transform},
};

/// A light occluder that prevents light passing through it, casting shadows.
///
/// This is commonly used as a component within [`LightOcluder2dBundle`].
#[derive(Default, Component)]
pub struct LightOccluder2d {
/// The shape of the light occluder.
pub shape: LightOccluder2dShape,
}

/// Shape data for a light occluder.
pub enum LightOccluder2dShape {
/// A rectangular light occluder.
Rectangle {
/// Half of the width and height of the rectangle.
half_size: Vec2,
},
}

impl Default for LightOccluder2dShape {
fn default() -> Self {
Self::Rectangle {
half_size: Vec2::splat(0.0),
}
}
}

/// A bundle of components for rendering a [`LightOccluder2d`] entity.
#[derive(Bundle, Default)]
pub struct LightOccluder2dBundle {
/// Specifies the rendering properties of the light occluder
pub light_occluder: LightOccluder2d,
/// The local transform of the light occluder, relative to its parent.
pub transform: Transform,
/// The absolute transform of the light occluder. This should generally not be written to directly.
pub global_transform: GlobalTransform,
/// User indication of whether an entity is visible.
pub visibility: Visibility,
/// Inherited visibility of an entity.
pub inherited_visibility: InheritedVisibility,
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering.
pub view_visibility: ViewVisibility,
}
62 changes: 54 additions & 8 deletions src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,29 @@ use bevy::{
gpu_component_array_buffer::GpuComponentArrayBufferPlugin,
render_graph::{RenderGraphApp, ViewNodeRunner},
render_resource::SpecializedRenderPipelines,
view::{check_visibility, VisibilitySystems},
view::{check_visibility, prepare_view_targets, VisibilitySystems},
Render, RenderApp, RenderSet,
},
};

use crate::{
light::{AmbientLight2d, PointLight2d},
occluder::LightOccluder2d,
render::{
extract::{
extract_ambient_lights, extract_point_lights, ExtractedAmbientLight2d,
ExtractedPointLight2d,
extract_ambient_lights, extract_light_occluders, extract_point_lights,
ExtractedAmbientLight2d, ExtractedLightOccluder2d, ExtractedPointLight2d,
},
light_map::{
prepare_light_map_texture, LightMapNode, LightMapPass, LightMapPipeline,
LIGHT_MAP_SHADER,
},
lighting::{
prepare_lighting_pipelines, LightingNode, LightingPass, LightingPipeline,
LIGHTING_SHADER,
},
sdf::{prepare_sdf_texture, SdfNode, SdfPass, SdfPipeline, SDF_SHADER},
TYPES_SHADER, VIEW_TRANSFORMATIONS_SHADER,
},
};

Expand All @@ -33,22 +40,41 @@ pub struct Light2dPlugin;

impl Plugin for Light2dPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(app, TYPES_SHADER, "render/types.wgsl", Shader::from_wgsl);
load_internal_asset!(
app,
VIEW_TRANSFORMATIONS_SHADER,
"render/view_transformations.wgsl",
Shader::from_wgsl
);
load_internal_asset!(app, SDF_SHADER, "render/sdf/sdf.wgsl", Shader::from_wgsl);
load_internal_asset!(
app,
LIGHTING_SHADER,
"render/lighting/lighting.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
LIGHT_MAP_SHADER,
"render/light_map/light_map.wgsl",
Shader::from_wgsl
);

app.add_plugins((
UniformComponentPlugin::<ExtractedAmbientLight2d>::default(),
GpuComponentArrayBufferPlugin::<ExtractedPointLight2d>::default(),
GpuComponentArrayBufferPlugin::<ExtractedLightOccluder2d>::default(),
))
.register_type::<AmbientLight2d>()
.register_type::<PointLight2d>()
.add_systems(
PostUpdate,
check_visibility::<With<PointLight2d>>.in_set(VisibilitySystems::CheckVisibility),
(
check_visibility::<With<PointLight2d>>,
check_visibility::<With<LightOccluder2d>>,
)
.in_set(VisibilitySystems::CheckVisibility),
);

let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
Expand All @@ -59,21 +85,41 @@ impl Plugin for Light2dPlugin {
.init_resource::<SpecializedRenderPipelines<LightingPipeline>>()
.add_systems(
ExtractSchedule,
(extract_point_lights, extract_ambient_lights),
(
extract_point_lights,
extract_light_occluders,
extract_ambient_lights,
),
)
.add_systems(
Render,
prepare_lighting_pipelines.in_set(RenderSet::Prepare),
(
prepare_lighting_pipelines.in_set(RenderSet::Prepare),
prepare_sdf_texture
.after(prepare_view_targets)
.in_set(RenderSet::ManageViews),
prepare_light_map_texture
.after(prepare_view_targets)
.in_set(RenderSet::ManageViews),
),
)
.add_render_graph_node::<ViewNodeRunner<LightingNode>>(Core2d, LightingPass)
.add_render_graph_edge(Core2d, Node2d::EndMainPass, LightingPass);
.add_render_graph_node::<ViewNodeRunner<SdfNode>>(Core2d, SdfPass)
.add_render_graph_node::<ViewNodeRunner<LightMapNode>>(Core2d, LightMapPass)
.add_render_graph_edges(
Core2d,
(Node2d::EndMainPass, SdfPass, LightMapPass, LightingPass),
);
}

fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};

render_app.init_resource::<LightingPipeline>();
render_app
.init_resource::<LightingPipeline>()
.init_resource::<SdfPipeline>()
.init_resource::<LightMapPipeline>();
}
}
Loading

0 comments on commit 30db46a

Please sign in to comment.