diff --git a/Cargo.toml b/Cargo.toml index 12d8ef458f0d3..23dabb745ad43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -343,6 +343,9 @@ bevy_solari = ["bevy_internal/bevy_solari"] # Provides sprite functionality bevy_sprite = ["bevy_internal/bevy_sprite"] +# Provides 2D lighting +bevy_sprite_light = ["bevy_internal/bevy_sprite_light"] + # Provides sprite rendering functionality bevy_sprite_render = ["bevy_internal/bevy_sprite_render"] @@ -839,6 +842,18 @@ doc-scrape-examples = true hidden = true # 2D Rendering +[[example]] +name = "lighting_2d" +path = "examples/2d/lighting_2d.rs" +doc-scrape-examples = true +required-features = ["bevy_sprite_light"] + +[package.metadata.example.lighting_2d] +name = "2D Lighting" +description = "Illustrates lighting in 2d" +category = "2D Rendering" +wasm = true + [[example]] name = "bloom_2d" path = "examples/2d/bloom_2d.rs" diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 7464e773ac6e9..506e5e311be77 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -280,6 +280,10 @@ bevy_sprite_render = [ "bevy_core_pipeline", "bevy_gizmos_render?/bevy_sprite_render", ] +bevy_sprite_light = [ + "dep:bevy_sprite_light", + "bevy_sprite_render?/bevy_sprite_light", +] bevy_ui_render = ["dep:bevy_ui_render", "bevy_sprite_render", "bevy_ui"] # Feathers ships its built-in icons as PNGs, so enable the PNG feature bevy_feathers = ["dep:bevy_feathers", "png"] @@ -603,6 +607,7 @@ bevy_world_serialization = { path = "../bevy_world_serialization", optional = tr bevy_scene = { path = "../bevy_scene", optional = true, version = "0.20.0-dev" } bevy_solari = { path = "../bevy_solari", optional = true, version = "0.20.0-dev" } bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.20.0-dev" } +bevy_sprite_light = { path = "../bevy_sprite_light", optional = true, version = "0.20.0-dev" } bevy_sprite_render = { path = "../bevy_sprite_render", optional = true, version = "0.20.0-dev" } bevy_state = { path = "../bevy_state", optional = true, version = "0.20.0-dev", default-features = false, features = [ "bevy_app", diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index f2135c2d6780e..a6476e21f7535 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -95,6 +95,8 @@ pub use bevy_shape as shape; pub use bevy_solari as solari; #[cfg(feature = "bevy_sprite")] pub use bevy_sprite as sprite; +#[cfg(feature = "bevy_sprite_light")] +pub use bevy_sprite_light as sprite_light; #[cfg(feature = "bevy_sprite_render")] pub use bevy_sprite_render as sprite_render; #[cfg(feature = "bevy_state")] diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index 8f56a50c4a091..07f9880d32d2a 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -83,6 +83,10 @@ pub use crate::sprite::prelude::*; #[cfg(feature = "bevy_sprite_render")] pub use crate::sprite_render::prelude::*; +#[doc(hidden)] +#[cfg(feature = "bevy_sprite_light")] +pub use crate::sprite_light::prelude::*; + #[doc(hidden)] #[cfg(feature = "bevy_text")] pub use crate::text::prelude::*; diff --git a/crates/bevy_sprite_light/Cargo.toml b/crates/bevy_sprite_light/Cargo.toml new file mode 100644 index 0000000000000..d7369100dca75 --- /dev/null +++ b/crates/bevy_sprite_light/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "bevy_sprite_light" +version = "0.20.0-dev" +edition = "2024" +description = "Provides 2d lighting for Bevy Engine" +homepage = "https://bevy.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] +rust-version = "1.94.0" + +[dependencies] +bevy_camera = { path = "../bevy_camera", version = "0.20.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.20.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.20.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.20.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.20.0-dev" } + +[lints] +workspace = true diff --git a/crates/bevy_sprite_light/LICENSE-APACHE b/crates/bevy_sprite_light/LICENSE-APACHE new file mode 100644 index 0000000000000..d9a10c0d8e868 --- /dev/null +++ b/crates/bevy_sprite_light/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/bevy_sprite_light/LICENSE-MIT b/crates/bevy_sprite_light/LICENSE-MIT new file mode 100644 index 0000000000000..9cf106272ac3b --- /dev/null +++ b/crates/bevy_sprite_light/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/bevy_sprite_light/src/ambient_light.rs b/crates/bevy_sprite_light/src/ambient_light.rs new file mode 100644 index 0000000000000..c0e794c7599d4 --- /dev/null +++ b/crates/bevy_sprite_light/src/ambient_light.rs @@ -0,0 +1,26 @@ +use bevy_color::Color; +use bevy_ecs::prelude::*; +use bevy_reflect::prelude::*; + +/// Light applied to every 2d entity. +/// +/// The default is white at full brightness, so the brightness needs to be turned down in order +/// for other light sources to take effect. +#[derive(Resource, Clone, Debug, Reflect)] +#[reflect(Resource, Debug, Default, Clone)] +pub struct GlobalAmbientLight2d { + /// The color of the ambient light. + pub color: Color, + /// The brightness of the ambient light. `1.0` is full brightness. `0.0` will result in a + /// completely dark scene. + pub brightness: f32, +} + +impl Default for GlobalAmbientLight2d { + fn default() -> Self { + Self { + color: Color::WHITE, + brightness: 1.0, + } + } +} diff --git a/crates/bevy_sprite_light/src/lib.rs b/crates/bevy_sprite_light/src/lib.rs new file mode 100644 index 0000000000000..f90f375739c2f --- /dev/null +++ b/crates/bevy_sprite_light/src/lib.rs @@ -0,0 +1,22 @@ +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" +)] + +//! This crate provides 2d lighting support for the Bevy game engine. + +mod ambient_light; +mod point_light; + +pub use ambient_light::GlobalAmbientLight2d; +pub use point_light::PointLight2d; + +/// The 2d lighting prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. +pub mod prelude { + #[doc(hidden)] + pub use crate::{GlobalAmbientLight2d, PointLight2d}; +} diff --git a/crates/bevy_sprite_light/src/point_light.rs b/crates/bevy_sprite_light/src/point_light.rs new file mode 100644 index 0000000000000..f7ee2abfdec5c --- /dev/null +++ b/crates/bevy_sprite_light/src/point_light.rs @@ -0,0 +1,32 @@ +use bevy_camera::visibility::Visibility; +use bevy_color::Color; +use bevy_ecs::prelude::*; +use bevy_reflect::prelude::*; +use bevy_transform::components::Transform; + +/// A light that provides illumination in all directions. +#[derive(Component, Debug, Clone, Copy, Reflect)] +#[reflect(Component, Default, Debug, Clone)] +#[require(Transform, Visibility)] +pub struct PointLight2d { + /// The color of the light. + pub color: Color, + /// The intensity of the light. Higher values make the light brighter. + pub intensity: f32, + /// The range of the light. Illumination will only occur within the light's range. + pub range: f32, + /// 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, +} + +impl Default for PointLight2d { + fn default() -> Self { + Self { + color: Color::WHITE, + intensity: 1.0, + range: 100.0, + falloff: 0.0, + } + } +} diff --git a/crates/bevy_sprite_render/Cargo.toml b/crates/bevy_sprite_render/Cargo.toml index d5429fa593234..c7470c5196e8e 100644 --- a/crates/bevy_sprite_render/Cargo.toml +++ b/crates/bevy_sprite_render/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["bevy"] webgl = [] webgpu = [] bevy_text = ["dep:bevy_text", "bevy_sprite/bevy_text"] +bevy_sprite_light = ["dep:bevy_sprite_light"] [dependencies] # bevy @@ -29,6 +30,7 @@ bevy_shape = { path = "../bevy_shape", version = "0.20.0-dev" } bevy_shader = { path = "../bevy_shader", version = "0.20.0-dev" } bevy_material = { path = "../bevy_material", version = "0.20.0-dev" } bevy_sprite = { path = "../bevy_sprite", version = "0.20.0-dev" } +bevy_sprite_light = { path = "../bevy_sprite_light", version = "0.20.0-dev", optional = true } bevy_text = { path = "../bevy_text", version = "0.20.0-dev", optional = true } bevy_reflect = { path = "../bevy_reflect", version = "0.20.0-dev" } bevy_render = { path = "../bevy_render", version = "0.20.0-dev" } diff --git a/crates/bevy_sprite_render/src/lib.rs b/crates/bevy_sprite_render/src/lib.rs index fc720b4cc4233..6a4d9721401d6 100644 --- a/crates/bevy_sprite_render/src/lib.rs +++ b/crates/bevy_sprite_render/src/lib.rs @@ -10,6 +10,8 @@ extern crate alloc; +#[cfg(feature = "bevy_sprite_light")] +mod light; mod mesh2d; mod render; mod sprite_mesh; @@ -72,6 +74,10 @@ impl Plugin for SpriteRenderPlugin { TilemapChunkPlugin, TilemapChunkMaterialPlugin, )); + + #[cfg(feature = "bevy_sprite_light")] + app.add_plugins(light::Lighting2dPlugin); + app.register_required_components::(); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { diff --git a/crates/bevy_sprite_render/src/light/ambient_light.rs b/crates/bevy_sprite_render/src/light/ambient_light.rs new file mode 100644 index 0000000000000..d1c5cc9563aac --- /dev/null +++ b/crates/bevy_sprite_render/src/light/ambient_light.rs @@ -0,0 +1,12 @@ +use bevy_render::{extract_resource::ExtractResource, RenderApp}; +use bevy_sprite_light::GlobalAmbientLight2d; + +use super::Lighting2dPlugin; + +impl ExtractResource for GlobalAmbientLight2d { + type Source = GlobalAmbientLight2d; + + fn extract_resource(source: &Self::Source) -> Self { + source.clone() + } +} diff --git a/crates/bevy_sprite_render/src/light/gpu.rs b/crates/bevy_sprite_render/src/light/gpu.rs new file mode 100644 index 0000000000000..668850953663b --- /dev/null +++ b/crates/bevy_sprite_render/src/light/gpu.rs @@ -0,0 +1,84 @@ +use bevy_color::ColorToComponents; +use bevy_ecs::prelude::*; +use bevy_math::{Vec2, Vec3}; +use bevy_render::{ + render_resource::{ShaderSize, ShaderType, UniformBuffer}, + renderer::{RenderDevice, RenderQueue}, +}; +use bevy_sprite_light::GlobalAmbientLight2d; +use bevy_utils::once; +use tracing::warn; + +use super::point_light::ExtractedPointLight2d; + +pub const MAX_POINT_LIGHTS_2D: usize = 256; + +#[derive(Default, Clone, Copy, ShaderType)] +pub struct GpuPointLight2d { + pub position: Vec2, + pub range: f32, + pub intensity: f32, + pub color: Vec3, + pub falloff: f32, +} + +#[derive(Clone, ShaderType)] +pub struct Lights2dUniform { + pub ambient: Vec3, + pub point_light_count: u32, + pub point_lights: [GpuPointLight2d; MAX_POINT_LIGHTS_2D], +} + +// WebGL2 only guarantees 16 KiB per uniform binding. +const _: () = assert!(Lights2dUniform::SHADER_SIZE.get() <= 16384); + +impl Default for Lights2dUniform { + fn default() -> Self { + Self { + ambient: Vec3::ZERO, + point_light_count: 0, + point_lights: [GpuPointLight2d::default(); MAX_POINT_LIGHTS_2D], + } + } +} + +#[derive(Resource, Default)] +pub struct Lights2dBuffer { + pub buffer: UniformBuffer, +} + +pub fn prepare_lights_2d_buffer( + render_device: Res, + render_queue: Res, + mut lights_buffer: ResMut, + ambient: Res, + point_lights: Query<&ExtractedPointLight2d>, +) { + let uniform = lights_buffer.buffer.get_mut(); + uniform.ambient = ambient.color.to_linear().to_vec3() * ambient.brightness; + + let mut count = 0; + + for light in &point_lights { + if count == MAX_POINT_LIGHTS_2D { + once!(warn!( + "More than {MAX_POINT_LIGHTS_2D} `PointLight2d`s are visible. The rest will be ignored." + )); + break; + } + uniform.point_lights[count] = GpuPointLight2d { + position: light.transform.translation().truncate(), + range: light.range, + intensity: light.intensity, + color: light.color.to_linear().to_vec3(), + falloff: light.falloff, + }; + count += 1; + } + + uniform.point_light_count = count as u32; + + lights_buffer + .buffer + .write_buffer(&render_device, &render_queue); +} diff --git a/crates/bevy_sprite_render/src/light/lighting.wesl b/crates/bevy_sprite_render/src/light/lighting.wesl new file mode 100644 index 0000000000000..81c70e914f07a --- /dev/null +++ b/crates/bevy_sprite_render/src/light/lighting.wesl @@ -0,0 +1,26 @@ +import package::mesh2d::view_bindings::lights_2d; + +fn apply_lighting_2d(color: vec4, world_position: vec2) -> vec4 { + var light = lights_2d.ambient; + + for (var i = 0u; i < lights_2d.point_light_count; i++) { + let point_light = lights_2d.point_lights[i]; + let dist = distance(world_position, point_light.position); + if dist < point_light.range { + light += point_light.color * attenuation(dist, point_light.range, point_light.intensity, point_light.falloff); + } + } + + return vec4(color.rgb * light, color.a); +} + +// See https://lisyarus.github.io/blog/posts/point-light-attenuation.html +fn attenuation(dist: f32, range: f32, intensity: f32, falloff: f32) -> f32 { + let s = dist / range; + if s > 1.0 { + return 0.0; + } + let s2 = s * s; + let window = 1.0 - s2; + return intensity * window * window / (1.0 + falloff * s2); +} diff --git a/crates/bevy_sprite_render/src/light/mod.rs b/crates/bevy_sprite_render/src/light/mod.rs new file mode 100644 index 0000000000000..0c60135369e5c --- /dev/null +++ b/crates/bevy_sprite_render/src/light/mod.rs @@ -0,0 +1,39 @@ +mod ambient_light; +mod gpu; +mod point_light; + +use bevy_app::{App, Plugin}; +use bevy_ecs::schedule::IntoScheduleConfigs; +use bevy_extract::extract_component::ExtractComponentPlugin; +use bevy_render::{ + extract_resource::ExtractResourcePlugin, GpuResourceAppExt, Render, RenderApp, RenderSystems, +}; +use bevy_shader::load_shader_library; +use bevy_sprite_light::{GlobalAmbientLight2d, PointLight2d}; +use gpu::prepare_lights_2d_buffer; +pub use gpu::{Lights2dBuffer, Lights2dUniform, MAX_POINT_LIGHTS_2D}; + +/// Adds 2d lighting support. +#[derive(Default)] +pub struct Lighting2dPlugin; + +impl Plugin for Lighting2dPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "types.wesl"); + load_shader_library!(app, "lighting.wesl"); + + app.init_resource::().add_plugins(( + ExtractComponentPlugin::::default(), + ExtractResourcePlugin::::default(), + )); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_gpu_resource::() + .add_systems( + Render, + prepare_lights_2d_buffer.in_set(RenderSystems::PrepareResources), + ); + } + } +} diff --git a/crates/bevy_sprite_render/src/light/point_light.rs b/crates/bevy_sprite_render/src/light/point_light.rs new file mode 100644 index 0000000000000..eca3ca23df2dd --- /dev/null +++ b/crates/bevy_sprite_render/src/light/point_light.rs @@ -0,0 +1,46 @@ +use bevy_camera::visibility::InheritedVisibility; +use bevy_color::Color; +use bevy_ecs::{component::Component, query::QueryItem}; +use bevy_render::{extract_component::ExtractComponent, sync_component::SyncComponent, RenderApp}; +use bevy_sprite_light::PointLight2d; +use bevy_transform::components::GlobalTransform; + +use super::Lighting2dPlugin; + +#[derive(Component)] +pub struct ExtractedPointLight2d { + pub color: Color, + pub transform: GlobalTransform, + pub range: f32, + pub intensity: f32, + pub falloff: f32, +} + +impl SyncComponent for PointLight2d { + type Target = ExtractedPointLight2d; +} + +impl ExtractComponent for PointLight2d { + type QueryData = ( + &'static PointLight2d, + &'static GlobalTransform, + &'static InheritedVisibility, + ); + type QueryFilter = (); + type Out = ExtractedPointLight2d; + + fn extract_component( + (light, transform, visibility): QueryItem<'_, '_, Self::QueryData>, + ) -> Option { + if !visibility.get() { + return None; + } + Some(ExtractedPointLight2d { + color: light.color, + transform: *transform, + range: light.range, + intensity: light.intensity, + falloff: light.falloff, + }) + } +} diff --git a/crates/bevy_sprite_render/src/light/types.wesl b/crates/bevy_sprite_render/src/light/types.wesl new file mode 100644 index 0000000000000..4ec1c2083d0e7 --- /dev/null +++ b/crates/bevy_sprite_render/src/light/types.wesl @@ -0,0 +1,13 @@ +struct PointLight2d { + position: vec2, + range: f32, + intensity: f32, + color: vec3, + falloff: f32, +} + +struct Lights2d { + ambient: vec3, + point_light_count: u32, + point_lights: array, +} diff --git a/crates/bevy_sprite_render/src/mesh2d/color_material.wesl b/crates/bevy_sprite_render/src/mesh2d/color_material.wesl index 587ca55f4fa89..1d8818af2908a 100644 --- a/crates/bevy_sprite_render/src/mesh2d/color_material.wesl +++ b/crates/bevy_sprite_render/src/mesh2d/color_material.wesl @@ -4,6 +4,8 @@ import package::mesh2d::{ bindings::mesh, }; import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d}; +@if(LIGHTING_2D) +import package::light::lighting::apply_lighting_2d; @if(TONEMAP_IN_SHADER) import bevy_core_pipeline::tonemapping; @@ -86,6 +88,8 @@ fn fragment( output_color = alpha_discard(flags, alpha_cutoff, output_color); +@if(LIGHTING_2D) + output_color = apply_lighting_2d(output_color, in.world_position.xy); @if(TONEMAP_IN_SHADER) output_color = tonemapping::tone_mapping(output_color, view.color_grading); @if(SRGB_OUTPUT) diff --git a/crates/bevy_sprite_render/src/mesh2d/mesh.rs b/crates/bevy_sprite_render/src/mesh2d/mesh.rs index 3d527cab557bf..45dfc72e95aea 100644 --- a/crates/bevy_sprite_render/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite_render/src/mesh2d/mesh.rs @@ -457,19 +457,25 @@ pub fn init_mesh_2d_pipeline( asset_server: Res, ) { let tonemapping_lut_entries = get_lut_bind_group_layout_entries(); - let view_layout = BindGroupLayoutDescriptor::new( - "mesh2d_view_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::VERTEX_FRAGMENT, - ( - uniform_buffer::(true), - uniform_buffer::(false), - tonemapping_lut_entries[0].visibility(ShaderStages::FRAGMENT), - tonemapping_lut_entries[1].visibility(ShaderStages::FRAGMENT), - ), + + let view_layout_entries = DynamicBindGroupLayoutEntries::sequential( + ShaderStages::VERTEX_FRAGMENT, + ( + uniform_buffer::(true), + uniform_buffer::(false), + tonemapping_lut_entries[0].visibility(ShaderStages::FRAGMENT), + tonemapping_lut_entries[1].visibility(ShaderStages::FRAGMENT), ), ); + #[cfg(feature = "bevy_sprite_light")] + let view_layout_entries = view_layout_entries.extend_with_indices((( + 4, + uniform_buffer::(false).visibility(ShaderStages::FRAGMENT), + ),)); + + let view_layout = BindGroupLayoutDescriptor::new("mesh2d_view_layout", &view_layout_entries); + let limits = render_device.limits(); let mesh_layout = BindGroupLayoutDescriptor::new( "mesh2d_layout", @@ -837,6 +843,15 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { shader_defs.push("OKLAB_OUTPUT".into()); } + #[cfg(feature = "bevy_sprite_light")] + { + shader_defs.push("LIGHTING_2D".into()); + shader_defs.push(ShaderDefVal::UInt( + "MAX_POINT_LIGHTS_2D".into(), + crate::light::MAX_POINT_LIGHTS_2D as u32, + )); + } + let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?; let format = key.target_format(); @@ -965,6 +980,7 @@ pub fn prepare_mesh2d_view_bind_groups( tonemapping_luts: Res, images: Res>, fallback_image: Res, + #[cfg(feature = "bevy_sprite_light")] lights_2d_buffer: Res, ) { let (Some(view_binding), Some(globals)) = ( view_uniforms.uniforms.binding(), @@ -972,19 +988,26 @@ pub fn prepare_mesh2d_view_bind_groups( ) else { return; }; + #[cfg(feature = "bevy_sprite_light")] + let Some(lights_2d) = lights_2d_buffer.buffer.binding() else { + return; + }; for (entity, tonemapping) in &views { let lut_bindings = get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image); + let entries = DynamicBindGroupEntries::sequential(( + view_binding.clone(), + globals.clone(), + lut_bindings.0, + lut_bindings.1, + )); + #[cfg(feature = "bevy_sprite_light")] + let entries = entries.extend_with_indices(((4, lights_2d.clone()),)); let view_bind_group = render_device.create_bind_group( "mesh2d_view_bind_group", &pipeline_cache.get_bind_group_layout(&mesh2d_pipeline.view_layout), - &BindGroupEntries::sequential(( - view_binding.clone(), - globals.clone(), - lut_bindings.0, - lut_bindings.1, - )), + &entries, ); commands.entity(entity).insert(Mesh2dViewBindGroup { diff --git a/crates/bevy_sprite_render/src/mesh2d/view_bindings.wesl b/crates/bevy_sprite_render/src/mesh2d/view_bindings.wesl index e938f6e7f769e..225e9c9e1d647 100644 --- a/crates/bevy_sprite_render/src/mesh2d/view_bindings.wesl +++ b/crates/bevy_sprite_render/src/mesh2d/view_bindings.wesl @@ -1,9 +1,15 @@ import bevy_render::view::View; import bevy_render::globals::Globals; +@if(LIGHTING_2D) +import package::light::types::Lights2d; + @group(0) @binding(0) var view: View; @group(0) @binding(1) var globals: Globals; @group(0) @binding(2) var dt_lut_texture: texture_3d; @group(0) @binding(3) var dt_lut_sampler: sampler; + +@if(LIGHTING_2D) +@group(0) @binding(4) var lights_2d: Lights2d; diff --git a/crates/bevy_sprite_render/src/sprite_mesh/sprite_material.wesl b/crates/bevy_sprite_render/src/sprite_mesh/sprite_material.wesl index f6e54f3909632..7764cea32ac84 100644 --- a/crates/bevy_sprite_render/src/sprite_mesh/sprite_material.wesl +++ b/crates/bevy_sprite_render/src/sprite_mesh/sprite_material.wesl @@ -9,6 +9,8 @@ import package::{ }; @if(BINDLESS) import package::sprite_mesh::bindings::material_indices; +@if(LIGHTING_2D) +import package::light::lighting::apply_lighting_2d; @if(TONEMAP_IN_SHADER) import bevy_core_pipeline::tonemapping; @@ -73,6 +75,9 @@ fn fragment( ) -> @location(0) vec4 { var output_color = sprite_functions::sample_final_color(mesh.uv, mesh.instance_index); +@if(LIGHTING_2D) + output_color = apply_lighting_2d(output_color, mesh.world_position.xy); + @if(TONEMAP_IN_SHADER) output_color = tonemapping::tone_mapping(output_color, view.color_grading); diff --git a/crates/bevy_sprite_render/src/tilemap_chunk/tilemap_chunk_material.wesl b/crates/bevy_sprite_render/src/tilemap_chunk/tilemap_chunk_material.wesl index 4eb2fe7a6c37c..e0505b3ef304a 100644 --- a/crates/bevy_sprite_render/src/tilemap_chunk/tilemap_chunk_material.wesl +++ b/crates/bevy_sprite_render/src/tilemap_chunk/tilemap_chunk_material.wesl @@ -3,6 +3,8 @@ import package::mesh2d::{ view_bindings::view, vertex_output::VertexOutput, }; +@if(LIGHTING_2D) +import package::light::lighting::apply_lighting_2d; @group(constants::MATERIAL_BIND_GROUP) @binding(0) var tileset: texture_2d_array; @group(constants::MATERIAL_BIND_GROUP) @binding(1) var tileset_sampler: sampler; @@ -65,11 +67,14 @@ fn fragment(in: VertexOutput) -> @location(0) vec4 { } let tex_color = textureSample(tileset, tileset_sampler, local_uv, tile.tileset_index); - let final_color = tex_color * tile.color; + var output_color = tex_color * tile.color; - if (final_color.a < 0.001) { + if (output_color.a < 0.001) { discard; } - return final_color; +@if(LIGHTING_2D) + output_color= apply_lighting_2d(output_color, in.world_position.xy); + + return output_color; } diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 173709096af16..7d11fad2371d7 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -105,6 +105,7 @@ This is the complete `bevy` cargo feature list, without "profiles" or "collectio |bevy_shader|Provides shaders usable through asset handles.| |bevy_solari|Provides raytraced lighting (experimental)| |bevy_sprite|Provides sprite functionality| +|bevy_sprite_light|Provides 2D lighting| |bevy_sprite_render|Provides sprite rendering functionality| |bevy_state|Enable built in global state machines| |bevy_text|Provides text functionality| diff --git a/examples/2d/lighting_2d.rs b/examples/2d/lighting_2d.rs new file mode 100644 index 0000000000000..662e546868628 --- /dev/null +++ b/examples/2d/lighting_2d.rs @@ -0,0 +1,130 @@ +//! Illustrates lighting in 2d. + +use bevy::{color::palettes::css::*, math::ops, prelude::*}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .insert_resource(GlobalAmbientLight2d { + brightness: 0.1, + ..default() + }) + .add_systems(Startup, scene.spawn()) + .add_systems(Update, (toggle_ambient_light, movement, orbit)) + .run(); +} + +#[derive(Component, Default, Clone)] +struct Movable; + +#[derive(Component, Default, Clone)] +struct Orbit { + center: Vec2, + radius: Vec2, + speed: f32, +} + +fn scene() -> impl SceneList { + bsn_list! { + // We need a background, as the clear color is not lit. + Mesh2d(asset_value(Rectangle::new(4000.0, 4000.0))) + MeshMaterial2d(asset_value(ColorMaterial::from_color(Color::WHITE))) + -- + Sprite { image: "branding/icon.png" } + Transform::from_xyz(0.0, 0.0, 1.0) + Movable + -- + PointLight2d { color: RED, range: 300.0 } + Transform::from_xyz(-200.0, 100.0, 0.0) + -- + PointLight2d { color: LIME, range: 300.0 } + Orbit { + center: Vec2::new(50.0, -30.0), + radius: Vec2::new(280.0, 160.0), + speed: 0.8, + } + -- + PointLight2d { color: BLUE, range: 300.0 } + Orbit { + center: Vec2::new(150.0, 40.0), + radius: Vec2::new(180.0, 120.0), + speed: -1.3, + } + -- + Text + Node { + position_type: PositionType::Absolute, + top: px(12), + left: px(12), + } + Children [ + TextSpan("Ambient light is on\n") + -- + TextSpan("\n") + -- + TextSpan("Controls\n") + -- + TextSpan("---------------\n") + -- + TextSpan("Arrow keys - Move objects\n") + -- + TextSpan("Space - Toggle ambient light") + ] + -- + Camera2d + } +} + +fn toggle_ambient_light( + key_input: Res>, + mut ambient_light: ResMut, + text: Single>, + mut writer: TextUiWriter, +) { + if key_input.just_pressed(KeyCode::Space) { + if ambient_light.brightness > 0. { + ambient_light.brightness = 0.; + } else { + ambient_light.brightness = 0.1; + } + + let entity = *text; + let ambient_light_state_text: &str = match ambient_light.brightness { + 0. => "off", + _ => "on", + }; + *writer.text(entity, 1) = format!("Ambient light is {ambient_light_state_text}\n"); + } +} + +fn orbit(time: Res