diff --git a/.cargo/config_fast_builds.toml b/.cargo/config_fast_builds.toml index ae5581b058337..372a97d37090c 100644 --- a/.cargo/config_fast_builds.toml +++ b/.cargo/config_fast_builds.toml @@ -144,3 +144,8 @@ rustflags = [ # In most cases the gains are negligible, but if you are on macos and have slow compile times you should see significant gains. # [profile.dev] # debug = 1 + +# This is enables you to run the CI tool using `cargo ci`. +# This is not enabled by default, you need to copy this file to `config.toml`. +[alias] +ci = "run --package ci --" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bc582b9118ce..f7425545af51a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,7 +218,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check for typos - uses: crate-ci/typos@v1.20.10 + uses: crate-ci/typos@v1.21.0 - name: Typos info if: failure() run: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e4002371d665b..49e6b2cd497c5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,6 +27,9 @@ concurrency: jobs: build-and-deploy: runs-on: ubuntu-latest + # Only run this job when on the main Bevy repository. Without this, it would also run on forks + # where developers work on the main branch but have not enabled Github Pages. + if: ${{ github.repository == 'bevyengine/bevy' }} environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c52189e663ecd..4efe748bcfa1c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,4 +53,3 @@ jobs: title: "Preparing Next Release" body: | Preparing next release. This PR has been auto-generated. - UI tests have not been automatically bumped to the latest version, please fix them manually. diff --git a/.gitignore b/.gitignore index c97fdeedaf45b..db8ddeb9d0279 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ crates/bevy_asset/imported_assets imported_assets example_showcase_config.ron +example-showcase-reports/ diff --git a/Cargo.toml b/Cargo.toml index 245e996a63e60..c219ccdd41d30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,10 @@ rust-version = "1.77.0" [workspace] exclude = [ "benches", - "crates/bevy_ecs_compile_fail_tests", - "crates/bevy_macros_compile_fail_tests", - "crates/bevy_reflect_compile_fail_tests", - "crates/bevy_compile_test_utils", + "crates/bevy_derive/compile_fail", + "crates/bevy_ecs/compile_fail", + "crates/bevy_reflect/compile_fail", + "tools/compile_fail_utils", ] members = [ "crates/*", @@ -55,6 +55,7 @@ workspace = true default = [ "animation", "bevy_asset", + "bevy_state", "bevy_audio", "bevy_color", "bevy_gilrs", @@ -67,7 +68,7 @@ default = [ "bevy_sprite", "bevy_text", "bevy_ui", - "multi-threaded", + "multi_threaded", "png", "hdr", "vorbis", @@ -252,7 +253,7 @@ symphonia-wav = ["bevy_internal/symphonia-wav"] serialize = ["bevy_internal/serialize"] # Enables multithreaded parallelism in the engine. Disabling it forces all engine tasks to run on a single thread. -multi-threaded = ["bevy_internal/multi-threaded"] +multi_threaded = ["bevy_internal/multi_threaded"] # Use async-io's implementation of block_on instead of futures-lite's implementation. This is preferred if your application uses async-io. async-io = ["bevy_internal/async-io"] @@ -302,6 +303,11 @@ shader_format_spirv = ["bevy_internal/shader_format_spirv"] # Enable support for transmission-related textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs pbr_transmission_textures = ["bevy_internal/pbr_transmission_textures"] +# Enable support for multi-layer material textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs +pbr_multi_layer_material_textures = [ + "bevy_internal/pbr_multi_layer_material_textures", +] + # Enable some limitations to be able to use WebGL2. Please refer to the [WebGL2 and WebGPU](https://github.com/bevyengine/bevy/tree/latest/examples#webgl2-and-webgpu) section of the examples README for more information on how to run Wasm builds with WebGPU. webgl2 = ["bevy_internal/webgl"] @@ -329,6 +335,9 @@ meshlet_processor = ["bevy_internal/meshlet_processor"] # Enable support for the ios_simulator by downgrading some rendering capabilities ios_simulator = ["bevy_internal/ios_simulator"] +# Enable built in global state machines +bevy_state = ["bevy_internal/bevy_state"] + [dependencies] bevy_internal = { path = "crates/bevy_internal", version = "0.14.0-dev", default-features = false } @@ -678,6 +687,17 @@ description = "A scene showcasing the distance fog effect" category = "3D Rendering" wasm = true +[[example]] +name = "auto_exposure" +path = "examples/3d/auto_exposure.rs" +doc-scrape-examples = true + +[package.metadata.example.auto_exposure] +name = "Auto Exposure" +description = "A scene showcasing auto exposure" +category = "3D Rendering" +wasm = false + [[example]] name = "blend_modes" path = "examples/3d/blend_modes.rs" @@ -1258,6 +1278,17 @@ description = "An application that runs with default plugins and displays an emp category = "Application" wasm = false +[[example]] +name = "headless_renderer" +path = "examples/app/headless_renderer.rs" +doc-scrape-examples = true + +[package.metadata.example.headless_renderer] +name = "Headless Renderer" +description = "An application that runs with no window, but renders into image file" +category = "Application" +wasm = false + [[example]] name = "without_winit" path = "examples/app/without_winit.rs" @@ -1371,6 +1402,17 @@ description = "Demonstrates how to process and load custom assets" category = "Assets" wasm = false +[[example]] +name = "repeated_texture" +path = "examples/asset/repeated_texture.rs" +doc-scrape-examples = true + +[package.metadata.example.repeated_texture] +name = "Repeated texture configuration" +description = "How to configure the texture to repeat instead of the default clamp to edges" +category = "Assets" +wasm = true + # Async Tasks [[example]] name = "async_compute" @@ -1691,35 +1733,35 @@ wasm = false [[example]] name = "state" -path = "examples/ecs/state.rs" +path = "examples/state/state.rs" doc-scrape-examples = true [package.metadata.example.state] name = "State" description = "Illustrates how to use States to control transitioning from a Menu state to an InGame state" -category = "ECS (Entity Component System)" +category = "State" wasm = false [[example]] name = "sub_states" -path = "examples/ecs/sub_states.rs" +path = "examples/state/sub_states.rs" doc-scrape-examples = true [package.metadata.example.sub_states] name = "Sub States" description = "Using Sub States for hierarchical state handling." -category = "ECS (Entity Component System)" +category = "State" wasm = false [[example]] name = "computed_states" -path = "examples/ecs/computed_states.rs" +path = "examples/state/computed_states.rs" doc-scrape-examples = true [package.metadata.example.computed_states] name = "Computed States" description = "Advanced state patterns using Computed States" -category = "ECS (Entity Component System)" +category = "State" wasm = false [[example]] @@ -2025,6 +2067,17 @@ description = "Demonstrates how reflection in Bevy provides a way to dynamically category = "Reflection" wasm = false +[[example]] +name = "dynamic_types" +path = "examples/reflection/dynamic_types.rs" +doc-scrape-examples = true + +[package.metadata.example.dynamic_types] +name = "Dynamic Types" +description = "How dynamic types are used with reflection" +category = "Reflection" +wasm = false + [[example]] name = "generic_reflection" path = "examples/reflection/generic_reflection.rs" @@ -2961,6 +3014,40 @@ description = "Demonstrates color grading" category = "3D Rendering" wasm = true +[[example]] +name = "clearcoat" +path = "examples/3d/clearcoat.rs" +doc-scrape-examples = true +required-features = ["pbr_multi_layer_material_textures"] + +[package.metadata.example.clearcoat] +name = "Clearcoat" +description = "Demonstrates the clearcoat PBR feature" +category = "3D Rendering" +wasm = false + +[[example]] +name = "depth_of_field" +path = "examples/3d/depth_of_field.rs" +doc-scrape-examples = true + +[package.metadata.example.depth_of_field] +name = "Depth of field" +description = "Demonstrates depth of field" +category = "3D Rendering" +wasm = false + +[[example]] +name = "volumetric_fog" +path = "examples/3d/volumetric_fog.rs" +doc-scrape-examples = true + +[package.metadata.example.volumetric_fog] +name = "Volumetric fog" +description = "Demonstrates volumetric fog and lighting" +category = "3D Rendering" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/assets/models/DepthOfFieldExample/CircuitBoardLightmap.hdr b/assets/models/DepthOfFieldExample/CircuitBoardLightmap.hdr new file mode 100644 index 0000000000000..b63becf3f7f70 Binary files /dev/null and b/assets/models/DepthOfFieldExample/CircuitBoardLightmap.hdr differ diff --git a/assets/models/DepthOfFieldExample/DepthOfFieldExample.glb b/assets/models/DepthOfFieldExample/DepthOfFieldExample.glb new file mode 100644 index 0000000000000..e518089aa7c59 Binary files /dev/null and b/assets/models/DepthOfFieldExample/DepthOfFieldExample.glb differ diff --git a/assets/models/GolfBall/GolfBall.glb b/assets/models/GolfBall/GolfBall.glb new file mode 100644 index 0000000000000..bd2f5e8d982e2 Binary files /dev/null and b/assets/models/GolfBall/GolfBall.glb differ diff --git a/assets/models/VolumetricFogExample/VolumetricFogExample.glb b/assets/models/VolumetricFogExample/VolumetricFogExample.glb new file mode 100644 index 0000000000000..0f6179bba1d99 Binary files /dev/null and b/assets/models/VolumetricFogExample/VolumetricFogExample.glb differ diff --git a/assets/shaders/array_texture.wgsl b/assets/shaders/array_texture.wgsl index 7c0216f73e592..3fa77933b2b14 100644 --- a/assets/shaders/array_texture.wgsl +++ b/assets/shaders/array_texture.wgsl @@ -3,6 +3,7 @@ mesh_view_bindings::view, pbr_types::{STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT, PbrInput, pbr_input_new}, pbr_functions as fns, + pbr_bindings, } #import bevy_core_pipeline::tonemapping::tone_mapping @@ -37,19 +38,21 @@ fn fragment( pbr_input.is_orthographic = view.projection[3].w == 1.0; + pbr_input.N = normalize(pbr_input.world_normal); + +#ifdef VERTEX_TANGENTS + let Nt = textureSampleBias(pbr_bindings::normal_map_texture, pbr_bindings::normal_map_sampler, mesh.uv, view.mip_bias).rgb; pbr_input.N = fns::apply_normal_mapping( pbr_input.material.flags, mesh.world_normal, double_sided, is_front, -#ifdef VERTEX_TANGENTS -#ifdef STANDARD_MATERIAL_NORMAL_MAP mesh.world_tangent, -#endif -#endif - mesh.uv, + Nt, view.mip_bias, ); +#endif + pbr_input.V = fns::calculate_view(mesh.world_position, pbr_input.is_orthographic); return tone_mapping(fns::apply_pbr_lighting(pbr_input), view.color_grading); diff --git a/assets/shaders/tonemapping_test_patterns.wgsl b/assets/shaders/tonemapping_test_patterns.wgsl index 891a66f3a1f45..7fe88bf5485b3 100644 --- a/assets/shaders/tonemapping_test_patterns.wgsl +++ b/assets/shaders/tonemapping_test_patterns.wgsl @@ -1,9 +1,10 @@ #import bevy_pbr::{ mesh_view_bindings, forward_io::VertexOutput, - utils::PI, } +#import bevy_render::maths::PI + #ifdef TONEMAP_IN_SHADER #import bevy_core_pipeline::tonemapping::tone_mapping #endif diff --git a/assets/textures/BlueNoise-Normal.png b/assets/textures/BlueNoise-Normal.png new file mode 100644 index 0000000000000..b6e5a996f8df7 Binary files /dev/null and b/assets/textures/BlueNoise-Normal.png differ diff --git a/assets/textures/ScratchedGold-Normal.png b/assets/textures/ScratchedGold-Normal.png new file mode 100644 index 0000000000000..9bb80c0f08b18 Binary files /dev/null and b/assets/textures/ScratchedGold-Normal.png differ diff --git a/assets/textures/basic_metering_mask.png b/assets/textures/basic_metering_mask.png new file mode 100644 index 0000000000000..16c1051c958a9 Binary files /dev/null and b/assets/textures/basic_metering_mask.png differ diff --git a/assets/textures/fantasy_ui_borders/panel-border-010-repeated.png b/assets/textures/fantasy_ui_borders/panel-border-010-repeated.png new file mode 100644 index 0000000000000..afa859521f6fa Binary files /dev/null and b/assets/textures/fantasy_ui_borders/panel-border-010-repeated.png differ diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 417b3a225d243..3df074a75c045 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -11,7 +11,7 @@ rand = "0.8" rand_chacha = "0.3" criterion = { version = "0.3", features = ["html_reports"] } bevy_app = { path = "../crates/bevy_app" } -bevy_ecs = { path = "../crates/bevy_ecs", features = ["multi-threaded"] } +bevy_ecs = { path = "../crates/bevy_ecs", features = ["multi_threaded"] } bevy_reflect = { path = "../crates/bevy_reflect" } bevy_tasks = { path = "../crates/bevy_tasks" } bevy_utils = { path = "../crates/bevy_utils" } diff --git a/benches/benches/bevy_render/render_layers.rs b/benches/benches/bevy_render/render_layers.rs new file mode 100644 index 0000000000000..84f6b8907754c --- /dev/null +++ b/benches/benches/bevy_render/render_layers.rs @@ -0,0 +1,19 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +use bevy_render::view::RenderLayers; + +fn render_layers(c: &mut Criterion) { + c.bench_function("layers_intersect", |b| { + let layer_a = RenderLayers::layer(1).with(2); + let layer_b = RenderLayers::layer(1); + b.iter(|| { + black_box(layer_a.intersects(&layer_b)) + }); + }); +} + +criterion_group!( + benches, + render_layers, +); +criterion_main!(benches); diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index 666485a305d30..4e59ccc8b2875 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -1,5 +1,5 @@ use crate::util; -use bevy_color::{ClampColor, Laba, LinearRgba, Oklaba, Srgba, Xyza}; +use bevy_color::{Laba, LinearRgba, Oklaba, Srgba, Xyza}; use bevy_ecs::world::World; use bevy_math::*; use bevy_reflect::Reflect; @@ -63,7 +63,7 @@ macro_rules! impl_color_animatable { #[inline] fn interpolate(a: &Self, b: &Self, t: f32) -> Self { let value = *a * (1. - t) + *b * t; - value.clamped() + value } #[inline] @@ -76,7 +76,7 @@ macro_rules! impl_color_animatable { value = Self::interpolate(&value, &input.value, input.weight); } } - value.clamped() + value } } }; diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index 64333bfc9846c..1b4cfdb791cd9 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -11,9 +11,10 @@ keywords = ["bevy"] [features] trace = [] bevy_debug_stepping = [] -default = ["bevy_reflect"] +default = ["bevy_reflect", "bevy_state"] bevy_reflect = ["dep:bevy_reflect", "bevy_ecs/bevy_reflect"] serialize = ["bevy_ecs/serde"] +bevy_state = ["dep:bevy_state"] [dependencies] # bevy @@ -22,6 +23,7 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev", default-features = fa bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", optional = true } bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" } +bevy_state = { path = "../bevy_state", optional = true, version = "0.14.0-dev" } # other serde = { version = "1.0", features = ["derive"], optional = true } diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 80fb05f2b9ac2..176c5caf60a56 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -7,9 +7,11 @@ use bevy_ecs::{ event::{event_update_system, ManualEventReader}, intern::Interned, prelude::*, - schedule::{FreelyMutableState, ScheduleBuildSettings, ScheduleLabel}, + schedule::{ScheduleBuildSettings, ScheduleLabel}, system::SystemId, }; +#[cfg(feature = "bevy_state")] +use bevy_state::{prelude::*, state::FreelyMutableState}; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; use bevy_utils::{tracing::debug, HashMap}; @@ -264,6 +266,7 @@ impl App { self.sub_apps.iter().any(|s| s.is_building_plugins()) } + #[cfg(feature = "bevy_state")] /// Initializes a [`State`] with standard starting values. /// /// This method is idempotent: it has no effect when called again using the same generic type. @@ -281,6 +284,7 @@ impl App { self } + #[cfg(feature = "bevy_state")] /// Inserts a specific [`State`] to the current [`App`] and overrides any [`State`] previously /// added of the same type. /// @@ -297,23 +301,19 @@ impl App { self } + #[cfg(feature = "bevy_state")] /// Sets up a type implementing [`ComputedStates`]. /// /// This method is idempotent: it has no effect when called again using the same generic type. - /// - /// For each source state the derived state depends on, it adds this state's derivation - /// to it's [`ComputeDependantStates`](bevy_ecs::schedule::ComputeDependantStates) schedule. pub fn add_computed_state(&mut self) -> &mut Self { self.main_mut().add_computed_state::(); self } + #[cfg(feature = "bevy_state")] /// Sets up a type implementing [`SubStates`]. /// /// This method is idempotent: it has no effect when called again using the same generic type. - /// - /// For each source state the derived state depends on, it adds this state's existence check - /// to it's [`ComputeDependantStates`](bevy_ecs::schedule::ComputeDependantStates) schedule. pub fn add_sub_state(&mut self) -> &mut Self { self.main_mut().add_sub_state::(); self @@ -983,10 +983,7 @@ impl Termination for AppExit { mod tests { use std::{marker::PhantomData, mem}; - use bevy_ecs::{ - schedule::{OnEnter, States}, - system::Commands, - }; + use bevy_ecs::{schedule::ScheduleLabel, system::Commands}; use crate::{App, AppExit, Plugin}; @@ -1059,11 +1056,9 @@ mod tests { App::new().add_plugins(PluginRun); } - #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] - enum AppState { - #[default] - MainMenu, - } + #[derive(ScheduleLabel, Hash, Clone, PartialEq, Eq, Debug)] + struct EnterMainMenu; + fn bar(mut commands: Commands) { commands.spawn_empty(); } @@ -1075,20 +1070,9 @@ mod tests { #[test] fn add_systems_should_create_schedule_if_it_does_not_exist() { let mut app = App::new(); - app.init_state::() - .add_systems(OnEnter(AppState::MainMenu), (foo, bar)); - - app.world_mut().run_schedule(OnEnter(AppState::MainMenu)); - assert_eq!(app.world().entities().len(), 2); - } - - #[test] - fn add_systems_should_create_schedule_if_it_does_not_exist2() { - let mut app = App::new(); - app.add_systems(OnEnter(AppState::MainMenu), (foo, bar)) - .init_state::(); + app.add_systems(EnterMainMenu, (foo, bar)); - app.world_mut().run_schedule(OnEnter(AppState::MainMenu)); + app.world_mut().run_schedule(EnterMainMenu); assert_eq!(app.world().entities().len(), 2); } diff --git a/crates/bevy_app/src/main_schedule.rs b/crates/bevy_app/src/main_schedule.rs index 2399cf01c1216..16e5871e41704 100644 --- a/crates/bevy_app/src/main_schedule.rs +++ b/crates/bevy_app/src/main_schedule.rs @@ -1,9 +1,11 @@ use crate::{App, Plugin}; use bevy_ecs::{ - schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel, StateTransition}, + schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel}, system::{Local, Resource}, world::{Mut, World}, }; +#[cfg(feature = "bevy_state")] +use bevy_state::state::StateTransition; /// The schedule that contains the app logic that is evaluated each tick of [`App::update()`]. /// diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index 4b0eab29273ee..b6974cc60f7e0 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -2,12 +2,15 @@ use crate::{App, InternedAppLabel, Plugin, Plugins, PluginsState, Startup}; use bevy_ecs::{ event::EventRegistry, prelude::*, - schedule::{ - setup_state_transitions_in_world, FreelyMutableState, InternedScheduleLabel, - ScheduleBuildSettings, ScheduleLabel, - }, + schedule::{InternedScheduleLabel, ScheduleBuildSettings, ScheduleLabel}, system::SystemId, }; +#[cfg(feature = "bevy_state")] +use bevy_state::{ + prelude::*, + state::{setup_state_transitions_in_world, FreelyMutableState}, +}; + #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; use bevy_utils::{HashMap, HashSet}; @@ -177,15 +180,8 @@ impl SubApp { schedule: impl ScheduleLabel, systems: impl IntoSystemConfigs, ) -> &mut Self { - let label = schedule.intern(); let mut schedules = self.world.resource_mut::(); - if let Some(schedule) = schedules.get_mut(label) { - schedule.add_systems(systems); - } else { - let mut new_schedule = Schedule::new(label); - new_schedule.add_systems(systems); - schedules.insert(new_schedule); - } + schedules.add_systems(schedule, systems); self } @@ -205,15 +201,8 @@ impl SubApp { schedule: impl ScheduleLabel, sets: impl IntoSystemSetConfigs, ) -> &mut Self { - let label = schedule.intern(); let mut schedules = self.world.resource_mut::(); - if let Some(schedule) = schedules.get_mut(label) { - schedule.configure_sets(sets); - } else { - let mut new_schedule = Schedule::new(label); - new_schedule.configure_sets(sets); - schedules.insert(new_schedule); - } + schedules.configure_sets(schedule, sets); self } @@ -304,18 +293,12 @@ impl SubApp { let schedule = schedule.intern(); let mut schedules = self.world.resource_mut::(); - if let Some(schedule) = schedules.get_mut(schedule) { - let schedule: &mut Schedule = schedule; - schedule.ignore_ambiguity(a, b); - } else { - let mut new_schedule = Schedule::new(schedule); - new_schedule.ignore_ambiguity(a, b); - schedules.insert(new_schedule); - } + schedules.ignore_ambiguity(schedule, a, b); self } + #[cfg(feature = "bevy_state")] /// See [`App::init_state`]. pub fn init_state(&mut self) -> &mut Self { if !self.world.contains_resource::>() { @@ -330,6 +313,7 @@ impl SubApp { self } + #[cfg(feature = "bevy_state")] /// See [`App::insert_state`]. pub fn insert_state(&mut self, state: S) -> &mut Self { if !self.world.contains_resource::>() { @@ -345,6 +329,7 @@ impl SubApp { self } + #[cfg(feature = "bevy_state")] /// See [`App::add_computed_state`]. pub fn add_computed_state(&mut self) -> &mut Self { if !self @@ -360,6 +345,7 @@ impl SubApp { self } + #[cfg(feature = "bevy_state")] /// See [`App::add_sub_state`]. pub fn add_sub_state(&mut self) -> &mut Self { if !self diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 6d5e3f5fcd56b..e380be18b287a 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["bevy"] [features] file_watcher = ["notify-debouncer-full", "watch"] embedded_watcher = ["file_watcher"] -multi-threaded = ["bevy_tasks/multi-threaded"] +multi_threaded = ["bevy_tasks/multi_threaded"] asset_processor = [] watch = [] trace = [] @@ -47,7 +47,11 @@ bevy_winit = { path = "../bevy_winit", version = "0.14.0-dev" } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = { version = "0.2" } -web-sys = { version = "0.3", features = ["Request", "Window", "Response"] } +web-sys = { version = "0.3", features = [ + "Window", + "Response", + "WorkerGlobalScope", +] } wasm-bindgen-futures = "0.4" js-sys = "0.3" diff --git a/crates/bevy_asset/src/io/file/mod.rs b/crates/bevy_asset/src/io/file/mod.rs index 19a9ffb0cdae8..3bbc5074fbf2b 100644 --- a/crates/bevy_asset/src/io/file/mod.rs +++ b/crates/bevy_asset/src/io/file/mod.rs @@ -1,9 +1,9 @@ #[cfg(feature = "file_watcher")] mod file_watcher; -#[cfg(feature = "multi-threaded")] +#[cfg(feature = "multi_threaded")] mod file_asset; -#[cfg(not(feature = "multi-threaded"))] +#[cfg(not(feature = "multi_threaded"))] mod sync_file_asset; use bevy_utils::tracing::error; diff --git a/crates/bevy_asset/src/io/source.rs b/crates/bevy_asset/src/io/source.rs index cd42d31f01de1..3a9082542df10 100644 --- a/crates/bevy_asset/src/io/source.rs +++ b/crates/bevy_asset/src/io/source.rs @@ -525,7 +525,7 @@ impl AssetSource { } } -/// A collection of [`AssetSources`]. +/// A collection of [`AssetSource`]s. pub struct AssetSources { sources: HashMap, AssetSource>, default: AssetSource, diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 4b3be75ecd80c..ea8caf003a1dd 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -55,18 +55,17 @@ use bevy_app::{App, Last, Plugin, PreUpdate}; use bevy_ecs::{ reflect::AppTypeRegistry, schedule::{IntoSystemConfigs, IntoSystemSetConfigs, SystemSet}, - system::Resource, world::FromWorld, }; use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath}; use bevy_utils::{tracing::error, HashSet}; use std::{any::TypeId, sync::Arc}; -#[cfg(all(feature = "file_watcher", not(feature = "multi-threaded")))] +#[cfg(all(feature = "file_watcher", not(feature = "multi_threaded")))] compile_error!( "The \"file_watcher\" feature for hot reloading requires the \ - \"multi-threaded\" feature to be functional.\n\ - Consider either disabling the \"file_watcher\" feature or enabling \"multi-threaded\"" + \"multi_threaded\" feature to be functional.\n\ + Consider either disabling the \"file_watcher\" feature or enabling \"multi_threaded\"" ); /// Provides "asset" loading and processing functionality. An [`Asset`] is a "runtime value" that is loaded from an [`AssetSource`], @@ -90,6 +89,8 @@ pub struct AssetPlugin { pub watch_for_changes_override: Option, /// The [`AssetMode`] to use for this server. pub mode: AssetMode, + /// How/If asset meta files should be checked. + pub meta_check: AssetMetaCheck, } #[derive(Debug)] @@ -118,8 +119,7 @@ pub enum AssetMode { /// Configures how / if meta files will be checked. If an asset's meta file is not checked, the default meta for the asset /// will be used. -// TODO: To avoid breaking Bevy 0.12 users in 0.12.1, this is a Resource. In Bevy 0.13 this should be changed to a field on AssetPlugin (if it is still needed). -#[derive(Debug, Default, Clone, Resource)] +#[derive(Debug, Default, Clone)] pub enum AssetMetaCheck { /// Always check if assets have meta files. If the meta does not exist, the default meta will be used. #[default] @@ -137,6 +137,7 @@ impl Default for AssetPlugin { file_path: Self::DEFAULT_UNPROCESSED_FILE_PATH.to_string(), processed_file_path: Self::DEFAULT_PROCESSED_FILE_PATH.to_string(), watch_for_changes_override: None, + meta_check: AssetMetaCheck::default(), } } } @@ -171,16 +172,11 @@ impl Plugin for AssetPlugin { AssetMode::Unprocessed => { let mut builders = app.world_mut().resource_mut::(); let sources = builders.build_sources(watch, false); - let meta_check = app - .world() - .get_resource::() - .cloned() - .unwrap_or_else(AssetMetaCheck::default); app.insert_resource(AssetServer::new_with_meta_check( sources, AssetServerMode::Unprocessed, - meta_check, + self.meta_check.clone(), watch, )); } @@ -659,8 +655,8 @@ mod tests { #[test] fn load_dependencies() { // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded - #[cfg(not(feature = "multi-threaded"))] - panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded"); + #[cfg(not(feature = "multi_threaded"))] + panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded"); let dir = Dir::default(); @@ -980,8 +976,8 @@ mod tests { #[test] fn failure_load_states() { // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded - #[cfg(not(feature = "multi-threaded"))] - panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded"); + #[cfg(not(feature = "multi_threaded"))] + panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded"); let dir = Dir::default(); @@ -1145,8 +1141,8 @@ mod tests { #[test] fn manual_asset_management() { // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded - #[cfg(not(feature = "multi-threaded"))] - panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded"); + #[cfg(not(feature = "multi_threaded"))] + panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded"); let dir = Dir::default(); let dep_path = "dep.cool.ron"; @@ -1246,8 +1242,8 @@ mod tests { #[test] fn load_folder() { // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded - #[cfg(not(feature = "multi-threaded"))] - panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded"); + #[cfg(not(feature = "multi_threaded"))] + panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded"); let dir = Dir::default(); diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index 31452a0f85840..bd33f9bb15796 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -152,9 +152,9 @@ impl AssetProcessor { /// Starts the processor in a background thread. pub fn start(_processor: Res) { - #[cfg(any(target_arch = "wasm32", not(feature = "multi-threaded")))] + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] error!("Cannot run AssetProcessor in single threaded mode (or WASM) yet."); - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] { let processor = _processor.clone(); std::thread::spawn(move || { @@ -171,7 +171,7 @@ impl AssetProcessor { /// * Scan the unprocessed [`AssetReader`] and remove any final processed assets that are invalid or no longer exist. /// * For each asset in the unprocessed [`AssetReader`], kick off a new "process job", which will process the asset /// (if the latest version of the asset has not been processed). - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] pub fn process_assets(&self) { let start_time = std::time::Instant::now(); debug!("Processing Assets"); @@ -322,9 +322,9 @@ impl AssetProcessor { "Folder {} was added. Attempting to re-process", AssetPath::from_path(&path).with_source(source.id()) ); - #[cfg(any(target_arch = "wasm32", not(feature = "multi-threaded")))] + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] error!("AddFolder event cannot be handled in single threaded mode (or WASM) yet."); - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] IoTaskPool::get().scope(|scope| { scope.spawn(async move { self.process_assets_internal(scope, source, path) @@ -439,7 +439,7 @@ impl AssetProcessor { } #[allow(unused)] - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] async fn process_assets_internal<'scope>( &'scope self, scope: &'scope bevy_tasks::Scope<'scope, '_, ()>, diff --git a/crates/bevy_asset/src/processor/process.rs b/crates/bevy_asset/src/processor/process.rs index 442c9451c868d..349c047005586 100644 --- a/crates/bevy_asset/src/processor/process.rs +++ b/crates/bevy_asset/src/processor/process.rs @@ -167,13 +167,14 @@ pub enum ProcessError { ExtensionRequired, } -impl< - Loader: AssetLoader, - T: AssetTransformer, - Saver: AssetSaver, - > Process for LoadTransformAndSave +impl Process for LoadTransformAndSave +where + Loader: AssetLoader, + Transformer: AssetTransformer, + Saver: AssetSaver, { - type Settings = LoadTransformAndSaveSettings; + type Settings = + LoadTransformAndSaveSettings; type OutputLoader = Saver::OutputLoader; async fn process<'a>( @@ -200,7 +201,8 @@ impl< .await .map_err(|err| ProcessError::AssetTransformError(err.into()))?; - let saved_asset = SavedAsset::::from_transformed(&post_transformed_asset); + let saved_asset = + SavedAsset::::from_transformed(&post_transformed_asset); let output_settings = self .saver diff --git a/crates/bevy_color/Cargo.toml b/crates/bevy_color/Cargo.toml index 76d6ce4d312df..a7136770b691a 100644 --- a/crates/bevy_color/Cargo.toml +++ b/crates/bevy_color/Cargo.toml @@ -7,6 +7,7 @@ homepage = "https://bevyengine.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy", "color"] +rust-version = "1.76.0" [dependencies] bevy_math = { path = "../bevy_math", version = "0.14.0-dev" } diff --git a/crates/bevy_color/src/color.rs b/crates/bevy_color/src/color.rs index 25ed54809ec77..4ddc9599a1d3b 100644 --- a/crates/bevy_color/src/color.rs +++ b/crates/bevy_color/src/color.rs @@ -1,5 +1,6 @@ use crate::{ - Alpha, Hsla, Hsva, Hwba, Laba, Lcha, LinearRgba, Oklaba, Oklcha, Srgba, StandardColor, Xyza, + color_difference::EuclideanDistance, Alpha, Hsla, Hsva, Hue, Hwba, Laba, Lcha, LinearRgba, + Luminance, Mix, Oklaba, Oklcha, Srgba, StandardColor, Xyza, }; use bevy_reflect::prelude::*; @@ -11,6 +12,33 @@ use bevy_reflect::prelude::*; ///
#[doc = include_str!("../docs/diagrams/model_graph.svg")] ///
+/// +/// # Operations +/// +/// [`Color`] supports all the standard color operations, such as [mixing](Mix), +/// [luminance](Luminance) and [hue](Hue) adjustment, [clamping](ClampColor), +/// and [diffing](EuclideanDistance). These operations delegate to the concrete color space contained +/// by [`Color`], but will convert to [`Oklch`](Oklcha) for operations which aren't supported in the +/// current space. After performing the operation, if a conversion was required, the result will be +/// converted back into the original color space. +/// +/// ```rust +/// # use bevy_color::{Hue, Color}; +/// let red_hsv = Color::hsv(0., 1., 1.); +/// let red_srgb = Color::srgb(1., 0., 0.); +/// +/// // HSV has a definition of hue, so it will be returned. +/// red_hsv.hue(); +/// +/// // SRGB doesn't have a native definition for hue. +/// // Converts to Oklch and returns that result. +/// red_srgb.hue(); +/// ``` +/// +/// [`Oklch`](Oklcha) has been chosen as the intermediary space in cases where conversion is required +/// due to its perceptual uniformity and broad support for Bevy's color operations. +/// To avoid the cost of repeated conversion, and ensure consistent results where that is desired, +/// first convert this [`Color`] into your desired color space. #[derive(Debug, Clone, Copy, PartialEq, Reflect)] #[reflect(PartialEq, Default)] #[cfg_attr( @@ -621,3 +649,158 @@ impl From for Xyza { } } } + +/// Color space chosen for operations on `Color`. +type ChosenColorSpace = Oklcha; + +impl Luminance for Color { + fn luminance(&self) -> f32 { + match self { + Color::Srgba(x) => x.luminance(), + Color::LinearRgba(x) => x.luminance(), + Color::Hsla(x) => x.luminance(), + Color::Hsva(x) => ChosenColorSpace::from(*x).luminance(), + Color::Hwba(x) => ChosenColorSpace::from(*x).luminance(), + Color::Laba(x) => x.luminance(), + Color::Lcha(x) => x.luminance(), + Color::Oklaba(x) => x.luminance(), + Color::Oklcha(x) => x.luminance(), + Color::Xyza(x) => x.luminance(), + } + } + + fn with_luminance(&self, value: f32) -> Self { + let mut new = *self; + + match &mut new { + Color::Srgba(x) => *x = x.with_luminance(value), + Color::LinearRgba(x) => *x = x.with_luminance(value), + Color::Hsla(x) => *x = x.with_luminance(value), + Color::Hsva(x) => *x = ChosenColorSpace::from(*x).with_luminance(value).into(), + Color::Hwba(x) => *x = ChosenColorSpace::from(*x).with_luminance(value).into(), + Color::Laba(x) => *x = x.with_luminance(value), + Color::Lcha(x) => *x = x.with_luminance(value), + Color::Oklaba(x) => *x = x.with_luminance(value), + Color::Oklcha(x) => *x = x.with_luminance(value), + Color::Xyza(x) => *x = x.with_luminance(value), + } + + new + } + + fn darker(&self, amount: f32) -> Self { + let mut new = *self; + + match &mut new { + Color::Srgba(x) => *x = x.darker(amount), + Color::LinearRgba(x) => *x = x.darker(amount), + Color::Hsla(x) => *x = x.darker(amount), + Color::Hsva(x) => *x = ChosenColorSpace::from(*x).darker(amount).into(), + Color::Hwba(x) => *x = ChosenColorSpace::from(*x).darker(amount).into(), + Color::Laba(x) => *x = x.darker(amount), + Color::Lcha(x) => *x = x.darker(amount), + Color::Oklaba(x) => *x = x.darker(amount), + Color::Oklcha(x) => *x = x.darker(amount), + Color::Xyza(x) => *x = x.darker(amount), + } + + new + } + + fn lighter(&self, amount: f32) -> Self { + let mut new = *self; + + match &mut new { + Color::Srgba(x) => *x = x.lighter(amount), + Color::LinearRgba(x) => *x = x.lighter(amount), + Color::Hsla(x) => *x = x.lighter(amount), + Color::Hsva(x) => *x = ChosenColorSpace::from(*x).lighter(amount).into(), + Color::Hwba(x) => *x = ChosenColorSpace::from(*x).lighter(amount).into(), + Color::Laba(x) => *x = x.lighter(amount), + Color::Lcha(x) => *x = x.lighter(amount), + Color::Oklaba(x) => *x = x.lighter(amount), + Color::Oklcha(x) => *x = x.lighter(amount), + Color::Xyza(x) => *x = x.lighter(amount), + } + + new + } +} + +impl Hue for Color { + fn with_hue(&self, hue: f32) -> Self { + let mut new = *self; + + match &mut new { + Color::Srgba(x) => *x = ChosenColorSpace::from(*x).with_hue(hue).into(), + Color::LinearRgba(x) => *x = ChosenColorSpace::from(*x).with_hue(hue).into(), + Color::Hsla(x) => *x = x.with_hue(hue), + Color::Hsva(x) => *x = x.with_hue(hue), + Color::Hwba(x) => *x = x.with_hue(hue), + Color::Laba(x) => *x = ChosenColorSpace::from(*x).with_hue(hue).into(), + Color::Lcha(x) => *x = x.with_hue(hue), + Color::Oklaba(x) => *x = ChosenColorSpace::from(*x).with_hue(hue).into(), + Color::Oklcha(x) => *x = x.with_hue(hue), + Color::Xyza(x) => *x = ChosenColorSpace::from(*x).with_hue(hue).into(), + } + + new + } + + fn hue(&self) -> f32 { + match self { + Color::Srgba(x) => ChosenColorSpace::from(*x).hue(), + Color::LinearRgba(x) => ChosenColorSpace::from(*x).hue(), + Color::Hsla(x) => x.hue(), + Color::Hsva(x) => x.hue(), + Color::Hwba(x) => x.hue(), + Color::Laba(x) => ChosenColorSpace::from(*x).hue(), + Color::Lcha(x) => x.hue(), + Color::Oklaba(x) => ChosenColorSpace::from(*x).hue(), + Color::Oklcha(x) => x.hue(), + Color::Xyza(x) => ChosenColorSpace::from(*x).hue(), + } + } + + fn set_hue(&mut self, hue: f32) { + *self = self.with_hue(hue); + } +} + +impl Mix for Color { + fn mix(&self, other: &Self, factor: f32) -> Self { + let mut new = *self; + + match &mut new { + Color::Srgba(x) => *x = x.mix(&(*other).into(), factor), + Color::LinearRgba(x) => *x = x.mix(&(*other).into(), factor), + Color::Hsla(x) => *x = x.mix(&(*other).into(), factor), + Color::Hsva(x) => *x = x.mix(&(*other).into(), factor), + Color::Hwba(x) => *x = x.mix(&(*other).into(), factor), + Color::Laba(x) => *x = x.mix(&(*other).into(), factor), + Color::Lcha(x) => *x = x.mix(&(*other).into(), factor), + Color::Oklaba(x) => *x = x.mix(&(*other).into(), factor), + Color::Oklcha(x) => *x = x.mix(&(*other).into(), factor), + Color::Xyza(x) => *x = x.mix(&(*other).into(), factor), + } + + new + } +} + +impl EuclideanDistance for Color { + fn distance_squared(&self, other: &Self) -> f32 { + match self { + Color::Srgba(x) => x.distance_squared(&(*other).into()), + Color::LinearRgba(x) => x.distance_squared(&(*other).into()), + Color::Hsla(x) => ChosenColorSpace::from(*x).distance_squared(&(*other).into()), + Color::Hsva(x) => ChosenColorSpace::from(*x).distance_squared(&(*other).into()), + Color::Hwba(x) => ChosenColorSpace::from(*x).distance_squared(&(*other).into()), + Color::Laba(x) => ChosenColorSpace::from(*x).distance_squared(&(*other).into()), + Color::Lcha(x) => ChosenColorSpace::from(*x).distance_squared(&(*other).into()), + Color::Oklaba(x) => x.distance_squared(&(*other).into()), + Color::Oklcha(x) => x.distance_squared(&(*other).into()), + Color::Xyza(x) => ChosenColorSpace::from(*x).distance_squared(&(*other).into()), + } + } +} diff --git a/crates/bevy_color/src/color_ops.rs b/crates/bevy_color/src/color_ops.rs index 1f39f3254c489..e37592bdd4dba 100644 --- a/crates/bevy_color/src/color_ops.rs +++ b/crates/bevy_color/src/color_ops.rs @@ -1,3 +1,5 @@ +use bevy_math::{Vec3, Vec4}; + /// Methods for changing the luminance of a color. Note that these methods are not /// guaranteed to produce consistent results across color spaces, /// but will be within a given space. @@ -80,21 +82,24 @@ pub trait Hue: Sized { } } -/// Trait with methods for asserting a colorspace is within bounds. -/// -/// During ordinary usage (e.g. reading images from disk, rendering images, picking colors for UI), colors should always be within their ordinary bounds (such as 0 to 1 for RGB colors). -/// However, some applications, such as high dynamic range rendering or bloom rely on unbounded colors to naturally represent a wider array of choices. -pub trait ClampColor: Sized { - /// Return a new version of this color clamped, with all fields in bounds. - fn clamped(&self) -> Self; - - /// Changes all the fields of this color to ensure they are within bounds. - fn clamp(&mut self) { - *self = self.clamped(); - } - - /// Are all the fields of this color in bounds? - fn is_within_bounds(&self) -> bool; +/// Trait with methods for converting colors to non-color types +pub trait ColorToComponents { + /// Convert to an f32 array + fn to_f32_array(self) -> [f32; 4]; + /// Convert to an f32 array without the alpha value + fn to_f32_array_no_alpha(self) -> [f32; 3]; + /// Convert to a Vec4 + fn to_vec4(self) -> Vec4; + /// Convert to a Vec3 + fn to_vec3(self) -> Vec3; + /// Convert from an f32 array + fn from_f32_array(color: [f32; 4]) -> Self; + /// Convert from an f32 array without the alpha value + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self; + /// Convert from a Vec4 + fn from_vec4(color: Vec4) -> Self; + /// Convert from a Vec3 + fn from_vec3(color: Vec3) -> Self; } /// Utility function for interpolating hue values. This ensures that the interpolation diff --git a/crates/bevy_color/src/hsla.rs b/crates/bevy_color/src/hsla.rs index 3c225b369ddab..66aface9b4408 100644 --- a/crates/bevy_color/src/hsla.rs +++ b/crates/bevy_color/src/hsla.rs @@ -1,7 +1,8 @@ use crate::{ - Alpha, ClampColor, Hsva, Hue, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, - Xyza, + Alpha, ColorToComponents, Hsva, Hue, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, + StandardColor, Xyza, }; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in Hue-Saturation-Lightness (HSL) color space with alpha. @@ -177,21 +178,57 @@ impl Luminance for Hsla { } } -impl ClampColor for Hsla { - fn clamped(&self) -> Self { +impl ColorToComponents for Hsla { + fn to_f32_array(self) -> [f32; 4] { + [self.hue, self.saturation, self.lightness, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.hue, self.saturation, self.lightness] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.hue, self.saturation, self.lightness, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.hue, self.saturation, self.lightness) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + hue: color[0], + saturation: color[1], + lightness: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { Self { - hue: self.hue.rem_euclid(360.), - saturation: self.saturation.clamp(0., 1.), - lightness: self.lightness.clamp(0., 1.), - alpha: self.alpha.clamp(0., 1.), + hue: color[0], + saturation: color[1], + lightness: color[2], + alpha: 1.0, } } - fn is_within_bounds(&self) -> bool { - (0. ..=360.).contains(&self.hue) - && (0. ..=1.).contains(&self.saturation) - && (0. ..=1.).contains(&self.lightness) - && (0. ..=1.).contains(&self.alpha) + fn from_vec4(color: Vec4) -> Self { + Self { + hue: color[0], + saturation: color[1], + lightness: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + hue: color[0], + saturation: color[1], + lightness: color[2], + alpha: 1.0, + } } } @@ -385,21 +422,4 @@ mod tests { assert_approx_eq!(color.hue, reference.hue, 0.001); } } - - #[test] - fn test_clamp() { - let color_1 = Hsla::hsl(361., 2., -1.); - let color_2 = Hsla::hsl(250.2762, 1., 0.67); - let mut color_3 = Hsla::hsl(-50., 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Hsla::hsl(1., 1., 0.)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Hsla::hsl(310., 1., 1.)); - } } diff --git a/crates/bevy_color/src/hsva.rs b/crates/bevy_color/src/hsva.rs index f58cdefac5659..a66ca4c43bc91 100644 --- a/crates/bevy_color/src/hsva.rs +++ b/crates/bevy_color/src/hsva.rs @@ -1,4 +1,7 @@ -use crate::{Alpha, ClampColor, Hue, Hwba, Lcha, LinearRgba, Mix, Srgba, StandardColor, Xyza}; +use crate::{ + Alpha, ColorToComponents, Hue, Hwba, Lcha, LinearRgba, Mix, Srgba, StandardColor, Xyza, +}; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in Hue-Saturation-Value (HSV) color space with alpha. @@ -120,24 +123,6 @@ impl Hue for Hsva { } } -impl ClampColor for Hsva { - fn clamped(&self) -> Self { - Self { - hue: self.hue.rem_euclid(360.), - saturation: self.saturation.clamp(0., 1.), - value: self.value.clamp(0., 1.), - alpha: self.alpha.clamp(0., 1.), - } - } - - fn is_within_bounds(&self) -> bool { - (0. ..=360.).contains(&self.hue) - && (0. ..=1.).contains(&self.saturation) - && (0. ..=1.).contains(&self.value) - && (0. ..=1.).contains(&self.alpha) - } -} - impl From for Hwba { fn from( Hsva { @@ -172,6 +157,60 @@ impl From for Hsva { } } +impl ColorToComponents for Hsva { + fn to_f32_array(self) -> [f32; 4] { + [self.hue, self.saturation, self.value, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.hue, self.saturation, self.value] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.hue, self.saturation, self.value, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.hue, self.saturation, self.value) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + hue: color[0], + saturation: color[1], + value: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + hue: color[0], + saturation: color[1], + value: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { + Self { + hue: color[0], + saturation: color[1], + value: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + hue: color[0], + saturation: color[1], + value: color[2], + alpha: 1.0, + } + } +} + // Derived Conversions impl From for Hsva { @@ -258,21 +297,4 @@ mod tests { assert_approx_eq!(color.hsv.alpha, hsv2.alpha, 0.001); } } - - #[test] - fn test_clamp() { - let color_1 = Hsva::hsv(361., 2., -1.); - let color_2 = Hsva::hsv(250.2762, 1., 0.67); - let mut color_3 = Hsva::hsv(-50., 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Hsva::hsv(1., 1., 0.)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Hsva::hsv(310., 1., 1.)); - } } diff --git a/crates/bevy_color/src/hwba.rs b/crates/bevy_color/src/hwba.rs index f5e7cf3a93c38..0f6f9a6b568b1 100644 --- a/crates/bevy_color/src/hwba.rs +++ b/crates/bevy_color/src/hwba.rs @@ -2,7 +2,8 @@ //! in [_HWB - A More Intuitive Hue-Based Color Model_] by _Smith et al_. //! //! [_HWB - A More Intuitive Hue-Based Color Model_]: https://web.archive.org/web/20240226005220/http://alvyray.com/Papers/CG/HWB_JGTv208.pdf -use crate::{Alpha, ClampColor, Hue, Lcha, LinearRgba, Mix, Srgba, StandardColor, Xyza}; +use crate::{Alpha, ColorToComponents, Hue, Lcha, LinearRgba, Mix, Srgba, StandardColor, Xyza}; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in Hue-Whiteness-Blackness (HWB) color space with alpha. @@ -124,21 +125,57 @@ impl Hue for Hwba { } } -impl ClampColor for Hwba { - fn clamped(&self) -> Self { +impl ColorToComponents for Hwba { + fn to_f32_array(self) -> [f32; 4] { + [self.hue, self.whiteness, self.blackness, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.hue, self.whiteness, self.blackness] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.hue, self.whiteness, self.blackness, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.hue, self.whiteness, self.blackness) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + hue: color[0], + whiteness: color[1], + blackness: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { Self { - hue: self.hue.rem_euclid(360.), - whiteness: self.whiteness.clamp(0., 1.), - blackness: self.blackness.clamp(0., 1.), - alpha: self.alpha.clamp(0., 1.), + hue: color[0], + whiteness: color[1], + blackness: color[2], + alpha: 1.0, } } - fn is_within_bounds(&self) -> bool { - (0. ..=360.).contains(&self.hue) - && (0. ..=1.).contains(&self.whiteness) - && (0. ..=1.).contains(&self.blackness) - && (0. ..=1.).contains(&self.alpha) + fn from_vec4(color: Vec4) -> Self { + Self { + hue: color[0], + whiteness: color[1], + blackness: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + hue: color[0], + whiteness: color[1], + blackness: color[2], + alpha: 1.0, + } } } @@ -291,21 +328,4 @@ mod tests { assert_approx_eq!(color.hwb.alpha, hwb2.alpha, 0.001); } } - - #[test] - fn test_clamp() { - let color_1 = Hwba::hwb(361., 2., -1.); - let color_2 = Hwba::hwb(250.2762, 1., 0.67); - let mut color_3 = Hwba::hwb(-50., 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Hwba::hwb(1., 1., 0.)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Hwba::hwb(310., 1., 1.)); - } } diff --git a/crates/bevy_color/src/laba.rs b/crates/bevy_color/src/laba.rs index 2ce9b55b72393..8e35336335370 100644 --- a/crates/bevy_color/src/laba.rs +++ b/crates/bevy_color/src/laba.rs @@ -1,7 +1,8 @@ use crate::{ - impl_componentwise_vector_space, Alpha, ClampColor, Hsla, Hsva, Hwba, LinearRgba, Luminance, - Mix, Oklaba, Srgba, StandardColor, Xyza, + impl_componentwise_vector_space, Alpha, ColorToComponents, Hsla, Hsva, Hwba, LinearRgba, + Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, }; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in LAB color space, with alpha @@ -117,24 +118,6 @@ impl Alpha for Laba { } } -impl ClampColor for Laba { - fn clamped(&self) -> Self { - Self { - lightness: self.lightness.clamp(0., 1.5), - a: self.a.clamp(-1.5, 1.5), - b: self.b.clamp(-1.5, 1.5), - alpha: self.alpha.clamp(0., 1.), - } - } - - fn is_within_bounds(&self) -> bool { - (0. ..=1.5).contains(&self.lightness) - && (-1.5..=1.5).contains(&self.a) - && (-1.5..=1.5).contains(&self.b) - && (0. ..=1.).contains(&self.alpha) - } -} - impl Luminance for Laba { #[inline] fn with_luminance(&self, lightness: f32) -> Self { @@ -164,6 +147,60 @@ impl Luminance for Laba { } } +impl ColorToComponents for Laba { + fn to_f32_array(self) -> [f32; 4] { + [self.lightness, self.a, self.b, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.lightness, self.a, self.b] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.lightness, self.a, self.b, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.lightness, self.a, self.b) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + lightness: color[0], + a: color[1], + b: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + lightness: color[0], + a: color[1], + b: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { + Self { + lightness: color[0], + a: color[1], + b: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + lightness: color[0], + a: color[1], + b: color[2], + alpha: 1.0, + } + } +} + impl From for Xyza { fn from( Laba { @@ -377,21 +414,4 @@ mod tests { assert_approx_eq!(color.lab.alpha, laba.alpha, 0.001); } } - - #[test] - fn test_clamp() { - let color_1 = Laba::lab(-1., 2., -2.); - let color_2 = Laba::lab(1., 1.5, -1.2); - let mut color_3 = Laba::lab(-0.4, 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Laba::lab(0., 1.5, -1.5)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Laba::lab(0., 1., 1.)); - } } diff --git a/crates/bevy_color/src/lcha.rs b/crates/bevy_color/src/lcha.rs index b33236d6f10ed..4058ef4d4be63 100644 --- a/crates/bevy_color/src/lcha.rs +++ b/crates/bevy_color/src/lcha.rs @@ -1,4 +1,7 @@ -use crate::{Alpha, ClampColor, Hue, Laba, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza}; +use crate::{ + Alpha, ColorToComponents, Hue, Laba, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza, +}; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in LCH color space, with alpha @@ -182,21 +185,57 @@ impl Luminance for Lcha { } } -impl ClampColor for Lcha { - fn clamped(&self) -> Self { +impl ColorToComponents for Lcha { + fn to_f32_array(self) -> [f32; 4] { + [self.lightness, self.chroma, self.hue, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.lightness, self.chroma, self.hue] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.lightness, self.chroma, self.hue, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.lightness, self.chroma, self.hue) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { Self { - lightness: self.lightness.clamp(0., 1.5), - chroma: self.chroma.clamp(0., 1.5), - hue: self.hue.rem_euclid(360.), - alpha: self.alpha.clamp(0., 1.), + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: color[3], } } - fn is_within_bounds(&self) -> bool { - (0. ..=1.5).contains(&self.lightness) - && (0. ..=1.5).contains(&self.chroma) - && (0. ..=360.).contains(&self.hue) - && (0. ..=1.).contains(&self.alpha) + fn from_vec3(color: Vec3) -> Self { + Self { + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: 1.0, + } } } @@ -346,21 +385,4 @@ mod tests { assert_approx_eq!(color.lch.alpha, lcha.alpha, 0.001); } } - - #[test] - fn test_clamp() { - let color_1 = Lcha::lch(-1., 2., 400.); - let color_2 = Lcha::lch(1., 1.5, 249.54); - let mut color_3 = Lcha::lch(-0.4, 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Lcha::lch(0., 1.5, 40.)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Lcha::lch(0., 1., 1.)); - } } diff --git a/crates/bevy_color/src/linear_rgba.rs b/crates/bevy_color/src/linear_rgba.rs index 3eb0646ff0979..9306b8927b352 100644 --- a/crates/bevy_color/src/linear_rgba.rs +++ b/crates/bevy_color/src/linear_rgba.rs @@ -1,8 +1,8 @@ use crate::{ - color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ClampColor, + color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ColorToComponents, Luminance, Mix, StandardColor, }; -use bevy_math::Vec4; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; use bytemuck::{Pod, Zeroable}; @@ -263,33 +263,57 @@ impl EuclideanDistance for LinearRgba { } } -impl ClampColor for LinearRgba { - fn clamped(&self) -> Self { +impl ColorToComponents for LinearRgba { + fn to_f32_array(self) -> [f32; 4] { + [self.red, self.green, self.blue, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.red, self.green, self.blue] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.red, self.green, self.blue, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.red, self.green, self.blue) + } + + fn from_f32_array(color: [f32; 4]) -> Self { Self { - red: self.red.clamp(0., 1.), - green: self.green.clamp(0., 1.), - blue: self.blue.clamp(0., 1.), - alpha: self.alpha.clamp(0., 1.), + red: color[0], + green: color[1], + blue: color[2], + alpha: color[3], } } - fn is_within_bounds(&self) -> bool { - (0. ..=1.).contains(&self.red) - && (0. ..=1.).contains(&self.green) - && (0. ..=1.).contains(&self.blue) - && (0. ..=1.).contains(&self.alpha) + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + red: color[0], + green: color[1], + blue: color[2], + alpha: 1.0, + } } -} -impl From for [f32; 4] { - fn from(color: LinearRgba) -> Self { - [color.red, color.green, color.blue, color.alpha] + fn from_vec4(color: Vec4) -> Self { + Self { + red: color[0], + green: color[1], + blue: color[2], + alpha: color[3], + } } -} -impl From for Vec4 { - fn from(color: LinearRgba) -> Self { - Vec4::new(color.red, color.green, color.blue, color.alpha) + fn from_vec3(color: Vec3) -> Self { + Self { + red: color[0], + green: color[1], + blue: color[2], + alpha: 1.0, + } } } @@ -413,21 +437,4 @@ mod tests { let twice_as_light = color.lighter(0.2); assert!(lighter2.distance_squared(&twice_as_light) < 0.0001); } - - #[test] - fn test_clamp() { - let color_1 = LinearRgba::rgb(2., -1., 0.4); - let color_2 = LinearRgba::rgb(0.031, 0.749, 1.); - let mut color_3 = LinearRgba::rgb(-1., 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), LinearRgba::rgb(1., 0., 0.4)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, LinearRgba::rgb(0., 1., 1.)); - } } diff --git a/crates/bevy_color/src/oklaba.rs b/crates/bevy_color/src/oklaba.rs index 858091a719de6..1f4785d070c02 100644 --- a/crates/bevy_color/src/oklaba.rs +++ b/crates/bevy_color/src/oklaba.rs @@ -1,7 +1,8 @@ use crate::{ - color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ClampColor, Hsla, - Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza, + color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ColorToComponents, + Hsla, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza, }; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in Oklab color space, with alpha @@ -155,21 +156,57 @@ impl EuclideanDistance for Oklaba { } } -impl ClampColor for Oklaba { - fn clamped(&self) -> Self { +impl ColorToComponents for Oklaba { + fn to_f32_array(self) -> [f32; 4] { + [self.lightness, self.a, self.b, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.lightness, self.a, self.b] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.lightness, self.a, self.b, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.lightness, self.a, self.b) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + lightness: color[0], + a: color[1], + b: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + lightness: color[0], + a: color[1], + b: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { Self { - lightness: self.lightness.clamp(0., 1.), - a: self.a.clamp(-1., 1.), - b: self.b.clamp(-1., 1.), - alpha: self.alpha.clamp(0., 1.), + lightness: color[0], + a: color[1], + b: color[2], + alpha: color[3], } } - fn is_within_bounds(&self) -> bool { - (0. ..=1.).contains(&self.lightness) - && (-1. ..=1.).contains(&self.a) - && (-1. ..=1.).contains(&self.b) - && (0. ..=1.).contains(&self.alpha) + fn from_vec3(color: Vec3) -> Self { + Self { + lightness: color[0], + a: color[1], + b: color[2], + alpha: 1.0, + } } } @@ -350,21 +387,4 @@ mod tests { assert_approx_eq!(oklaba.b, oklaba2.b, 0.001); assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001); } - - #[test] - fn test_clamp() { - let color_1 = Oklaba::lab(-1., 2., -2.); - let color_2 = Oklaba::lab(1., 0.42, -0.4); - let mut color_3 = Oklaba::lab(-0.4, 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Oklaba::lab(0., 1., -1.)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Oklaba::lab(0., 1., 1.)); - } } diff --git a/crates/bevy_color/src/oklcha.rs b/crates/bevy_color/src/oklcha.rs index 01b21b6780560..18e6f532887ff 100644 --- a/crates/bevy_color/src/oklcha.rs +++ b/crates/bevy_color/src/oklcha.rs @@ -1,7 +1,8 @@ use crate::{ - color_difference::EuclideanDistance, Alpha, ClampColor, Hsla, Hsva, Hue, Hwba, Laba, Lcha, - LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, + color_difference::EuclideanDistance, Alpha, ColorToComponents, Hsla, Hsva, Hue, Hwba, Laba, + Lcha, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, }; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in Oklch color space, with alpha @@ -190,6 +191,60 @@ impl EuclideanDistance for Oklcha { } } +impl ColorToComponents for Oklcha { + fn to_f32_array(self) -> [f32; 4] { + [self.lightness, self.chroma, self.hue, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.lightness, self.chroma, self.hue] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.lightness, self.chroma, self.hue, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.lightness, self.chroma, self.hue) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { + Self { + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: 1.0, + } + } +} + impl From for Oklcha { fn from( Oklaba { @@ -225,24 +280,6 @@ impl From for Oklaba { } } -impl ClampColor for Oklcha { - fn clamped(&self) -> Self { - Self { - lightness: self.lightness.clamp(0., 1.), - chroma: self.chroma.clamp(0., 1.), - hue: self.hue.rem_euclid(360.), - alpha: self.alpha.clamp(0., 1.), - } - } - - fn is_within_bounds(&self) -> bool { - (0. ..=1.).contains(&self.lightness) - && (0. ..=1.).contains(&self.chroma) - && (0. ..=360.).contains(&self.hue) - && (0. ..=1.).contains(&self.alpha) - } -} - // Derived Conversions impl From for Oklcha { @@ -389,21 +426,4 @@ mod tests { assert_approx_eq!(oklcha.hue, oklcha2.hue, 0.001); assert_approx_eq!(oklcha.alpha, oklcha2.alpha, 0.001); } - - #[test] - fn test_clamp() { - let color_1 = Oklcha::lch(-1., 2., 400.); - let color_2 = Oklcha::lch(1., 1., 249.54); - let mut color_3 = Oklcha::lch(-0.4, 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Oklcha::lch(0., 1., 40.)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Oklcha::lch(0., 1., 1.)); - } } diff --git a/crates/bevy_color/src/srgba.rs b/crates/bevy_color/src/srgba.rs index 43c82fda25d68..0a4411aa6bceb 100644 --- a/crates/bevy_color/src/srgba.rs +++ b/crates/bevy_color/src/srgba.rs @@ -1,9 +1,9 @@ use crate::color_difference::EuclideanDistance; use crate::{ - impl_componentwise_vector_space, Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor, - Xyza, + impl_componentwise_vector_space, Alpha, ColorToComponents, LinearRgba, Luminance, Mix, + StandardColor, Xyza, }; -use bevy_math::Vec4; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; use thiserror::Error; @@ -314,21 +314,57 @@ impl EuclideanDistance for Srgba { } } -impl ClampColor for Srgba { - fn clamped(&self) -> Self { +impl ColorToComponents for Srgba { + fn to_f32_array(self) -> [f32; 4] { + [self.red, self.green, self.blue, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.red, self.green, self.blue] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.red, self.green, self.blue, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.red, self.green, self.blue) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + red: color[0], + green: color[1], + blue: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + red: color[0], + green: color[1], + blue: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { Self { - red: self.red.clamp(0., 1.), - green: self.green.clamp(0., 1.), - blue: self.blue.clamp(0., 1.), - alpha: self.alpha.clamp(0., 1.), + red: color[0], + green: color[1], + blue: color[2], + alpha: color[3], } } - fn is_within_bounds(&self) -> bool { - (0. ..=1.).contains(&self.red) - && (0. ..=1.).contains(&self.green) - && (0. ..=1.).contains(&self.blue) - && (0. ..=1.).contains(&self.alpha) + fn from_vec3(color: Vec3) -> Self { + Self { + red: color[0], + green: color[1], + blue: color[2], + alpha: 1.0, + } } } @@ -356,18 +392,6 @@ impl From for LinearRgba { } } -impl From for [f32; 4] { - fn from(color: Srgba) -> Self { - [color.red, color.green, color.blue, color.alpha] - } -} - -impl From for Vec4 { - fn from(color: Srgba) -> Self { - Vec4::new(color.red, color.green, color.blue, color.alpha) - } -} - // Derived Conversions impl From for Srgba { @@ -473,21 +497,4 @@ mod tests { assert!(matches!(Srgba::hex("yyy"), Err(HexColorError::Parse(_)))); assert!(matches!(Srgba::hex("##fff"), Err(HexColorError::Parse(_)))); } - - #[test] - fn test_clamp() { - let color_1 = Srgba::rgb(2., -1., 0.4); - let color_2 = Srgba::rgb(0.031, 0.749, 1.); - let mut color_3 = Srgba::rgb(-1., 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Srgba::rgb(1., 0., 0.4)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Srgba::rgb(0., 1., 1.)); - } } diff --git a/crates/bevy_color/src/xyza.rs b/crates/bevy_color/src/xyza.rs index d3baf464f472e..6929ca5ca5799 100644 --- a/crates/bevy_color/src/xyza.rs +++ b/crates/bevy_color/src/xyza.rs @@ -1,6 +1,8 @@ use crate::{ - impl_componentwise_vector_space, Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor, + impl_componentwise_vector_space, Alpha, ColorToComponents, LinearRgba, Luminance, Mix, + StandardColor, }; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// [CIE 1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) color space, also known as XYZ, with an alpha channel. @@ -142,21 +144,57 @@ impl Mix for Xyza { } } -impl ClampColor for Xyza { - fn clamped(&self) -> Self { +impl ColorToComponents for Xyza { + fn to_f32_array(self) -> [f32; 4] { + [self.x, self.y, self.z, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.x, self.y, self.z] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.x, self.y, self.z, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.x, self.y, self.z) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + x: color[0], + y: color[1], + z: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + x: color[0], + y: color[1], + z: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { Self { - x: self.x.clamp(0., 1.), - y: self.y.clamp(0., 1.), - z: self.z.clamp(0., 1.), - alpha: self.alpha.clamp(0., 1.), + x: color[0], + y: color[1], + z: color[2], + alpha: color[3], } } - fn is_within_bounds(&self) -> bool { - (0. ..=1.).contains(&self.x) - && (0. ..=1.).contains(&self.y) - && (0. ..=1.).contains(&self.z) - && (0. ..=1.).contains(&self.alpha) + fn from_vec3(color: Vec3) -> Self { + Self { + x: color[0], + y: color[1], + z: color[2], + alpha: 1.0, + } } } @@ -234,21 +272,4 @@ mod tests { assert_approx_eq!(color.xyz.alpha, xyz2.alpha, 0.001); } } - - #[test] - fn test_clamp() { - let color_1 = Xyza::xyz(2., -1., 0.4); - let color_2 = Xyza::xyz(0.031, 0.749, 1.); - let mut color_3 = Xyza::xyz(-1., 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Xyza::xyz(1., 0., 0.4)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Xyza::xyz(0., 1., 1.)); - } } diff --git a/crates/bevy_core/Cargo.toml b/crates/bevy_core/Cargo.toml index d149581ebb93e..0af854725fd77 100644 --- a/crates/bevy_core/Cargo.toml +++ b/crates/bevy_core/Cargo.toml @@ -8,7 +8,6 @@ repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] - [dependencies] # bevy bevy_app = { path = "../bevy_app", version = "0.14.0-dev", features = [ @@ -32,6 +31,7 @@ serialize = ["dep:serde"] [dev-dependencies] crossbeam-channel = "0.5.0" +serde_test = "1.0" [lints] workspace = true diff --git a/crates/bevy_core/src/lib.rs b/crates/bevy_core/src/lib.rs index 073460b3b3796..dc93ddf2c0bef 100644 --- a/crates/bevy_core/src/lib.rs +++ b/crates/bevy_core/src/lib.rs @@ -80,7 +80,7 @@ fn tick_global_task_pools(_main_thread_marker: Option>) { /// [`FrameCount`] will wrap to 0 after exceeding [`u32::MAX`]. Within reasonable /// assumptions, one may exploit wrapping arithmetic to determine the number of frames /// that have elapsed between two observations – see [`u32::wrapping_sub()`]. -#[derive(Debug, Default, Resource, Clone, Copy)] +#[derive(Debug, Default, Resource, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct FrameCount(pub u32); /// Adds frame counting functionality to Apps. diff --git a/crates/bevy_core/src/serde.rs b/crates/bevy_core/src/serde.rs index f8835e9a5f8d2..fc4d81b4bd055 100644 --- a/crates/bevy_core/src/serde.rs +++ b/crates/bevy_core/src/serde.rs @@ -9,6 +9,7 @@ use serde::{ }; use super::name::Name; +use super::FrameCount; impl Serialize for Name { fn serialize(&self, serializer: S) -> Result { @@ -39,3 +40,52 @@ impl<'de> Visitor<'de> for EntityVisitor { Ok(Name::new(v)) } } + +// Manually implementing serialize/deserialize allows us to use a more compact representation as simple integers +impl Serialize for FrameCount { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_u32(self.0) + } +} + +impl<'de> Deserialize<'de> for FrameCount { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_u32(FrameVisitor) + } +} + +struct FrameVisitor; + +impl<'de> Visitor<'de> for FrameVisitor { + type Value = FrameCount; + + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str(any::type_name::()) + } + + fn visit_u32(self, v: u32) -> Result + where + E: Error, + { + Ok(FrameCount(v)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use serde_test::{assert_tokens, Token}; + + #[test] + fn test_serde_name() { + let name = Name::new("MyComponent"); + assert_tokens(&name, &[Token::String("MyComponent")]); + } + + #[test] + fn test_serde_frame_count() { + let frame_count = FrameCount(100); + assert_tokens(&frame_count, &[Token::U32(100)]); + } +} diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index e6846d0ec161d..1b13d9b22878c 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -37,6 +37,8 @@ serde = { version = "1", features = ["derive"] } bitflags = "2.3" radsort = "0.1" nonmax = "0.5" +smallvec = "1" +thiserror = "1.0" [lints] workspace = true diff --git a/crates/bevy_core_pipeline/src/auto_exposure/auto_exposure.wgsl b/crates/bevy_core_pipeline/src/auto_exposure/auto_exposure.wgsl new file mode 100644 index 0000000000000..a4eca41477481 --- /dev/null +++ b/crates/bevy_core_pipeline/src/auto_exposure/auto_exposure.wgsl @@ -0,0 +1,193 @@ +// Auto exposure +// +// This shader computes an auto exposure value for the current frame, +// which is then used as an exposure correction in the tone mapping shader. +// +// The auto exposure value is computed in two passes: +// * The compute_histogram pass calculates a histogram of the luminance values in the scene, +// taking into account the metering mask texture. The metering mask is a grayscale texture +// that defines the areas of the screen that should be given more weight when calculating +// the average luminance value. For example, the middle area of the screen might be more important +// than the edges. +// * The compute_average pass calculates the average luminance value of the scene, taking +// into account the low_percent and high_percent settings. These settings define the +// percentage of the histogram that should be excluded when calculating the average. This +// is useful to avoid overexposure when you have a lot of shadows, or underexposure when you +// have a lot of bright specular reflections. +// +// The final target_exposure is finally used to smoothly adjust the exposure value over time. + +#import bevy_render::view::View +#import bevy_render::globals::Globals + +// Constant to convert RGB to luminance, taken from Real Time Rendering, Vol 4 pg. 278, 4th edition +const RGB_TO_LUM = vec3(0.2125, 0.7154, 0.0721); + +struct AutoExposure { + min_log_lum: f32, + inv_log_lum_range: f32, + log_lum_range: f32, + low_percent: f32, + high_percent: f32, + speed_up: f32, + speed_down: f32, + exponential_transition_distance: f32, +} + +struct CompensationCurve { + min_log_lum: f32, + inv_log_lum_range: f32, + min_compensation: f32, + compensation_range: f32, +} + +@group(0) @binding(0) var globals: Globals; + +@group(0) @binding(1) var settings: AutoExposure; + +@group(0) @binding(2) var tex_color: texture_2d; + +@group(0) @binding(3) var tex_mask: texture_2d; + +@group(0) @binding(4) var tex_compensation: texture_1d; + +@group(0) @binding(5) var compensation_curve: CompensationCurve; + +@group(0) @binding(6) var histogram: array, 64>; + +@group(0) @binding(7) var exposure: f32; + +@group(0) @binding(8) var view: View; + +var histogram_shared: array, 64>; + +// For a given color, return the histogram bin index +fn color_to_bin(hdr: vec3) -> u32 { + // Convert color to luminance + let lum = dot(hdr, RGB_TO_LUM); + + if lum < exp2(settings.min_log_lum) { + return 0u; + } + + // Calculate the log_2 luminance and express it as a value in [0.0, 1.0] + // where 0.0 represents the minimum luminance, and 1.0 represents the max. + let log_lum = saturate((log2(lum) - settings.min_log_lum) * settings.inv_log_lum_range); + + // Map [0, 1] to [1, 63]. The zeroth bin is handled by the epsilon check above. + return u32(log_lum * 62.0 + 1.0); +} + +// Read the metering mask at the given UV coordinates, returning a weight for the histogram. +// +// Since the histogram is summed in the compute_average step, there is a limit to the amount of +// distinct values that can be represented. When using the chosen value of 16, the maximum +// amount of pixels that can be weighted and summed is 2^32 / 16 = 16384^2. +fn metering_weight(coords: vec2) -> u32 { + let pos = vec2(coords * vec2(textureDimensions(tex_mask))); + let mask = textureLoad(tex_mask, pos, 0).r; + return u32(mask * 16.0); +} + +@compute @workgroup_size(16, 16, 1) +fn compute_histogram( + @builtin(global_invocation_id) global_invocation_id: vec3, + @builtin(local_invocation_index) local_invocation_index: u32 +) { + // Clear the workgroup shared histogram + if local_invocation_index < 64 { + histogram_shared[local_invocation_index] = 0u; + } + + // Wait for all workgroup threads to clear the shared histogram + workgroupBarrier(); + + let dim = vec2(textureDimensions(tex_color)); + let uv = vec2(global_invocation_id.xy) / vec2(dim); + + if global_invocation_id.x < dim.x && global_invocation_id.y < dim.y { + let col = textureLoad(tex_color, vec2(global_invocation_id.xy), 0).rgb; + let index = color_to_bin(col); + let weight = metering_weight(uv); + + // Increment the shared histogram bin by the weight obtained from the metering mask + atomicAdd(&histogram_shared[index], weight); + } + + // Wait for all workgroup threads to finish updating the workgroup histogram + workgroupBarrier(); + + // Accumulate the workgroup histogram into the global histogram. + // Note that the global histogram was not cleared at the beginning, + // as it will be cleared in compute_average. + atomicAdd(&histogram[local_invocation_index], histogram_shared[local_invocation_index]); +} + +@compute @workgroup_size(1, 1, 1) +fn compute_average(@builtin(local_invocation_index) local_index: u32) { + var histogram_sum = 0u; + + // Calculate the cumulative histogram and clear the histogram bins. + // Each bin in the cumulative histogram contains the sum of all bins up to that point. + // This way we can quickly exclude the portion of lowest and highest samples as required by + // the low_percent and high_percent settings. + for (var i=0u; i<64u; i+=1u) { + histogram_sum += histogram[i]; + histogram_shared[i] = histogram_sum; + + // Clear the histogram bin for the next frame + histogram[i] = 0u; + } + + let first_index = u32(f32(histogram_sum) * settings.low_percent); + let last_index = u32(f32(histogram_sum) * settings.high_percent); + + var count = 0u; + var sum = 0.0; + for (var i=1u; i<64u; i+=1u) { + // The number of pixels in the bin. The histogram values are clamped to + // first_index and last_index to exclude the lowest and highest samples. + let bin_count = + clamp(histogram_shared[i], first_index, last_index) - + clamp(histogram_shared[i - 1u], first_index, last_index); + + sum += f32(bin_count) * f32(i); + count += bin_count; + } + + var target_exposure = 0.0; + + if count > 0u { + // The average luminance of the included histogram samples. + let avg_lum = sum / (f32(count) * 63.0) + * settings.log_lum_range + + settings.min_log_lum; + + // The position in the compensation curve texture to sample for avg_lum. + let u = (avg_lum - compensation_curve.min_log_lum) * compensation_curve.inv_log_lum_range; + + // The target exposure is the negative of the average log luminance. + // The compensation value is added to the target exposure to adjust the exposure for + // artistic purposes. + target_exposure = textureLoad(tex_compensation, i32(saturate(u) * 255.0), 0).r + * compensation_curve.compensation_range + + compensation_curve.min_compensation + - avg_lum; + } + + // Smoothly adjust the `exposure` towards the `target_exposure` + let delta = target_exposure - exposure; + if target_exposure > exposure { + let speed_down = settings.speed_down * globals.delta_time; + let exp_down = speed_down / settings.exponential_transition_distance; + exposure = exposure + min(speed_down, delta * exp_down); + } else { + let speed_up = settings.speed_up * globals.delta_time; + let exp_up = speed_up / settings.exponential_transition_distance; + exposure = exposure + max(-speed_up, delta * exp_up); + } + + // Apply the exposure to the color grading settings, from where it will be used for the color + // grading pass. + view.color_grading.exposure += exposure; +} diff --git a/crates/bevy_core_pipeline/src/auto_exposure/buffers.rs b/crates/bevy_core_pipeline/src/auto_exposure/buffers.rs new file mode 100644 index 0000000000000..5a6d4330f2b4c --- /dev/null +++ b/crates/bevy_core_pipeline/src/auto_exposure/buffers.rs @@ -0,0 +1,87 @@ +use bevy_ecs::prelude::*; +use bevy_render::{ + render_resource::{StorageBuffer, UniformBuffer}, + renderer::{RenderDevice, RenderQueue}, + Extract, +}; +use bevy_utils::{Entry, HashMap}; + +use super::pipeline::AutoExposureSettingsUniform; +use super::AutoExposureSettings; + +#[derive(Resource, Default)] +pub(super) struct AutoExposureBuffers { + pub(super) buffers: HashMap, +} + +pub(super) struct AutoExposureBuffer { + pub(super) state: StorageBuffer, + pub(super) settings: UniformBuffer, +} + +#[derive(Resource)] +pub(super) struct ExtractedStateBuffers { + changed: Vec<(Entity, AutoExposureSettings)>, + removed: Vec, +} + +pub(super) fn extract_buffers( + mut commands: Commands, + changed: Extract>>, + mut removed: Extract>, +) { + commands.insert_resource(ExtractedStateBuffers { + changed: changed + .iter() + .map(|(entity, settings)| (entity, settings.clone())) + .collect(), + removed: removed.read().collect(), + }); +} + +pub(super) fn prepare_buffers( + device: Res, + queue: Res, + mut extracted: ResMut, + mut buffers: ResMut, +) { + for (entity, settings) in extracted.changed.drain(..) { + let (min_log_lum, max_log_lum) = settings.range.into_inner(); + let (low_percent, high_percent) = settings.filter.into_inner(); + let initial_state = 0.0f32.clamp(min_log_lum, max_log_lum); + + let settings = AutoExposureSettingsUniform { + min_log_lum, + inv_log_lum_range: 1.0 / (max_log_lum - min_log_lum), + log_lum_range: max_log_lum - min_log_lum, + low_percent, + high_percent, + speed_up: settings.speed_brighten, + speed_down: settings.speed_darken, + exponential_transition_distance: settings.exponential_transition_distance, + }; + + match buffers.buffers.entry(entity) { + Entry::Occupied(mut entry) => { + // Update the settings buffer, but skip updating the state buffer. + // The state buffer is skipped so that the animation stays continuous. + let value = entry.get_mut(); + value.settings.set(settings); + value.settings.write_buffer(&device, &queue); + } + Entry::Vacant(entry) => { + let value = entry.insert(AutoExposureBuffer { + state: StorageBuffer::from(initial_state), + settings: UniformBuffer::from(settings), + }); + + value.state.write_buffer(&device, &queue); + value.settings.write_buffer(&device, &queue); + } + } + } + + for entity in extracted.removed.drain(..) { + buffers.buffers.remove(&entity); + } +} diff --git a/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs b/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs new file mode 100644 index 0000000000000..880d13d9177af --- /dev/null +++ b/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs @@ -0,0 +1,226 @@ +use bevy_asset::prelude::*; +use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; +use bevy_math::{cubic_splines::CubicGenerator, FloatExt, Vec2}; +use bevy_reflect::prelude::*; +use bevy_render::{ + render_asset::{RenderAsset, RenderAssetUsages}, + render_resource::{ + Extent3d, ShaderType, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, + TextureView, UniformBuffer, + }, + renderer::{RenderDevice, RenderQueue}, +}; +use thiserror::Error; + +const LUT_SIZE: usize = 256; + +/// An auto exposure compensation curve. +/// This curve is used to map the average log luminance of a scene to an +/// exposure compensation value, to allow for fine control over the final exposure. +#[derive(Asset, Reflect, Debug, Clone)] +#[reflect(Default)] +pub struct AutoExposureCompensationCurve { + /// The minimum log luminance value in the curve. (the x-axis) + min_log_lum: f32, + /// The maximum log luminance value in the curve. (the x-axis) + max_log_lum: f32, + /// The minimum exposure compensation value in the curve. (the y-axis) + min_compensation: f32, + /// The maximum exposure compensation value in the curve. (the y-axis) + max_compensation: f32, + /// The lookup table for the curve. Uploaded to the GPU as a 1D texture. + /// Each value in the LUT is a `u8` representing a normalized exposure compensation value: + /// * `0` maps to `min_compensation` + /// * `255` maps to `max_compensation` + /// The position in the LUT corresponds to the normalized log luminance value. + /// * `0` maps to `min_log_lum` + /// * `LUT_SIZE - 1` maps to `max_log_lum` + lut: [u8; LUT_SIZE], +} + +/// Various errors that can occur when constructing an [`AutoExposureCompensationCurve`]. +#[derive(Error, Debug)] +pub enum AutoExposureCompensationCurveError { + /// A discontinuity was found in the curve. + #[error("discontinuity found between curve segments")] + DiscontinuityFound, + /// The curve is not monotonically increasing on the x-axis. + #[error("curve is not monotonically increasing on the x-axis")] + NotMonotonic, +} + +impl Default for AutoExposureCompensationCurve { + fn default() -> Self { + Self { + min_log_lum: 0.0, + max_log_lum: 0.0, + min_compensation: 0.0, + max_compensation: 0.0, + lut: [0; LUT_SIZE], + } + } +} + +impl AutoExposureCompensationCurve { + const SAMPLES_PER_SEGMENT: usize = 64; + + /// Build an [`AutoExposureCompensationCurve`] from a [`CubicGenerator`], where: + /// - x represents the average log luminance of the scene in EV-100; + /// - y represents the exposure compensation value in F-stops. + /// + /// # Errors + /// + /// If the curve is not monotonically increasing on the x-axis, + /// returns [`AutoExposureCompensationCurveError::NotMonotonic`]. + /// + /// If a discontinuity is found between curve segments, + /// returns [`AutoExposureCompensationCurveError::DiscontinuityFound`]. + /// + /// # Example + /// + /// ``` + /// # use bevy_asset::prelude::*; + /// # use bevy_math::vec2; + /// # use bevy_math::cubic_splines::*; + /// # use bevy_core_pipeline::auto_exposure::AutoExposureCompensationCurve; + /// # let mut compensation_curves = Assets::::default(); + /// let curve: Handle = compensation_curves.add( + /// AutoExposureCompensationCurve::from_curve(LinearSpline::new([ + /// vec2(-4.0, -2.0), + /// vec2(0.0, 0.0), + /// vec2(2.0, 0.0), + /// vec2(4.0, 2.0), + /// ])) + /// .unwrap() + /// ); + /// ``` + pub fn from_curve(curve: T) -> Result + where + T: CubicGenerator, + { + let curve = curve.to_curve(); + + let min_log_lum = curve.position(0.0).x; + let max_log_lum = curve.position(curve.segments().len() as f32).x; + let log_lum_range = max_log_lum - min_log_lum; + + let mut lut = [0.0; LUT_SIZE]; + + let mut previous = curve.position(0.0); + let mut min_compensation = previous.y; + let mut max_compensation = previous.y; + + for segment in curve { + if segment.position(0.0) != previous { + return Err(AutoExposureCompensationCurveError::DiscontinuityFound); + } + + for i in 1..Self::SAMPLES_PER_SEGMENT { + let current = segment.position(i as f32 / (Self::SAMPLES_PER_SEGMENT - 1) as f32); + + if current.x < previous.x { + return Err(AutoExposureCompensationCurveError::NotMonotonic); + } + + // Find the range of LUT entries that this line segment covers. + let (lut_begin, lut_end) = ( + ((previous.x - min_log_lum) / log_lum_range) * (LUT_SIZE - 1) as f32, + ((current.x - min_log_lum) / log_lum_range) * (LUT_SIZE - 1) as f32, + ); + let lut_inv_range = 1.0 / (lut_end - lut_begin); + + // Iterate over all LUT entries whose pixel centers fall within the current segment. + #[allow(clippy::needless_range_loop)] + for i in lut_begin.ceil() as usize..=lut_end.floor() as usize { + let t = (i as f32 - lut_begin) * lut_inv_range; + lut[i] = previous.y.lerp(current.y, t); + min_compensation = min_compensation.min(lut[i]); + max_compensation = max_compensation.max(lut[i]); + } + + previous = current; + } + } + + let compensation_range = max_compensation - min_compensation; + + Ok(Self { + min_log_lum, + max_log_lum, + min_compensation, + max_compensation, + lut: if compensation_range > 0.0 { + let scale = 255.0 / compensation_range; + lut.map(|f: f32| ((f - min_compensation) * scale) as u8) + } else { + [0; LUT_SIZE] + }, + }) + } +} + +/// The GPU-representation of an [`AutoExposureCompensationCurve`]. +/// Consists of a [`TextureView`] with the curve's data, +/// and a [`UniformBuffer`] with the curve's extents. +pub struct GpuAutoExposureCompensationCurve { + pub(super) texture_view: TextureView, + pub(super) extents: UniformBuffer, +} + +#[derive(ShaderType, Clone, Copy)] +pub(super) struct AutoExposureCompensationCurveUniform { + min_log_lum: f32, + inv_log_lum_range: f32, + min_compensation: f32, + compensation_range: f32, +} + +impl RenderAsset for GpuAutoExposureCompensationCurve { + type SourceAsset = AutoExposureCompensationCurve; + type Param = (SRes, SRes); + + fn asset_usage(_: &Self::SourceAsset) -> RenderAssetUsages { + RenderAssetUsages::RENDER_WORLD + } + + fn prepare_asset( + source: Self::SourceAsset, + (render_device, render_queue): &mut SystemParamItem, + ) -> Result> { + let texture = render_device.create_texture_with_data( + render_queue, + &TextureDescriptor { + label: None, + size: Extent3d { + width: LUT_SIZE as u32, + height: 1, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D1, + format: TextureFormat::R8Unorm, + usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, + view_formats: &[TextureFormat::R8Unorm], + }, + Default::default(), + &source.lut, + ); + + let texture_view = texture.create_view(&Default::default()); + + let mut extents = UniformBuffer::from(AutoExposureCompensationCurveUniform { + min_log_lum: source.min_log_lum, + inv_log_lum_range: 1.0 / (source.max_log_lum - source.min_log_lum), + min_compensation: source.min_compensation, + compensation_range: source.max_compensation - source.min_compensation, + }); + + extents.write_buffer(render_device, render_queue); + + Ok(GpuAutoExposureCompensationCurve { + texture_view, + extents, + }) + } +} diff --git a/crates/bevy_core_pipeline/src/auto_exposure/mod.rs b/crates/bevy_core_pipeline/src/auto_exposure/mod.rs new file mode 100644 index 0000000000000..148e74d063576 --- /dev/null +++ b/crates/bevy_core_pipeline/src/auto_exposure/mod.rs @@ -0,0 +1,131 @@ +use bevy_app::prelude::*; +use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle}; +use bevy_ecs::prelude::*; +use bevy_render::extract_component::ExtractComponentPlugin; +use bevy_render::render_asset::RenderAssetPlugin; +use bevy_render::render_resource::Shader; +use bevy_render::ExtractSchedule; +use bevy_render::{ + render_graph::RenderGraphApp, + render_resource::{ + Buffer, BufferDescriptor, BufferUsages, PipelineCache, SpecializedComputePipelines, + }, + renderer::RenderDevice, + Render, RenderApp, RenderSet, +}; + +mod buffers; +mod compensation_curve; +mod node; +mod pipeline; +mod settings; + +use buffers::{extract_buffers, prepare_buffers, AutoExposureBuffers}; +pub use compensation_curve::{AutoExposureCompensationCurve, AutoExposureCompensationCurveError}; +use node::AutoExposureNode; +use pipeline::{ + AutoExposurePass, AutoExposurePipeline, ViewAutoExposurePipeline, METERING_SHADER_HANDLE, +}; +pub use settings::AutoExposureSettings; + +use crate::auto_exposure::compensation_curve::GpuAutoExposureCompensationCurve; +use crate::core_3d::graph::{Core3d, Node3d}; + +/// Plugin for the auto exposure feature. +/// +/// See [`AutoExposureSettings`] for more details. +pub struct AutoExposurePlugin; + +#[derive(Resource)] +struct AutoExposureResources { + histogram: Buffer, +} + +impl Plugin for AutoExposurePlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + METERING_SHADER_HANDLE, + "auto_exposure.wgsl", + Shader::from_wgsl + ); + + app.add_plugins(RenderAssetPlugin::::default()) + .register_type::() + .init_asset::() + .register_asset_reflect::(); + app.world_mut() + .resource_mut::>() + .insert(&Handle::default(), AutoExposureCompensationCurve::default()); + + app.register_type::(); + app.add_plugins(ExtractComponentPlugin::::default()); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::>() + .init_resource::() + .add_systems(ExtractSchedule, extract_buffers) + .add_systems( + Render, + ( + prepare_buffers.in_set(RenderSet::Prepare), + queue_view_auto_exposure_pipelines.in_set(RenderSet::Queue), + ), + ) + .add_render_graph_node::(Core3d, node::AutoExposure) + .add_render_graph_edges( + Core3d, + (Node3d::EndMainPass, node::AutoExposure, Node3d::Tonemapping), + ); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.init_resource::(); + render_app.init_resource::(); + } +} + +impl FromWorld for AutoExposureResources { + fn from_world(world: &mut World) -> Self { + Self { + histogram: world + .resource::() + .create_buffer(&BufferDescriptor { + label: Some("histogram buffer"), + size: pipeline::HISTOGRAM_BIN_COUNT * 4, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + } + } +} + +fn queue_view_auto_exposure_pipelines( + mut commands: Commands, + pipeline_cache: ResMut, + mut compute_pipelines: ResMut>, + pipeline: Res, + view_targets: Query<(Entity, &AutoExposureSettings)>, +) { + for (entity, settings) in view_targets.iter() { + let histogram_pipeline = + compute_pipelines.specialize(&pipeline_cache, &pipeline, AutoExposurePass::Histogram); + let average_pipeline = + compute_pipelines.specialize(&pipeline_cache, &pipeline, AutoExposurePass::Average); + + commands.entity(entity).insert(ViewAutoExposurePipeline { + histogram_pipeline, + mean_luminance_pipeline: average_pipeline, + compensation_curve: settings.compensation_curve.clone(), + metering_mask: settings.metering_mask.clone(), + }); + } +} diff --git a/crates/bevy_core_pipeline/src/auto_exposure/node.rs b/crates/bevy_core_pipeline/src/auto_exposure/node.rs new file mode 100644 index 0000000000000..222efe5c62bd0 --- /dev/null +++ b/crates/bevy_core_pipeline/src/auto_exposure/node.rs @@ -0,0 +1,141 @@ +use super::{ + buffers::AutoExposureBuffers, + compensation_curve::GpuAutoExposureCompensationCurve, + pipeline::{AutoExposurePipeline, ViewAutoExposurePipeline}, + AutoExposureResources, +}; +use bevy_ecs::{ + query::QueryState, + system::lifetimeless::Read, + world::{FromWorld, World}, +}; +use bevy_render::{ + globals::GlobalsBuffer, + render_asset::RenderAssets, + render_graph::*, + render_resource::*, + renderer::RenderContext, + texture::{FallbackImage, GpuImage}, + view::{ExtractedView, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, +}; + +#[derive(RenderLabel, Debug, Clone, Hash, PartialEq, Eq)] +pub struct AutoExposure; + +pub struct AutoExposureNode { + query: QueryState<( + Read, + Read, + Read, + Read, + )>, +} + +impl FromWorld for AutoExposureNode { + fn from_world(world: &mut World) -> Self { + Self { + query: QueryState::new(world), + } + } +} + +impl Node for AutoExposureNode { + fn update(&mut self, world: &mut World) { + self.query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.view_entity(); + let pipeline_cache = world.resource::(); + let pipeline = world.resource::(); + let resources = world.resource::(); + + let view_uniforms_resource = world.resource::(); + let view_uniforms = &view_uniforms_resource.uniforms; + let view_uniforms_buffer = view_uniforms.buffer().unwrap(); + + let globals_buffer = world.resource::(); + + let auto_exposure_buffers = world.resource::(); + + let ( + Ok((view_uniform_offset, view_target, auto_exposure, view)), + Some(auto_exposure_buffers), + ) = ( + self.query.get_manual(world, view_entity), + auto_exposure_buffers.buffers.get(&view_entity), + ) + else { + return Ok(()); + }; + + let (Some(histogram_pipeline), Some(average_pipeline)) = ( + pipeline_cache.get_compute_pipeline(auto_exposure.histogram_pipeline), + pipeline_cache.get_compute_pipeline(auto_exposure.mean_luminance_pipeline), + ) else { + return Ok(()); + }; + + let source = view_target.main_texture_view(); + + let fallback = world.resource::(); + let mask = world + .resource::>() + .get(&auto_exposure.metering_mask); + let mask = mask + .map(|i| &i.texture_view) + .unwrap_or(&fallback.d2.texture_view); + + let Some(compensation_curve) = world + .resource::>() + .get(&auto_exposure.compensation_curve) + else { + return Ok(()); + }; + + let compute_bind_group = render_context.render_device().create_bind_group( + None, + &pipeline.histogram_layout, + &BindGroupEntries::sequential(( + &globals_buffer.buffer, + &auto_exposure_buffers.settings, + source, + mask, + &compensation_curve.texture_view, + &compensation_curve.extents, + resources.histogram.as_entire_buffer_binding(), + &auto_exposure_buffers.state, + BufferBinding { + buffer: view_uniforms_buffer, + size: Some(ViewUniform::min_size()), + offset: 0, + }, + )), + ); + + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("auto_exposure_pass"), + timestamp_writes: None, + }); + + compute_pass.set_bind_group(0, &compute_bind_group, &[view_uniform_offset.offset]); + compute_pass.set_pipeline(histogram_pipeline); + compute_pass.dispatch_workgroups( + view.viewport.z.div_ceil(16), + view.viewport.w.div_ceil(16), + 1, + ); + compute_pass.set_pipeline(average_pipeline); + compute_pass.dispatch_workgroups(1, 1, 1); + + Ok(()) + } +} diff --git a/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs b/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs new file mode 100644 index 0000000000000..eacff931c7211 --- /dev/null +++ b/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs @@ -0,0 +1,94 @@ +use super::compensation_curve::{ + AutoExposureCompensationCurve, AutoExposureCompensationCurveUniform, +}; +use bevy_asset::prelude::*; +use bevy_ecs::prelude::*; +use bevy_render::{ + globals::GlobalsUniform, + render_resource::{binding_types::*, *}, + renderer::RenderDevice, + texture::Image, + view::ViewUniform, +}; +use std::num::NonZeroU64; + +#[derive(Resource)] +pub struct AutoExposurePipeline { + pub histogram_layout: BindGroupLayout, + pub histogram_shader: Handle, +} + +#[derive(Component)] +pub struct ViewAutoExposurePipeline { + pub histogram_pipeline: CachedComputePipelineId, + pub mean_luminance_pipeline: CachedComputePipelineId, + pub compensation_curve: Handle, + pub metering_mask: Handle, +} + +#[derive(ShaderType, Clone, Copy)] +pub struct AutoExposureSettingsUniform { + pub(super) min_log_lum: f32, + pub(super) inv_log_lum_range: f32, + pub(super) log_lum_range: f32, + pub(super) low_percent: f32, + pub(super) high_percent: f32, + pub(super) speed_up: f32, + pub(super) speed_down: f32, + pub(super) exponential_transition_distance: f32, +} + +#[derive(PartialEq, Eq, Hash, Clone)] +pub enum AutoExposurePass { + Histogram, + Average, +} + +pub const METERING_SHADER_HANDLE: Handle = Handle::weak_from_u128(12987620402995522466); + +pub const HISTOGRAM_BIN_COUNT: u64 = 64; + +impl FromWorld for AutoExposurePipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + Self { + histogram_layout: render_device.create_bind_group_layout( + "compute histogram bind group", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + uniform_buffer::(false), + uniform_buffer::(false), + texture_2d(TextureSampleType::Float { filterable: false }), + texture_2d(TextureSampleType::Float { filterable: false }), + texture_1d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(false), + storage_buffer_sized(false, NonZeroU64::new(HISTOGRAM_BIN_COUNT * 4)), + storage_buffer_sized(false, NonZeroU64::new(4)), + storage_buffer::(true), + ), + ), + ), + histogram_shader: METERING_SHADER_HANDLE.clone(), + } + } +} + +impl SpecializedComputePipeline for AutoExposurePipeline { + type Key = AutoExposurePass; + + fn specialize(&self, pass: AutoExposurePass) -> ComputePipelineDescriptor { + ComputePipelineDescriptor { + label: Some("luminance compute pipeline".into()), + layout: vec![self.histogram_layout.clone()], + shader: self.histogram_shader.clone(), + shader_defs: vec![], + entry_point: match pass { + AutoExposurePass::Histogram => "compute_histogram".into(), + AutoExposurePass::Average => "compute_average".into(), + }, + push_constant_ranges: vec![], + } + } +} diff --git a/crates/bevy_core_pipeline/src/auto_exposure/settings.rs b/crates/bevy_core_pipeline/src/auto_exposure/settings.rs new file mode 100644 index 0000000000000..ccef945ef7863 --- /dev/null +++ b/crates/bevy_core_pipeline/src/auto_exposure/settings.rs @@ -0,0 +1,102 @@ +use std::ops::RangeInclusive; + +use super::compensation_curve::AutoExposureCompensationCurve; +use bevy_asset::Handle; +use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; +use bevy_reflect::Reflect; +use bevy_render::{extract_component::ExtractComponent, texture::Image}; +use bevy_utils::default; + +/// Component that enables auto exposure for an HDR-enabled 2d or 3d camera. +/// +/// Auto exposure adjusts the exposure of the camera automatically to +/// simulate the human eye's ability to adapt to different lighting conditions. +/// +/// Bevy's implementation builds a 64 bin histogram of the scene's luminance, +/// and then adjusts the exposure so that the average brightness of the final +/// render will be middle gray. Because it's using a histogram, some details can +/// be selectively ignored or emphasized. Outliers like shadows and specular +/// highlights can be ignored, and certain areas can be given more (or less) +/// weight based on a mask. +/// +/// # Usage Notes +/// +/// **Auto Exposure requires compute shaders and is not compatible with WebGL2.** +/// +#[derive(Component, Clone, Reflect, ExtractComponent)] +#[reflect(Component)] +pub struct AutoExposureSettings { + /// The range of exposure values for the histogram. + /// + /// Pixel values below this range will be ignored, and pixel values above this range will be + /// clamped in the sense that they will count towards the highest bin in the histogram. + /// The default value is `-8.0..=8.0`. + pub range: RangeInclusive, + + /// The portion of the histogram to consider when metering. + /// + /// By default, the darkest 10% and the brightest 10% of samples are ignored, + /// so the default value is `0.10..=0.90`. + pub filter: RangeInclusive, + + /// The speed at which the exposure adapts from dark to bright scenes, in F-stops per second. + pub speed_brighten: f32, + + /// The speed at which the exposure adapts from bright to dark scenes, in F-stops per second. + pub speed_darken: f32, + + /// The distance in F-stops from the target exposure from where to transition from animating + /// in linear fashion to animating exponentially. This helps against jittering when the + /// target exposure keeps on changing slightly from frame to frame, while still maintaining + /// a relatively slow animation for big changes in scene brightness. + /// + /// ```text + /// ev + /// ➔●┐ + /// | ⬈ ├ exponential section + /// │ ⬈ ┘ + /// │ ⬈ ┐ + /// │ ⬈ ├ linear section + /// │⬈ ┘ + /// ●───────────────────────── time + /// ``` + /// + /// The default value is 1.5. + pub exponential_transition_distance: f32, + + /// The mask to apply when metering. The mask will cover the entire screen, where: + /// * `(0.0, 0.0)` is the top-left corner, + /// * `(1.0, 1.0)` is the bottom-right corner. + /// Only the red channel of the texture is used. + /// The sample at the current screen position will be used to weight the contribution + /// of each pixel to the histogram: + /// * 0.0 means the pixel will not contribute to the histogram, + /// * 1.0 means the pixel will contribute fully to the histogram. + /// + /// The default value is a white image, so all pixels contribute equally. + /// + /// # Usage Notes + /// + /// The mask is quantized to 16 discrete levels because of limitations in the compute shader + /// implementation. + pub metering_mask: Handle, + + /// Exposure compensation curve to apply after metering. + /// The default value is a flat line at 0.0. + /// For more information, see [`AutoExposureCompensationCurve`]. + pub compensation_curve: Handle, +} + +impl Default for AutoExposureSettings { + fn default() -> Self { + Self { + range: -8.0..=8.0, + filter: 0.10..=0.90, + speed_brighten: 3.0, + speed_darken: 1.0, + exponential_transition_distance: 1.5, + metering_mask: default(), + compensation_curve: default(), + } + } +} diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index 419a82ff6013f..1dc253c758461 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -81,7 +81,7 @@ impl Plugin for BloomPlugin { .add_render_graph_node::>(Core2d, Node2d::Bloom) .add_render_graph_edges( Core2d, - (Node2d::MainPass, Node2d::Bloom, Node2d::Tonemapping), + (Node2d::EndMainPass, Node2d::Bloom, Node2d::Tonemapping), ); } diff --git a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs similarity index 65% rename from crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs rename to crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs index 320141818af16..9d3e89e877c93 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs @@ -3,73 +3,58 @@ use bevy_ecs::prelude::*; use bevy_render::{ camera::ExtractedCamera, diagnostic::RecordDiagnostics, - render_graph::{Node, NodeRunError, RenderGraphContext}, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::SortedRenderPhase, render_resource::RenderPassDescriptor, renderer::RenderContext, - view::{ExtractedView, ViewTarget}, + view::ViewTarget, }; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; -pub struct MainPass2dNode { - query: QueryState< - ( - &'static ExtractedCamera, - &'static SortedRenderPhase, - &'static ViewTarget, - ), - With, - >, -} +#[derive(Default)] +pub struct MainTransparentPass2dNode {} -impl FromWorld for MainPass2dNode { - fn from_world(world: &mut World) -> Self { - Self { - query: world.query_filtered(), - } - } -} +impl ViewNode for MainTransparentPass2dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static SortedRenderPhase, + &'static ViewTarget, + ); -impl Node for MainPass2dNode { - fn update(&mut self, world: &mut World) { - self.query.update_archetypes(world); - } - - fn run( + fn run<'w>( &self, graph: &mut RenderGraphContext, - render_context: &mut RenderContext, - world: &World, + render_context: &mut RenderContext<'w>, + (camera, transparent_phase, target): bevy_ecs::query::QueryItem<'w, Self::ViewQuery>, + world: &'w World, ) -> Result<(), NodeRunError> { let view_entity = graph.view_entity(); - let Ok((camera, transparent_phase, target)) = self.query.get_manual(world, view_entity) - else { - // no target - return Ok(()); - }; + // This needs to run at least once to clear the background color, even if there are no items to render { #[cfg(feature = "trace")] - let _main_pass_2d = info_span!("main_pass_2d").entered(); + let _main_pass_2d = info_span!("main_transparent_pass_2d").entered(); let diagnostics = render_context.diagnostic_recorder(); let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { - label: Some("main_pass_2d"), + label: Some("main_transparent_pass_2d"), color_attachments: &[Some(target.get_color_attachment())], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, }); - let pass_span = diagnostics.pass_span(&mut render_pass, "main_pass_2d"); + let pass_span = diagnostics.pass_span(&mut render_pass, "main_transparent_pass_2d"); if let Some(viewport) = camera.viewport.as_ref() { render_pass.set_camera_viewport(viewport); } - transparent_phase.render(&mut render_pass, world, view_entity); + if !transparent_phase.items.is_empty() { + transparent_phase.render(&mut render_pass, world, view_entity); + } pass_span.end(&mut render_pass); } diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 85f986c111dd4..cf48b4030f5b5 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -1,5 +1,5 @@ mod camera_2d; -mod main_pass_2d_node; +mod main_transparent_pass_2d_node; pub mod graph { use bevy_render::render_graph::{RenderLabel, RenderSubGraph}; @@ -14,7 +14,9 @@ pub mod graph { #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] pub enum Node2d { MsaaWriteback, - MainPass, + StartMainPass, + MainTransparentPass, + EndMainPass, Bloom, Tonemapping, Fxaa, @@ -27,7 +29,7 @@ pub mod graph { use std::ops::Range; pub use camera_2d::*; -pub use main_pass_2d_node::*; +pub use main_transparent_pass_2d_node::*; use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; @@ -68,14 +70,21 @@ impl Plugin for Core2dPlugin { render_app .add_render_sub_graph(Core2d) - .add_render_graph_node::(Core2d, Node2d::MainPass) + .add_render_graph_node::(Core2d, Node2d::StartMainPass) + .add_render_graph_node::>( + Core2d, + Node2d::MainTransparentPass, + ) + .add_render_graph_node::(Core2d, Node2d::EndMainPass) .add_render_graph_node::>(Core2d, Node2d::Tonemapping) .add_render_graph_node::(Core2d, Node2d::EndMainPassPostProcessing) .add_render_graph_node::>(Core2d, Node2d::Upscaling) .add_render_graph_edges( Core2d, ( - Node2d::MainPass, + Node2d::StartMainPass, + Node2d::MainTransparentPass, + Node2d::EndMainPass, Node2d::Tonemapping, Node2d::EndMainPassPostProcessing, Node2d::Upscaling, diff --git a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs index 9787146e11038..cf062d340ff79 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -65,7 +65,7 @@ impl Default for Camera3d { #[derive(Clone, Copy, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize)] -pub struct Camera3dDepthTextureUsage(u32); +pub struct Camera3dDepthTextureUsage(pub u32); impl From for Camera3dDepthTextureUsage { fn from(value: TextureUsages) -> Self { diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index e5cd6c049173f..4d608a6a0efad 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -28,6 +28,8 @@ pub mod graph { Taa, MotionBlur, Bloom, + AutoExposure, + DepthOfField, Tonemapping, Fxaa, Upscaling, @@ -79,6 +81,7 @@ use crate::{ AlphaMask3dDeferred, Opaque3dDeferred, DEFERRED_LIGHTING_PASS_ID_FORMAT, DEFERRED_PREPASS_FORMAT, }, + dof::DepthOfFieldNode, prepass::{ node::PrepassNode, AlphaMask3dPrepass, DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass, Opaque3dPrepass, OpaqueNoLightmap3dBinKey, ViewPrepassTextures, @@ -151,6 +154,7 @@ impl Plugin for Core3dPlugin { Node3d::MainTransparentPass, ) .add_render_graph_node::(Core3d, Node3d::EndMainPass) + .add_render_graph_node::>(Core3d, Node3d::DepthOfField) .add_render_graph_node::>(Core3d, Node3d::Tonemapping) .add_render_graph_node::(Core3d, Node3d::EndMainPassPostProcessing) .add_render_graph_node::>(Core3d, Node3d::Upscaling) diff --git a/crates/bevy_core_pipeline/src/dof/dof.wgsl b/crates/bevy_core_pipeline/src/dof/dof.wgsl new file mode 100644 index 0000000000000..e1ea4c22f17d1 --- /dev/null +++ b/crates/bevy_core_pipeline/src/dof/dof.wgsl @@ -0,0 +1,301 @@ +// Performs depth of field postprocessing, with both Gaussian and bokeh kernels. +// +// Gaussian blur is performed as a separable convolution: first blurring in the +// X direction, and then in the Y direction. This is asymptotically more +// efficient than performing a 2D convolution. +// +// The Bokeh blur uses a similar, but more complex, separable convolution +// technique. The algorithm is described in Colin Barré-Brisebois, "Hexagonal +// Bokeh Blur Revisited" [1]. It's motivated by the observation that we can use +// separable convolutions not only to produce boxes but to produce +// parallelograms. Thus, by performing three separable convolutions in sequence, +// we can produce a hexagonal shape. The first and second convolutions are done +// simultaneously using multiple render targets to cut the total number of +// passes down to two. +// +// [1]: https://colinbarrebrisebois.com/2017/04/18/hexagonal-bokeh-blur-revisited-part-2-improved-2-pass-version/ + +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +#import bevy_pbr::mesh_view_bindings::view +#import bevy_pbr::view_transformations::depth_ndc_to_view_z +#import bevy_render::view::View + +// Parameters that control the depth of field effect. See +// `bevy_core_pipeline::dof::DepthOfFieldUniforms` for information on what these +// parameters mean. +struct DepthOfFieldParams { + /// The distance in meters to the location in focus. + focal_distance: f32, + + /// The [focal length]. Physically speaking, this represents "the distance + /// from the center of the lens to the principal foci of the lens". The + /// default value, 50 mm, is considered representative of human eyesight. + /// Real-world lenses range from anywhere from 5 mm for "fisheye" lenses to + /// 2000 mm for "super-telephoto" lenses designed for very distant objects. + /// + /// The higher the value, the more blurry objects not in focus will be. + /// + /// [focal length]: https://en.wikipedia.org/wiki/Focal_length + focal_length: f32, + + /// The premultiplied factor that we scale the circle of confusion by. + /// + /// This is calculated as `focal_length² / (sensor_height * aperture_f_stops)`. + coc_scale_factor: f32, + + /// The maximum diameter, in pixels, that we allow a circle of confusion to be. + /// + /// A circle of confusion essentially describes the size of a blur. + /// + /// This value is nonphysical but is useful for avoiding pathologically-slow + /// behavior. + max_circle_of_confusion_diameter: f32, + + /// The depth value that we clamp distant objects to. See the comment in + /// [`DepthOfFieldSettings`] for more information. + max_depth: f32, + + /// Padding. + pad_a: u32, + /// Padding. + pad_b: u32, + /// Padding. + pad_c: u32, +} + +// The first bokeh pass outputs to two render targets. We declare them here. +struct DualOutput { + // The vertical output. + @location(0) output_0: vec4, + // The diagonal output. + @location(1) output_1: vec4, +} + +// @group(0) @binding(0) is `mesh_view_bindings::view`. + +// The depth texture for the main view. +#ifdef MULTISAMPLED +@group(0) @binding(1) var depth_texture: texture_depth_multisampled_2d; +#else // MULTISAMPLED +@group(0) @binding(1) var depth_texture: texture_depth_2d; +#endif // MULTISAMPLED + +// The main color texture. +@group(0) @binding(2) var color_texture_a: texture_2d; + +// The auxiliary color texture that we're sampling from. This is only used as +// part of the second bokeh pass. +#ifdef DUAL_INPUT +@group(0) @binding(3) var color_texture_b: texture_2d; +#endif // DUAL_INPUT + +// The global uniforms, representing data backed by buffers shared among all +// views in the scene. + +// The parameters that control the depth of field effect. +@group(1) @binding(0) var dof_params: DepthOfFieldParams; + +// The sampler that's used to fetch texels from the source color buffer. +@group(1) @binding(1) var color_texture_sampler: sampler; + +// cos(-30°), used for the bokeh blur. +const COS_NEG_FRAC_PI_6: f32 = 0.8660254037844387; +// sin(-30°), used for the bokeh blur. +const SIN_NEG_FRAC_PI_6: f32 = -0.5; +// cos(-150°), used for the bokeh blur. +const COS_NEG_FRAC_PI_5_6: f32 = -0.8660254037844387; +// sin(-150°), used for the bokeh blur. +const SIN_NEG_FRAC_PI_5_6: f32 = -0.5; + +// Calculates and returns the diameter (not the radius) of the [circle of +// confusion]. +// +// [circle of confusion]: https://en.wikipedia.org/wiki/Circle_of_confusion +fn calculate_circle_of_confusion(in_frag_coord: vec4) -> f32 { + // Unpack the depth of field parameters. + let focus = dof_params.focal_distance; + let f = dof_params.focal_length; + let scale = dof_params.coc_scale_factor; + let max_coc_diameter = dof_params.max_circle_of_confusion_diameter; + + // Sample the depth. + let frag_coord = vec2(floor(in_frag_coord.xy)); + let raw_depth = textureLoad(depth_texture, frag_coord, 0); + let depth = min(-depth_ndc_to_view_z(raw_depth), dof_params.max_depth); + + // Calculate the circle of confusion. + // + // This is just the formula from Wikipedia [1]. + // + // [1]: https://en.wikipedia.org/wiki/Circle_of_confusion#Determining_a_circle_of_confusion_diameter_from_the_object_field + let candidate_coc = scale * abs(depth - focus) / (depth * (focus - f)); + + let framebuffer_size = vec2(textureDimensions(color_texture_a)); + return clamp(candidate_coc * framebuffer_size.y, 0.0, max_coc_diameter); +} + +// Performs a single direction of the separable Gaussian blur kernel. +// +// * `frag_coord` is the screen-space pixel coordinate of the fragment (i.e. the +// `position` input to the fragment). +// +// * `coc` is the diameter (not the radius) of the circle of confusion for this +// fragment. +// +// * `frag_offset` is the vector, in screen-space units, from one sample to the +// next. For a horizontal blur this will be `vec2(1.0, 0.0)`; for a vertical +// blur this will be `vec2(0.0, 1.0)`. +// +// Returns the resulting color of the fragment. +fn gaussian_blur(frag_coord: vec4, coc: f32, frag_offset: vec2) -> vec4 { + // Usually σ (the standard deviation) is half the radius, and the radius is + // half the CoC. So we multiply by 0.25. + let sigma = coc * 0.25; + + // 1.5σ is a good, somewhat aggressive default for support—the number of + // texels on each side of the center that we process. + let support = i32(ceil(sigma * 1.5)); + let uv = frag_coord.xy / vec2(textureDimensions(color_texture_a)); + let offset = frag_offset / vec2(textureDimensions(color_texture_a)); + + // The probability density function of the Gaussian blur is (up to constant factors) `exp(-1 / 2σ² * + // x²). We precalculate the constant factor here to avoid having to + // calculate it in the inner loop. + let exp_factor = -1.0 / (2.0 * sigma * sigma); + + // Accumulate samples on both sides of the current texel. Go two at a time, + // taking advantage of bilinear filtering. + var sum = textureSampleLevel(color_texture_a, color_texture_sampler, uv, 0.0).rgb; + var weight_sum = 1.0; + for (var i = 1; i <= support; i += 2) { + // This is a well-known trick to reduce the number of needed texture + // samples by a factor of two. We seek to accumulate two adjacent + // samples c₀ and c₁ with weights w₀ and w₁ respectively, with a single + // texture sample at a carefully chosen location. Observe that: + // + // k ⋅ lerp(c₀, c₁, t) = w₀⋅c₀ + w₁⋅c₁ + // + // w₁ + // if k = w₀ + w₁ and t = ─────── + // w₀ + w₁ + // + // Therefore, if we sample at a distance of t = w₁ / (w₀ + w₁) texels in + // between the two texel centers and scale by k = w₀ + w₁ afterward, we + // effectively evaluate w₀⋅c₀ + w₁⋅c₁ with a single texture lookup. + let w0 = exp(exp_factor * f32(i) * f32(i)); + let w1 = exp(exp_factor * f32(i + 1) * f32(i + 1)); + let uv_offset = offset * (f32(i) + w1 / (w0 + w1)); + let weight = w0 + w1; + + sum += ( + textureSampleLevel(color_texture_a, color_texture_sampler, uv + uv_offset, 0.0).rgb + + textureSampleLevel(color_texture_a, color_texture_sampler, uv - uv_offset, 0.0).rgb + ) * weight; + weight_sum += weight * 2.0; + } + + return vec4(sum / weight_sum, 1.0); +} + +// Performs a box blur in a single direction, sampling `color_texture_a`. +// +// * `frag_coord` is the screen-space pixel coordinate of the fragment (i.e. the +// `position` input to the fragment). +// +// * `coc` is the diameter (not the radius) of the circle of confusion for this +// fragment. +// +// * `frag_offset` is the vector, in screen-space units, from one sample to the +// next. This need not be horizontal or vertical. +fn box_blur_a(frag_coord: vec4, coc: f32, frag_offset: vec2) -> vec4 { + let support = i32(round(coc * 0.5)); + let uv = frag_coord.xy / vec2(textureDimensions(color_texture_a)); + let offset = frag_offset / vec2(textureDimensions(color_texture_a)); + + // Accumulate samples in a single direction. + var sum = vec3(0.0); + for (var i = 0; i <= support; i += 1) { + sum += textureSampleLevel( + color_texture_a, color_texture_sampler, uv + offset * f32(i), 0.0).rgb; + } + + return vec4(sum / vec3(1.0 + f32(support)), 1.0); +} + +// Performs a box blur in a single direction, sampling `color_texture_b`. +// +// * `frag_coord` is the screen-space pixel coordinate of the fragment (i.e. the +// `position` input to the fragment). +// +// * `coc` is the diameter (not the radius) of the circle of confusion for this +// fragment. +// +// * `frag_offset` is the vector, in screen-space units, from one sample to the +// next. This need not be horizontal or vertical. +#ifdef DUAL_INPUT +fn box_blur_b(frag_coord: vec4, coc: f32, frag_offset: vec2) -> vec4 { + let support = i32(round(coc * 0.5)); + let uv = frag_coord.xy / vec2(textureDimensions(color_texture_b)); + let offset = frag_offset / vec2(textureDimensions(color_texture_b)); + + // Accumulate samples in a single direction. + var sum = vec3(0.0); + for (var i = 0; i <= support; i += 1) { + sum += textureSampleLevel( + color_texture_b, color_texture_sampler, uv + offset * f32(i), 0.0).rgb; + } + + return vec4(sum / vec3(1.0 + f32(support)), 1.0); +} +#endif + +// Calculates the horizontal component of the separable Gaussian blur. +@fragment +fn gaussian_horizontal(in: FullscreenVertexOutput) -> @location(0) vec4 { + let coc = calculate_circle_of_confusion(in.position); + return gaussian_blur(in.position, coc, vec2(1.0, 0.0)); +} + +// Calculates the vertical component of the separable Gaussian blur. +@fragment +fn gaussian_vertical(in: FullscreenVertexOutput) -> @location(0) vec4 { + let coc = calculate_circle_of_confusion(in.position); + return gaussian_blur(in.position, coc, vec2(0.0, 1.0)); +} + +// Calculates the vertical and first diagonal components of the separable +// hexagonal bokeh blur. +// +// ╱ +// ╱ +// • +// │ +// │ +@fragment +fn bokeh_pass_0(in: FullscreenVertexOutput) -> DualOutput { + let coc = calculate_circle_of_confusion(in.position); + let vertical = box_blur_a(in.position, coc, vec2(0.0, 1.0)); + let diagonal = box_blur_a(in.position, coc, vec2(COS_NEG_FRAC_PI_6, SIN_NEG_FRAC_PI_6)); + + // Note that the diagonal part is pre-mixed with the vertical component. + var output: DualOutput; + output.output_0 = vertical; + output.output_1 = mix(vertical, diagonal, 0.5); + return output; +} + +// Calculates the second diagonal components of the separable hexagonal bokeh +// blur. +// +// ╲ ╱ +// ╲ ╱ +// • +#ifdef DUAL_INPUT +@fragment +fn bokeh_pass_1(in: FullscreenVertexOutput) -> @location(0) vec4 { + let coc = calculate_circle_of_confusion(in.position); + let output_0 = box_blur_a(in.position, coc, vec2(COS_NEG_FRAC_PI_6, SIN_NEG_FRAC_PI_6)); + let output_1 = box_blur_b(in.position, coc, vec2(COS_NEG_FRAC_PI_5_6, SIN_NEG_FRAC_PI_5_6)); + return mix(output_0, output_1, 0.5); +} +#endif diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_core_pipeline/src/dof/mod.rs new file mode 100644 index 0000000000000..52d44627101cc --- /dev/null +++ b/crates/bevy_core_pipeline/src/dof/mod.rs @@ -0,0 +1,907 @@ +//! Depth of field, a postprocessing effect that simulates camera focus. +//! +//! By default, Bevy renders all objects in full focus: regardless of depth, all +//! objects are rendered perfectly sharp (up to output resolution). Real lenses, +//! however, can only focus on objects at a specific distance. The distance +//! between the nearest and furthest objects that are in focus is known as +//! [depth of field], and this term is used more generally in computer graphics +//! to refer to the effect that simulates focus of lenses. +//! +//! Attaching [`DepthOfFieldSettings`] to a camera causes Bevy to simulate the +//! focus of a camera lens. Generally, Bevy's implementation of depth of field +//! is optimized for speed instead of physical accuracy. Nevertheless, the depth +//! of field effect in Bevy is based on physical parameters. +//! +//! [Depth of field]: https://en.wikipedia.org/wiki/Depth_of_field + +use std::f32::INFINITY; + +use bevy_app::{App, Plugin}; +use bevy_asset::{load_internal_asset, Handle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{QueryItem, With}, + schedule::IntoSystemConfigs as _, + system::{lifetimeless::Read, Commands, Query, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_render::{ + camera::{PhysicalCameraParameters, Projection}, + extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin}, + render_graph::{ + NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner, + }, + render_resource::{ + binding_types::{ + sampler, texture_2d, texture_depth_2d, texture_depth_2d_multisampled, uniform_buffer, + }, + BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, + CachedRenderPipelineId, ColorTargetState, ColorWrites, FilterMode, FragmentState, LoadOp, + Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, + RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader, + ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, StoreOp, + TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages, + }, + renderer::{RenderContext, RenderDevice}, + texture::{BevyDefault, CachedTexture, TextureCache}, + view::{ + prepare_view_targets, ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniform, + ViewUniformOffset, ViewUniforms, + }, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, +}; +use bevy_utils::{info_once, prelude::default, warn_once}; +use smallvec::SmallVec; + +use crate::{ + core_3d::{ + graph::{Core3d, Node3d}, + Camera3d, + }, + fullscreen_vertex_shader::fullscreen_shader_vertex_state, +}; + +const DOF_SHADER_HANDLE: Handle = Handle::weak_from_u128(2031861180739216043); + +/// A plugin that adds support for the depth of field effect to Bevy. +pub struct DepthOfFieldPlugin; + +/// Depth of field settings. +#[derive(Component, Clone, Copy)] +pub struct DepthOfFieldSettings { + /// The appearance of the effect. + pub mode: DepthOfFieldMode, + + /// The distance in meters to the location in focus. + pub focal_distance: f32, + + /// The height of the [image sensor format] in meters. + /// + /// Focal length is derived from the FOV and this value. The default is + /// 18.66mm, matching the [Super 35] format, which is popular in cinema. + /// + /// [image sensor format]: https://en.wikipedia.org/wiki/Image_sensor_format + /// + /// [Super 35]: https://en.wikipedia.org/wiki/Super_35 + pub sensor_height: f32, + + /// Along with the focal length, controls how much objects not in focus are + /// blurred. + pub aperture_f_stops: f32, + + /// The maximum diameter, in pixels, that we allow a circle of confusion to be. + /// + /// A circle of confusion essentially describes the size of a blur. + /// + /// This value is nonphysical but is useful for avoiding pathologically-slow + /// behavior. + pub max_circle_of_confusion_diameter: f32, + + /// Objects are never considered to be farther away than this distance as + /// far as depth of field is concerned, even if they actually are. + /// + /// This is primarily useful for skyboxes and background colors. The Bevy + /// renderer considers them to be infinitely far away. Without this value, + /// that would cause the circle of confusion to be infinitely large, capped + /// only by the `max_circle_of_confusion_diameter`. As that's unsightly, + /// this value can be used to essentially adjust how "far away" the skybox + /// or background are. + pub max_depth: f32, +} + +/// Controls the appearance of the effect. +#[derive(Component, Clone, Copy, Default, PartialEq, Debug)] +pub enum DepthOfFieldMode { + /// A more accurate simulation, in which circles of confusion generate + /// "spots" of light. + /// + /// For more information, see [Wikipedia's article on *bokeh*]. + /// + /// This doesn't work on WebGPU. + /// + /// [Wikipedia's article on *bokeh*]: https://en.wikipedia.org/wiki/Bokeh + Bokeh, + + /// A faster simulation, in which out-of-focus areas are simply blurred. + /// + /// This is less accurate to actual lens behavior and is generally less + /// aesthetically pleasing but requires less video memory bandwidth. + /// + /// This is the default. + /// + /// This works on native and WebGPU. + /// If targeting native platforms, consider using [`DepthOfFieldMode::Bokeh`] instead. + #[default] + Gaussian, +} + +/// Data about the depth of field effect that's uploaded to the GPU. +#[derive(Clone, Copy, Component, ShaderType)] +pub struct DepthOfFieldUniform { + /// The distance in meters to the location in focus. + focal_distance: f32, + + /// The focal length. See the comment in `DepthOfFieldParams` in `dof.wgsl` + /// for more information. + focal_length: f32, + + /// The premultiplied factor that we scale the circle of confusion by. + /// + /// This is calculated as `focal_length² / (sensor_height * + /// aperture_f_stops)`. + coc_scale_factor: f32, + + /// The maximum circle of confusion diameter in pixels. See the comment in + /// [`DepthOfFieldSettings`] for more information. + max_circle_of_confusion_diameter: f32, + + /// The depth value that we clamp distant objects to. See the comment in + /// [`DepthOfFieldSettings`] for more information. + max_depth: f32, + + /// Padding. + pad_a: u32, + /// Padding. + pad_b: u32, + /// Padding. + pad_c: u32, +} + +/// A key that uniquely identifies depth of field pipelines. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct DepthOfFieldPipelineKey { + /// Whether we're doing Gaussian or bokeh blur. + pass: DofPass, + /// Whether we're using HDR. + hdr: bool, + /// Whether the render target is multisampled. + multisample: bool, +} + +/// Identifies a specific depth of field render pass. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +enum DofPass { + /// The first, horizontal, Gaussian blur pass. + GaussianHorizontal, + /// The second, vertical, Gaussian blur pass. + GaussianVertical, + /// The first bokeh pass: vertical and diagonal. + BokehPass0, + /// The second bokeh pass: two diagonals. + BokehPass1, +} + +impl Plugin for DepthOfFieldPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!(app, DOF_SHADER_HANDLE, "dof.wgsl", Shader::from_wgsl); + + app.add_plugins(UniformComponentPlugin::::default()); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::>() + .init_resource::() + .add_systems(ExtractSchedule, extract_depth_of_field_settings) + .add_systems( + Render, + ( + configure_depth_of_field_view_targets, + prepare_auxiliary_depth_of_field_textures, + ) + .after(prepare_view_targets) + .in_set(RenderSet::ManageViews), + ) + .add_systems( + Render, + ( + prepare_depth_of_field_view_bind_group_layouts, + prepare_depth_of_field_pipelines, + ) + .chain() + .in_set(RenderSet::Prepare), + ) + .add_systems( + Render, + prepare_depth_of_field_global_bind_group.in_set(RenderSet::PrepareBindGroups), + ) + .add_render_graph_node::>(Core3d, Node3d::DepthOfField) + .add_render_graph_edges( + Core3d, + (Node3d::Bloom, Node3d::DepthOfField, Node3d::Tonemapping), + ); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.init_resource::(); + } +} + +/// The node in the render graph for depth of field. +#[derive(Default)] +pub struct DepthOfFieldNode; + +/// The layout for the bind group shared among all invocations of the depth of +/// field shader. +#[derive(Resource, Clone)] +pub struct DepthOfFieldGlobalBindGroupLayout { + /// The layout. + layout: BindGroupLayout, + /// The sampler used to sample from the color buffer or buffers. + color_texture_sampler: Sampler, +} + +/// The bind group shared among all invocations of the depth of field shader, +/// regardless of view. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct DepthOfFieldGlobalBindGroup(Option); + +#[derive(Component)] +pub enum DepthOfFieldPipelines { + Gaussian { + horizontal: CachedRenderPipelineId, + vertical: CachedRenderPipelineId, + }, + Bokeh { + pass_0: CachedRenderPipelineId, + pass_1: CachedRenderPipelineId, + }, +} + +struct DepthOfFieldPipelineRenderInfo { + pass_label: &'static str, + view_bind_group_label: &'static str, + pipeline: CachedRenderPipelineId, + is_dual_input: bool, + is_dual_output: bool, +} + +/// The extra texture used as the second render target for the hexagonal bokeh +/// blur. +/// +/// This is the same size and format as the main view target texture. It'll only +/// be present if bokeh is being used. +#[derive(Component, Deref, DerefMut)] +pub struct AuxiliaryDepthOfFieldTexture(CachedTexture); + +/// Bind group layouts for depth of field specific to a single view. +#[derive(Component, Clone)] +pub struct ViewDepthOfFieldBindGroupLayouts { + /// The bind group layout for passes that take only one input. + single_input: BindGroupLayout, + + /// The bind group layout for the second bokeh pass, which takes two inputs. + /// + /// This will only be present if bokeh is in use. + dual_input: Option, +} + +/// Information needed to specialize the pipeline corresponding to a pass of the +/// depth of field shader. +pub struct DepthOfFieldPipeline { + /// The bind group layouts specific to each view. + view_bind_group_layouts: ViewDepthOfFieldBindGroupLayouts, + /// The bind group layout shared among all invocations of the depth of field + /// shader. + global_bind_group_layout: BindGroupLayout, +} + +impl ViewNode for DepthOfFieldNode { + type ViewQuery = ( + Read, + Read, + Read, + Read, + Read, + Read>, + Option>, + ); + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + ( + view_uniform_offset, + view_target, + view_depth_texture, + view_pipelines, + view_bind_group_layouts, + dof_settings_uniform_index, + auxiliary_dof_texture, + ): QueryItem<'w, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let view_uniforms = world.resource::(); + let global_bind_group = world.resource::(); + + // We can be in either Gaussian blur or bokeh mode here. Both modes are + // similar, consisting of two passes each. We factor out the information + // specific to each pass into + // [`DepthOfFieldPipelines::pipeline_render_info`]. + for pipeline_render_info in view_pipelines.pipeline_render_info().iter() { + let (Some(render_pipeline), Some(view_uniforms_binding), Some(global_bind_group)) = ( + pipeline_cache.get_render_pipeline(pipeline_render_info.pipeline), + view_uniforms.uniforms.binding(), + &**global_bind_group, + ) else { + return Ok(()); + }; + + // We use most of the postprocess infrastructure here. However, + // because the bokeh pass has an additional render target, we have + // to manage a secondary *auxiliary* texture alongside the textures + // managed by the postprocessing logic. + let postprocess = view_target.post_process_write(); + + let view_bind_group = if pipeline_render_info.is_dual_input { + let (Some(auxiliary_dof_texture), Some(dual_input_bind_group_layout)) = ( + auxiliary_dof_texture, + view_bind_group_layouts.dual_input.as_ref(), + ) else { + warn_once!("Should have created the auxiliary depth of field texture by now"); + continue; + }; + render_context.render_device().create_bind_group( + Some(pipeline_render_info.view_bind_group_label), + dual_input_bind_group_layout, + &BindGroupEntries::sequential(( + view_uniforms_binding, + view_depth_texture.view(), + postprocess.source, + &auxiliary_dof_texture.default_view, + )), + ) + } else { + render_context.render_device().create_bind_group( + Some(pipeline_render_info.view_bind_group_label), + &view_bind_group_layouts.single_input, + &BindGroupEntries::sequential(( + view_uniforms_binding, + view_depth_texture.view(), + postprocess.source, + )), + ) + }; + + // Push the first input attachment. + let mut color_attachments: SmallVec<[_; 2]> = SmallVec::new(); + color_attachments.push(Some(RenderPassColorAttachment { + view: postprocess.destination, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(default()), + store: StoreOp::Store, + }, + })); + + // The first pass of the bokeh shader has two color outputs, not + // one. Handle this case by attaching the auxiliary texture, which + // should have been created by now in + // `prepare_auxiliary_depth_of_field_textures``. + if pipeline_render_info.is_dual_output { + let Some(auxiliary_dof_texture) = auxiliary_dof_texture else { + warn_once!("Should have created the auxiliary depth of field texture by now"); + continue; + }; + color_attachments.push(Some(RenderPassColorAttachment { + view: &auxiliary_dof_texture.default_view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(default()), + store: StoreOp::Store, + }, + })); + } + + let render_pass_descriptor = RenderPassDescriptor { + label: Some(pipeline_render_info.pass_label), + color_attachments: &color_attachments, + ..default() + }; + + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&render_pass_descriptor); + render_pass.set_pipeline(render_pipeline); + // Set the per-view bind group. + render_pass.set_bind_group(0, &view_bind_group, &[view_uniform_offset.offset]); + // Set the global bind group shared among all invocations of the shader. + render_pass.set_bind_group(1, global_bind_group, &[dof_settings_uniform_index.index()]); + // Render the full-screen pass. + render_pass.draw(0..3, 0..1); + } + + Ok(()) + } +} + +impl Default for DepthOfFieldSettings { + fn default() -> Self { + let physical_camera_default = PhysicalCameraParameters::default(); + Self { + focal_distance: 10.0, + aperture_f_stops: physical_camera_default.aperture_f_stops, + sensor_height: physical_camera_default.sensor_height, + max_circle_of_confusion_diameter: 64.0, + max_depth: INFINITY, + mode: DepthOfFieldMode::Bokeh, + } + } +} + +impl DepthOfFieldSettings { + /// Initializes [`DepthOfFieldSettings`] from a set of + /// [`PhysicalCameraParameters`]. + /// + /// By passing the same [`PhysicalCameraParameters`] object to this function + /// and to [`bevy_render::camera::Exposure::from_physical_camera`], matching + /// results for both the exposure and depth of field effects can be + /// obtained. + /// + /// All fields of the returned [`DepthOfFieldSettings`] other than + /// `focal_length` and `aperture_f_stops` are set to their default values. + pub fn from_physical_camera(camera: &PhysicalCameraParameters) -> DepthOfFieldSettings { + DepthOfFieldSettings { + sensor_height: camera.sensor_height, + aperture_f_stops: camera.aperture_f_stops, + ..default() + } + } +} + +impl FromWorld for DepthOfFieldGlobalBindGroupLayout { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + // Create the bind group layout that will be shared among all instances + // of the depth of field shader. + let layout = render_device.create_bind_group_layout( + Some("depth of field global bind group layout"), + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + // `dof_params` + uniform_buffer::(true), + // `color_texture_sampler` + sampler(SamplerBindingType::Filtering), + ), + ), + ); + + // Create the color texture sampler. + let sampler = render_device.create_sampler(&SamplerDescriptor { + label: Some("depth of field sampler"), + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..default() + }); + + DepthOfFieldGlobalBindGroupLayout { + color_texture_sampler: sampler, + layout, + } + } +} + +/// Creates the bind group layouts for the depth of field effect that are +/// specific to each view. +pub fn prepare_depth_of_field_view_bind_group_layouts( + mut commands: Commands, + view_targets: Query<(Entity, &DepthOfFieldSettings)>, + msaa: Res, + render_device: Res, +) { + for (view, dof_settings) in view_targets.iter() { + // Create the bind group layout for the passes that take one input. + let single_input = render_device.create_bind_group_layout( + Some("depth of field bind group layout (single input)"), + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + uniform_buffer::(true), + if *msaa != Msaa::Off { + texture_depth_2d_multisampled() + } else { + texture_depth_2d() + }, + texture_2d(TextureSampleType::Float { filterable: true }), + ), + ), + ); + + // If needed, create the bind group layout for the second bokeh pass, + // which takes two inputs. We only need to do this if bokeh is in use. + let dual_input = match dof_settings.mode { + DepthOfFieldMode::Gaussian => None, + DepthOfFieldMode::Bokeh => Some(render_device.create_bind_group_layout( + Some("depth of field bind group layout (dual input)"), + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + uniform_buffer::(true), + if *msaa != Msaa::Off { + texture_depth_2d_multisampled() + } else { + texture_depth_2d() + }, + texture_2d(TextureSampleType::Float { filterable: true }), + texture_2d(TextureSampleType::Float { filterable: true }), + ), + ), + )), + }; + + commands + .entity(view) + .insert(ViewDepthOfFieldBindGroupLayouts { + single_input, + dual_input, + }); + } +} + +/// Configures depth textures so that the depth of field shader can read from +/// them. +/// +/// By default, the depth buffers that Bevy creates aren't able to be bound as +/// textures. The depth of field shader, however, needs to read from them. So we +/// need to set the appropriate flag to tell Bevy to make samplable depth +/// buffers. +pub fn configure_depth_of_field_view_targets( + mut view_targets: Query<&mut Camera3d, With>, +) { + for mut camera_3d in view_targets.iter_mut() { + let mut depth_texture_usages = TextureUsages::from(camera_3d.depth_texture_usages); + depth_texture_usages |= TextureUsages::TEXTURE_BINDING; + camera_3d.depth_texture_usages = depth_texture_usages.into(); + } +} + +/// Creates depth of field bind group 1, which is shared among all instances of +/// the depth of field shader. +pub fn prepare_depth_of_field_global_bind_group( + global_bind_group_layout: Res, + mut dof_bind_group: ResMut, + dof_settings_uniforms: Res>, + render_device: Res, +) { + let Some(dof_settings_uniforms) = dof_settings_uniforms.binding() else { + return; + }; + + **dof_bind_group = Some(render_device.create_bind_group( + Some("depth of field global bind group"), + &global_bind_group_layout.layout, + &BindGroupEntries::sequential(( + dof_settings_uniforms, // `dof_params` + &global_bind_group_layout.color_texture_sampler, // `color_texture_sampler` + )), + )); +} + +/// Creates the second render target texture that the first pass of the bokeh +/// effect needs. +pub fn prepare_auxiliary_depth_of_field_textures( + mut commands: Commands, + render_device: Res, + mut texture_cache: ResMut, + mut view_targets: Query<(Entity, &ViewTarget, &DepthOfFieldSettings)>, +) { + for (entity, view_target, dof_settings) in view_targets.iter_mut() { + // An auxiliary texture is only needed for bokeh. + if dof_settings.mode != DepthOfFieldMode::Bokeh { + continue; + } + + // The texture matches the main view target texture. + let texture_descriptor = TextureDescriptor { + label: Some("depth of field auxiliary texture"), + size: view_target.main_texture().size(), + mip_level_count: 1, + sample_count: view_target.main_texture().sample_count(), + dimension: TextureDimension::D2, + format: view_target.main_texture_format(), + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }; + + let texture = texture_cache.get(&render_device, texture_descriptor); + + commands + .entity(entity) + .insert(AuxiliaryDepthOfFieldTexture(texture)); + } +} + +/// Specializes the depth of field pipelines specific to a view. +pub fn prepare_depth_of_field_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + msaa: Res, + global_bind_group_layout: Res, + view_targets: Query<( + Entity, + &ExtractedView, + &DepthOfFieldSettings, + &ViewDepthOfFieldBindGroupLayouts, + )>, +) { + for (entity, view, dof_settings, view_bind_group_layouts) in view_targets.iter() { + let dof_pipeline = DepthOfFieldPipeline { + view_bind_group_layouts: view_bind_group_layouts.clone(), + global_bind_group_layout: global_bind_group_layout.layout.clone(), + }; + + // We'll need these two flags to create the `DepthOfFieldPipelineKey`s. + let (hdr, multisample) = (view.hdr, *msaa != Msaa::Off); + + // Go ahead and specialize the pipelines. + match dof_settings.mode { + DepthOfFieldMode::Gaussian => { + commands + .entity(entity) + .insert(DepthOfFieldPipelines::Gaussian { + horizontal: pipelines.specialize( + &pipeline_cache, + &dof_pipeline, + DepthOfFieldPipelineKey { + hdr, + multisample, + pass: DofPass::GaussianHorizontal, + }, + ), + vertical: pipelines.specialize( + &pipeline_cache, + &dof_pipeline, + DepthOfFieldPipelineKey { + hdr, + multisample, + pass: DofPass::GaussianVertical, + }, + ), + }); + } + + DepthOfFieldMode::Bokeh => { + commands + .entity(entity) + .insert(DepthOfFieldPipelines::Bokeh { + pass_0: pipelines.specialize( + &pipeline_cache, + &dof_pipeline, + DepthOfFieldPipelineKey { + hdr, + multisample, + pass: DofPass::BokehPass0, + }, + ), + pass_1: pipelines.specialize( + &pipeline_cache, + &dof_pipeline, + DepthOfFieldPipelineKey { + hdr, + multisample, + pass: DofPass::BokehPass1, + }, + ), + }); + } + } + } +} + +impl SpecializedRenderPipeline for DepthOfFieldPipeline { + type Key = DepthOfFieldPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + // Build up our pipeline layout. + let (mut layout, mut shader_defs) = (vec![], vec![]); + let mut targets = vec![Some(ColorTargetState { + format: if key.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + blend: None, + write_mask: ColorWrites::ALL, + })]; + + // Select bind group 0, the view-specific bind group. + match key.pass { + DofPass::GaussianHorizontal | DofPass::GaussianVertical => { + // Gaussian blurs take only a single input and output. + layout.push(self.view_bind_group_layouts.single_input.clone()); + } + DofPass::BokehPass0 => { + // The first bokeh pass takes one input and produces two outputs. + layout.push(self.view_bind_group_layouts.single_input.clone()); + targets.push(targets[0].clone()); + } + DofPass::BokehPass1 => { + // The second bokeh pass takes the two outputs from the first + // bokeh pass and produces a single output. + let dual_input_bind_group_layout = self + .view_bind_group_layouts + .dual_input + .as_ref() + .expect("Dual-input depth of field bind group should have been created by now") + .clone(); + layout.push(dual_input_bind_group_layout); + shader_defs.push("DUAL_INPUT".into()); + } + } + + // Add bind group 1, the global bind group. + layout.push(self.global_bind_group_layout.clone()); + + if key.multisample { + shader_defs.push("MULTISAMPLED".into()); + } + + RenderPipelineDescriptor { + label: Some("depth of field pipeline".into()), + layout, + push_constant_ranges: vec![], + vertex: fullscreen_shader_vertex_state(), + primitive: default(), + depth_stencil: None, + multisample: default(), + fragment: Some(FragmentState { + shader: DOF_SHADER_HANDLE, + shader_defs, + entry_point: match key.pass { + DofPass::GaussianHorizontal => "gaussian_horizontal".into(), + DofPass::GaussianVertical => "gaussian_vertical".into(), + DofPass::BokehPass0 => "bokeh_pass_0".into(), + DofPass::BokehPass1 => "bokeh_pass_1".into(), + }, + targets, + }), + } + } +} + +/// Extracts all [`DepthOfFieldSettings`] components into the render world. +fn extract_depth_of_field_settings( + mut commands: Commands, + mut query: Extract>, +) { + if !DEPTH_TEXTURE_SAMPLING_SUPPORTED { + info_once!( + "Disabling depth of field on this platform because depth textures aren't supported correctly" + ); + return; + } + + for (entity, dof_settings, projection) in query.iter_mut() { + // Depth of field is nonsensical without a perspective projection. + let Projection::Perspective(ref perspective_projection) = *projection else { + continue; + }; + + let focal_length = + calculate_focal_length(dof_settings.sensor_height, perspective_projection.fov); + + // Convert `DepthOfFieldSettings` to `DepthOfFieldUniform`. + commands.get_or_spawn(entity).insert(( + *dof_settings, + DepthOfFieldUniform { + focal_distance: dof_settings.focal_distance, + focal_length, + coc_scale_factor: focal_length * focal_length + / (dof_settings.sensor_height * dof_settings.aperture_f_stops), + max_circle_of_confusion_diameter: dof_settings.max_circle_of_confusion_diameter, + max_depth: dof_settings.max_depth, + pad_a: 0, + pad_b: 0, + pad_c: 0, + }, + )); + } +} + +/// Given the sensor height and the FOV, returns the focal length. +/// +/// See . +pub fn calculate_focal_length(sensor_height: f32, fov: f32) -> f32 { + 0.5 * sensor_height / f32::tan(0.5 * fov) +} + +impl DepthOfFieldPipelines { + /// Populates the information that the `DepthOfFieldNode` needs for the two + /// depth of field render passes. + fn pipeline_render_info(&self) -> [DepthOfFieldPipelineRenderInfo; 2] { + match *self { + DepthOfFieldPipelines::Gaussian { + horizontal: horizontal_pipeline, + vertical: vertical_pipeline, + } => [ + DepthOfFieldPipelineRenderInfo { + pass_label: "depth of field pass (horizontal Gaussian)", + view_bind_group_label: "depth of field view bind group (horizontal Gaussian)", + pipeline: horizontal_pipeline, + is_dual_input: false, + is_dual_output: false, + }, + DepthOfFieldPipelineRenderInfo { + pass_label: "depth of field pass (vertical Gaussian)", + view_bind_group_label: "depth of field view bind group (vertical Gaussian)", + pipeline: vertical_pipeline, + is_dual_input: false, + is_dual_output: false, + }, + ], + + DepthOfFieldPipelines::Bokeh { + pass_0: pass_0_pipeline, + pass_1: pass_1_pipeline, + } => [ + DepthOfFieldPipelineRenderInfo { + pass_label: "depth of field pass (bokeh pass 0)", + view_bind_group_label: "depth of field view bind group (bokeh pass 0)", + pipeline: pass_0_pipeline, + is_dual_input: false, + is_dual_output: true, + }, + DepthOfFieldPipelineRenderInfo { + pass_label: "depth of field pass (bokeh pass 1)", + view_bind_group_label: "depth of field view bind group (bokeh pass 1)", + pipeline: pass_1_pipeline, + is_dual_input: true, + is_dual_output: false, + }, + ], + } + } +} + +/// Returns true if multisampled depth textures are supported on this platform. +/// +/// In theory, Naga supports depth textures on WebGL 2. In practice, it doesn't, +/// because of a silly bug whereby Naga assumes that all depth textures are +/// `sampler2DShadow` and will cheerfully generate invalid GLSL that tries to +/// perform non-percentage-closer-filtering with such a sampler. Therefore we +/// disable depth of field entirely on WebGL 2. +#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] +const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = false; + +/// Returns true if multisampled depth textures are supported on this platform. +/// +/// In theory, Naga supports depth textures on WebGL 2. In practice, it doesn't, +/// because of a silly bug whereby Naga assumes that all depth textures are +/// `sampler2DShadow` and will cheerfully generate invalid GLSL that tries to +/// perform non-percentage-closer-filtering with such a sampler. Therefore we +/// disable depth of field entirely on WebGL 2. +#[cfg(any(feature = "webgpu", not(target_arch = "wasm32")))] +const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = true; diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index 9627addf0732a..dffeff4fc4e32 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -7,12 +7,14 @@ html_favicon_url = "https://bevyengine.org/assets/icon.png" )] +pub mod auto_exposure; pub mod blit; pub mod bloom; pub mod contrast_adaptive_sharpening; pub mod core_2d; pub mod core_3d; pub mod deferred; +pub mod dof; pub mod fullscreen_vertex_shader; pub mod fxaa; pub mod motion_blur; @@ -52,6 +54,7 @@ use crate::{ core_2d::Core2dPlugin, core_3d::Core3dPlugin, deferred::copy_lighting_id::CopyDeferredLightingIdPlugin, + dof::DepthOfFieldPlugin, fullscreen_vertex_shader::FULLSCREEN_SHADER_HANDLE, fxaa::FxaaPlugin, motion_blur::MotionBlurPlugin, @@ -92,6 +95,7 @@ impl Plugin for CorePipelinePlugin { FxaaPlugin, CASPlugin, MotionBlurPlugin, + DepthOfFieldPlugin, )); } } diff --git a/crates/bevy_core_pipeline/src/motion_blur/mod.rs b/crates/bevy_core_pipeline/src/motion_blur/mod.rs index 3bb8ea3dba878..11fc5fbe112ac 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/mod.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/mod.rs @@ -98,9 +98,9 @@ pub struct MotionBlur { /// Setting this to `3` will result in `3 * 2 + 1 = 7` samples. Setting this to `0` is /// equivalent to disabling motion blur. pub samples: u32, - #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] // WebGL2 structs must be 16 byte aligned. - pub _webgl2_padding: bevy_math::Vec3, + pub _webgl2_padding: bevy_math::Vec2, } impl Default for MotionBlur { @@ -108,8 +108,8 @@ impl Default for MotionBlur { Self { shutter_angle: 0.5, samples: 1, - #[cfg(all(feature = "webgl", target_arch = "wasm32"))] - _webgl2_padding: bevy_math::Vec3::default(), + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding: Default::default(), } } } diff --git a/crates/bevy_core_pipeline/src/motion_blur/motion_blur.wgsl b/crates/bevy_core_pipeline/src/motion_blur/motion_blur.wgsl index bcc0a35adbb1c..e102d4c6b48e0 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/motion_blur.wgsl +++ b/crates/bevy_core_pipeline/src/motion_blur/motion_blur.wgsl @@ -18,7 +18,7 @@ struct MotionBlur { samples: u32, #ifdef SIXTEEN_BYTE_ALIGNMENT // WebGL2 structs must be 16 byte aligned. - _webgl2_padding: vec3 + _webgl2_padding: vec2 #endif } @group(0) @binding(4) var settings: MotionBlur; diff --git a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs index e9724ba6b45b2..7ad94d8874253 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs @@ -113,7 +113,7 @@ impl SpecializedRenderPipeline for MotionBlurPipeline { shader_defs.push(ShaderDefVal::from("MULTISAMPLED")); } - #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] { shader_defs.push("NO_DEPTH_TEXTURE_SUPPORT".into()); shader_defs.push("SIXTEEN_BYTE_ALIGNMENT".into()); diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs index 8d19a6df136f0..5165f51ab08da 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -31,7 +31,7 @@ impl Plugin for MsaaWritebackPlugin { { render_app .add_render_graph_node::(Core2d, Node2d::MsaaWriteback) - .add_render_graph_edge(Core2d, Node2d::MsaaWriteback, Node2d::MainPass); + .add_render_graph_edge(Core2d, Node2d::MsaaWriteback, Node2d::StartMainPass); } { render_app diff --git a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl index 42f0d960b66d7..62d0c928613db 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl +++ b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl @@ -1,7 +1,10 @@ #define_import_path bevy_core_pipeline::tonemapping -#import bevy_render::view::ColorGrading -#import bevy_pbr::utils::{PI_2, hsv_to_rgb, rgb_to_hsv}; +#import bevy_render::{ + view::ColorGrading, + color_operations::{hsv_to_rgb, rgb_to_hsv}, + maths::PI_2 +} // hack !! not sure what to do with this #ifdef TONEMAPPING_PASS @@ -30,7 +33,7 @@ fn sample_current_lut(p: vec3) -> vec3 { return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb; #else ifdef TONEMAP_METHOD_BLENDER_FILMIC return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb; -#else +#else return vec3(1.0, 0.0, 1.0); #endif } @@ -42,8 +45,8 @@ fn sample_current_lut(p: vec3) -> vec3 { fn rgb_to_ycbcr(col: vec3) -> vec3 { let m = mat3x3( - 0.2126, 0.7152, 0.0722, - -0.1146, -0.3854, 0.5, + 0.2126, 0.7152, 0.0722, + -0.1146, -0.3854, 0.5, 0.5, -0.4542, -0.0458 ); return col * m; @@ -51,8 +54,8 @@ fn rgb_to_ycbcr(col: vec3) -> vec3 { fn ycbcr_to_rgb(col: vec3) -> vec3 { let m = mat3x3( - 1.0, 0.0, 1.5748, - 1.0, -0.1873, -0.4681, + 1.0, 0.0, 1.5748, + 1.0, -0.1873, -0.4681, 1.0, 1.8556, 0.0 ); return max(vec3(0.0), col * m); @@ -122,14 +125,14 @@ fn RRTAndODTFit(v: vec3) -> vec3 { return a / b; } -fn ACESFitted(color: vec3) -> vec3 { +fn ACESFitted(color: vec3) -> vec3 { var fitted_color = color; // sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT let rgb_to_rrt = mat3x3( vec3(0.59719, 0.35458, 0.04823), vec3(0.07600, 0.90834, 0.01566), - vec3(0.02840, 0.13383, 0.83777) + vec3(0.02840, 0.13383, 0.83777) ); // ODT_SAT => XYZ => D60_2_D65 => sRGB @@ -224,7 +227,7 @@ fn applyAgXLog(Image: vec3) -> vec3 { prepared_image = vec3(r, g, b); prepared_image = convertOpenDomainToNormalizedLog2_(prepared_image, -10.0, 6.5); - + prepared_image = clamp(prepared_image, vec3(0.0), vec3(1.0)); return prepared_image; } @@ -368,6 +371,10 @@ fn tone_mapping(in: vec4, in_color_grading: ColorGrading) -> vec4 { // applies individually to shadows, midtones, and highlights. #ifdef SECTIONAL_COLOR_GRADING color = sectional_color_grading(color, &color_grading); +#else + // If we're not doing sectional color grading, the exposure might still need + // to be applied, for example when using auto exposure. + color = color * powsafe(vec3(2.0), color_grading.exposure); #endif // tone_mapping @@ -385,14 +392,14 @@ fn tone_mapping(in: vec4, in_color_grading: ColorGrading) -> vec4 { #else ifdef TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM color = somewhat_boring_display_transform(color.rgb); #else ifdef TONEMAP_METHOD_TONY_MC_MAPFACE - color = sample_tony_mc_mapface_lut(color); + color = sample_tony_mc_mapface_lut(color); #else ifdef TONEMAP_METHOD_BLENDER_FILMIC color = sample_blender_filmic_lut(color.rgb); #endif // Perceptual post tonemapping grading color = saturation(color, color_grading.post_saturation); - + return vec4(color, in.a); } diff --git a/crates/bevy_derive/compile_fail/.gitignore b/crates/bevy_derive/compile_fail/.gitignore new file mode 100644 index 0000000000000..ea8c4bf7f35f6 --- /dev/null +++ b/crates/bevy_derive/compile_fail/.gitignore @@ -0,0 +1 @@ +/target diff --git a/crates/bevy_macros_compile_fail_tests/Cargo.toml b/crates/bevy_derive/compile_fail/Cargo.toml similarity index 55% rename from crates/bevy_macros_compile_fail_tests/Cargo.toml rename to crates/bevy_derive/compile_fail/Cargo.toml index 047511f3b685d..45dcf8aaafbe7 100644 --- a/crates/bevy_macros_compile_fail_tests/Cargo.toml +++ b/crates/bevy_derive/compile_fail/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bevy_macros_compile_fail_tests" +name = "bevy_derive_compile_fail" edition = "2021" description = "Compile fail tests for Bevy Engine's various macros" homepage = "https://bevyengine.org" @@ -8,11 +8,10 @@ license = "MIT OR Apache-2.0" publish = false [dependencies] -# ui_test dies if we don't specify the version. See oli-obk/ui_test#211 -bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" } +bevy_derive = { path = "../" } [dev-dependencies] -bevy_compile_test_utils = { path = "../bevy_compile_test_utils" } +compile_fail_utils = { path = "../../../tools/compile_fail_utils" } [[test]] name = "derive" diff --git a/crates/bevy_macros_compile_fail_tests/README.md b/crates/bevy_derive/compile_fail/README.md similarity index 66% rename from crates/bevy_macros_compile_fail_tests/README.md rename to crates/bevy_derive/compile_fail/README.md index ad4e58f44796b..671c9e827ba74 100644 --- a/crates/bevy_macros_compile_fail_tests/README.md +++ b/crates/bevy_derive/compile_fail/README.md @@ -3,6 +3,6 @@ This crate is not part of the Bevy workspace in order to not fail `crater` tests for Bevy. The tests assert on the exact compiler errors and can easily fail for new Rust versions due to updated compiler errors (e.g. changes in spans). -The `CI` workflow executes these tests on the stable rust toolchain (see [tools/ci](../../tools/ci/src/main.rs)). +The `CI` workflow executes these tests on the stable rust toolchain (see [tools/ci](../../../tools/ci/src/main.rs)). -For information on writing tests see [bevy_compile_test_utils/README.md](../bevy_compile_test_utils/README.md). +For information on writing tests see [compile_fail_utils/README.md](../../../tools/compile_fail_utils/README.md). diff --git a/crates/bevy_ecs_compile_fail_tests/src/lib.rs b/crates/bevy_derive/compile_fail/src/lib.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/src/lib.rs rename to crates/bevy_derive/compile_fail/src/lib.rs diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_derive/invalid_attribute_fail.rs b/crates/bevy_derive/compile_fail/tests/deref_derive/invalid_attribute_fail.rs similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_derive/invalid_attribute_fail.rs rename to crates/bevy_derive/compile_fail/tests/deref_derive/invalid_attribute_fail.rs diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_derive/invalid_attribute_fail.stderr b/crates/bevy_derive/compile_fail/tests/deref_derive/invalid_attribute_fail.stderr similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_derive/invalid_attribute_fail.stderr rename to crates/bevy_derive/compile_fail/tests/deref_derive/invalid_attribute_fail.stderr diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_derive/invalid_item_fail.rs b/crates/bevy_derive/compile_fail/tests/deref_derive/invalid_item_fail.rs similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_derive/invalid_item_fail.rs rename to crates/bevy_derive/compile_fail/tests/deref_derive/invalid_item_fail.rs diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_derive/invalid_item_fail.stderr b/crates/bevy_derive/compile_fail/tests/deref_derive/invalid_item_fail.stderr similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_derive/invalid_item_fail.stderr rename to crates/bevy_derive/compile_fail/tests/deref_derive/invalid_item_fail.stderr diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_derive/missing_attribute_fail.rs b/crates/bevy_derive/compile_fail/tests/deref_derive/missing_attribute_fail.rs similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_derive/missing_attribute_fail.rs rename to crates/bevy_derive/compile_fail/tests/deref_derive/missing_attribute_fail.rs diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_derive/missing_attribute_fail.stderr b/crates/bevy_derive/compile_fail/tests/deref_derive/missing_attribute_fail.stderr similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_derive/missing_attribute_fail.stderr rename to crates/bevy_derive/compile_fail/tests/deref_derive/missing_attribute_fail.stderr diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_derive/multiple_attributes_fail.rs b/crates/bevy_derive/compile_fail/tests/deref_derive/multiple_attributes_fail.rs similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_derive/multiple_attributes_fail.rs rename to crates/bevy_derive/compile_fail/tests/deref_derive/multiple_attributes_fail.rs diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_derive/multiple_attributes_fail.stderr b/crates/bevy_derive/compile_fail/tests/deref_derive/multiple_attributes_fail.stderr similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_derive/multiple_attributes_fail.stderr rename to crates/bevy_derive/compile_fail/tests/deref_derive/multiple_attributes_fail.stderr diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_derive/multiple_fields_pass.rs b/crates/bevy_derive/compile_fail/tests/deref_derive/multiple_fields_pass.rs similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_derive/multiple_fields_pass.rs rename to crates/bevy_derive/compile_fail/tests/deref_derive/multiple_fields_pass.rs diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_derive/single_field_pass.rs b/crates/bevy_derive/compile_fail/tests/deref_derive/single_field_pass.rs similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_derive/single_field_pass.rs rename to crates/bevy_derive/compile_fail/tests/deref_derive/single_field_pass.rs diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/invalid_attribute_fail.rs b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/invalid_attribute_fail.rs similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/invalid_attribute_fail.rs rename to crates/bevy_derive/compile_fail/tests/deref_mut_derive/invalid_attribute_fail.rs diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/invalid_attribute_fail.stderr b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/invalid_attribute_fail.stderr similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/invalid_attribute_fail.stderr rename to crates/bevy_derive/compile_fail/tests/deref_mut_derive/invalid_attribute_fail.stderr diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/invalid_item_fail.rs b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/invalid_item_fail.rs similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/invalid_item_fail.rs rename to crates/bevy_derive/compile_fail/tests/deref_mut_derive/invalid_item_fail.rs diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/invalid_item_fail.stderr b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/invalid_item_fail.stderr similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/invalid_item_fail.stderr rename to crates/bevy_derive/compile_fail/tests/deref_mut_derive/invalid_item_fail.stderr diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/mismatched_target_type_fail.rs b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/mismatched_target_type_fail.rs similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/mismatched_target_type_fail.rs rename to crates/bevy_derive/compile_fail/tests/deref_mut_derive/mismatched_target_type_fail.rs diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/mismatched_target_type_fail.stderr b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/mismatched_target_type_fail.stderr similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/mismatched_target_type_fail.stderr rename to crates/bevy_derive/compile_fail/tests/deref_mut_derive/mismatched_target_type_fail.stderr diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/missing_attribute_fail.rs b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/missing_attribute_fail.rs similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/missing_attribute_fail.rs rename to crates/bevy_derive/compile_fail/tests/deref_mut_derive/missing_attribute_fail.rs diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/missing_attribute_fail.stderr b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/missing_attribute_fail.stderr similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/missing_attribute_fail.stderr rename to crates/bevy_derive/compile_fail/tests/deref_mut_derive/missing_attribute_fail.stderr diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/missing_deref_fail.rs b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/missing_deref_fail.rs similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/missing_deref_fail.rs rename to crates/bevy_derive/compile_fail/tests/deref_mut_derive/missing_deref_fail.rs diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/missing_deref_fail.stderr b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/missing_deref_fail.stderr similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/missing_deref_fail.stderr rename to crates/bevy_derive/compile_fail/tests/deref_mut_derive/missing_deref_fail.stderr diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/multiple_attributes_fail.rs b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/multiple_attributes_fail.rs similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/multiple_attributes_fail.rs rename to crates/bevy_derive/compile_fail/tests/deref_mut_derive/multiple_attributes_fail.rs diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/multiple_attributes_fail.stderr b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/multiple_attributes_fail.stderr similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/multiple_attributes_fail.stderr rename to crates/bevy_derive/compile_fail/tests/deref_mut_derive/multiple_attributes_fail.stderr diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/multiple_fields_pass.rs b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/multiple_fields_pass.rs similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/multiple_fields_pass.rs rename to crates/bevy_derive/compile_fail/tests/deref_mut_derive/multiple_fields_pass.rs diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/single_field_pass.rs b/crates/bevy_derive/compile_fail/tests/deref_mut_derive/single_field_pass.rs similarity index 100% rename from crates/bevy_macros_compile_fail_tests/tests/deref_mut_derive/single_field_pass.rs rename to crates/bevy_derive/compile_fail/tests/deref_mut_derive/single_field_pass.rs diff --git a/crates/bevy_derive/compile_fail/tests/derive.rs b/crates/bevy_derive/compile_fail/tests/derive.rs new file mode 100644 index 0000000000000..caabcbf332ea2 --- /dev/null +++ b/crates/bevy_derive/compile_fail/tests/derive.rs @@ -0,0 +1,4 @@ +fn main() -> compile_fail_utils::ui_test::Result<()> { + compile_fail_utils::test_multiple(["tests/deref_derive", "tests/deref_mut_derive"]) +} + diff --git a/crates/bevy_dev_tools/src/ui_debug_overlay/mod.rs b/crates/bevy_dev_tools/src/ui_debug_overlay/mod.rs index 3029a05c9b6cc..6382cfa9038fb 100644 --- a/crates/bevy_dev_tools/src/ui_debug_overlay/mod.rs +++ b/crates/bevy_dev_tools/src/ui_debug_overlay/mod.rs @@ -28,7 +28,7 @@ mod inset; /// The [`Camera::order`] index used by the layout debug camera. pub const LAYOUT_DEBUG_CAMERA_ORDER: isize = 255; /// The [`RenderLayers`] used by the debug gizmos and the debug camera. -pub const LAYOUT_DEBUG_LAYERS: RenderLayers = RenderLayers::none().with(16); +pub const LAYOUT_DEBUG_LAYERS: RenderLayers = RenderLayers::layer(16); #[derive(Clone, Copy)] struct LayoutRect { @@ -101,7 +101,7 @@ fn update_debug_camera( }, ..default() }, - LAYOUT_DEBUG_LAYERS, + LAYOUT_DEBUG_LAYERS.clone(), DebugOverlayCamera, Name::new("Layout Debug Camera"), )) @@ -109,7 +109,7 @@ fn update_debug_camera( }; if let Some((config, _)) = gizmo_config.get_config_mut_dyn(&TypeId::of::()) { config.enabled = true; - config.render_layers = LAYOUT_DEBUG_LAYERS; + config.render_layers = LAYOUT_DEBUG_LAYERS.clone(); } let cam = *options.layout_gizmos_camera.get_or_insert_with(spawn_cam); let Ok(mut cam) = debug_cams.get_mut(cam) else { diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 62a7fce6551c9..8b7eee80981a0 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -8,10 +8,11 @@ repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["ecs", "game", "bevy"] categories = ["game-engines", "data-structures"] +rust-version = "1.77.0" [features] trace = [] -multi-threaded = ["bevy_tasks/multi-threaded", "arrayvec"] +multi_threaded = ["bevy_tasks/multi_threaded", "arrayvec"] bevy_debug_stepping = [] default = ["bevy_reflect"] diff --git a/crates/bevy_ecs/compile_fail/.gitignore b/crates/bevy_ecs/compile_fail/.gitignore new file mode 100644 index 0000000000000..ea8c4bf7f35f6 --- /dev/null +++ b/crates/bevy_ecs/compile_fail/.gitignore @@ -0,0 +1 @@ +/target diff --git a/crates/bevy_ecs_compile_fail_tests/Cargo.toml b/crates/bevy_ecs/compile_fail/Cargo.toml similarity index 56% rename from crates/bevy_ecs_compile_fail_tests/Cargo.toml rename to crates/bevy_ecs/compile_fail/Cargo.toml index 5d27ac490f215..76f7ec8b8aaa4 100644 --- a/crates/bevy_ecs_compile_fail_tests/Cargo.toml +++ b/crates/bevy_ecs/compile_fail/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bevy_ecs_compile_fail_tests" +name = "bevy_ecs_compile_fail" edition = "2021" description = "Compile fail tests for Bevy Engine's entity component system" homepage = "https://bevyengine.org" @@ -8,11 +8,10 @@ license = "MIT OR Apache-2.0" publish = false [dependencies] -# ui_test dies if we don't specify the version. See oli-obk/ui_test#211 -bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" } +bevy_ecs = { path = "../" } [dev-dependencies] -bevy_compile_test_utils = { path = "../bevy_compile_test_utils" } +compile_fail_utils = { path = "../../../tools/compile_fail_utils" } [[test]] name = "ui" diff --git a/crates/bevy_ecs_compile_fail_tests/README.md b/crates/bevy_ecs/compile_fail/README.md similarity index 68% rename from crates/bevy_ecs_compile_fail_tests/README.md rename to crates/bevy_ecs/compile_fail/README.md index 5fbdbb2ca70d8..8f949bbb7d586 100644 --- a/crates/bevy_ecs_compile_fail_tests/README.md +++ b/crates/bevy_ecs/compile_fail/README.md @@ -2,6 +2,6 @@ This crate is separate from `bevy_ecs` and not part of the Bevy workspace in order to not fail `crater` tests for Bevy. The tests assert on the exact compiler errors and can easily fail for new Rust versions due to updated compiler errors (e.g. changes in spans). -The `CI` workflow executes these tests on the stable rust toolchain (see [tools/ci](../../tools/ci/src/main.rs)). +The `CI` workflow executes these tests on the stable rust toolchain (see [tools/ci](../../../tools/ci/src/main.rs)). -For information on writing tests see [bevy_compile_test_utils/README.md](../bevy_compile_test_utils/README.md). +For information on writing tests see [compile_fail_utils/README.md](../../../tools/compile_fail_utils/README.md). diff --git a/crates/bevy_macros_compile_fail_tests/src/lib.rs b/crates/bevy_ecs/compile_fail/src/lib.rs similarity index 100% rename from crates/bevy_macros_compile_fail_tests/src/lib.rs rename to crates/bevy_ecs/compile_fail/src/lib.rs diff --git a/crates/bevy_ecs/compile_fail/tests/ui.rs b/crates/bevy_ecs/compile_fail/tests/ui.rs new file mode 100644 index 0000000000000..ad8933776a1c6 --- /dev/null +++ b/crates/bevy_ecs/compile_fail/tests/ui.rs @@ -0,0 +1,3 @@ +fn main() -> compile_fail_utils::ui_test::Result<()> { + compile_fail_utils::test("tests/ui") +} diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/entity_ref_mut_lifetime_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/entity_ref_mut_lifetime_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/entity_ref_mut_lifetime_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/entity_ref_mut_lifetime_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/entity_ref_mut_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/entity_ref_mut_lifetime_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/entity_ref_mut_lifetime_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/entity_ref_mut_lifetime_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_exact_sized_iterator_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/query_exact_sized_iterator_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/query_exact_sized_iterator_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/query_exact_sized_iterator_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_exact_sized_iterator_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/query_exact_sized_iterator_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/query_exact_sized_iterator_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/query_exact_sized_iterator_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_combinations_mut_iterator_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/query_iter_combinations_mut_iterator_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_combinations_mut_iterator_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/query_iter_combinations_mut_iterator_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_combinations_mut_iterator_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/query_iter_combinations_mut_iterator_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_combinations_mut_iterator_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/query_iter_combinations_mut_iterator_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_many_mut_iterator_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/query_iter_many_mut_iterator_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_many_mut_iterator_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/query_iter_many_mut_iterator_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_many_mut_iterator_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/query_iter_many_mut_iterator_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_many_mut_iterator_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/query_iter_many_mut_iterator_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_lifetime_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/query_lifetime_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/query_lifetime_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/query_lifetime_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/query_lifetime_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/query_lifetime_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/query_lifetime_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_to_readonly.rs b/crates/bevy_ecs/compile_fail/tests/ui/query_to_readonly.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/query_to_readonly.rs rename to crates/bevy_ecs/compile_fail/tests/ui/query_to_readonly.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_to_readonly.stderr b/crates/bevy_ecs/compile_fail/tests/ui/query_to_readonly.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/query_to_readonly.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/query_to_readonly.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_transmute_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/query_transmute_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/query_transmute_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/query_transmute_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_transmute_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/query_transmute_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/query_transmute_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/query_transmute_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_param_derive_readonly.rs b/crates/bevy_ecs/compile_fail/tests/ui/system_param_derive_readonly.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_param_derive_readonly.rs rename to crates/bevy_ecs/compile_fail/tests/ui/system_param_derive_readonly.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_param_derive_readonly.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_param_derive_readonly.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_param_derive_readonly.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/system_param_derive_readonly.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_get_lifetime_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_lifetime_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_get_lifetime_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/system_query_get_lifetime_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_get_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_lifetime_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_get_lifetime_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/system_query_get_lifetime_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_get_many_lifetime_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_lifetime_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_get_many_lifetime_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_lifetime_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_get_many_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_lifetime_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_get_many_lifetime_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_lifetime_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_get_many_mut_lifetime_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_mut_lifetime_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_get_many_mut_lifetime_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_mut_lifetime_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_get_many_mut_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_mut_lifetime_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_get_many_mut_lifetime_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_mut_lifetime_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_iter_lifetime_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_lifetime_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_iter_lifetime_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_lifetime_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_iter_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_lifetime_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_iter_lifetime_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_lifetime_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_iter_many_mut_lifetime_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_many_mut_lifetime_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_iter_many_mut_lifetime_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_many_mut_lifetime_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_iter_many_mut_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_many_mut_lifetime_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_iter_many_mut_lifetime_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_many_mut_lifetime_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_set_get_lifetime_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/system_query_set_get_lifetime_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_set_get_lifetime_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/system_query_set_get_lifetime_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_set_get_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_query_set_get_lifetime_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_set_get_lifetime_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/system_query_set_get_lifetime_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_set_iter_lifetime_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/system_query_set_iter_lifetime_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_set_iter_lifetime_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/system_query_set_iter_lifetime_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_set_iter_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_query_set_iter_lifetime_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_query_set_iter_lifetime_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/system_query_set_iter_lifetime_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_state_get_lifetime_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/system_state_get_lifetime_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_state_get_lifetime_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/system_state_get_lifetime_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_state_get_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_state_get_lifetime_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_state_get_lifetime_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/system_state_get_lifetime_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_state_iter_lifetime_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/system_state_iter_lifetime_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_state_iter_lifetime_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/system_state_iter_lifetime_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_state_iter_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_state_iter_lifetime_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_state_iter_lifetime_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/system_state_iter_lifetime_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_state_iter_mut_overlap_safety.rs b/crates/bevy_ecs/compile_fail/tests/ui/system_state_iter_mut_overlap_safety.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_state_iter_mut_overlap_safety.rs rename to crates/bevy_ecs/compile_fail/tests/ui/system_state_iter_mut_overlap_safety.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_state_iter_mut_overlap_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_state_iter_mut_overlap_safety.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/system_state_iter_mut_overlap_safety.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/system_state_iter_mut_overlap_safety.stderr diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/world_query_derive.rs b/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.rs similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/world_query_derive.rs rename to crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.rs diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/world_query_derive.stderr b/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.stderr similarity index 100% rename from crates/bevy_ecs_compile_fail_tests/tests/ui/world_query_derive.stderr rename to crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.stderr diff --git a/crates/bevy_ecs/macros/src/world_query.rs b/crates/bevy_ecs/macros/src/world_query.rs index 0206b10d8c669..8675458cdf06b 100644 --- a/crates/bevy_ecs/macros/src/world_query.rs +++ b/crates/bevy_ecs/macros/src/world_query.rs @@ -170,9 +170,9 @@ pub(crate) fn world_query_impl( } } - fn get_state(world: &#path::world::World) -> Option<#state_struct_name #user_ty_generics> { + fn get_state(components: &#path::component::Components) -> Option<#state_struct_name #user_ty_generics> { Some(#state_struct_name { - #(#named_field_idents: <#field_types>::get_state(world)?,)* + #(#named_field_idents: <#field_types>::get_state(components)?,)* }) } diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index d4cc1a73af4dd..03e8a40b55a02 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -728,6 +728,65 @@ change_detection_impl!(Ref<'w, T>, T,); impl_debug!(Ref<'w, T>,); /// Unique mutable borrow of an entity's component or of a resource. +/// +/// This can be used in queries to opt into change detection on both their mutable and immutable forms, as opposed to +/// `&mut T`, which only provides access to change detection while in its mutable form: +/// +/// ```rust +/// # use bevy_ecs::prelude::*; +/// # use bevy_ecs::query::QueryData; +/// # +/// #[derive(Component, Clone)] +/// struct Name(String); +/// +/// #[derive(Component, Clone, Copy)] +/// struct Health(f32); +/// +/// #[derive(Component, Clone, Copy)] +/// struct Position { +/// x: f32, +/// y: f32, +/// }; +/// +/// #[derive(Component, Clone, Copy)] +/// struct Player { +/// id: usize, +/// }; +/// +/// #[derive(QueryData)] +/// #[query_data(mutable)] +/// struct PlayerQuery { +/// id: &'static Player, +/// +/// // Reacting to `PlayerName` changes is expensive, so we need to enable change detection when reading it. +/// name: Mut<'static, Name>, +/// +/// health: &'static mut Health, +/// position: &'static mut Position, +/// } +/// +/// fn update_player_avatars(players_query: Query) { +/// // The item returned by the iterator is of type `PlayerQueryReadOnlyItem`. +/// for player in players_query.iter() { +/// if player.name.is_changed() { +/// // Update the player's name. This clones a String, and so is more expensive. +/// update_player_name(player.id, player.name.clone()); +/// } +/// +/// // Update the health bar. +/// update_player_health(player.id, *player.health); +/// +/// // Update the player's position. +/// update_player_position(player.id, *player.position); +/// } +/// } +/// +/// # bevy_ecs::system::assert_is_system(update_player_avatars); +/// +/// # fn update_player_name(player: &Player, new_name: Name) {} +/// # fn update_player_health(player: &Player, new_health: Health) {} +/// # fn update_player_position(player: &Player, new_position: Position) {} +/// ``` pub struct Mut<'w, T: ?Sized> { pub(crate) value: &'w mut T, pub(crate) ticks: TicksMut<'w>, diff --git a/crates/bevy_ecs/src/event.rs b/crates/bevy_ecs/src/event.rs index d4fada1139298..5f5b7092f047e 100644 --- a/crates/bevy_ecs/src/event.rs +++ b/crates/bevy_ecs/src/event.rs @@ -928,12 +928,12 @@ impl<'a, E: Event> EventParIter<'a, E> { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool pub fn for_each_with_id) + Send + Sync + Clone>(self, func: FN) { - #[cfg(any(target_arch = "wasm32", not(feature = "multi-threaded")))] + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] { self.into_iter().for_each(|(e, i)| func(e, i)); } - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] { let pool = bevy_tasks::ComputeTaskPool::get(); let thread_count = pool.thread_num(); @@ -1509,7 +1509,7 @@ mod tests { ); } - #[cfg(feature = "multi-threaded")] + #[cfg(feature = "multi_threaded")] #[test] fn test_events_par_iter() { use std::{collections::HashSet, sync::mpsc}; diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 89ea9f8fb915a..ca6e42f3c938c 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -49,10 +49,8 @@ pub mod prelude { query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, removal_detection::RemovedComponents, schedule::{ - apply_deferred, apply_state_transition, common_conditions::*, ComputedStates, - Condition, IntoSystemConfigs, IntoSystemSet, IntoSystemSetConfigs, NextState, OnEnter, - OnExit, OnTransition, Schedule, Schedules, State, StateSet, StateTransition, - StateTransitionEvent, States, SubStates, SystemSet, + apply_deferred, common_conditions::*, Condition, IntoSystemConfigs, IntoSystemSet, + IntoSystemSetConfigs, Schedule, Schedules, SystemSet, }, system::{ Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands, diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 545b15f8dad55..29a89363135d9 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -1,7 +1,7 @@ use crate::{ archetype::{Archetype, Archetypes}, change_detection::{Ticks, TicksMut}, - component::{Component, ComponentId, StorageType, Tick}, + component::{Component, ComponentId, Components, StorageType, Tick}, entity::{Entities, Entity, EntityLocation}, query::{Access, DebugCheckedUnwrap, FilteredAccess, WorldQuery}, storage::{ComponentSparseSet, Table, TableRow}, @@ -333,7 +333,7 @@ unsafe impl WorldQuery for Entity { fn init_state(_world: &mut World) {} - fn get_state(_world: &World) -> Option<()> { + fn get_state(_components: &Components) -> Option<()> { Some(()) } @@ -405,7 +405,7 @@ unsafe impl WorldQuery for EntityLocation { fn init_state(_world: &mut World) {} - fn get_state(_world: &World) -> Option<()> { + fn get_state(_components: &Components) -> Option<()> { Some(()) } @@ -484,7 +484,7 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { fn init_state(_world: &mut World) {} - fn get_state(_world: &World) -> Option<()> { + fn get_state(_components: &Components) -> Option<()> { Some(()) } @@ -560,7 +560,7 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { fn init_state(_world: &mut World) {} - fn get_state(_world: &World) -> Option<()> { + fn get_state(_components: &Components) -> Option<()> { Some(()) } @@ -660,7 +660,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { FilteredAccess::default() } - fn get_state(_world: &World) -> Option { + fn get_state(_components: &Components) -> Option { Some(FilteredAccess::default()) } @@ -772,7 +772,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { FilteredAccess::default() } - fn get_state(_world: &World) -> Option { + fn get_state(_components: &Components) -> Option { Some(FilteredAccess::default()) } @@ -844,7 +844,7 @@ unsafe impl WorldQuery for &Archetype { fn init_state(_world: &mut World) {} - fn get_state(_world: &World) -> Option<()> { + fn get_state(_components: &Components) -> Option<()> { Some(()) } @@ -995,8 +995,8 @@ unsafe impl WorldQuery for &T { world.init_component::() } - fn get_state(world: &World) -> Option { - world.component_id::() + fn get_state(components: &Components) -> Option { + components.component_id::() } fn matches_component_set( @@ -1178,8 +1178,8 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { world.init_component::() } - fn get_state(world: &World) -> Option { - world.component_id::() + fn get_state(components: &Components) -> Option { + components.component_id::() } fn matches_component_set( @@ -1361,8 +1361,8 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { world.init_component::() } - fn get_state(world: &World) -> Option { - world.component_id::() + fn get_state(components: &Components) -> Option { + components.component_id::() } fn matches_component_set( @@ -1378,6 +1378,107 @@ unsafe impl<'__w, T: Component> QueryData for &'__w mut T { type ReadOnly = &'__w T; } +/// When `Mut` is used in a query, it will be converted to `Ref` when transformed into its read-only form, providing access to change detection methods. +/// +/// By contrast `&mut T` will result in a `Mut` item in mutable form to record mutations, but result in a bare `&T` in read-only form. +/// +/// SAFETY: +/// `fetch` accesses a single component mutably. +/// This is sound because `update_component_access` and `update_archetype_component_access` add write access for that component and panic when appropriate. +/// `update_component_access` adds a `With` filter for a component. +/// This is sound because `matches_component_set` returns whether the set contains that component. +unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { + type Item<'w> = Mut<'w, T>; + type Fetch<'w> = WriteFetch<'w, T>; + type State = ComponentId; + + // Forwarded to `&mut T` + fn shrink<'wlong: 'wshort, 'wshort>(item: Mut<'wlong, T>) -> Mut<'wshort, T> { + <&mut T as WorldQuery>::shrink(item) + } + + #[inline] + // Forwarded to `&mut T` + unsafe fn init_fetch<'w>( + world: UnsafeWorldCell<'w>, + state: &ComponentId, + last_run: Tick, + this_run: Tick, + ) -> WriteFetch<'w, T> { + <&mut T as WorldQuery>::init_fetch(world, state, last_run, this_run) + } + + // Forwarded to `&mut T` + const IS_DENSE: bool = <&mut T as WorldQuery>::IS_DENSE; + + #[inline] + // Forwarded to `&mut T` + unsafe fn set_archetype<'w>( + fetch: &mut WriteFetch<'w, T>, + state: &ComponentId, + archetype: &'w Archetype, + table: &'w Table, + ) { + <&mut T as WorldQuery>::set_archetype(fetch, state, archetype, table); + } + + #[inline] + // Forwarded to `&mut T` + unsafe fn set_table<'w>(fetch: &mut WriteFetch<'w, T>, state: &ComponentId, table: &'w Table) { + <&mut T as WorldQuery>::set_table(fetch, state, table); + } + + #[inline(always)] + // Forwarded to `&mut T` + unsafe fn fetch<'w>( + // Rust complains about lifetime bounds not matching the trait if I directly use `WriteFetch<'w, T>` right here. + // But it complains nowhere else in the entire trait implementation. + fetch: &mut Self::Fetch<'w>, + entity: Entity, + table_row: TableRow, + ) -> Mut<'w, T> { + <&mut T as WorldQuery>::fetch(fetch, entity, table_row) + } + + // NOT forwarded to `&mut T` + fn update_component_access( + &component_id: &ComponentId, + access: &mut FilteredAccess, + ) { + // Update component access here instead of in `<&mut T as WorldQuery>` to avoid erroneously referencing + // `&mut T` in error message. + assert!( + !access.access().has_read(component_id), + "Mut<{}> conflicts with a previous access in this query. Mutable component access mut be unique.", + std::any::type_name::(), + ); + access.add_write(component_id); + } + + // Forwarded to `&mut T` + fn init_state(world: &mut World) -> ComponentId { + <&mut T as WorldQuery>::init_state(world) + } + + // Forwarded to `&mut T` + fn get_state(components: &Components) -> Option { + <&mut T as WorldQuery>::get_state(components) + } + + // Forwarded to `&mut T` + fn matches_component_set( + state: &ComponentId, + set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + <&mut T as WorldQuery>::matches_component_set(state, set_contains_id) + } +} + +// SAFETY: access of `Ref` is a subset of `Mut` +unsafe impl<'__w, T: Component> QueryData for Mut<'__w, T> { + type ReadOnly = Ref<'__w, T>; +} + #[doc(hidden)] pub struct OptionFetch<'w, T: WorldQuery> { fetch: T::Fetch<'w>, @@ -1480,8 +1581,8 @@ unsafe impl WorldQuery for Option { T::init_state(world) } - fn get_state(world: &World) -> Option { - T::get_state(world) + fn get_state(components: &Components) -> Option { + T::get_state(components) } fn matches_component_set( @@ -1635,8 +1736,8 @@ unsafe impl WorldQuery for Has { world.init_component::() } - fn get_state(world: &World) -> Option { - world.component_id::() + fn get_state(components: &Components) -> Option { + components.component_id::() } fn matches_component_set( @@ -1776,13 +1877,13 @@ macro_rules! impl_anytuple_fetch { *_access = _new_access; } - - fn init_state(_world: &mut World) -> Self::State { - ($($name::init_state(_world),)*) + #[allow(unused_variables)] + fn init_state(world: &mut World) -> Self::State { + ($($name::init_state(world),)*) } - - fn get_state(_world: &World) -> Option { - Some(($($name::get_state(_world)?,)*)) + #[allow(unused_variables)] + fn get_state(components: &Components) -> Option { + Some(($($name::get_state(components)?,)*)) } fn matches_component_set(_state: &Self::State, _set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { @@ -1858,8 +1959,8 @@ unsafe impl WorldQuery for NopWorldQuery { D::init_state(world) } - fn get_state(world: &World) -> Option { - D::get_state(world) + fn get_state(components: &Components) -> Option { + D::get_state(components) } fn matches_component_set( @@ -1923,7 +2024,7 @@ unsafe impl WorldQuery for PhantomData { fn init_state(_world: &mut World) -> Self::State {} - fn get_state(_world: &World) -> Option { + fn get_state(_components: &Components) -> Option { Some(()) } diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index d5a1c3839efd0..646d1d0a088a5 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -1,6 +1,6 @@ use crate::{ archetype::Archetype, - component::{Component, ComponentId, StorageType, Tick}, + component::{Component, ComponentId, Components, StorageType, Tick}, entity::Entity, query::{DebugCheckedUnwrap, FilteredAccess, WorldQuery}, storage::{Column, ComponentSparseSet, Table, TableRow}, @@ -183,8 +183,8 @@ unsafe impl WorldQuery for With { world.init_component::() } - fn get_state(world: &World) -> Option { - world.component_id::() + fn get_state(components: &Components) -> Option { + components.component_id::() } fn matches_component_set( @@ -291,8 +291,8 @@ unsafe impl WorldQuery for Without { world.init_component::() } - fn get_state(world: &World) -> Option { - world.component_id::() + fn get_state(components: &Components) -> Option { + components.component_id::() } fn matches_component_set( @@ -461,8 +461,8 @@ macro_rules! impl_or_query_filter { ($($filter::init_state(world),)*) } - fn get_state(world: &World) -> Option { - Some(($($filter::get_state(world)?,)*)) + fn get_state(components: &Components) -> Option { + Some(($($filter::get_state(components)?,)*)) } fn matches_component_set(_state: &Self::State, _set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { @@ -691,8 +691,8 @@ unsafe impl WorldQuery for Added { world.init_component::() } - fn get_state(world: &World) -> Option { - world.component_id::() + fn get_state(components: &Components) -> Option { + components.component_id::() } fn matches_component_set( @@ -900,8 +900,8 @@ unsafe impl WorldQuery for Changed { world.init_component::() } - fn get_state(world: &World) -> Option { - world.component_id::() + fn get_state(components: &Components) -> Option { + components.component_id::() } fn matches_component_set( diff --git a/crates/bevy_ecs/src/query/par_iter.rs b/crates/bevy_ecs/src/query/par_iter.rs index f433c31014a5b..7889228ba7af6 100644 --- a/crates/bevy_ecs/src/query/par_iter.rs +++ b/crates/bevy_ecs/src/query/par_iter.rs @@ -78,7 +78,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { func(&mut init, item); init }; - #[cfg(any(target_arch = "wasm32", not(feature = "multi-threaded")))] + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] { let init = init(); // SAFETY: @@ -93,7 +93,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { .fold(init, func); } } - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] { let thread_count = bevy_tasks::ComputeTaskPool::get().thread_num(); if thread_count <= 1 { @@ -122,7 +122,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { } } - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] fn get_batch_size(&self, thread_count: usize) -> usize { let max_items = || { let id_iter = self.state.matched_storage_ids.iter(); diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 5e52178837072..b59822ef1a1c7 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1,7 +1,7 @@ use crate::{ archetype::{Archetype, ArchetypeComponentId, ArchetypeGeneration, ArchetypeId}, batching::BatchingStrategy, - component::{ComponentId, Tick}, + component::{ComponentId, Components, Tick}, entity::Entity, prelude::FromWorld, query::{ @@ -476,8 +476,8 @@ impl QueryState { /// You might end up with a mix of archetypes that only matched the original query + archetypes that only match /// the new [`QueryState`]. Most of the safe methods on [`QueryState`] call [`QueryState::update_archetypes`] internally, so this /// best used through a [`Query`](crate::system::Query). - pub fn transmute(&self, world: &World) -> QueryState { - self.transmute_filtered::(world) + pub fn transmute(&self, components: &Components) -> QueryState { + self.transmute_filtered::(components) } /// Creates a new [`QueryState`] with the same underlying [`FilteredAccess`], matched tables and archetypes @@ -486,11 +486,11 @@ impl QueryState { /// Panics if `NewD` or `NewF` require accesses that this query does not have. pub fn transmute_filtered( &self, - world: &World, + components: &Components, ) -> QueryState { let mut component_access = FilteredAccess::default(); - let mut fetch_state = NewD::get_state(world).expect("Could not create fetch_state, Please initialize all referenced components before transmuting."); - let filter_state = NewF::get_state(world).expect("Could not create filter_state, Please initialize all referenced components before transmuting."); + let mut fetch_state = NewD::get_state(components).expect("Could not create fetch_state, Please initialize all referenced components before transmuting."); + let filter_state = NewF::get_state(components).expect("Could not create filter_state, Please initialize all referenced components before transmuting."); NewD::set_access(&mut fetch_state, &self.component_access); NewD::update_component_access(&fetch_state, &mut component_access); @@ -544,10 +544,10 @@ impl QueryState { /// Will panic if `NewD` contains accesses not in `Q` or `OtherQ`. pub fn join( &self, - world: &World, + components: &Components, other: &QueryState, ) -> QueryState { - self.join_filtered::<_, (), NewD, ()>(world, other) + self.join_filtered::<_, (), NewD, ()>(components, other) } /// Use this to combine two queries. The data accessed will be the intersection @@ -563,7 +563,7 @@ impl QueryState { NewF: QueryFilter, >( &self, - world: &World, + components: &Components, other: &QueryState, ) -> QueryState { if self.world_id != other.world_id { @@ -571,9 +571,9 @@ impl QueryState { } let mut component_access = FilteredAccess::default(); - let mut new_fetch_state = NewD::get_state(world) + let mut new_fetch_state = NewD::get_state(components) .expect("Could not create fetch_state, Please initialize all referenced components before transmuting."); - let new_filter_state = NewF::get_state(world) + let new_filter_state = NewF::get_state(components) .expect("Could not create filter_state, Please initialize all referenced components before transmuting."); NewD::set_access(&mut new_fetch_state, &self.component_access); @@ -1393,7 +1393,7 @@ impl QueryState { /// with a mismatched [`WorldId`] is unsound. /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] pub(crate) unsafe fn par_fold_init_unchecked_manual<'w, T, FN, INIT>( &self, init_accum: INIT, @@ -1779,7 +1779,7 @@ mod tests { world.spawn((A(1), B(0))); let query_state = world.query::<(&A, &B)>(); - let mut new_query_state = query_state.transmute::<&A>(&world); + let mut new_query_state = query_state.transmute::<&A>(world.components()); assert_eq!(new_query_state.iter(&world).len(), 1); let a = new_query_state.single(&world); @@ -1793,7 +1793,7 @@ mod tests { world.spawn((A(1), B(0), C(0))); let query_state = world.query_filtered::<(&A, &B), Without>(); - let mut new_query_state = query_state.transmute::<&A>(&world); + let mut new_query_state = query_state.transmute::<&A>(world.components()); // even though we change the query to not have Without, we do not get the component with C. let a = new_query_state.single(&world); @@ -1807,7 +1807,7 @@ mod tests { let entity = world.spawn(A(10)).id(); let q = world.query::<()>(); - let mut q = q.transmute::(&world); + let mut q = q.transmute::(world.components()); assert_eq!(q.single(&world), entity); } @@ -1817,11 +1817,11 @@ mod tests { world.spawn(A(10)); let q = world.query::<&A>(); - let mut new_q = q.transmute::>(&world); + let mut new_q = q.transmute::>(world.components()); assert!(new_q.single(&world).is_added()); let q = world.query::>(); - let _ = q.transmute::<&A>(&world); + let _ = q.transmute::<&A>(world.components()); } #[test] @@ -1830,8 +1830,8 @@ mod tests { world.spawn(A(0)); let q = world.query::<&mut A>(); - let _ = q.transmute::>(&world); - let _ = q.transmute::<&A>(&world); + let _ = q.transmute::>(world.components()); + let _ = q.transmute::<&A>(world.components()); } #[test] @@ -1840,7 +1840,7 @@ mod tests { world.spawn(A(0)); let q: QueryState> = world.query::(); - let _ = q.transmute::(&world); + let _ = q.transmute::(world.components()); } #[test] @@ -1849,8 +1849,8 @@ mod tests { world.spawn((A(0), B(0))); let query_state = world.query::<(Option<&A>, &B)>(); - let _ = query_state.transmute::>(&world); - let _ = query_state.transmute::<&B>(&world); + let _ = query_state.transmute::>(world.components()); + let _ = query_state.transmute::<&B>(world.components()); } #[test] @@ -1864,7 +1864,7 @@ mod tests { world.spawn(A(0)); let query_state = world.query::<&A>(); - let mut _new_query_state = query_state.transmute::<(&A, &B)>(&world); + let mut _new_query_state = query_state.transmute::<(&A, &B)>(world.components()); } #[test] @@ -1876,7 +1876,7 @@ mod tests { world.spawn(A(0)); let query_state = world.query::<&A>(); - let mut _new_query_state = query_state.transmute::<&mut A>(&world); + let mut _new_query_state = query_state.transmute::<&mut A>(world.components()); } #[test] @@ -1888,7 +1888,7 @@ mod tests { world.spawn(C(0)); let query_state = world.query::>(); - let mut new_query_state = query_state.transmute::<&A>(&world); + let mut new_query_state = query_state.transmute::<&A>(world.components()); let x = new_query_state.single(&world); assert_eq!(x.0, 1234); } @@ -1902,15 +1902,15 @@ mod tests { world.init_component::(); let q = world.query::(); - let _ = q.transmute::<&A>(&world); + let _ = q.transmute::<&A>(world.components()); } #[test] fn can_transmute_filtered_entity() { let mut world = World::new(); let entity = world.spawn((A(0), B(1))).id(); - let query = - QueryState::<(Entity, &A, &B)>::new(&mut world).transmute::(&world); + let query = QueryState::<(Entity, &A, &B)>::new(&mut world) + .transmute::(world.components()); let mut query = query; // Our result is completely untyped @@ -1927,7 +1927,7 @@ mod tests { let entity_a = world.spawn(A(0)).id(); let mut query = QueryState::<(Entity, &A, Has)>::new(&mut world) - .transmute_filtered::<(Entity, Has), Added>(&world); + .transmute_filtered::<(Entity, Has), Added>(world.components()); assert_eq!((entity_a, false), query.single(&world)); @@ -1947,7 +1947,7 @@ mod tests { let entity_a = world.spawn(A(0)).id(); let mut detection_query = QueryState::<(Entity, &A)>::new(&mut world) - .transmute_filtered::>(&world); + .transmute_filtered::>(world.components()); let mut change_query = QueryState::<&mut A>::new(&mut world); assert_eq!(entity_a, detection_query.single(&world)); @@ -1970,7 +1970,7 @@ mod tests { world.init_component::(); world.init_component::(); let query = QueryState::<&A>::new(&mut world); - let _new_query = query.transmute_filtered::>(&world); + let _new_query = query.transmute_filtered::>(world.components()); } #[test] @@ -1983,7 +1983,8 @@ mod tests { let query_1 = QueryState::<&A, Without>::new(&mut world); let query_2 = QueryState::<&B, Without>::new(&mut world); - let mut new_query: QueryState = query_1.join_filtered(&world, &query_2); + let mut new_query: QueryState = + query_1.join_filtered(world.components(), &query_2); assert_eq!(new_query.single(&world), entity_ab); } @@ -1998,7 +1999,8 @@ mod tests { let query_1 = QueryState::<&A>::new(&mut world); let query_2 = QueryState::<&B, Without>::new(&mut world); - let mut new_query: QueryState = query_1.join_filtered(&world, &query_2); + let mut new_query: QueryState = + query_1.join_filtered(world.components(), &query_2); assert!(new_query.get(&world, entity_ab).is_ok()); // should not be able to get entity with c. @@ -2014,7 +2016,7 @@ mod tests { world.init_component::(); let query_1 = QueryState::<&A>::new(&mut world); let query_2 = QueryState::<&B>::new(&mut world); - let _query: QueryState<&C> = query_1.join(&world, &query_2); + let _query: QueryState<&C> = query_1.join(world.components(), &query_2); } #[test] @@ -2028,6 +2030,6 @@ mod tests { let mut world = World::new(); let query_1 = QueryState::<&A, Without>::new(&mut world); let query_2 = QueryState::<&B, Without>::new(&mut world); - let _: QueryState> = query_1.join_filtered(&world, &query_2); + let _: QueryState> = query_1.join_filtered(world.components(), &query_2); } } diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index 19c6a3254b129..7c8283cbfe196 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -1,6 +1,6 @@ use crate::{ archetype::Archetype, - component::{ComponentId, Tick}, + component::{ComponentId, Components, Tick}, entity::Entity, query::FilteredAccess, storage::{Table, TableRow}, @@ -130,8 +130,8 @@ pub unsafe trait WorldQuery { fn init_state(world: &mut World) -> Self::State; /// Attempts to initialize a [`State`](WorldQuery::State) for this [`WorldQuery`] type using read-only - /// access to the [`World`]. - fn get_state(world: &World) -> Option; + /// access to [`Components`]. + fn get_state(components: &Components) -> Option; /// Returns `true` if this query matches a set of components. Otherwise, returns `false`. /// @@ -212,13 +212,13 @@ macro_rules! impl_tuple_world_query { let ($($name,)*) = state; $($name::update_component_access($name, _access);)* } - - fn init_state(_world: &mut World) -> Self::State { - ($($name::init_state(_world),)*) + #[allow(unused_variables)] + fn init_state(world: &mut World) -> Self::State { + ($($name::init_state(world),)*) } - - fn get_state(_world: &World) -> Option { - Some(($($name::get_state(_world)?,)*)) + #[allow(unused_variables)] + fn get_state(components: &Components) -> Option { + Some(($($name::get_state(components)?,)*)) } fn matches_component_set(state: &Self::State, _set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index 4997bfd1103da..7d2f1ab6173ea 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -194,15 +194,12 @@ mod sealed { /// A collection of [run conditions](Condition) that may be useful in any bevy app. pub mod common_conditions { - use bevy_utils::warn_once; - use super::NotSystem; use crate::{ change_detection::DetectChanges, event::{Event, EventReader}, prelude::{Component, Query, With}, removal_detection::RemovedComponents, - schedule::{State, States}, system::{IntoSystem, Res, Resource, System}, }; @@ -657,173 +654,6 @@ pub mod common_conditions { } } - /// A [`Condition`](super::Condition)-satisfying system that returns `true` - /// if the state machine exists. - /// - /// # Example - /// - /// ``` - /// # use bevy_ecs::prelude::*; - /// # #[derive(Resource, Default)] - /// # struct Counter(u8); - /// # let mut app = Schedule::default(); - /// # let mut world = World::new(); - /// # world.init_resource::(); - /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] - /// enum GameState { - /// #[default] - /// Playing, - /// Paused, - /// } - /// - /// app.add_systems( - /// // `state_exists` will only return true if the - /// // given state exists - /// my_system.run_if(state_exists::), - /// ); - /// - /// fn my_system(mut counter: ResMut) { - /// counter.0 += 1; - /// } - /// - /// // `GameState` does not yet exist `my_system` won't run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 0); - /// - /// world.init_resource::>(); - /// - /// // `GameState` now exists so `my_system` will run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 1); - /// ``` - pub fn state_exists(current_state: Option>>) -> bool { - current_state.is_some() - } - - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` - /// if the state machine is currently in `state`. - /// - /// Will return `false` if the state does not exist or if not in `state`. - /// - /// # Example - /// - /// ``` - /// # use bevy_ecs::prelude::*; - /// # #[derive(Resource, Default)] - /// # struct Counter(u8); - /// # let mut app = Schedule::default(); - /// # let mut world = World::new(); - /// # world.init_resource::(); - /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] - /// enum GameState { - /// #[default] - /// Playing, - /// Paused, - /// } - /// - /// world.init_resource::>(); - /// - /// app.add_systems(( - /// // `in_state` will only return true if the - /// // given state equals the given value - /// play_system.run_if(in_state(GameState::Playing)), - /// pause_system.run_if(in_state(GameState::Paused)), - /// )); - /// - /// fn play_system(mut counter: ResMut) { - /// counter.0 += 1; - /// } - /// - /// fn pause_system(mut counter: ResMut) { - /// counter.0 -= 1; - /// } - /// - /// // We default to `GameState::Playing` so `play_system` runs - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 1); - /// - /// *world.resource_mut::>() = State::new(GameState::Paused); - /// - /// // Now that we are in `GameState::Pause`, `pause_system` will run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 0); - /// ``` - pub fn in_state(state: S) -> impl FnMut(Option>>) -> bool + Clone { - move |current_state: Option>>| match current_state { - Some(current_state) => *current_state == state, - None => { - warn_once!("No state matching the type for {} exists - did you forget to `init_state` when initializing the app?", { - let debug_state = format!("{state:?}"); - let result = debug_state - .split("::") - .next() - .unwrap_or("Unknown State Type"); - result.to_string() - }); - - false - } - } - } - - /// A [`Condition`](super::Condition)-satisfying system that returns `true` - /// if the state machine changed state. - /// - /// To do things on transitions to/from specific states, use their respective OnEnter/OnExit - /// schedules. Use this run condition if you want to detect any change, regardless of the value. - /// - /// Returns false if the state does not exist or the state has not changed. - /// - /// # Example - /// - /// ``` - /// # use bevy_ecs::prelude::*; - /// # #[derive(Resource, Default)] - /// # struct Counter(u8); - /// # let mut app = Schedule::default(); - /// # let mut world = World::new(); - /// # world.init_resource::(); - /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] - /// enum GameState { - /// #[default] - /// Playing, - /// Paused, - /// } - /// - /// world.init_resource::>(); - /// - /// app.add_systems( - /// // `state_changed` will only return true if the - /// // given states value has just been updated or - /// // the state has just been added - /// my_system.run_if(state_changed::), - /// ); - /// - /// fn my_system(mut counter: ResMut) { - /// counter.0 += 1; - /// } - /// - /// // `GameState` has just been added so `my_system` will run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 1); - /// - /// // `GameState` has not been updated so `my_system` will not run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 1); - /// - /// *world.resource_mut::>() = State::new(GameState::Paused); - /// - /// // Now that `GameState` has been updated `my_system` will run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 2); - /// ``` - pub fn state_changed(current_state: Option>>) -> bool { - let Some(current_state) = current_state else { - return false; - }; - current_state.is_changed() - } - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` /// if there are any new events of the given type since it was last called. /// @@ -1032,7 +862,6 @@ mod tests { use crate as bevy_ecs; use crate::component::Component; use crate::schedule::IntoSystemConfigs; - use crate::schedule::{State, States}; use crate::system::Local; use crate::{change_detection::ResMut, schedule::Schedule, world::World}; use bevy_ecs_macros::Event; @@ -1131,20 +960,15 @@ mod tests { schedule.run(&mut world); assert_eq!(world.resource::().0, 0); } - - #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] - enum TestState { - #[default] - A, - B, - } - #[derive(Component)] struct TestComponent; #[derive(Event)] struct TestEvent; + #[derive(Resource)] + struct TestResource(()); + fn test_system() {} // Ensure distributive_run_if compiles with the common conditions. @@ -1153,15 +977,12 @@ mod tests { Schedule::default().add_systems( (test_system, test_system) .distributive_run_if(run_once()) - .distributive_run_if(resource_exists::>) - .distributive_run_if(resource_added::>) - .distributive_run_if(resource_changed::>) - .distributive_run_if(resource_exists_and_changed::>) - .distributive_run_if(resource_changed_or_removed::>()) - .distributive_run_if(resource_removed::>()) - .distributive_run_if(state_exists::) - .distributive_run_if(in_state(TestState::A).or_else(in_state(TestState::B))) - .distributive_run_if(state_changed::) + .distributive_run_if(resource_exists::) + .distributive_run_if(resource_added::) + .distributive_run_if(resource_changed::) + .distributive_run_if(resource_exists_and_changed::) + .distributive_run_if(resource_changed_or_removed::()) + .distributive_run_if(resource_removed::()) .distributive_run_if(on_event::()) .distributive_run_if(any_with_component::) .distributive_run_if(not(run_once())), diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index 3bf2f7edeb454..3d76fdb184231 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -38,18 +38,18 @@ pub enum ExecutorKind { /// /// Useful if you're dealing with a single-threaded environment, saving your threads for /// other things, or just trying minimize overhead. - #[cfg_attr(any(target_arch = "wasm32", not(feature = "multi-threaded")), default)] + #[cfg_attr(any(target_arch = "wasm32", not(feature = "multi_threaded")), default)] SingleThreaded, /// Like [`SingleThreaded`](ExecutorKind::SingleThreaded) but calls [`apply_deferred`](crate::system::System::apply_deferred) /// immediately after running each system. Simple, /// Runs the schedule using a thread pool. Non-conflicting systems can run in parallel. - #[cfg_attr(all(not(target_arch = "wasm32"), feature = "multi-threaded"), default)] + #[cfg_attr(all(not(target_arch = "wasm32"), feature = "multi_threaded"), default)] MultiThreaded, } /// Holds systems and conditions of a [`Schedule`](super::Schedule) sorted in topological order -/// (along with dependency information for multi-threaded execution). +/// (along with dependency information for `multi_threaded` execution). /// /// Since the arrays are sorted in the same order, elements are referenced by their index. /// [`FixedBitSet`] is used as a smaller, more efficient substitute of `HashSet`. diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index fa9a19058081e..39606d998e3a2 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -317,7 +317,7 @@ impl<'scope, 'env: 'scope, 'sys> Context<'scope, 'env, 'sys> { } impl MultiThreadedExecutor { - /// Creates a new multi-threaded executor for use with a [`Schedule`]. + /// Creates a new `multi_threaded` executor for use with a [`Schedule`]. /// /// [`Schedule`]: crate::schedule::Schedule pub fn new() -> Self { diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index b38f7adb67923..184bef250a6b5 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -7,7 +7,6 @@ mod graph_utils; #[allow(clippy::module_inception)] mod schedule; mod set; -mod state; mod stepping; pub use self::condition::*; @@ -16,7 +15,6 @@ pub use self::executor::*; use self::graph_utils::*; pub use self::schedule::*; pub use self::set::*; -pub use self::state::*; pub use self::graph_utils::NodeId; diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 380529e3892eb..c9dc06438f0cf 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -78,6 +78,13 @@ impl Schedules { self.inner.get_mut(&label.intern()) } + /// Returns a mutable reference to the schedules associated with `label`, creating one if it doesn't already exist. + pub fn entry(&mut self, label: impl ScheduleLabel) -> &mut Schedule { + self.inner + .entry(label.intern()) + .or_insert_with(|| Schedule::new(label)) + } + /// Returns an iterator over all schedules. Iteration order is undefined. pub fn iter(&self) -> impl Iterator { self.inner @@ -146,6 +153,51 @@ impl Schedules { info!("{}", message); } + + /// Adds one or more systems to the [`Schedule`] matching the provided [`ScheduleLabel`]. + pub fn add_systems( + &mut self, + schedule: impl ScheduleLabel, + systems: impl IntoSystemConfigs, + ) -> &mut Self { + self.entry(schedule).add_systems(systems); + + self + } + + /// Configures a collection of system sets in the provided schedule, adding any sets that do not exist. + #[track_caller] + pub fn configure_sets( + &mut self, + schedule: impl ScheduleLabel, + sets: impl IntoSystemSetConfigs, + ) -> &mut Self { + self.entry(schedule).configure_sets(sets); + + self + } + + /// Suppress warnings and errors that would result from systems in these sets having ambiguities + /// (conflicting access but indeterminate order) with systems in `set`. + /// + /// When possible, do this directly in the `.add_systems(Update, a.ambiguous_with(b))` call. + /// However, sometimes two independent plugins `A` and `B` are reported as ambiguous, which you + /// can only suppress as the consumer of both. + #[track_caller] + pub fn ignore_ambiguity( + &mut self, + schedule: impl ScheduleLabel, + a: S1, + b: S2, + ) -> &mut Self + where + S1: IntoSystemSet, + S2: IntoSystemSet, + { + self.entry(schedule).ignore_ambiguity(a, b); + + self + } } fn make_executor(kind: ExecutorKind) -> Box { @@ -1367,7 +1419,7 @@ impl ScheduleGraph { let hg_node_count = self.hierarchy.graph.node_count(); // get the number of dependencies and the immediate dependents of each system - // (needed by multi-threaded executor to run systems in the correct order) + // (needed by multi_threaded executor to run systems in the correct order) let mut system_dependencies = Vec::with_capacity(sys_count); let mut system_dependents = Vec::with_capacity(sys_count); for &sys_id in &dg_system_ids { @@ -1985,16 +2037,21 @@ pub struct ScheduleNotInitialized; #[cfg(test)] mod tests { + use bevy_ecs_macros::ScheduleLabel; + use crate::{ self as bevy_ecs, prelude::{Res, Resource}, schedule::{ - IntoSystemConfigs, IntoSystemSetConfigs, Schedule, ScheduleBuildSettings, SystemSet, + tests::ResMut, IntoSystemConfigs, IntoSystemSetConfigs, Schedule, + ScheduleBuildSettings, SystemSet, }, system::Commands, world::World, }; + use super::Schedules; + #[derive(Resource)] struct Resource1; @@ -2452,4 +2509,127 @@ mod tests { }); } } + + #[derive(ScheduleLabel, Hash, Debug, Clone, PartialEq, Eq)] + struct TestSchedule; + + #[derive(Resource)] + struct CheckSystemRan(usize); + + #[test] + fn add_systems_to_existing_schedule() { + let mut schedules = Schedules::default(); + let schedule = Schedule::new(TestSchedule); + + schedules.insert(schedule); + schedules.add_systems(TestSchedule, |mut ran: ResMut| ran.0 += 1); + + let mut world = World::new(); + + world.insert_resource(CheckSystemRan(0)); + world.insert_resource(schedules); + world.run_schedule(TestSchedule); + + let value = world + .get_resource::() + .expect("CheckSystemRan Resource Should Exist"); + assert_eq!(value.0, 1); + } + + #[test] + fn add_systems_to_non_existing_schedule() { + let mut schedules = Schedules::default(); + + schedules.add_systems(TestSchedule, |mut ran: ResMut| ran.0 += 1); + + let mut world = World::new(); + + world.insert_resource(CheckSystemRan(0)); + world.insert_resource(schedules); + world.run_schedule(TestSchedule); + + let value = world + .get_resource::() + .expect("CheckSystemRan Resource Should Exist"); + assert_eq!(value.0, 1); + } + + #[derive(SystemSet, Debug, Hash, Clone, PartialEq, Eq)] + enum TestSet { + First, + Second, + } + + #[test] + fn configure_set_on_existing_schedule() { + let mut schedules = Schedules::default(); + let schedule = Schedule::new(TestSchedule); + + schedules.insert(schedule); + + schedules.configure_sets(TestSchedule, (TestSet::First, TestSet::Second).chain()); + schedules.add_systems( + TestSchedule, + (|mut ran: ResMut| { + assert_eq!(ran.0, 0); + ran.0 += 1; + }) + .in_set(TestSet::First), + ); + + schedules.add_systems( + TestSchedule, + (|mut ran: ResMut| { + assert_eq!(ran.0, 1); + ran.0 += 1; + }) + .in_set(TestSet::Second), + ); + + let mut world = World::new(); + + world.insert_resource(CheckSystemRan(0)); + world.insert_resource(schedules); + world.run_schedule(TestSchedule); + + let value = world + .get_resource::() + .expect("CheckSystemRan Resource Should Exist"); + assert_eq!(value.0, 2); + } + + #[test] + fn configure_set_on_new_schedule() { + let mut schedules = Schedules::default(); + + schedules.configure_sets(TestSchedule, (TestSet::First, TestSet::Second).chain()); + schedules.add_systems( + TestSchedule, + (|mut ran: ResMut| { + assert_eq!(ran.0, 0); + ran.0 += 1; + }) + .in_set(TestSet::First), + ); + + schedules.add_systems( + TestSchedule, + (|mut ran: ResMut| { + assert_eq!(ran.0, 1); + ran.0 += 1; + }) + .in_set(TestSet::Second), + ); + + let mut world = World::new(); + + world.insert_resource(CheckSystemRan(0)); + world.insert_resource(schedules); + world.run_schedule(TestSchedule); + + let value = world + .get_resource::() + .expect("CheckSystemRan Resource Should Exist"); + assert_eq!(value.0, 2); + } } diff --git a/crates/bevy_ecs/src/schedule/state.rs b/crates/bevy_ecs/src/schedule/state.rs deleted file mode 100644 index 51b70d894966f..0000000000000 --- a/crates/bevy_ecs/src/schedule/state.rs +++ /dev/null @@ -1,1517 +0,0 @@ -//! In Bevy, states are app-wide interdependent, finite state machines that are generally used to model the large scale structure of your program: whether a game is paused, if the player is in combat, if assets are loaded and so on. -//! -//! This module provides 3 distinct types of state, all of which implement the [`States`] trait: -//! -//! - Standard [`States`] can only be changed by manually setting the [`NextState`] resource. -//! These states are the baseline on which the other state types are built, and can be used on -//! their own for many simple patterns. See the [state example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/state.rs) -//! for a simple use case. -//! - [`SubStates`] are children of other states - they can be changed manually using [`NextState`], -//! but are removed from the [`World`] if the source states aren't in the right state. See the [sub_states example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/sub_states.rs) -//! for a simple use case based on the derive macro, or read the trait docs for more complex scenarios. -//! - [`ComputedStates`] are fully derived from other states - they provide a [`compute`](ComputedStates::compute) method -//! that takes in the source states and returns their derived value. They are particularly useful for situations -//! where a simplified view of the source states is necessary - such as having an `InAMenu` computed state, derived -//! from a source state that defines multiple distinct menus. See the [computed state example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/computed_states.rs) -//! to see usage samples for these states. -//! -//! Most of the utilities around state involve running systems during transitions between states, or -//! determining whether to run certain systems, though they can be used more directly as well. This -//! makes it easier to transition between menus, add loading screens, pause games, and the more. -//! -//! Specifically, Bevy provides the following utilities: -//! -//! - 3 Transition Schedules - [`OnEnter`], [`OnExit`] and [`OnTransition`] - which are used -//! to trigger systems specifically during matching transitions. -//! - A [`StateTransitionEvent`] that gets fired when a given state changes. -//! - The [`in_state`](crate::schedule::condition::in_state) and [`state_changed`](crate::schedule::condition:state_changed) run conditions - which are used -//! to determine whether a system should run based on the current state. - -use std::fmt::Debug; -use std::hash::Hash; -use std::marker::PhantomData; -use std::mem; -use std::ops::Deref; - -use crate as bevy_ecs; -use crate::event::{Event, EventReader, EventWriter}; -use crate::prelude::{FromWorld, Local, Res, ResMut}; -#[cfg(feature = "bevy_reflect")] -use crate::reflect::ReflectResource; -use crate::schedule::ScheduleLabel; -use crate::system::{Commands, In, IntoSystem, Resource}; -use crate::world::World; - -use bevy_ecs_macros::SystemSet; -pub use bevy_ecs_macros::{States, SubStates}; -use bevy_utils::all_tuples; - -use self::sealed::StateSetSealed; - -use super::{InternedScheduleLabel, IntoSystemConfigs, IntoSystemSetConfigs, Schedule, Schedules}; - -/// Types that can define world-wide states in a finite-state machine. -/// -/// The [`Default`] trait defines the starting state. -/// Multiple states can be defined for the same world, -/// allowing you to classify the state of the world across orthogonal dimensions. -/// You can access the current state of type `T` with the [`State`] resource, -/// and the queued state with the [`NextState`] resource. -/// -/// State transitions typically occur in the [`OnEnter`] and [`OnExit`] schedules, -/// which can be run by triggering the [`StateTransition`] schedule. -/// -/// Types used as [`ComputedStates`] do not need to and should not derive [`States`]. -/// [`ComputedStates`] should not be manually mutated: functionality provided -/// by the [`States`] derive and the associated [`FreelyMutableState`] trait. -/// -/// # Example -/// -/// ``` -/// use bevy_ecs::prelude::States; -/// -/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] -/// enum GameState { -/// #[default] -/// MainMenu, -/// SettingsMenu, -/// InGame, -/// } -/// -/// ``` -pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug { - /// How many other states this state depends on. - /// Used to help order transitions and de-duplicate [`ComputedStates`], as well as prevent cyclical - /// `ComputedState` dependencies. - const DEPENDENCY_DEPTH: usize = 1; -} - -/// This trait allows a state to be mutated directly using the [`NextState`] resource. -/// -/// While ordinary states are freely mutable (and implement this trait as part of their derive macro), -/// computed states are not: instead, they can *only* change when the states that drive them do. -pub trait FreelyMutableState: States { - /// This function registers all the necessary systems to apply state changes and run transition schedules - fn register_state(schedule: &mut Schedule) { - schedule - .add_systems( - apply_state_transition::.in_set(ApplyStateTransition::::apply()), - ) - .add_systems( - should_run_transition::> - .pipe(run_enter::) - .in_set(StateTransitionSteps::EnterSchedules), - ) - .add_systems( - should_run_transition::> - .pipe(run_exit::) - .in_set(StateTransitionSteps::ExitSchedules), - ) - .add_systems( - should_run_transition::> - .pipe(run_transition::) - .in_set(StateTransitionSteps::TransitionSchedules), - ) - .configure_sets( - ApplyStateTransition::::apply() - .in_set(StateTransitionSteps::ManualTransitions), - ); - } -} - -/// The label of a [`Schedule`] that runs whenever [`State`] -/// enters this state. -#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] -pub struct OnEnter(pub S); - -/// The label of a [`Schedule`] that runs whenever [`State`] -/// exits this state. -#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] -pub struct OnExit(pub S); - -/// The label of a [`Schedule`] that **only** runs whenever [`State`] -/// exits the `from` state, AND enters the `to` state. -/// -/// Systems added to this schedule are always ran *after* [`OnExit`], and *before* [`OnEnter`]. -#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] -pub struct OnTransition { - /// The state being exited. - pub from: S, - /// The state being entered. - pub to: S, -} - -/// A finite-state machine whose transitions have associated schedules -/// ([`OnEnter(state)`] and [`OnExit(state)`]). -/// -/// The current state value can be accessed through this resource. To *change* the state, -/// queue a transition in the [`NextState`] resource, and it will be applied by the next -/// [`apply_state_transition::`] system. -/// -/// The starting state is defined via the [`Default`] implementation for `S`. -/// -/// ``` -/// use bevy_ecs::prelude::*; -/// -/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] -/// enum GameState { -/// #[default] -/// MainMenu, -/// SettingsMenu, -/// InGame, -/// } -/// -/// fn game_logic(game_state: Res>) { -/// match game_state.get() { -/// GameState::InGame => { -/// // Run game logic here... -/// }, -/// _ => {}, -/// } -/// } -/// ``` -#[derive(Resource, Debug)] -#[cfg_attr( - feature = "bevy_reflect", - derive(bevy_reflect::Reflect), - reflect(Resource) -)] -pub struct State(S); - -impl State { - /// Creates a new state with a specific value. - /// - /// To change the state use [`NextState`] rather than using this to modify the `State`. - pub fn new(state: S) -> Self { - Self(state) - } - - /// Get the current state. - pub fn get(&self) -> &S { - &self.0 - } -} - -impl FromWorld for State { - fn from_world(world: &mut World) -> Self { - Self(S::from_world(world)) - } -} - -impl PartialEq for State { - fn eq(&self, other: &S) -> bool { - self.get() == other - } -} - -impl Deref for State { - type Target = S; - - fn deref(&self) -> &Self::Target { - self.get() - } -} - -/// The next state of [`State`]. -/// -/// To queue a transition, just set the contained value to `Some(next_state)`. -/// Note that these transitions can be overridden by other systems: -/// only the actual value of this resource at the time of [`apply_state_transition`] matters. -/// -/// ``` -/// use bevy_ecs::prelude::*; -/// -/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] -/// enum GameState { -/// #[default] -/// MainMenu, -/// SettingsMenu, -/// InGame, -/// } -/// -/// fn start_game(mut next_game_state: ResMut>) { -/// next_game_state.set(GameState::InGame); -/// } -/// ``` -#[derive(Resource, Debug, Default)] -#[cfg_attr( - feature = "bevy_reflect", - derive(bevy_reflect::Reflect), - reflect(Resource) -)] -pub enum NextState { - /// No state transition is pending - #[default] - Unchanged, - /// There is a pending transition for state `S` - Pending(S), -} - -impl NextState { - /// Tentatively set a pending state transition to `Some(state)`. - pub fn set(&mut self, state: S) { - *self = Self::Pending(state); - } - - /// Remove any pending changes to [`State`] - pub fn reset(&mut self) { - *self = Self::Unchanged; - } -} - -/// Event sent when any state transition of `S` happens. -/// -/// If you know exactly what state you want to respond to ahead of time, consider [`OnEnter`], [`OnTransition`], or [`OnExit`] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Event)] -pub struct StateTransitionEvent { - /// the state we were in before - pub before: Option, - /// the state we're in now - pub after: Option, -} - -/// Runs [state transitions](States). -#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] -pub struct StateTransition; - -/// Applies manual state transitions using [`NextState`]. -/// -/// These system sets are run sequentially, in the order of the enum variants. -#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] -enum StateTransitionSteps { - ManualTransitions, - DependentTransitions, - ExitSchedules, - TransitionSchedules, - EnterSchedules, -} - -/// Defines a system set to aid with dependent state ordering -#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] -pub struct ApplyStateTransition(PhantomData); - -impl ApplyStateTransition { - fn apply() -> Self { - Self(PhantomData) - } -} - -/// This function actually applies a state change, and registers the required -/// schedules for downstream computed states and transition schedules. -/// -/// The `new_state` is an option to allow for removal - `None` will trigger the -/// removal of the `State` resource from the [`World`]. -fn internal_apply_state_transition( - mut event: EventWriter>, - mut commands: Commands, - current_state: Option>>, - new_state: Option, -) { - match new_state { - Some(entered) => { - match current_state { - // If the [`State`] resource exists, and the state is not the one we are - // entering - we need to set the new value, compute dependant states, send transition events - // and register transition schedules. - Some(mut state_resource) => { - if *state_resource != entered { - let exited = mem::replace(&mut state_resource.0, entered.clone()); - - event.send(StateTransitionEvent { - before: Some(exited.clone()), - after: Some(entered.clone()), - }); - } - } - None => { - // If the [`State`] resource does not exist, we create it, compute dependant states, send a transition event and register the `OnEnter` schedule. - commands.insert_resource(State(entered.clone())); - - event.send(StateTransitionEvent { - before: None, - after: Some(entered.clone()), - }); - } - }; - } - None => { - // We first remove the [`State`] resource, and if one existed we compute dependant states, send a transition event and run the `OnExit` schedule. - if let Some(resource) = current_state { - commands.remove_resource::>(); - - event.send(StateTransitionEvent { - before: Some(resource.get().clone()), - after: None, - }); - } - } - } -} - -/// Sets up the schedules and systems for handling state transitions -/// within a [`World`]. -/// -/// Runs automatically when using `App` to insert states, but needs to -/// be added manually in other situations. -pub fn setup_state_transitions_in_world( - world: &mut World, - startup_label: Option, -) { - let mut schedules = world.get_resource_or_insert_with(Schedules::default); - if schedules.contains(StateTransition) { - return; - } - let mut schedule = Schedule::new(StateTransition); - schedule.configure_sets( - ( - StateTransitionSteps::ManualTransitions, - StateTransitionSteps::DependentTransitions, - StateTransitionSteps::ExitSchedules, - StateTransitionSteps::TransitionSchedules, - StateTransitionSteps::EnterSchedules, - ) - .chain(), - ); - schedules.insert(schedule); - - if let Some(startup) = startup_label { - match schedules.get_mut(startup) { - Some(schedule) => { - schedule.add_systems(|world: &mut World| { - let _ = world.try_run_schedule(StateTransition); - }); - } - None => { - let mut schedule = Schedule::new(startup); - - schedule.add_systems(|world: &mut World| { - let _ = world.try_run_schedule(StateTransition); - }); - - schedules.insert(schedule); - } - } - } -} - -/// If a new state is queued in [`NextState`], this system: -/// - Takes the new state value from [`NextState`] and updates [`State`]. -/// - Sends a relevant [`StateTransitionEvent`] -/// - Runs the [`OnExit(exited_state)`] schedule, if it exists. -/// - Runs the [`OnTransition { from: exited_state, to: entered_state }`](OnTransition), if it exists. -/// - Derive any dependent states through the [`ComputeDependantStates::`] schedule, if it exists. -/// - Runs the [`OnEnter(entered_state)`] schedule, if it exists. -/// -/// If the [`State`] resource does not exist, it does nothing. Removing or adding states -/// should be done at App creation or at your own risk. -/// -/// For [`SubStates`] - it only applies the state if the `SubState` currently exists. Otherwise, it is wiped. -/// When a `SubState` is re-created, it will use the result of it's `should_exist` method. -pub fn apply_state_transition( - event: EventWriter>, - commands: Commands, - current_state: Option>>, - next_state: Option>>, -) { - // We want to check if the State and NextState resources exist - let Some(next_state_resource) = next_state else { - return; - }; - - match next_state_resource.as_ref() { - NextState::Pending(new_state) => { - if let Some(current_state) = current_state { - if new_state != current_state.get() { - let new_state = new_state.clone(); - internal_apply_state_transition( - event, - commands, - Some(current_state), - Some(new_state), - ); - } - } - } - NextState::Unchanged => { - // This is the default value, so we don't need to re-insert the resource - return; - } - } - - *next_state_resource.value = NextState::::Unchanged; -} - -fn should_run_transition( - first: Local, - res: Option>>, - mut event: EventReader>, -) -> (Option>, PhantomData) { - if !*first.0 { - *first.0 = true; - if let Some(res) = res { - event.clear(); - - return ( - Some(StateTransitionEvent { - before: None, - after: Some(res.get().clone()), - }), - PhantomData, - ); - } - } - (event.read().last().cloned(), PhantomData) -} - -fn run_enter( - In((transition, _)): In<(Option>, PhantomData>)>, - world: &mut World, -) { - let Some(transition) = transition else { - return; - }; - - let Some(after) = transition.after else { - return; - }; - - let _ = world.try_run_schedule(OnEnter(after)); -} - -fn run_exit( - In((transition, _)): In<(Option>, PhantomData>)>, - world: &mut World, -) { - let Some(transition) = transition else { - return; - }; - - let Some(before) = transition.before else { - return; - }; - - let _ = world.try_run_schedule(OnExit(before)); -} - -fn run_transition( - In((transition, _)): In<( - Option>, - PhantomData>, - )>, - world: &mut World, -) { - let Some(transition) = transition else { - return; - }; - let Some(from) = transition.before else { - return; - }; - let Some(to) = transition.after else { - return; - }; - - let _ = world.try_run_schedule(OnTransition { from, to }); -} - -/// A state whose value is automatically computed based on the values of other [`States`]. -/// -/// A **computed state** is a state that is deterministically derived from a set of `SourceStates`. -/// The [`StateSet`] is passed into the `compute` method whenever one of them changes, and the -/// result becomes the state's value. -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// -/// /// Computed States require some state to derive from -/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] -/// enum AppState { -/// #[default] -/// Menu, -/// InGame { paused: bool } -/// } -/// -/// -/// #[derive(Clone, PartialEq, Eq, Hash, Debug)] -/// struct InGame; -/// -/// impl ComputedStates for InGame { -/// /// We set the source state to be the state, or a tuple of states, -/// /// we want to depend on. You can also wrap each state in an Option, -/// /// if you want the computed state to execute even if the state doesn't -/// /// currently exist in the world. -/// type SourceStates = AppState; -/// -/// /// We then define the compute function, which takes in -/// /// your SourceStates -/// fn compute(sources: AppState) -> Option { -/// match sources { -/// /// When we are in game, we want to return the InGame state -/// AppState::InGame { .. } => Some(InGame), -/// /// Otherwise, we don't want the `State` resource to exist, -/// /// so we return None. -/// _ => None -/// } -/// } -/// } -/// ``` -/// -/// you can then add it to an App, and from there you use the state as normal -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// -/// # struct App; -/// # impl App { -/// # fn new() -> Self { App } -/// # fn init_state(&mut self) -> &mut Self {self} -/// # fn add_computed_state(&mut self) -> &mut Self {self} -/// # } -/// # struct AppState; -/// # struct InGame; -/// -/// App::new() -/// .init_state::() -/// .add_computed_state::(); -/// ``` -pub trait ComputedStates: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug { - /// The set of states from which the [`Self`] is derived. - /// - /// This can either be a single type that implements [`States`], an Option of a type - /// that implements [`States`], or a tuple - /// containing multiple types that implement [`States`] or Optional versions of them. - /// - /// For example, `(MapState, EnemyState)` is valid, as is `(MapState, Option)` - type SourceStates: StateSet; - - /// Computes the next value of [`State`]. - /// This function gets called whenever one of the [`SourceStates`](Self::SourceStates) changes. - /// - /// If the result is [`None`], the [`State`] resource will be removed from the world. - fn compute(sources: Self::SourceStates) -> Option; - - /// This function sets up systems that compute the state whenever one of the [`SourceStates`](Self::SourceStates) - /// change. It is called by `App::add_computed_state`, but can be called manually if `App` is not - /// used. - fn register_computed_state_systems(schedule: &mut Schedule) { - Self::SourceStates::register_computed_state_systems_in_schedule::(schedule); - } -} - -impl States for S { - const DEPENDENCY_DEPTH: usize = S::SourceStates::SET_DEPENDENCY_DEPTH + 1; -} - -mod sealed { - /// Sealed trait used to prevent external implementations of [`StateSet`](super::StateSet). - pub trait StateSetSealed {} -} - -/// A [`States`] type or tuple of types which implement [`States`]. -/// -/// This trait is used allow implementors of [`States`], as well -/// as tuples containing exclusively implementors of [`States`], to -/// be used as [`ComputedStates::SourceStates`]. -/// -/// It is sealed, and auto implemented for all [`States`] types and -/// tuples containing them. -pub trait StateSet: sealed::StateSetSealed { - /// The total [`DEPENDENCY_DEPTH`](`States::DEPENDENCY_DEPTH`) of all - /// the states that are part of this [`StateSet`], added together. - /// - /// Used to de-duplicate computed state executions and prevent cyclic - /// computed states. - const SET_DEPENDENCY_DEPTH: usize; - - /// Sets up the systems needed to compute `T` whenever any `State` in this - /// `StateSet` is changed. - fn register_computed_state_systems_in_schedule>( - schedule: &mut Schedule, - ); - - /// Sets up the systems needed to compute whether `T` exists whenever any `State` in this - /// `StateSet` is changed. - fn register_sub_state_systems_in_schedule>( - schedule: &mut Schedule, - ); -} - -/// The `InnerStateSet` trait is used to isolate [`ComputedStates`] & [`SubStates`] from -/// needing to wrap all state dependencies in an [`Option`]. -/// -/// Some [`ComputedStates`]'s might need to exist in different states based on the existence -/// of other states. So we needed the ability to use[`Option`] when appropriate. -/// -/// The isolation works because it is implemented for both S & [`Option`], and has the `RawState` associated type -/// that allows it to know what the resource in the world should be. We can then essentially "unwrap" it in our -/// `StateSet` implementation - and the behaviour of that unwrapping will depend on the arguments expected by the -/// the [`ComputedStates`] & [`SubStates]`. -trait InnerStateSet: Sized { - type RawState: States; - - const DEPENDENCY_DEPTH: usize; - - fn convert_to_usable_state(wrapped: Option<&State>) -> Option; -} - -impl InnerStateSet for S { - type RawState = Self; - - const DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH; - - fn convert_to_usable_state(wrapped: Option<&State>) -> Option { - wrapped.map(|v| v.0.clone()) - } -} - -impl InnerStateSet for Option { - type RawState = S; - - const DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH; - - fn convert_to_usable_state(wrapped: Option<&State>) -> Option { - Some(wrapped.map(|v| v.0.clone())) - } -} - -impl StateSetSealed for S {} - -impl StateSet for S { - const SET_DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH; - - fn register_computed_state_systems_in_schedule>( - schedule: &mut Schedule, - ) { - let system = |mut parent_changed: EventReader>, - event: EventWriter>, - commands: Commands, - current_state: Option>>, - state_set: Option>>| { - if parent_changed.is_empty() { - return; - } - parent_changed.clear(); - - let new_state = - if let Some(state_set) = S::convert_to_usable_state(state_set.as_deref()) { - T::compute(state_set) - } else { - None - }; - - internal_apply_state_transition(event, commands, current_state, new_state); - }; - - schedule - .add_systems(system.in_set(ApplyStateTransition::::apply())) - .add_systems( - should_run_transition::> - .pipe(run_enter::) - .in_set(StateTransitionSteps::EnterSchedules), - ) - .add_systems( - should_run_transition::> - .pipe(run_exit::) - .in_set(StateTransitionSteps::ExitSchedules), - ) - .add_systems( - should_run_transition::> - .pipe(run_transition::) - .in_set(StateTransitionSteps::TransitionSchedules), - ) - .configure_sets( - ApplyStateTransition::::apply() - .in_set(StateTransitionSteps::DependentTransitions) - .after(ApplyStateTransition::::apply()), - ); - } - - fn register_sub_state_systems_in_schedule>( - schedule: &mut Schedule, - ) { - let system = |mut parent_changed: EventReader>, - event: EventWriter>, - commands: Commands, - current_state: Option>>, - state_set: Option>>| { - if parent_changed.is_empty() { - return; - } - parent_changed.clear(); - - let new_state = - if let Some(state_set) = S::convert_to_usable_state(state_set.as_deref()) { - T::should_exist(state_set) - } else { - None - }; - - match new_state { - Some(value) => { - if current_state.is_none() { - internal_apply_state_transition( - event, - commands, - current_state, - Some(value), - ); - } - } - None => { - internal_apply_state_transition(event, commands, current_state, None); - } - }; - }; - - schedule - .add_systems(system.in_set(ApplyStateTransition::::apply())) - .add_systems( - apply_state_transition::.in_set(StateTransitionSteps::ManualTransitions), - ) - .add_systems( - should_run_transition::> - .pipe(run_enter::) - .in_set(StateTransitionSteps::EnterSchedules), - ) - .add_systems( - should_run_transition::> - .pipe(run_exit::) - .in_set(StateTransitionSteps::ExitSchedules), - ) - .add_systems( - should_run_transition::> - .pipe(run_transition::) - .in_set(StateTransitionSteps::TransitionSchedules), - ) - .configure_sets( - ApplyStateTransition::::apply() - .in_set(StateTransitionSteps::DependentTransitions) - .after(ApplyStateTransition::::apply()), - ); - } -} - -/// A sub-state is a state that exists only when the source state meet certain conditions, -/// but unlike [`ComputedStates`] - while they exist they can be manually modified. -/// -/// The default approach to creating [`SubStates`] is using the derive macro, and defining a single source state -/// and value to determine it's existence. -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// -/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] -/// enum AppState { -/// #[default] -/// Menu, -/// InGame -/// } -/// -/// -/// #[derive(SubStates, Clone, PartialEq, Eq, Hash, Debug, Default)] -/// #[source(AppState = AppState::InGame)] -/// enum GamePhase { -/// #[default] -/// Setup, -/// Battle, -/// Conclusion -/// } -/// ``` -/// -/// you can then add it to an App, and from there you use the state as normal: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// -/// # struct App; -/// # impl App { -/// # fn new() -> Self { App } -/// # fn init_state(&mut self) -> &mut Self {self} -/// # fn add_sub_state(&mut self) -> &mut Self {self} -/// # } -/// # struct AppState; -/// # struct GamePhase; -/// -/// App::new() -/// .init_state::() -/// .add_sub_state::(); -/// ``` -/// -/// In more complex situations, the recommendation is to use an intermediary computed state, like so: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// -/// /// Computed States require some state to derive from -/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] -/// enum AppState { -/// #[default] -/// Menu, -/// InGame { paused: bool } -/// } -/// -/// #[derive(Clone, PartialEq, Eq, Hash, Debug)] -/// struct InGame; -/// -/// impl ComputedStates for InGame { -/// /// We set the source state to be the state, or set of states, -/// /// we want to depend on. Any of the states can be wrapped in an Option. -/// type SourceStates = Option; -/// -/// /// We then define the compute function, which takes in the AppState -/// fn compute(sources: Option) -> Option { -/// match sources { -/// /// When we are in game, we want to return the InGame state -/// Some(AppState::InGame { .. }) => Some(InGame), -/// /// Otherwise, we don't want the `State` resource to exist, -/// /// so we return None. -/// _ => None -/// } -/// } -/// } -/// -/// #[derive(SubStates, Clone, PartialEq, Eq, Hash, Debug, Default)] -/// #[source(InGame = InGame)] -/// enum GamePhase { -/// #[default] -/// Setup, -/// Battle, -/// Conclusion -/// } -/// ``` -/// -/// However, you can also manually implement them. If you do so, you'll also need to manually implement the `States` & `FreelyMutableState` traits. -/// Unlike the derive, this does not require an implementation of [`Default`], since you are providing the `exists` function -/// directly. -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # use bevy_ecs::schedule::FreelyMutableState; -/// -/// /// Computed States require some state to derive from -/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] -/// enum AppState { -/// #[default] -/// Menu, -/// InGame { paused: bool } -/// } -/// -/// #[derive(Clone, PartialEq, Eq, Hash, Debug)] -/// enum GamePhase { -/// Setup, -/// Battle, -/// Conclusion -/// } -/// -/// impl SubStates for GamePhase { -/// /// We set the source state to be the state, or set of states, -/// /// we want to depend on. Any of the states can be wrapped in an Option. -/// type SourceStates = Option; -/// -/// /// We then define the compute function, which takes in the [`Self::SourceStates`] -/// fn should_exist(sources: Option) -> Option { -/// match sources { -/// /// When we are in game, so we want a GamePhase state to exist, and the default is -/// /// GamePhase::Setup -/// Some(AppState::InGame { .. }) => Some(GamePhase::Setup), -/// /// Otherwise, we don't want the `State` resource to exist, -/// /// so we return None. -/// _ => None -/// } -/// } -/// } -/// -/// impl States for GamePhase { -/// const DEPENDENCY_DEPTH : usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; -/// } -/// -/// impl FreelyMutableState for GamePhase {} -/// ``` -pub trait SubStates: States + FreelyMutableState { - /// The set of states from which the [`Self`] is derived. - /// - /// This can either be a single type that implements [`States`], or a tuple - /// containing multiple types that implement [`States`], or any combination of - /// types implementing [`States`] and Options of types implementing [`States`] - type SourceStates: StateSet; - - /// This function gets called whenever one of the [`SourceStates`](Self::SourceStates) changes. - /// The result is used to determine the existence of [`State`]. - /// - /// If the result is [`None`], the [`State`] resource will be removed from the world, otherwise - /// if the [`State`] resource doesn't exist - it will be created with the [`Some`] value. - fn should_exist(sources: Self::SourceStates) -> Option; - - /// This function sets up systems that compute the state whenever one of the [`SourceStates`](Self::SourceStates) - /// change. It is called by `App::add_computed_state`, but can be called manually if `App` is not - /// used. - fn register_sub_state_systems(schedule: &mut Schedule) { - Self::SourceStates::register_sub_state_systems_in_schedule::(schedule); - } -} - -macro_rules! impl_state_set_sealed_tuples { - ($(($param: ident, $val: ident, $evt: ident)), *) => { - impl<$($param: InnerStateSet),*> StateSetSealed for ($($param,)*) {} - - impl<$($param: InnerStateSet),*> StateSet for ($($param,)*) { - - const SET_DEPENDENCY_DEPTH : usize = $($param::DEPENDENCY_DEPTH +)* 0; - - - fn register_computed_state_systems_in_schedule>( - schedule: &mut Schedule, - ) { - let system = |($(mut $evt),*,): ($(EventReader>),*,), event: EventWriter>, commands: Commands, current_state: Option>>, ($($val),*,): ($(Option>>),*,)| { - if ($($evt.is_empty())&&*) { - return; - } - $($evt.clear();)* - - let new_state = if let ($(Some($val)),*,) = ($($param::convert_to_usable_state($val.as_deref())),*,) { - T::compute(($($val),*, )) - } else { - None - }; - - internal_apply_state_transition(event, commands, current_state, new_state); - }; - - schedule - .add_systems(system.in_set(ApplyStateTransition::::apply())) - .add_systems(should_run_transition::>.pipe(run_enter::).in_set(StateTransitionSteps::EnterSchedules)) - .add_systems(should_run_transition::>.pipe(run_exit::).in_set(StateTransitionSteps::ExitSchedules)) - .add_systems(should_run_transition::>.pipe(run_transition::).in_set(StateTransitionSteps::TransitionSchedules)) - .configure_sets( - ApplyStateTransition::::apply() - .in_set(StateTransitionSteps::DependentTransitions) - $(.after(ApplyStateTransition::<$param::RawState>::apply()))* - ); - } - - fn register_sub_state_systems_in_schedule>( - schedule: &mut Schedule, - ) { - let system = |($(mut $evt),*,): ($(EventReader>),*,), event: EventWriter>, commands: Commands, current_state: Option>>, ($($val),*,): ($(Option>>),*,)| { - if ($($evt.is_empty())&&*) { - return; - } - $($evt.clear();)* - - let new_state = if let ($(Some($val)),*,) = ($($param::convert_to_usable_state($val.as_deref())),*,) { - T::should_exist(($($val),*, )) - } else { - None - }; - match new_state { - Some(value) => { - if current_state.is_none() { - internal_apply_state_transition(event, commands, current_state, Some(value)); - } - } - None => { - internal_apply_state_transition(event, commands, current_state, None); - }, - }; - }; - - schedule - .add_systems(system.in_set(ApplyStateTransition::::apply())) - .add_systems(apply_state_transition::.in_set(StateTransitionSteps::ManualTransitions)) - .add_systems(should_run_transition::>.pipe(run_enter::).in_set(StateTransitionSteps::EnterSchedules)) - .add_systems(should_run_transition::>.pipe(run_exit::).in_set(StateTransitionSteps::ExitSchedules)) - .add_systems(should_run_transition::>.pipe(run_transition::).in_set(StateTransitionSteps::TransitionSchedules)) - .configure_sets( - ApplyStateTransition::::apply() - .in_set(StateTransitionSteps::DependentTransitions) - $(.after(ApplyStateTransition::<$param::RawState>::apply()))* - ); - } - } - }; -} - -all_tuples!(impl_state_set_sealed_tuples, 1, 15, S, s, ereader); - -#[cfg(test)] -mod tests { - use bevy_ecs_macros::SubStates; - - use super::*; - use crate as bevy_ecs; - - use crate::event::EventRegistry; - - use crate::prelude::ResMut; - - #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] - enum SimpleState { - #[default] - A, - B(bool), - } - - #[derive(PartialEq, Eq, Debug, Hash, Clone)] - enum TestComputedState { - BisTrue, - BisFalse, - } - - impl ComputedStates for TestComputedState { - type SourceStates = Option; - - fn compute(sources: Option) -> Option { - sources.and_then(|source| match source { - SimpleState::A => None, - SimpleState::B(value) => Some(if value { Self::BisTrue } else { Self::BisFalse }), - }) - } - } - - #[test] - fn computed_state_with_a_single_source_is_correctly_derived() { - let mut world = World::new(); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - world.init_resource::>(); - let mut schedules = Schedules::new(); - let mut apply_changes = Schedule::new(StateTransition); - TestComputedState::register_computed_state_systems(&mut apply_changes); - SimpleState::register_state(&mut apply_changes); - schedules.insert(apply_changes); - - world.insert_resource(schedules); - - setup_state_transitions_in_world(&mut world, None); - - world.run_schedule(StateTransition); - assert_eq!(world.resource::>().0, SimpleState::A); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(SimpleState::B(true))); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(true) - ); - assert_eq!( - world.resource::>().0, - TestComputedState::BisTrue - ); - - world.insert_resource(NextState::Pending(SimpleState::B(false))); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(false) - ); - assert_eq!( - world.resource::>().0, - TestComputedState::BisFalse - ); - - world.insert_resource(NextState::Pending(SimpleState::A)); - world.run_schedule(StateTransition); - assert_eq!(world.resource::>().0, SimpleState::A); - assert!(!world.contains_resource::>()); - } - - #[derive(SubStates, PartialEq, Eq, Debug, Default, Hash, Clone)] - #[source(SimpleState = SimpleState::B(true))] - enum SubState { - #[default] - One, - Two, - } - - #[test] - fn sub_state_exists_only_when_allowed_but_can_be_modified_freely() { - let mut world = World::new(); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - world.init_resource::>(); - let mut schedules = Schedules::new(); - let mut apply_changes = Schedule::new(StateTransition); - SubState::register_sub_state_systems(&mut apply_changes); - SimpleState::register_state(&mut apply_changes); - schedules.insert(apply_changes); - - world.insert_resource(schedules); - - setup_state_transitions_in_world(&mut world, None); - - world.run_schedule(StateTransition); - assert_eq!(world.resource::>().0, SimpleState::A); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(SubState::Two)); - world.run_schedule(StateTransition); - assert_eq!(world.resource::>().0, SimpleState::A); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(SimpleState::B(true))); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(true) - ); - assert_eq!(world.resource::>().0, SubState::One); - - world.insert_resource(NextState::Pending(SubState::Two)); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(true) - ); - assert_eq!(world.resource::>().0, SubState::Two); - - world.insert_resource(NextState::Pending(SimpleState::B(false))); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(false) - ); - assert!(!world.contains_resource::>()); - } - - #[derive(SubStates, PartialEq, Eq, Debug, Default, Hash, Clone)] - #[source(TestComputedState = TestComputedState::BisTrue)] - enum SubStateOfComputed { - #[default] - One, - Two, - } - - #[test] - fn substate_of_computed_states_works_appropriately() { - let mut world = World::new(); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - world.init_resource::>(); - let mut schedules = Schedules::new(); - let mut apply_changes = Schedule::new(StateTransition); - TestComputedState::register_computed_state_systems(&mut apply_changes); - SubStateOfComputed::register_sub_state_systems(&mut apply_changes); - SimpleState::register_state(&mut apply_changes); - schedules.insert(apply_changes); - - world.insert_resource(schedules); - - setup_state_transitions_in_world(&mut world, None); - - world.run_schedule(StateTransition); - assert_eq!(world.resource::>().0, SimpleState::A); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(SubStateOfComputed::Two)); - world.run_schedule(StateTransition); - assert_eq!(world.resource::>().0, SimpleState::A); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(SimpleState::B(true))); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(true) - ); - assert_eq!( - world.resource::>().0, - SubStateOfComputed::One - ); - - world.insert_resource(NextState::Pending(SubStateOfComputed::Two)); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(true) - ); - assert_eq!( - world.resource::>().0, - SubStateOfComputed::Two - ); - - world.insert_resource(NextState::Pending(SimpleState::B(false))); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(false) - ); - assert!(!world.contains_resource::>()); - } - - #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] - struct OtherState { - a_flexible_value: &'static str, - another_value: u8, - } - - #[derive(PartialEq, Eq, Debug, Hash, Clone)] - enum ComplexComputedState { - InAAndStrIsBobOrJane, - InTrueBAndUsizeAbove8, - } - - impl ComputedStates for ComplexComputedState { - type SourceStates = (Option, Option); - - fn compute(sources: (Option, Option)) -> Option { - match sources { - (Some(simple), Some(complex)) => { - if simple == SimpleState::A - && (complex.a_flexible_value == "bob" || complex.a_flexible_value == "jane") - { - Some(ComplexComputedState::InAAndStrIsBobOrJane) - } else if simple == SimpleState::B(true) && complex.another_value > 8 { - Some(ComplexComputedState::InTrueBAndUsizeAbove8) - } else { - None - } - } - _ => None, - } - } - } - - #[test] - fn complex_computed_state_gets_derived_correctly() { - let mut world = World::new(); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - world.init_resource::>(); - world.init_resource::>(); - - let mut schedules = Schedules::new(); - let mut apply_changes = Schedule::new(StateTransition); - - ComplexComputedState::register_computed_state_systems(&mut apply_changes); - - SimpleState::register_state(&mut apply_changes); - OtherState::register_state(&mut apply_changes); - schedules.insert(apply_changes); - - world.insert_resource(schedules); - - setup_state_transitions_in_world(&mut world, None); - - world.run_schedule(StateTransition); - assert_eq!(world.resource::>().0, SimpleState::A); - assert_eq!( - world.resource::>().0, - OtherState::default() - ); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(SimpleState::B(true))); - world.run_schedule(StateTransition); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(OtherState { - a_flexible_value: "felix", - another_value: 13, - })); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - ComplexComputedState::InTrueBAndUsizeAbove8 - ); - - world.insert_resource(NextState::Pending(SimpleState::A)); - world.insert_resource(NextState::Pending(OtherState { - a_flexible_value: "jane", - another_value: 13, - })); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - ComplexComputedState::InAAndStrIsBobOrJane - ); - - world.insert_resource(NextState::Pending(SimpleState::B(false))); - world.insert_resource(NextState::Pending(OtherState { - a_flexible_value: "jane", - another_value: 13, - })); - world.run_schedule(StateTransition); - assert!(!world.contains_resource::>()); - } - - #[derive(Resource, Default)] - struct ComputedStateTransitionCounter { - enter: usize, - exit: usize, - } - - #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] - enum SimpleState2 { - #[default] - A1, - B2, - } - - #[derive(PartialEq, Eq, Debug, Hash, Clone)] - enum TestNewcomputedState { - A1, - B2, - B1, - } - - impl ComputedStates for TestNewcomputedState { - type SourceStates = (Option, Option); - - fn compute((s1, s2): (Option, Option)) -> Option { - match (s1, s2) { - (Some(SimpleState::A), Some(SimpleState2::A1)) => Some(TestNewcomputedState::A1), - (Some(SimpleState::B(true)), Some(SimpleState2::B2)) => { - Some(TestNewcomputedState::B2) - } - (Some(SimpleState::B(true)), _) => Some(TestNewcomputedState::B1), - _ => None, - } - } - } - - #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] - struct Startup; - - #[test] - fn computed_state_transitions_are_produced_correctly() { - let mut world = World::new(); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - world.init_resource::>(); - world.init_resource::>(); - world.init_resource::(); - - setup_state_transitions_in_world(&mut world, Some(Startup.intern())); - - let mut schedules = world - .get_resource_mut::() - .expect("Schedules don't exist in world"); - let apply_changes = schedules - .get_mut(StateTransition) - .expect("State Transition Schedule Doesn't Exist"); - - TestNewcomputedState::register_computed_state_systems(apply_changes); - - SimpleState::register_state(apply_changes); - SimpleState2::register_state(apply_changes); - - schedules.insert({ - let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::A1)); - schedule.add_systems(|mut count: ResMut| { - count.enter += 1; - }); - schedule - }); - - schedules.insert({ - let mut schedule = Schedule::new(OnExit(TestNewcomputedState::A1)); - schedule.add_systems(|mut count: ResMut| { - count.exit += 1; - }); - schedule - }); - - schedules.insert({ - let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::B1)); - schedule.add_systems(|mut count: ResMut| { - count.enter += 1; - }); - schedule - }); - - schedules.insert({ - let mut schedule = Schedule::new(OnExit(TestNewcomputedState::B1)); - schedule.add_systems(|mut count: ResMut| { - count.exit += 1; - }); - schedule - }); - - schedules.insert({ - let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::B2)); - schedule.add_systems(|mut count: ResMut| { - count.enter += 1; - }); - schedule - }); - - schedules.insert({ - let mut schedule = Schedule::new(OnExit(TestNewcomputedState::B2)); - schedule.add_systems(|mut count: ResMut| { - count.exit += 1; - }); - schedule - }); - - world.init_resource::(); - - setup_state_transitions_in_world(&mut world, None); - - assert_eq!(world.resource::>().0, SimpleState::A); - assert_eq!(world.resource::>().0, SimpleState2::A1); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(SimpleState::B(true))); - world.insert_resource(NextState::Pending(SimpleState2::B2)); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - TestNewcomputedState::B2 - ); - assert_eq!(world.resource::().enter, 1); - assert_eq!(world.resource::().exit, 0); - - world.insert_resource(NextState::Pending(SimpleState2::A1)); - world.insert_resource(NextState::Pending(SimpleState::A)); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - TestNewcomputedState::A1 - ); - assert_eq!( - world.resource::().enter, - 2, - "Should Only Enter Twice" - ); - assert_eq!( - world.resource::().exit, - 1, - "Should Only Exit Once" - ); - - world.insert_resource(NextState::Pending(SimpleState::B(true))); - world.insert_resource(NextState::Pending(SimpleState2::B2)); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - TestNewcomputedState::B2 - ); - assert_eq!( - world.resource::().enter, - 3, - "Should Only Enter Three Times" - ); - assert_eq!( - world.resource::().exit, - 2, - "Should Only Exit Twice" - ); - - world.insert_resource(NextState::Pending(SimpleState::A)); - world.run_schedule(StateTransition); - assert!(!world.contains_resource::>()); - assert_eq!( - world.resource::().enter, - 3, - "Should Only Enter Three Times" - ); - assert_eq!( - world.resource::().exit, - 3, - "Should Only Exit Twice" - ); - } -} diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 724bcc6bb1ce0..61bac514065f4 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -1368,12 +1368,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn transmute_lens_filtered( &mut self, ) -> QueryLens<'_, NewD, NewF> { - // SAFETY: - // - We have exclusive access to the query - // - `self` has correctly captured its access - // - Access is checked to be a subset of the query's access when the state is created. - let world = unsafe { self.world.world() }; - let state = self.state.transmute_filtered::(world); + let components = self.world.components(); + let state = self.state.transmute_filtered::(components); QueryLens { world: self.world, state, @@ -1464,14 +1460,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { &mut self, other: &mut Query, ) -> QueryLens<'_, NewD, NewF> { - // SAFETY: - // - The queries have correctly captured their access. - // - We have exclusive access to both queries. - // - Access for QueryLens is checked when state is created. - let world = unsafe { self.world.world() }; + let components = self.world.components(); let state = self .state - .join_filtered::(world, other.state); + .join_filtered::(components, other.state); QueryLens { world: self.world, state, diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui.rs b/crates/bevy_ecs_compile_fail_tests/tests/ui.rs deleted file mode 100644 index 4fb6e0340bafd..0000000000000 --- a/crates/bevy_ecs_compile_fail_tests/tests/ui.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() -> bevy_compile_test_utils::ui_test::Result<()> { - bevy_compile_test_utils::test("tests/ui") -} diff --git a/crates/bevy_gizmos/src/config.rs b/crates/bevy_gizmos/src/config.rs index 92f6962c19384..c3030d9a3488f 100644 --- a/crates/bevy_gizmos/src/config.rs +++ b/crates/bevy_gizmos/src/config.rs @@ -197,7 +197,7 @@ impl From<&GizmoConfig> for GizmoMeshConfig { GizmoMeshConfig { line_perspective: item.line_perspective, line_style: item.line_style, - render_layers: item.render_layers, + render_layers: item.render_layers.clone(), } } } diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index c62e8b25d891f..f9fc93fd5ad1d 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -825,14 +825,9 @@ where if !self.gizmos.enabled { return; } - for axis in Vec3::AXES { + for axis in Dir3::AXES { self.gizmos - .circle( - self.position, - Dir3::new_unchecked(self.rotation * axis), - self.radius, - self.color, - ) + .circle(self.position, self.rotation * axis, self.radius, self.color) .segments(self.circle_segments); } } diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index d523f0dd5403f..c761189794cc9 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -39,6 +39,7 @@ pub mod config; pub mod gizmos; pub mod grid; pub mod primitives; +pub mod rounded_box; #[cfg(feature = "bevy_pbr")] pub mod light; diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index 660cec02c92d3..1a86976ff0622 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -269,9 +269,9 @@ fn queue_line_gizmos_2d( let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) | Mesh2dPipelineKey::from_hdr(view.hdr); + let render_layers = render_layers.unwrap_or_default(); for (entity, handle, config) in &line_gizmos { - let render_layers = render_layers.copied().unwrap_or_default(); - if !config.render_layers.intersects(&render_layers) { + if !config.render_layers.intersects(render_layers) { continue; } @@ -325,9 +325,9 @@ fn queue_line_joint_gizmos_2d( let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) | Mesh2dPipelineKey::from_hdr(view.hdr); + let render_layers = render_layers.unwrap_or_default(); for (entity, handle, config) in &line_gizmos { - let render_layers = render_layers.copied().unwrap_or_default(); - if !config.render_layers.intersects(&render_layers) { + if !config.render_layers.intersects(render_layers) { continue; } diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index e247220d541bc..bdcd75764b7a2 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -303,7 +303,7 @@ fn queue_line_gizmos_3d( (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), ) in &mut views { - let render_layers = render_layers.copied().unwrap_or_default(); + let render_layers = render_layers.unwrap_or_default(); let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) | MeshPipelineKey::from_hdr(view.hdr); @@ -325,7 +325,7 @@ fn queue_line_gizmos_3d( } for (entity, handle, config) in &line_gizmos { - if !config.render_layers.intersects(&render_layers) { + if !config.render_layers.intersects(render_layers) { continue; } @@ -389,7 +389,7 @@ fn queue_line_joint_gizmos_3d( (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), ) in &mut views { - let render_layers = render_layers.copied().unwrap_or_default(); + let render_layers = render_layers.unwrap_or_default(); let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) | MeshPipelineKey::from_hdr(view.hdr); @@ -411,7 +411,7 @@ fn queue_line_joint_gizmos_3d( } for (entity, handle, config) in &line_gizmos { - if !config.render_layers.intersects(&render_layers) { + if !config.render_layers.intersects(render_layers) { continue; } diff --git a/crates/bevy_gizmos/src/primitives/dim2.rs b/crates/bevy_gizmos/src/primitives/dim2.rs index 1162df7da9056..74028903b1df9 100644 --- a/crates/bevy_gizmos/src/primitives/dim2.rs +++ b/crates/bevy_gizmos/src/primitives/dim2.rs @@ -6,7 +6,7 @@ use super::helpers::*; use bevy_color::Color; use bevy_math::primitives::{ - BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, Ellipse, Line2d, Plane2d, Polygon, + Annulus, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Primitive2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, }; use bevy_math::{Dir2, Mat2, Vec2}; @@ -112,6 +112,32 @@ where } } +// annulus 2d + +impl<'w, 's, Config, Clear> GizmoPrimitive2d for Gizmos<'w, 's, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> = () where Self: 'a; + + fn primitive_2d( + &mut self, + primitive: Annulus, + position: Vec2, + angle: f32, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let color = color.into(); + self.primitive_2d(primitive.inner_circle, position, angle, color); + self.primitive_2d(primitive.outer_circle, position, angle, color); + } +} + // capsule 2d impl<'w, 's, Config, Clear> GizmoPrimitive2d for Gizmos<'w, 's, Config, Clear> diff --git a/crates/bevy_gizmos/src/primitives/dim3.rs b/crates/bevy_gizmos/src/primitives/dim3.rs index 66924fbb3f007..8af28f396a675 100644 --- a/crates/bevy_gizmos/src/primitives/dim3.rs +++ b/crates/bevy_gizmos/src/primitives/dim3.rs @@ -245,7 +245,7 @@ where let normal = self.rotation * *self.normal; self.gizmos .primitive_3d(self.normal, self.position, self.rotation, self.color); - let normals_normal = normal.any_orthonormal_vector(); + let normals_normal = self.rotation * self.normal.any_orthonormal_vector(); // draws the axes // get rotation for each direction diff --git a/crates/bevy_gizmos/src/rounded_box.rs b/crates/bevy_gizmos/src/rounded_box.rs new file mode 100644 index 0000000000000..cef07d960ca74 --- /dev/null +++ b/crates/bevy_gizmos/src/rounded_box.rs @@ -0,0 +1,380 @@ +//! Additional [`Gizmos`] Functions -- Rounded cuboids and rectangles +//! +//! Includes the implementation of [`Gizmos::rounded_rect`], [`Gizmos::rounded_rect_2d`] and [`Gizmos::rounded_cuboid`]. +//! and assorted support items. + +use std::f32::consts::FRAC_PI_2; + +use crate::prelude::{GizmoConfigGroup, Gizmos}; +use bevy_color::Color; +use bevy_math::{Quat, Vec2, Vec3}; +use bevy_transform::components::Transform; + +/// A builder returned by [`Gizmos::rounded_rect`] and [`Gizmos::rounded_rect_2d`] +pub struct RoundedRectBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + size: Vec2, + gizmos: &'a mut Gizmos<'w, 's, T>, + config: RoundedBoxConfig, +} +/// A builder returned by [`Gizmos::rounded_cuboid`] +pub struct RoundedCuboidBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + size: Vec3, + gizmos: &'a mut Gizmos<'w, 's, T>, + config: RoundedBoxConfig, +} +struct RoundedBoxConfig { + position: Vec3, + rotation: Quat, + color: Color, + corner_radius: f32, + arc_segments: usize, +} + +impl RoundedRectBuilder<'_, '_, '_, T> { + /// Change the radius of the corners to be `corner_radius`. + /// The default corner radius is [min axis of size] / 10.0 + pub fn corner_radius(mut self, corner_radius: f32) -> Self { + self.config.corner_radius = corner_radius; + self + } + + /// Change the segments of the arcs at the corners of the rectangle. + /// The default value is 8 + pub fn arc_segments(mut self, arc_segments: usize) -> Self { + self.config.arc_segments = arc_segments; + self + } +} +impl RoundedCuboidBuilder<'_, '_, '_, T> { + /// Change the radius of the edges to be `edge_radius`. + /// The default edge radius is [min axis of size] / 10.0 + pub fn edge_radius(mut self, edge_radius: f32) -> Self { + self.config.corner_radius = edge_radius; + self + } + + /// Change the segments of the arcs at the edges of the cuboid. + /// The default value is 8 + pub fn arc_segments(mut self, arc_segments: usize) -> Self { + self.config.arc_segments = arc_segments; + self + } +} + +impl Drop for RoundedRectBuilder<'_, '_, '_, T> { + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + let config = &self.config; + + // Calculate inner and outer half size and ensure that the edge_radius is <= any half_length + let mut outer_half_size = self.size.abs() / 2.0; + let inner_half_size = + (outer_half_size - Vec2::splat(config.corner_radius.abs())).max(Vec2::ZERO); + let corner_radius = (outer_half_size - inner_half_size).min_element(); + let mut inner_half_size = outer_half_size - Vec2::splat(corner_radius); + + if config.corner_radius < 0. { + std::mem::swap(&mut outer_half_size, &mut inner_half_size); + } + + // Handle cases where the rectangle collapses into simpler shapes + if outer_half_size.x * outer_half_size.y == 0. { + self.gizmos.line( + config.position + config.rotation * -outer_half_size.extend(0.), + config.position + config.rotation * outer_half_size.extend(0.), + config.color, + ); + return; + } + if corner_radius == 0. { + self.gizmos + .rect(config.position, config.rotation, self.size, config.color); + return; + } + + let vertices = [ + // top right + Vec3::new(inner_half_size.x, outer_half_size.y, 0.), + Vec3::new(inner_half_size.x, inner_half_size.y, 0.), + Vec3::new(outer_half_size.x, inner_half_size.y, 0.), + // bottom right + Vec3::new(outer_half_size.x, -inner_half_size.y, 0.), + Vec3::new(inner_half_size.x, -inner_half_size.y, 0.), + Vec3::new(inner_half_size.x, -outer_half_size.y, 0.), + // bottom left + Vec3::new(-inner_half_size.x, -outer_half_size.y, 0.), + Vec3::new(-inner_half_size.x, -inner_half_size.y, 0.), + Vec3::new(-outer_half_size.x, -inner_half_size.y, 0.), + // top left + Vec3::new(-outer_half_size.x, inner_half_size.y, 0.), + Vec3::new(-inner_half_size.x, inner_half_size.y, 0.), + Vec3::new(-inner_half_size.x, outer_half_size.y, 0.), + ] + .map(|v| config.position + config.rotation * v); + + for chunk in vertices.chunks_exact(3) { + self.gizmos + .short_arc_3d_between(chunk[1], chunk[0], chunk[2], config.color) + .segments(config.arc_segments); + } + + let edges = if config.corner_radius > 0. { + [ + (vertices[2], vertices[3]), + (vertices[5], vertices[6]), + (vertices[8], vertices[9]), + (vertices[11], vertices[0]), + ] + } else { + [ + (vertices[0], vertices[5]), + (vertices[3], vertices[8]), + (vertices[6], vertices[11]), + (vertices[9], vertices[2]), + ] + }; + + for (start, end) in edges { + self.gizmos.line(start, end, config.color); + } + } +} + +impl Drop for RoundedCuboidBuilder<'_, '_, '_, T> { + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + let config = &self.config; + + // Calculate inner and outer half size and ensure that the edge_radius is <= any half_length + let outer_half_size = self.size.abs() / 2.0; + let inner_half_size = + (outer_half_size - Vec3::splat(config.corner_radius.abs())).max(Vec3::ZERO); + let mut edge_radius = (outer_half_size - inner_half_size).min_element(); + let inner_half_size = outer_half_size - Vec3::splat(edge_radius); + edge_radius *= config.corner_radius.signum(); + + // Handle cases where the rounded cuboid collapses into simpler shapes + if edge_radius == 0.0 { + let transform = Transform::from_translation(config.position) + .with_rotation(config.rotation) + .with_scale(self.size); + self.gizmos.cuboid(transform, config.color); + return; + } + + let rects = [ + ( + Vec3::X, + Vec2::new(self.size.z, self.size.y), + Quat::from_rotation_y(FRAC_PI_2), + ), + ( + Vec3::Y, + Vec2::new(self.size.x, self.size.z), + Quat::from_rotation_x(FRAC_PI_2), + ), + (Vec3::Z, Vec2::new(self.size.x, self.size.y), Quat::IDENTITY), + ]; + + for (position, size, rotation) in rects { + let world_rotation = config.rotation * rotation; + let local_position = config.rotation * (position * inner_half_size); + self.gizmos + .rounded_rect( + config.position + local_position, + world_rotation, + size, + config.color, + ) + .arc_segments(config.arc_segments) + .corner_radius(edge_radius); + + self.gizmos + .rounded_rect( + config.position - local_position, + world_rotation, + size, + config.color, + ) + .arc_segments(config.arc_segments) + .corner_radius(edge_radius); + } + } +} + +impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { + /// Draw a wireframe rectangle with rounded corners in 3D. + /// + /// This should be called for each frame the rectangle needs to be rendered. + /// + /// # Arguments + /// + /// - `position`: The center point of the rectangle. + /// - `rotation`: defines orientation of the rectangle, by default we assume the rectangle is contained in a plane parallel to the XY plane. + /// - `size`: defines the size of the rectangle. This refers to the 'outer size', similar to a bounding box. + /// - `color`: color of the rectangle + /// + /// # Builder methods + /// + /// - The corner radius can be adjusted with the `.corner_radius(...)` method. + /// - The number of segments of the arcs at each corner (i.e. the level of detail) can be adjusted with the + /// `.arc_segments(...)` method. + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_render::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::palettes::css::GREEN; + /// fn system(mut gizmos: Gizmos) { + /// gizmos.rounded_rect( + /// Vec3::ZERO, + /// Quat::IDENTITY, + /// Vec2::ONE, + /// GREEN + /// ) + /// .corner_radius(0.25) + /// .arc_segments(10); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn rounded_rect( + &mut self, + position: Vec3, + rotation: Quat, + size: Vec2, + color: impl Into, + ) -> RoundedRectBuilder<'_, 'w, 's, T> { + let corner_radius = size.min_element() * DEFAULT_CORNER_RADIUS; + RoundedRectBuilder { + gizmos: self, + config: RoundedBoxConfig { + position, + rotation, + color: color.into(), + corner_radius, + arc_segments: DEFAULT_ARC_SEGMENTS, + }, + size, + } + } + + /// Draw a wireframe rectangle with rounded corners in 2D. + /// + /// This should be called for each frame the rectangle needs to be rendered. + /// + /// # Arguments + /// + /// - `position`: The center point of the rectangle. + /// - `rotation`: defines orientation of the rectangle. + /// - `size`: defines the size of the rectangle. This refers to the 'outer size', similar to a bounding box. + /// - `color`: color of the rectangle + /// + /// # Builder methods + /// + /// - The corner radius can be adjusted with the `.corner_radius(...)` method. + /// - The number of segments of the arcs at each corner (i.e. the level of detail) can be adjusted with the + /// `.arc_segments(...)` method. + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_render::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::palettes::css::GREEN; + /// fn system(mut gizmos: Gizmos) { + /// gizmos.rounded_rect_2d( + /// Vec2::ZERO, + /// 0., + /// Vec2::ONE, + /// GREEN + /// ) + /// .corner_radius(0.25) + /// .arc_segments(10); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn rounded_rect_2d( + &mut self, + position: Vec2, + rotation: f32, + size: Vec2, + color: impl Into, + ) -> RoundedRectBuilder<'_, 'w, 's, T> { + let corner_radius = size.min_element() * DEFAULT_CORNER_RADIUS; + RoundedRectBuilder { + gizmos: self, + config: RoundedBoxConfig { + position: position.extend(0.), + rotation: Quat::from_rotation_z(rotation), + color: color.into(), + corner_radius, + arc_segments: DEFAULT_ARC_SEGMENTS, + }, + size, + } + } + + /// Draw a wireframe cuboid with rounded corners in 3D. + /// + /// This should be called for each frame the cuboid needs to be rendered. + /// + /// # Arguments + /// + /// - `position`: The center point of the cuboid. + /// - `rotation`: defines orientation of the cuboid. + /// - `size`: defines the size of the cuboid. This refers to the 'outer size', similar to a bounding box. + /// - `color`: color of the cuboid + /// + /// # Builder methods + /// + /// - The edge radius can be adjusted with the `.edge_radius(...)` method. + /// - The number of segments of the arcs at each edge (i.e. the level of detail) can be adjusted with the + /// `.arc_segments(...)` method. + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_render::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::palettes::css::GREEN; + /// fn system(mut gizmos: Gizmos) { + /// gizmos.rounded_cuboid( + /// Vec3::ZERO, + /// Quat::IDENTITY, + /// Vec3::ONE, + /// GREEN + /// ) + /// .edge_radius(0.25) + /// .arc_segments(10); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn rounded_cuboid( + &mut self, + position: Vec3, + rotation: Quat, + size: Vec3, + color: impl Into, + ) -> RoundedCuboidBuilder<'_, 'w, 's, T> { + let corner_radius = size.min_element() * DEFAULT_CORNER_RADIUS; + RoundedCuboidBuilder { + gizmos: self, + config: RoundedBoxConfig { + position, + rotation, + color: color.into(), + corner_radius, + arc_segments: DEFAULT_ARC_SEGMENTS, + }, + size, + } + } +} + +const DEFAULT_ARC_SEGMENTS: usize = 8; +const DEFAULT_CORNER_RADIUS: f32 = 0.1; diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index 3fc387902f2c8..b74079b3866a8 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -9,8 +9,9 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -dds = [] -pbr_transmission_textures = [] +dds = ["bevy_render/dds"] +pbr_transmission_textures = ["bevy_pbr/pbr_transmission_textures"] +pbr_multi_layer_material_textures = [] [dependencies] # bevy diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 963b747488837..5f3dd5c32100a 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -13,7 +13,7 @@ use bevy_hierarchy::{BuildWorldChildren, WorldChildBuilder}; use bevy_math::{Affine2, Mat4, Vec3}; use bevy_pbr::{ DirectionalLight, DirectionalLightBundle, PbrBundle, PointLight, PointLightBundle, SpotLight, - SpotLightBundle, StandardMaterial, MAX_JOINTS, + SpotLightBundle, StandardMaterial, UvChannel, MAX_JOINTS, }; use bevy_render::{ alpha::AlphaMode, @@ -38,13 +38,18 @@ use bevy_tasks::IoTaskPool; use bevy_transform::components::Transform; use bevy_utils::tracing::{error, info_span, warn}; use bevy_utils::{HashMap, HashSet}; +use gltf::image::Source; use gltf::{ accessor::Iter, mesh::{util::ReadIndices, Mode}, texture::{Info, MagFilter, MinFilter, TextureTransform, WrappingMode}, Material, Node, Primitive, Semantic, }; +use gltf::{json, Document}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "pbr_multi_layer_material_textures")] +use serde_json::value; +use serde_json::Value; #[cfg(feature = "bevy_animation")] use smallvec::SmallVec; use std::io::Error; @@ -214,6 +219,22 @@ async fn load_gltf<'a, 'b, 'c>( { linear_textures.insert(texture.texture().index()); } + + // None of the clearcoat maps should be loaded as sRGB. + #[cfg(feature = "pbr_multi_layer_material_textures")] + for texture_field_name in [ + "clearcoatTexture", + "clearcoatRoughnessTexture", + "clearcoatNormalTexture", + ] { + if let Some(texture_index) = material_extension_texture_index( + &material, + "KHR_materials_clearcoat", + texture_field_name, + ) { + linear_textures.insert(texture_index); + } + } } #[cfg(feature = "bevy_animation")] @@ -390,7 +411,7 @@ async fn load_gltf<'a, 'b, 'c>( if !settings.load_materials.is_empty() { // NOTE: materials must be loaded after textures because image load() calls will happen before load_with_settings, preventing is_srgb from being set properly for material in gltf.materials() { - let handle = load_material(&material, load_context, false); + let handle = load_material(&material, load_context, &gltf.document, false); if let Some(name) = material.name() { named_materials.insert(name.into(), handle.clone()); } @@ -490,7 +511,7 @@ async fn load_gltf<'a, 'b, 'c>( { mesh.insert_attribute(Mesh::ATTRIBUTE_TANGENT, vertex_attribute); } else if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_some() - && primitive.material().normal_texture().is_some() + && material_needs_tangents(&primitive.material()) { bevy_utils::tracing::debug!( "Missing vertex tangents for {}, computing them using the mikktspace algorithm. Consider using a tool such as Blender to pre-compute the tangents.", file_name @@ -605,8 +626,11 @@ async fn load_gltf<'a, 'b, 'c>( &mut entity_to_skin_index_map, &mut active_camera_found, &Transform::default(), + #[cfg(feature = "bevy_animation")] &animation_roots, + #[cfg(feature = "bevy_animation")] None, + &gltf.document, ); if result.is_err() { err = Some(result); @@ -813,6 +837,7 @@ async fn load_image<'a, 'b>( fn load_material( material: &Material, load_context: &mut LoadContext, + document: &Document, is_scale_inverted: bool, ) -> Handle { let material_label = material_label(material, is_scale_inverted); @@ -821,10 +846,13 @@ fn load_material( // TODO: handle missing label handle errors here? let color = pbr.base_color_factor(); - let base_color_texture = pbr.base_color_texture().map(|info| { - // TODO: handle info.tex_coord() (the *set* index for the right texcoords) - texture_handle(load_context, &info.texture()) - }); + let base_color_channel = pbr + .base_color_texture() + .map(|info| get_uv_channel(material, "base color", info.tex_coord())) + .unwrap_or_default(); + let base_color_texture = pbr + .base_color_texture() + .map(|info| texture_handle(load_context, &info.texture())); let uv_transform = pbr .base_color_texture() @@ -834,15 +862,21 @@ fn load_material( }) .unwrap_or_default(); + let normal_map_channel = material + .normal_texture() + .map(|info| get_uv_channel(material, "normal map", info.tex_coord())) + .unwrap_or_default(); let normal_map_texture: Option> = material.normal_texture().map(|normal_texture| { // TODO: handle normal_texture.scale - // TODO: handle normal_texture.tex_coord() (the *set* index for the right texcoords) texture_handle(load_context, &normal_texture.texture()) }); + let metallic_roughness_channel = pbr + .metallic_roughness_texture() + .map(|info| get_uv_channel(material, "metallic/roughness", info.tex_coord())) + .unwrap_or_default(); let metallic_roughness_texture = pbr.metallic_roughness_texture().map(|info| { - // TODO: handle info.tex_coord() (the *set* index for the right texcoords) warn_on_differing_texture_transforms( material, &info, @@ -852,32 +886,49 @@ fn load_material( texture_handle(load_context, &info.texture()) }); + let occlusion_channel = material + .occlusion_texture() + .map(|info| get_uv_channel(material, "occlusion", info.tex_coord())) + .unwrap_or_default(); let occlusion_texture = material.occlusion_texture().map(|occlusion_texture| { - // TODO: handle occlusion_texture.tex_coord() (the *set* index for the right texcoords) // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) texture_handle(load_context, &occlusion_texture.texture()) }); let emissive = material.emissive_factor(); + let emissive_channel = material + .emissive_texture() + .map(|info| get_uv_channel(material, "emissive", info.tex_coord())) + .unwrap_or_default(); let emissive_texture = material.emissive_texture().map(|info| { - // TODO: handle occlusion_texture.tex_coord() (the *set* index for the right texcoords) // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) warn_on_differing_texture_transforms(material, &info, uv_transform, "emissive"); texture_handle(load_context, &info.texture()) }); #[cfg(feature = "pbr_transmission_textures")] - let (specular_transmission, specular_transmission_texture) = - material.transmission().map_or((0.0, None), |transmission| { - let transmission_texture: Option> = transmission - .transmission_texture() - .map(|transmission_texture| { - // TODO: handle transmission_texture.tex_coord() (the *set* index for the right texcoords) - texture_handle(load_context, &transmission_texture.texture()) - }); + let (specular_transmission, specular_transmission_channel, specular_transmission_texture) = + material + .transmission() + .map_or((0.0, UvChannel::Uv0, None), |transmission| { + let specular_transmission_channel = transmission + .transmission_texture() + .map(|info| { + get_uv_channel(material, "specular/transmission", info.tex_coord()) + }) + .unwrap_or_default(); + let transmission_texture: Option> = transmission + .transmission_texture() + .map(|transmission_texture| { + texture_handle(load_context, &transmission_texture.texture()) + }); - (transmission.transmission_factor(), transmission_texture) - }); + ( + transmission.transmission_factor(), + specular_transmission_channel, + transmission_texture, + ) + }); #[cfg(not(feature = "pbr_transmission_textures"))] let specular_transmission = material @@ -885,22 +936,33 @@ fn load_material( .map_or(0.0, |transmission| transmission.transmission_factor()); #[cfg(feature = "pbr_transmission_textures")] - let (thickness, thickness_texture, attenuation_distance, attenuation_color) = material - .volume() - .map_or((0.0, None, f32::INFINITY, [1.0, 1.0, 1.0]), |volume| { + let ( + thickness, + thickness_channel, + thickness_texture, + attenuation_distance, + attenuation_color, + ) = material.volume().map_or( + (0.0, UvChannel::Uv0, None, f32::INFINITY, [1.0, 1.0, 1.0]), + |volume| { + let thickness_channel = volume + .thickness_texture() + .map(|info| get_uv_channel(material, "thickness", info.tex_coord())) + .unwrap_or_default(); let thickness_texture: Option> = volume.thickness_texture().map(|thickness_texture| { - // TODO: handle thickness_texture.tex_coord() (the *set* index for the right texcoords) texture_handle(load_context, &thickness_texture.texture()) }); ( volume.thickness_factor(), + thickness_channel, thickness_texture, volume.attenuation_distance(), volume.attenuation_color(), ) - }); + }, + ); #[cfg(not(feature = "pbr_transmission_textures"))] let (thickness, attenuation_distance, attenuation_color) = @@ -916,6 +978,10 @@ fn load_material( let ior = material.ior().unwrap_or(1.5); + // Parse the `KHR_materials_clearcoat` extension data if necessary. + let clearcoat = + ClearcoatExtension::parse(load_context, document, material).unwrap_or_default(); + // We need to operate in the Linear color space and be willing to exceed 1.0 in our channels let base_emissive = LinearRgba::rgb(emissive[0], emissive[1], emissive[2]); let scaled_emissive = base_emissive * material.emissive_strength().unwrap_or(1.0); @@ -923,10 +989,13 @@ fn load_material( StandardMaterial { base_color: Color::linear_rgba(color[0], color[1], color[2], color[3]), + base_color_channel, base_color_texture, perceptual_roughness: pbr.roughness_factor(), metallic: pbr.metallic_factor(), + metallic_roughness_channel, metallic_roughness_texture, + normal_map_channel, normal_map_texture, double_sided: material.double_sided(), cull_mode: if material.double_sided() { @@ -936,14 +1005,20 @@ fn load_material( } else { Some(Face::Back) }, + occlusion_channel, occlusion_texture, emissive, + emissive_channel, emissive_texture, specular_transmission, #[cfg(feature = "pbr_transmission_textures")] + specular_transmission_channel, + #[cfg(feature = "pbr_transmission_textures")] specular_transmission_texture, thickness, #[cfg(feature = "pbr_transmission_textures")] + thickness_channel, + #[cfg(feature = "pbr_transmission_textures")] thickness_texture, ior, attenuation_distance, @@ -955,11 +1030,49 @@ fn load_material( unlit: material.unlit(), alpha_mode: alpha_mode(material), uv_transform, + clearcoat: clearcoat.clearcoat_factor.unwrap_or_default() as f32, + clearcoat_perceptual_roughness: clearcoat.clearcoat_roughness_factor.unwrap_or_default() + as f32, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_channel: clearcoat.clearcoat_channel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_texture: clearcoat.clearcoat_texture, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_channel: clearcoat.clearcoat_roughness_channel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_texture: clearcoat.clearcoat_roughness_texture, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_channel: clearcoat.clearcoat_normal_channel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_texture: clearcoat.clearcoat_normal_texture, ..Default::default() } }) } +fn get_uv_channel(material: &Material, texture_kind: &str, tex_coord: u32) -> UvChannel { + match tex_coord { + 0 => UvChannel::Uv0, + 1 => UvChannel::Uv1, + _ => { + let material_name = material + .name() + .map(|n| format!("the material \"{n}\"")) + .unwrap_or_else(|| "an unnamed material".to_string()); + let material_index = material + .index() + .map(|i| format!("index {i}")) + .unwrap_or_else(|| "default".to_string()); + warn!( + "Only 2 UV Channels are supported, but {material_name} ({material_index}) \ + has the TEXCOORD attribute {} on texture kind {texture_kind}, which will fallback to 0.", + tex_coord, + ); + UvChannel::Uv0 + } + } +} + fn convert_texture_transform_to_affine2(texture_transform: TextureTransform) -> Affine2 { Affine2::from_scale_angle_translation( texture_transform.scale().into(), @@ -1011,8 +1124,9 @@ fn load_node( entity_to_skin_index_map: &mut EntityHashMap, active_camera_found: &mut bool, parent_transform: &Transform, - animation_roots: &HashSet, - mut animation_context: Option, + #[cfg(feature = "bevy_animation")] animation_roots: &HashSet, + #[cfg(feature = "bevy_animation")] mut animation_context: Option, + document: &Document, ) -> Result<(), GltfError> { let mut gltf_error = None; let transform = node_transform(gltf_node); @@ -1120,7 +1234,7 @@ fn load_node( if !root_load_context.has_labeled_asset(&material_label) && !load_context.has_labeled_asset(&material_label) { - load_material(&material, load_context, is_scale_inverted); + load_material(&material, load_context, document, is_scale_inverted); } let primitive_label = primitive_label(&mesh, &primitive); @@ -1261,8 +1375,11 @@ fn load_node( entity_to_skin_index_map, active_camera_found, &world_transform, + #[cfg(feature = "bevy_animation")] animation_roots, + #[cfg(feature = "bevy_animation")] animation_context.clone(), + document, ) { gltf_error = Some(err); return; @@ -1333,11 +1450,11 @@ fn texture_label(texture: &gltf::Texture) -> String { fn texture_handle(load_context: &mut LoadContext, texture: &gltf::Texture) -> Handle { match texture.source().source() { - gltf::image::Source::View { .. } => { + Source::View { .. } => { let label = texture_label(texture); load_context.get_label_handle(&label) } - gltf::image::Source::Uri { uri, .. } => { + Source::Uri { uri, .. } => { let uri = percent_encoding::percent_decode_str(uri) .decode_utf8() .unwrap(); @@ -1354,6 +1471,24 @@ fn texture_handle(load_context: &mut LoadContext, texture: &gltf::Texture) -> Ha } } +/// Given a [`json::texture::Info`], returns the handle of the texture that this +/// refers to. +/// +/// This is a low-level function only used when the `gltf` crate has no support +/// for an extension, forcing us to parse its texture references manually. +#[allow(dead_code)] +fn texture_handle_from_info( + load_context: &mut LoadContext, + document: &Document, + texture_info: &json::texture::Info, +) -> Handle { + let texture = document + .textures() + .nth(texture_info.index.value()) + .expect("Texture info references a nonexistent texture"); + texture_handle(load_context, &texture) +} + /// Returns the label for the `node`. fn node_label(node: &Node) -> String { format!("Node{}", node.index()) @@ -1632,9 +1767,143 @@ struct AnimationContext { path: SmallVec<[Name; 8]>, } -#[cfg(not(feature = "bevy_animation"))] -#[derive(Clone)] -struct AnimationContext; +/// Parsed data from the `KHR_materials_clearcoat` extension. +/// +/// See the specification: +/// +#[derive(Default)] +struct ClearcoatExtension { + clearcoat_factor: Option, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_channel: UvChannel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_texture: Option>, + clearcoat_roughness_factor: Option, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_channel: UvChannel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_texture: Option>, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_channel: UvChannel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_texture: Option>, +} + +impl ClearcoatExtension { + #[allow(unused_variables)] + fn parse( + load_context: &mut LoadContext, + document: &Document, + material: &Material, + ) -> Option { + let extension = material + .extensions()? + .get("KHR_materials_clearcoat")? + .as_object()?; + + #[cfg(feature = "pbr_multi_layer_material_textures")] + let (clearcoat_channel, clearcoat_texture) = extension + .get("clearcoatTexture") + .and_then(|value| value::from_value::(value.clone()).ok()) + .map(|json_info| { + ( + get_uv_channel(material, "clearcoat", json_info.tex_coord), + texture_handle_from_info(load_context, document, &json_info), + ) + }) + .unzip(); + + #[cfg(feature = "pbr_multi_layer_material_textures")] + let (clearcoat_roughness_channel, clearcoat_roughness_texture) = extension + .get("clearcoatRoughnessTexture") + .and_then(|value| value::from_value::(value.clone()).ok()) + .map(|json_info| { + ( + get_uv_channel(material, "clearcoat roughness", json_info.tex_coord), + texture_handle_from_info(load_context, document, &json_info), + ) + }) + .unzip(); + + #[cfg(feature = "pbr_multi_layer_material_textures")] + let (clearcoat_normal_channel, clearcoat_normal_texture) = extension + .get("clearcoatNormalTexture") + .and_then(|value| value::from_value::(value.clone()).ok()) + .map(|json_info| { + ( + get_uv_channel(material, "clearcoat normal", json_info.tex_coord), + texture_handle_from_info(load_context, document, &json_info), + ) + }) + .unzip(); + + Some(ClearcoatExtension { + clearcoat_factor: extension.get("clearcoatFactor").and_then(Value::as_f64), + clearcoat_roughness_factor: extension + .get("clearcoatRoughnessFactor") + .and_then(Value::as_f64), + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_channel: clearcoat_channel.unwrap_or_default(), + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_texture, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_channel: clearcoat_roughness_channel.unwrap_or_default(), + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_texture, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_channel: clearcoat_normal_channel.unwrap_or_default(), + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_texture, + }) + } +} + +/// Returns the index (within the `textures` array) of the texture with the +/// given field name in the data for the material extension with the given name, +/// if there is one. +#[cfg(feature = "pbr_multi_layer_material_textures")] +fn material_extension_texture_index( + material: &Material, + extension_name: &str, + texture_field_name: &str, +) -> Option { + Some( + value::from_value::( + material + .extensions()? + .get(extension_name)? + .as_object()? + .get(texture_field_name)? + .clone(), + ) + .ok()? + .index + .value(), + ) +} + +/// Returns true if the material needs mesh tangents in order to be successfully +/// rendered. +/// +/// We generate them if this function returns true. +fn material_needs_tangents(material: &Material) -> bool { + if material.normal_texture().is_some() { + return true; + } + + #[cfg(feature = "pbr_multi_layer_material_textures")] + if material_extension_texture_index( + material, + "KHR_materials_clearcoat", + "clearcoatNormalTexture", + ) + .is_some() + { + return true; + } + + false +} #[cfg(test)] mod test { diff --git a/crates/bevy_input/src/button_input.rs b/crates/bevy_input/src/button_input.rs index 967bf850de0c1..3bb22f9409e1b 100644 --- a/crates/bevy_input/src/button_input.rs +++ b/crates/bevy_input/src/button_input.rs @@ -5,10 +5,6 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_utils::HashSet; use std::hash::Hash; -// unused import, but needed for intra doc link to work -#[allow(unused_imports)] -use bevy_ecs::schedule::State; - /// A "press-able" input of type `T`. /// /// ## Usage @@ -23,8 +19,8 @@ use bevy_ecs::schedule::State; /// ## Multiple systems /// /// In case multiple systems are checking for [`ButtonInput::just_pressed`] or [`ButtonInput::just_released`] -/// but only one should react, for example in the case of triggering -/// [`State`] change, you should consider clearing the input state, either by: +/// but only one should react, for example when modifying a +/// [`Resource`], you should consider clearing the input state, either by: /// /// * Using [`ButtonInput::clear_just_pressed`] or [`ButtonInput::clear_just_released`] instead. /// * Calling [`ButtonInput::clear`] or [`ButtonInput::reset`] immediately after the state change. diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index c5d9c2b0e153a..37038350386e2 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -77,11 +77,11 @@ serialize = [ "bevy_ui?/serialize", "bevy_color?/serialize", ] -multi-threaded = [ - "bevy_asset?/multi-threaded", - "bevy_ecs/multi-threaded", - "bevy_render?/multi-threaded", - "bevy_tasks/multi-threaded", +multi_threaded = [ + "bevy_asset?/multi_threaded", + "bevy_ecs/multi_threaded", + "bevy_render?/multi_threaded", + "bevy_tasks/multi_threaded", ] async-io = ["bevy_tasks/async-io"] @@ -98,6 +98,12 @@ pbr_transmission_textures = [ "bevy_gltf?/pbr_transmission_textures", ] +# Multi-layer material textures in `StandardMaterial`: +pbr_multi_layer_material_textures = [ + "bevy_pbr?/pbr_multi_layer_material_textures", + "bevy_gltf?/pbr_multi_layer_material_textures", +] + # Optimise for WebGL2 webgl = [ "bevy_core_pipeline?/webgl", @@ -173,6 +179,9 @@ bevy_dev_tools = ["dep:bevy_dev_tools"] # Enable support for the ios_simulator by downgrading some rendering capabilities ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"] +# Enable built in global state machines +bevy_state = ["dep:bevy_state", "bevy_app/bevy_state"] + [dependencies] # bevy bevy_a11y = { path = "../bevy_a11y", version = "0.14.0-dev" } @@ -181,6 +190,7 @@ bevy_core = { path = "../bevy_core", version = "0.14.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" } bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" } +bevy_state = { path = "../bevy_state", optional = true, version = "0.14.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" } bevy_input = { path = "../bevy_input", version = "0.14.0-dev" } bevy_log = { path = "../bevy_log", version = "0.14.0-dev" } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index acfa89d0b659a..10d595df6ea91 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -79,7 +79,7 @@ impl PluginGroup for DefaultPlugins { // compressed texture formats .add(bevy_render::texture::ImagePlugin::default()); - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] { group = group.add(bevy_render::pipelined_rendering::PipelinedRenderingPlugin); } diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 7eb982634e39c..f7f828c986bc4 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -52,6 +52,8 @@ pub use bevy_render as render; pub use bevy_scene as scene; #[cfg(feature = "bevy_sprite")] pub use bevy_sprite as sprite; +#[cfg(feature = "bevy_state")] +pub use bevy_state as state; pub use bevy_tasks as tasks; #[cfg(feature = "bevy_text")] pub use bevy_text as text; diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index aa256727810da..7566246296d36 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -62,3 +62,7 @@ pub use crate::gizmos::prelude::*; #[doc(hidden)] #[cfg(feature = "bevy_gilrs")] pub use crate::gilrs::*; + +#[doc(hidden)] +#[cfg(feature = "bevy_state")] +pub use crate::state::prelude::*; diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index f37f0729a847e..6e958eb6187ba 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -49,11 +49,19 @@ pub use bevy_utils::{ pub use tracing_subscriber; use bevy_app::{App, Plugin}; -use bevy_utils::tracing::Subscriber; use tracing_log::LogTracer; #[cfg(feature = "tracing-chrome")] use tracing_subscriber::fmt::{format::DefaultFields, FormattedFields}; -use tracing_subscriber::{prelude::*, registry::Registry, EnvFilter}; +use tracing_subscriber::{prelude::*, registry::Registry, EnvFilter, Layer}; +#[cfg(feature = "tracing-chrome")] +use {bevy_ecs::system::Resource, bevy_utils::synccell::SyncCell}; + +/// Wrapper resource for `tracing-chrome`'s flush guard. +/// When the guard is dropped the chrome log is written to file. +#[cfg(feature = "tracing-chrome")] +#[allow(dead_code)] +#[derive(Resource)] +pub(crate) struct FlushGuard(SyncCell); /// Adds logging to Apps. This plugin is part of the `DefaultPlugins`. Adding /// this plugin will setup a collector appropriate to your target platform: @@ -74,7 +82,7 @@ use tracing_subscriber::{prelude::*, registry::Registry, EnvFilter}; /// .add_plugins(DefaultPlugins.set(LogPlugin { /// level: Level::DEBUG, /// filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), -/// update_subscriber: None, +/// custom_layer: |_| None, /// })) /// .run(); /// } @@ -112,23 +120,28 @@ pub struct LogPlugin { /// This can be further filtered using the `filter` setting. pub level: Level, - /// Optionally apply extra transformations to the tracing subscriber, - /// such as adding [`Layer`](tracing_subscriber::layer::Layer)s. + /// Optionally add an extra [`Layer`] to the tracing subscriber + /// + /// This function is only called once, when the plugin is built. + /// + /// Because [`BoxedLayer`] takes a `dyn Layer`, `Vec` is also an acceptable return value. /// /// Access to [`App`] is also provided to allow for communication between the [`Subscriber`] /// and the [`App`]. - pub update_subscriber: Option BoxedSubscriber>, + /// + /// Please see the `examples/log_layers.rs` for a complete example. + pub custom_layer: fn(app: &mut App) -> Option, } -/// Alias for a boxed [`Subscriber`]. -pub type BoxedSubscriber = Box; +/// A boxed [`Layer`] that can be used with [`LogPlugin`]. +pub type BoxedLayer = Box + Send + Sync + 'static>; impl Default for LogPlugin { fn default() -> Self { Self { filter: "wgpu=error,naga=warn".to_string(), level: Level::INFO, - update_subscriber: None, + custom_layer: |_| None, } } } @@ -146,11 +159,16 @@ impl Plugin for LogPlugin { } let finished_subscriber; + let subscriber = Registry::default(); + + // add optional layer provided by user + let subscriber = subscriber.with((self.custom_layer)(app)); + let default_filter = { format!("{},{}", self.level, self.filter) }; let filter_layer = EnvFilter::try_from_default_env() .or_else(|_| EnvFilter::try_new(&default_filter)) .unwrap(); - let subscriber = Registry::default().with(filter_layer); + let subscriber = subscriber.with(filter_layer); #[cfg(feature = "trace")] let subscriber = subscriber.with(tracing_error::ErrorLayer::default()); @@ -177,7 +195,7 @@ impl Plugin for LogPlugin { } })) .build(); - app.insert_non_send_resource(guard); + app.insert_resource(FlushGuard(SyncCell::new(guard))); chrome_layer }; @@ -200,12 +218,7 @@ impl Plugin for LogPlugin { let subscriber = subscriber.with(chrome_layer); #[cfg(feature = "tracing-tracy")] let subscriber = subscriber.with(tracy_layer); - - if let Some(update_subscriber) = self.update_subscriber { - finished_subscriber = update_subscriber(app, Box::new(subscriber)); - } else { - finished_subscriber = Box::new(subscriber); - } + finished_subscriber = subscriber; } #[cfg(target_arch = "wasm32")] diff --git a/crates/bevy_macros_compile_fail_tests/tests/derive.rs b/crates/bevy_macros_compile_fail_tests/tests/derive.rs deleted file mode 100644 index 6e0ceaf7a5480..0000000000000 --- a/crates/bevy_macros_compile_fail_tests/tests/derive.rs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() -> bevy_compile_test_utils::ui_test::Result<()> { - bevy_compile_test_utils::test_multiple(["tests/deref_derive", "tests/deref_mut_derive"]) -} - diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 268f457719271..a1e362b56f574 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -7,6 +7,7 @@ homepage = "https://bevyengine.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] +rust-version = "1.68.2" [dependencies] glam = { version = "0.27", features = ["bytemuck"] } diff --git a/crates/bevy_math/src/bounding/bounded2d/mod.rs b/crates/bevy_math/src/bounding/bounded2d/mod.rs index 471e7487dbeb6..b97da70e3d17a 100644 --- a/crates/bevy_math/src/bounding/bounded2d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded2d/mod.rs @@ -271,12 +271,12 @@ mod aabb2d_tests { min: Vec2::new(-0.5, -1.), max: Vec2::new(1., 1.), }; - assert!((aabb.center() - Vec2::new(0.25, 0.)).length() < std::f32::EPSILON); + assert!((aabb.center() - Vec2::new(0.25, 0.)).length() < f32::EPSILON); let aabb = Aabb2d { min: Vec2::new(5., -10.), max: Vec2::new(10., -5.), }; - assert!((aabb.center() - Vec2::new(7.5, -7.5)).length() < std::f32::EPSILON); + assert!((aabb.center() - Vec2::new(7.5, -7.5)).length() < f32::EPSILON); } #[test] @@ -286,7 +286,7 @@ mod aabb2d_tests { max: Vec2::new(1., 1.), }; let half_size = aabb.half_size(); - assert!((half_size - Vec2::new(0.75, 1.)).length() < std::f32::EPSILON); + assert!((half_size - Vec2::new(0.75, 1.)).length() < f32::EPSILON); } #[test] @@ -295,12 +295,12 @@ mod aabb2d_tests { min: Vec2::new(-1., -1.), max: Vec2::new(1., 1.), }; - assert!((aabb.visible_area() - 4.).abs() < std::f32::EPSILON); + assert!((aabb.visible_area() - 4.).abs() < f32::EPSILON); let aabb = Aabb2d { min: Vec2::new(0., 0.), max: Vec2::new(1., 0.5), }; - assert!((aabb.visible_area() - 0.5).abs() < std::f32::EPSILON); + assert!((aabb.visible_area() - 0.5).abs() < f32::EPSILON); } #[test] @@ -332,8 +332,8 @@ mod aabb2d_tests { max: Vec2::new(0.75, 1.), }; let merged = a.merge(&b); - assert!((merged.min - Vec2::new(-2., -1.)).length() < std::f32::EPSILON); - assert!((merged.max - Vec2::new(1., 1.)).length() < std::f32::EPSILON); + assert!((merged.min - Vec2::new(-2., -1.)).length() < f32::EPSILON); + assert!((merged.max - Vec2::new(1., 1.)).length() < f32::EPSILON); assert!(merged.contains(&a)); assert!(merged.contains(&b)); assert!(!a.contains(&merged)); @@ -347,8 +347,8 @@ mod aabb2d_tests { max: Vec2::new(1., 1.), }; let padded = a.grow(Vec2::ONE); - assert!((padded.min - Vec2::new(-2., -2.)).length() < std::f32::EPSILON); - assert!((padded.max - Vec2::new(2., 2.)).length() < std::f32::EPSILON); + assert!((padded.min - Vec2::new(-2., -2.)).length() < f32::EPSILON); + assert!((padded.max - Vec2::new(2., 2.)).length() < f32::EPSILON); assert!(padded.contains(&a)); assert!(!a.contains(&padded)); } @@ -360,8 +360,8 @@ mod aabb2d_tests { max: Vec2::new(2., 2.), }; let shrunk = a.shrink(Vec2::ONE); - assert!((shrunk.min - Vec2::new(-1., -1.)).length() < std::f32::EPSILON); - assert!((shrunk.max - Vec2::new(1., 1.)).length() < std::f32::EPSILON); + assert!((shrunk.min - Vec2::new(-1., -1.)).length() < f32::EPSILON); + assert!((shrunk.max - Vec2::new(1., 1.)).length() < f32::EPSILON); assert!(a.contains(&shrunk)); assert!(!shrunk.contains(&a)); } @@ -373,8 +373,8 @@ mod aabb2d_tests { max: Vec2::ONE, }; let scaled = a.scale_around_center(Vec2::splat(2.)); - assert!((scaled.min - Vec2::splat(-2.)).length() < std::f32::EPSILON); - assert!((scaled.max - Vec2::splat(2.)).length() < std::f32::EPSILON); + assert!((scaled.min - Vec2::splat(-2.)).length() < f32::EPSILON); + assert!((scaled.max - Vec2::splat(2.)).length() < f32::EPSILON); assert!(!a.contains(&scaled)); assert!(scaled.contains(&a)); } @@ -647,8 +647,8 @@ mod bounding_circle_tests { let a = BoundingCircle::new(Vec2::ONE, 5.); let b = BoundingCircle::new(Vec2::new(1., -4.), 1.); let merged = a.merge(&b); - assert!((merged.center - Vec2::new(1., 0.5)).length() < std::f32::EPSILON); - assert!((merged.radius() - 5.5).abs() < std::f32::EPSILON); + assert!((merged.center - Vec2::new(1., 0.5)).length() < f32::EPSILON); + assert!((merged.radius() - 5.5).abs() < f32::EPSILON); assert!(merged.contains(&a)); assert!(merged.contains(&b)); assert!(!a.contains(&merged)); @@ -680,7 +680,7 @@ mod bounding_circle_tests { fn grow() { let a = BoundingCircle::new(Vec2::ONE, 5.); let padded = a.grow(1.25); - assert!((padded.radius() - 6.25).abs() < std::f32::EPSILON); + assert!((padded.radius() - 6.25).abs() < f32::EPSILON); assert!(padded.contains(&a)); assert!(!a.contains(&padded)); } @@ -689,7 +689,7 @@ mod bounding_circle_tests { fn shrink() { let a = BoundingCircle::new(Vec2::ONE, 5.); let shrunk = a.shrink(0.5); - assert!((shrunk.radius() - 4.5).abs() < std::f32::EPSILON); + assert!((shrunk.radius() - 4.5).abs() < f32::EPSILON); assert!(a.contains(&shrunk)); assert!(!shrunk.contains(&a)); } @@ -698,7 +698,7 @@ mod bounding_circle_tests { fn scale_around_center() { let a = BoundingCircle::new(Vec2::ONE, 5.); let scaled = a.scale_around_center(2.); - assert!((scaled.radius() - 10.).abs() < std::f32::EPSILON); + assert!((scaled.radius() - 10.).abs() < f32::EPSILON); assert!(!a.contains(&scaled)); assert!(scaled.contains(&a)); } diff --git a/crates/bevy_math/src/bounding/bounded3d/mod.rs b/crates/bevy_math/src/bounding/bounded3d/mod.rs index c069975dfa933..4c4ad16749e3d 100644 --- a/crates/bevy_math/src/bounding/bounded3d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded3d/mod.rs @@ -268,12 +268,12 @@ mod aabb3d_tests { min: Vec3A::new(-0.5, -1., -0.5), max: Vec3A::new(1., 1., 2.), }; - assert!((aabb.center() - Vec3A::new(0.25, 0., 0.75)).length() < std::f32::EPSILON); + assert!((aabb.center() - Vec3A::new(0.25, 0., 0.75)).length() < f32::EPSILON); let aabb = Aabb3d { min: Vec3A::new(5., 5., -10.), max: Vec3A::new(10., 10., -5.), }; - assert!((aabb.center() - Vec3A::new(7.5, 7.5, -7.5)).length() < std::f32::EPSILON); + assert!((aabb.center() - Vec3A::new(7.5, 7.5, -7.5)).length() < f32::EPSILON); } #[test] @@ -282,7 +282,7 @@ mod aabb3d_tests { min: Vec3A::new(-0.5, -1., -0.5), max: Vec3A::new(1., 1., 2.), }; - assert!((aabb.half_size() - Vec3A::new(0.75, 1., 1.25)).length() < std::f32::EPSILON); + assert!((aabb.half_size() - Vec3A::new(0.75, 1., 1.25)).length() < f32::EPSILON); } #[test] @@ -291,12 +291,12 @@ mod aabb3d_tests { min: Vec3A::new(-1., -1., -1.), max: Vec3A::new(1., 1., 1.), }; - assert!((aabb.visible_area() - 12.).abs() < std::f32::EPSILON); + assert!((aabb.visible_area() - 12.).abs() < f32::EPSILON); let aabb = Aabb3d { min: Vec3A::new(0., 0., 0.), max: Vec3A::new(1., 0.5, 0.25), }; - assert!((aabb.visible_area() - 0.875).abs() < std::f32::EPSILON); + assert!((aabb.visible_area() - 0.875).abs() < f32::EPSILON); } #[test] @@ -328,8 +328,8 @@ mod aabb3d_tests { max: Vec3A::new(0.75, 1., 2.), }; let merged = a.merge(&b); - assert!((merged.min - Vec3A::new(-2., -1., -1.)).length() < std::f32::EPSILON); - assert!((merged.max - Vec3A::new(1., 1., 2.)).length() < std::f32::EPSILON); + assert!((merged.min - Vec3A::new(-2., -1., -1.)).length() < f32::EPSILON); + assert!((merged.max - Vec3A::new(1., 1., 2.)).length() < f32::EPSILON); assert!(merged.contains(&a)); assert!(merged.contains(&b)); assert!(!a.contains(&merged)); @@ -343,8 +343,8 @@ mod aabb3d_tests { max: Vec3A::new(1., 1., 1.), }; let padded = a.grow(Vec3A::ONE); - assert!((padded.min - Vec3A::new(-2., -2., -2.)).length() < std::f32::EPSILON); - assert!((padded.max - Vec3A::new(2., 2., 2.)).length() < std::f32::EPSILON); + assert!((padded.min - Vec3A::new(-2., -2., -2.)).length() < f32::EPSILON); + assert!((padded.max - Vec3A::new(2., 2., 2.)).length() < f32::EPSILON); assert!(padded.contains(&a)); assert!(!a.contains(&padded)); } @@ -356,8 +356,8 @@ mod aabb3d_tests { max: Vec3A::new(2., 2., 2.), }; let shrunk = a.shrink(Vec3A::ONE); - assert!((shrunk.min - Vec3A::new(-1., -1., -1.)).length() < std::f32::EPSILON); - assert!((shrunk.max - Vec3A::new(1., 1., 1.)).length() < std::f32::EPSILON); + assert!((shrunk.min - Vec3A::new(-1., -1., -1.)).length() < f32::EPSILON); + assert!((shrunk.max - Vec3A::new(1., 1., 1.)).length() < f32::EPSILON); assert!(a.contains(&shrunk)); assert!(!shrunk.contains(&a)); } @@ -369,8 +369,8 @@ mod aabb3d_tests { max: Vec3A::ONE, }; let scaled = a.scale_around_center(Vec3A::splat(2.)); - assert!((scaled.min - Vec3A::splat(-2.)).length() < std::f32::EPSILON); - assert!((scaled.max - Vec3A::splat(2.)).length() < std::f32::EPSILON); + assert!((scaled.min - Vec3A::splat(-2.)).length() < f32::EPSILON); + assert!((scaled.max - Vec3A::splat(2.)).length() < f32::EPSILON); assert!(!a.contains(&scaled)); assert!(scaled.contains(&a)); } @@ -671,8 +671,8 @@ mod bounding_sphere_tests { let a = BoundingSphere::new(Vec3::ONE, 5.); let b = BoundingSphere::new(Vec3::new(1., 1., -4.), 1.); let merged = a.merge(&b); - assert!((merged.center - Vec3A::new(1., 1., 0.5)).length() < std::f32::EPSILON); - assert!((merged.radius() - 5.5).abs() < std::f32::EPSILON); + assert!((merged.center - Vec3A::new(1., 1., 0.5)).length() < f32::EPSILON); + assert!((merged.radius() - 5.5).abs() < f32::EPSILON); assert!(merged.contains(&a)); assert!(merged.contains(&b)); assert!(!a.contains(&merged)); @@ -704,7 +704,7 @@ mod bounding_sphere_tests { fn grow() { let a = BoundingSphere::new(Vec3::ONE, 5.); let padded = a.grow(1.25); - assert!((padded.radius() - 6.25).abs() < std::f32::EPSILON); + assert!((padded.radius() - 6.25).abs() < f32::EPSILON); assert!(padded.contains(&a)); assert!(!a.contains(&padded)); } @@ -713,7 +713,7 @@ mod bounding_sphere_tests { fn shrink() { let a = BoundingSphere::new(Vec3::ONE, 5.); let shrunk = a.shrink(0.5); - assert!((shrunk.radius() - 4.5).abs() < std::f32::EPSILON); + assert!((shrunk.radius() - 4.5).abs() < f32::EPSILON); assert!(a.contains(&shrunk)); assert!(!shrunk.contains(&a)); } @@ -722,7 +722,7 @@ mod bounding_sphere_tests { fn scale_around_center() { let a = BoundingSphere::new(Vec3::ONE, 5.); let scaled = a.scale_around_center(2.); - assert!((scaled.radius() - 10.).abs() < std::f32::EPSILON); + assert!((scaled.radius() - 10.).abs() < f32::EPSILON); assert!(!a.contains(&scaled)); assert!(scaled.contains(&a)); } diff --git a/crates/bevy_math/src/cubic_splines.rs b/crates/bevy_math/src/cubic_splines.rs index 05707a6e4185b..147a72944a265 100644 --- a/crates/bevy_math/src/cubic_splines.rs +++ b/crates/bevy_math/src/cubic_splines.rs @@ -40,8 +40,10 @@ use thiserror::Error; /// let bezier = CubicBezier::new(points).to_curve(); /// let positions: Vec<_> = bezier.iter_positions(100).collect(); /// ``` +#[derive(Clone, Debug)] pub struct CubicBezier { - control_points: Vec<[P; 4]>, + /// The control points of the Bezier curve + pub control_points: Vec<[P; 4]>, } impl CubicBezier

{ @@ -111,8 +113,10 @@ impl CubicGenerator

for CubicBezier

{ /// let hermite = CubicHermite::new(points, tangents).to_curve(); /// let positions: Vec<_> = hermite.iter_positions(100).collect(); /// ``` +#[derive(Clone, Debug)] pub struct CubicHermite { - control_points: Vec<(P, P)>, + /// The control points of the Hermite curve + pub control_points: Vec<(P, P)>, } impl CubicHermite

{ /// Create a new Hermite curve from sets of control points. @@ -177,9 +181,12 @@ impl CubicGenerator

for CubicHermite

{ /// let cardinal = CubicCardinalSpline::new(0.3, points).to_curve(); /// let positions: Vec<_> = cardinal.iter_positions(100).collect(); /// ``` +#[derive(Clone, Debug)] pub struct CubicCardinalSpline { - tension: f32, - control_points: Vec

, + /// Tension + pub tension: f32, + /// The control points of the Cardinal spline + pub control_points: Vec

, } impl CubicCardinalSpline

{ @@ -264,8 +271,10 @@ impl CubicGenerator

for CubicCardinalSpline

{ /// let b_spline = CubicBSpline::new(points).to_curve(); /// let positions: Vec<_> = b_spline.iter_positions(100).collect(); /// ``` +#[derive(Clone, Debug)] pub struct CubicBSpline { - control_points: Vec

, + /// The control points of the spline + pub control_points: Vec

, } impl CubicBSpline

{ /// Build a new B-Spline. @@ -303,7 +312,7 @@ impl CubicGenerator

for CubicBSpline

{ } /// Error during construction of [`CubicNurbs`] -#[derive(Debug, Error)] +#[derive(Clone, Debug, Error)] pub enum CubicNurbsError { /// Provided the wrong number of knots. #[error("Wrong number of knots: expected {expected}, provided {provided}")] @@ -381,10 +390,14 @@ pub enum CubicNurbsError { /// .to_curve(); /// let positions: Vec<_> = nurbs.iter_positions(100).collect(); /// ``` +#[derive(Clone, Debug)] pub struct CubicNurbs { - control_points: Vec

, - weights: Vec, - knots: Vec, + /// The control points of the NURBS + pub control_points: Vec

, + /// Weights + pub weights: Vec, + /// Knots + pub knots: Vec, } impl CubicNurbs

{ /// Build a Non-Uniform Rational B-Spline. @@ -585,8 +598,10 @@ impl RationalGenerator

for CubicNurbs

{ /// /// ### Continuity /// The curve is C0 continuous, meaning it has no holes or jumps. +#[derive(Clone, Debug)] pub struct LinearSpline { - points: Vec

, + /// The control points of the NURBS + pub points: Vec

, } impl LinearSpline

{ /// Create a new linear spline @@ -624,9 +639,10 @@ pub trait CubicGenerator { /// Can be evaluated as a parametric curve over the domain `[0, 1)`. /// /// Segments can be chained together to form a longer compound curve. -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct CubicSegment { - coeff: [P; 4], + /// Coefficients of the segment + pub coeff: [P; 4], } impl CubicSegment

{ @@ -685,7 +701,7 @@ impl CubicSegment { pub fn new_bezier(p1: impl Into, p2: impl Into) -> Self { let (p0, p3) = (Vec2::ZERO, Vec2::ONE); let bezier = CubicBezier::new([[p0, p1.into(), p2.into(), p3]]).to_curve(); - bezier.segments[0].clone() + bezier.segments[0] } /// Maximum allowable error for iterative Bezier solve @@ -784,7 +800,8 @@ impl CubicSegment { /// [`CubicBezier`]. #[derive(Clone, Debug, PartialEq)] pub struct CubicCurve { - segments: Vec>, + /// Segments of the curve + pub segments: Vec>, } impl CubicCurve

{ @@ -914,14 +931,14 @@ pub trait RationalGenerator { /// Can be evaluated as a parametric curve over the domain `[0, knot_span)`. /// /// Segments can be chained together to form a longer compound curve. -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct RationalSegment { /// The coefficients matrix of the cubic curve. - coeff: [P; 4], + pub coeff: [P; 4], /// The homogeneous weight coefficients. - weight_coeff: [f32; 4], + pub weight_coeff: [f32; 4], /// The width of the domain of this segment. - knot_span: f32, + pub knot_span: f32, } impl RationalSegment

{ @@ -1043,7 +1060,8 @@ impl RationalSegment

{ /// [`CubicNurbs`], or convert [`CubicCurve`] using `into/from`. #[derive(Clone, Debug, PartialEq)] pub struct RationalCurve { - segments: Vec>, + /// The segments in the curve + pub segments: Vec>, } impl RationalCurve

{ diff --git a/crates/bevy_math/src/direction.rs b/crates/bevy_math/src/direction.rs index d44c54a39938e..dbaf7fd671136 100644 --- a/crates/bevy_math/src/direction.rs +++ b/crates/bevy_math/src/direction.rs @@ -92,6 +92,8 @@ impl Dir2 { pub const NEG_X: Self = Self(Vec2::NEG_X); /// A unit vector pointing along the negative Y axis. pub const NEG_Y: Self = Self(Vec2::NEG_Y); + /// The directional axes. + pub const AXES: [Self; 2] = [Self::X, Self::Y]; /// Create a direction from a finite, nonzero [`Vec2`]. /// @@ -254,6 +256,8 @@ impl Dir3 { pub const NEG_Y: Self = Self(Vec3::NEG_Y); /// A unit vector pointing along the negative Z axis. pub const NEG_Z: Self = Self(Vec3::NEG_Z); + /// The directional axes. + pub const AXES: [Self; 3] = [Self::X, Self::Y, Self::Z]; /// Create a direction from a finite, nonzero [`Vec3`]. /// @@ -419,6 +423,8 @@ impl Dir3A { pub const NEG_Y: Self = Self(Vec3A::NEG_Y); /// A unit vector pointing along the negative Z axis. pub const NEG_Z: Self = Self(Vec3A::NEG_Z); + /// The directional axes. + pub const AXES: [Self; 3] = [Self::X, Self::Y, Self::Z]; /// Create a direction from a finite, nonzero [`Vec3A`]. /// diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 14660edc5b03b..c69491fac2f0a 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1,6 +1,6 @@ use std::f32::consts::PI; -use super::{Primitive2d, WindingOrder}; +use super::{Measured2d, Primitive2d, WindingOrder}; use crate::{Dir2, Vec2}; /// A circle primitive @@ -32,19 +32,6 @@ impl Circle { 2.0 * self.radius } - /// Get the area of the circle - #[inline(always)] - pub fn area(&self) -> f32 { - PI * self.radius.powi(2) - } - - /// Get the perimeter or circumference of the circle - #[inline(always)] - #[doc(alias = "circumference")] - pub fn perimeter(&self) -> f32 { - 2.0 * PI * self.radius - } - /// Finds the point on the circle that is closest to the given `point`. /// /// If the point is outside the circle, the returned point will be on the perimeter of the circle. @@ -65,6 +52,21 @@ impl Circle { } } +impl Measured2d for Circle { + /// Get the area of the circle + #[inline(always)] + fn area(&self) -> f32 { + PI * self.radius.powi(2) + } + + /// Get the perimeter or circumference of the circle + #[inline(always)] + #[doc(alias = "circumference")] + fn perimeter(&self) -> f32 { + 2.0 * PI * self.radius + } +} + /// An ellipse primitive #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -118,6 +120,17 @@ impl Ellipse { (a * a - b * b).sqrt() / a } + #[inline(always)] + /// Get the focal length of the ellipse. This corresponds to the distance between one of the foci and the center of the ellipse. + /// + /// The focal length of an ellipse is related to its eccentricity by `eccentricity = focal_length / semi_major` + pub fn focal_length(&self) -> f32 { + let a = self.semi_major(); + let b = self.semi_minor(); + + (a * a - b * b).sqrt() + } + /// Returns the length of the semi-major axis. This corresponds to the longest radius of the ellipse. #[inline(always)] pub fn semi_major(&self) -> f32 { @@ -129,12 +142,70 @@ impl Ellipse { pub fn semi_minor(&self) -> f32 { self.half_size.min_element() } +} +impl Measured2d for Ellipse { /// Get the area of the ellipse #[inline(always)] - pub fn area(&self) -> f32 { + fn area(&self) -> f32 { PI * self.half_size.x * self.half_size.y } + + #[inline(always)] + /// Get an approximation for the perimeter or circumference of the ellipse. + /// + /// The approximation is reasonably precise with a relative error less than 0.007%, getting more precise as the eccentricity of the ellipse decreases. + fn perimeter(&self) -> f32 { + let a = self.semi_major(); + let b = self.semi_minor(); + + // In the case that `a == b`, the ellipse is a circle + if a / b - 1. < 1e-5 { + return PI * (a + b); + }; + + // In the case that `a` is much larger than `b`, the ellipse is a line + if a / b > 1e4 { + return 4. * a; + }; + + // These values are the result of (0.5 choose n)^2 where n is the index in the array + // They could be calculated on the fly but hardcoding them yields more accurate and faster results + // because the actual calculation for these values involves factorials and numbers > 10^23 + const BINOMIAL_COEFFICIENTS: [f32; 21] = [ + 1., + 0.25, + 0.015625, + 0.00390625, + 0.0015258789, + 0.00074768066, + 0.00042057037, + 0.00025963783, + 0.00017140154, + 0.000119028846, + 0.00008599834, + 0.00006414339, + 0.000049109784, + 0.000038430585, + 0.000030636627, + 0.000024815668, + 0.000020380836, + 0.000016942893, + 0.000014236736, + 0.000012077564, + 0.000010333865, + ]; + + // The algorithm used here is the Gauss-Kummer infinite series expansion of the elliptic integral expression for the perimeter of ellipses + // For more information see https://www.wolframalpha.com/input/?i=gauss-kummer+series + // We only use the terms up to `i == 20` for this approximation + let h = ((a - b) / (a + b)).powi(2); + + PI * (a + b) + * (0..=20) + .map(|i| BINOMIAL_COEFFICIENTS[i] * h.powi(i as i32)) + .sum::() + } } /// A primitive shape formed by the region between two circles, also known as a ring. @@ -181,20 +252,6 @@ impl Annulus { self.outer_circle.radius - self.inner_circle.radius } - /// Get the area of the annulus - #[inline(always)] - pub fn area(&self) -> f32 { - PI * (self.outer_circle.radius.powi(2) - self.inner_circle.radius.powi(2)) - } - - /// Get the perimeter or circumference of the annulus, - /// which is the sum of the perimeters of the inner and outer circles. - #[inline(always)] - #[doc(alias = "circumference")] - pub fn perimeter(&self) -> f32 { - 2.0 * PI * (self.outer_circle.radius + self.inner_circle.radius) - } - /// Finds the point on the annulus that is closest to the given `point`: /// /// - If the point is outside of the annulus completely, the returned point will be on the outer perimeter. @@ -223,6 +280,22 @@ impl Annulus { } } +impl Measured2d for Annulus { + /// Get the area of the annulus + #[inline(always)] + fn area(&self) -> f32 { + PI * (self.outer_circle.radius.powi(2) - self.inner_circle.radius.powi(2)) + } + + /// Get the perimeter or circumference of the annulus, + /// which is the sum of the perimeters of the inner and outer circles. + #[inline(always)] + #[doc(alias = "circumference")] + fn perimeter(&self) -> f32 { + 2.0 * PI * (self.outer_circle.radius + self.inner_circle.radius) + } +} + /// An unbounded plane in 2D space. It forms a separating surface through the origin, /// stretching infinitely far #[derive(Clone, Copy, Debug, PartialEq)] @@ -404,25 +477,6 @@ impl Triangle2d { } } - /// Get the area of the triangle - #[inline(always)] - pub fn area(&self) -> f32 { - let [a, b, c] = self.vertices; - (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)).abs() / 2.0 - } - - /// Get the perimeter of the triangle - #[inline(always)] - pub fn perimeter(&self) -> f32 { - let [a, b, c] = self.vertices; - - let ab = a.distance(b); - let bc = b.distance(c); - let ca = c.distance(a); - - ab + bc + ca - } - /// Get the [`WindingOrder`] of the triangle #[inline(always)] #[doc(alias = "orientation")] @@ -481,6 +535,27 @@ impl Triangle2d { } } +impl Measured2d for Triangle2d { + /// Get the area of the triangle + #[inline(always)] + fn area(&self) -> f32 { + let [a, b, c] = self.vertices; + (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)).abs() / 2.0 + } + + /// Get the perimeter of the triangle + #[inline(always)] + fn perimeter(&self) -> f32 { + let [a, b, c] = self.vertices; + + let ab = a.distance(b); + let bc = b.distance(c); + let ca = c.distance(a); + + ab + bc + ca + } +} + /// A rectangle primitive #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -538,18 +613,6 @@ impl Rectangle { 2.0 * self.half_size } - /// Get the area of the rectangle - #[inline(always)] - pub fn area(&self) -> f32 { - 4.0 * self.half_size.x * self.half_size.y - } - - /// Get the perimeter of the rectangle - #[inline(always)] - pub fn perimeter(&self) -> f32 { - 4.0 * (self.half_size.x + self.half_size.y) - } - /// Finds the point on the rectangle that is closest to the given `point`. /// /// If the point is outside the rectangle, the returned point will be on the perimeter of the rectangle. @@ -561,6 +624,20 @@ impl Rectangle { } } +impl Measured2d for Rectangle { + /// Get the area of the rectangle + #[inline(always)] + fn area(&self) -> f32 { + 4.0 * self.half_size.x * self.half_size.y + } + + /// Get the perimeter of the rectangle + #[inline(always)] + fn perimeter(&self) -> f32 { + 4.0 * (self.half_size.x + self.half_size.y) + } +} + /// A polygon with N vertices. /// /// For a version without generics: [`BoxedPolygon`] @@ -646,10 +723,13 @@ impl RegularPolygon { /// /// # Panics /// - /// Panics if `circumradius` is non-positive + /// Panics if `circumradius` is negative #[inline(always)] pub fn new(circumradius: f32, sides: usize) -> Self { - assert!(circumradius > 0.0, "polygon has a non-positive radius"); + assert!( + circumradius.is_sign_positive(), + "polygon has a negative radius" + ); assert!(sides > 2, "polygon has less than 3 sides"); Self { @@ -682,20 +762,6 @@ impl RegularPolygon { 2.0 * self.circumradius() * (PI / self.sides as f32).sin() } - /// Get the area of the regular polygon - #[inline(always)] - pub fn area(&self) -> f32 { - let angle: f32 = 2.0 * PI / (self.sides as f32); - (self.sides as f32) * self.circumradius().powi(2) * angle.sin() / 2.0 - } - - /// Get the perimeter of the regular polygon. - /// This is the sum of its sides - #[inline(always)] - pub fn perimeter(&self) -> f32 { - self.sides as f32 * self.side_length() - } - /// Get the internal angle of the regular polygon in degrees. /// /// This is the angle formed by two adjacent sides with points @@ -749,6 +815,22 @@ impl RegularPolygon { } } +impl Measured2d for RegularPolygon { + /// Get the area of the regular polygon + #[inline(always)] + fn area(&self) -> f32 { + let angle: f32 = 2.0 * PI / (self.sides as f32); + (self.sides as f32) * self.circumradius().powi(2) * angle.sin() / 2.0 + } + + /// Get the perimeter of the regular polygon. + /// This is the sum of its sides + #[inline(always)] + fn perimeter(&self) -> f32 { + self.sides as f32 * self.side_length() + } +} + /// A 2D capsule primitive, also known as a stadium or pill shape. /// /// A two-dimensional capsule is defined as a neighborhood of points at a distance (radius) from a line @@ -861,6 +943,21 @@ mod tests { assert_eq!(circle.eccentricity(), 0., "incorrect circle eccentricity"); } + #[test] + fn ellipse_perimeter() { + let circle = Ellipse::new(1., 1.); + assert_relative_eq!(circle.perimeter(), 6.2831855); + + let line = Ellipse::new(75_000., 0.5); + assert_relative_eq!(line.perimeter(), 300_000.); + + let ellipse = Ellipse::new(0.5, 2.); + assert_relative_eq!(ellipse.perimeter(), 8.578423); + + let ellipse = Ellipse::new(5., 3.); + assert_relative_eq!(ellipse.perimeter(), 25.526999); + } + #[test] fn triangle_math() { let triangle = Triangle2d::new( diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index b3308acbf8089..e1b5ae512843e 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -1,6 +1,6 @@ use std::f32::consts::{FRAC_PI_3, PI}; -use super::{Circle, Primitive3d}; +use super::{Circle, Measured2d, Measured3d, Primitive2d, Primitive3d}; use crate::{Dir3, InvalidDirectionError, Mat3, Vec2, Vec3}; /// A sphere primitive @@ -32,18 +32,6 @@ impl Sphere { 2.0 * self.radius } - /// Get the surface area of the sphere - #[inline(always)] - pub fn area(&self) -> f32 { - 4.0 * PI * self.radius.powi(2) - } - - /// Get the volume of the sphere - #[inline(always)] - pub fn volume(&self) -> f32 { - 4.0 * FRAC_PI_3 * self.radius.powi(3) - } - /// Finds the point on the sphere that is closest to the given `point`. /// /// If the point is outside the sphere, the returned point will be on the surface of the sphere. @@ -64,6 +52,20 @@ impl Sphere { } } +impl Measured3d for Sphere { + /// Get the surface area of the sphere + #[inline(always)] + fn area(&self) -> f32 { + 4.0 * PI * self.radius.powi(2) + } + + /// Get the volume of the sphere + #[inline(always)] + fn volume(&self) -> f32 { + 4.0 * FRAC_PI_3 * self.radius.powi(3) + } +} + /// A bounded plane in 3D space. It forms a surface starting from the origin with a defined height and width. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -360,9 +362,21 @@ impl Cuboid { 2.0 * self.half_size } + /// Finds the point on the cuboid that is closest to the given `point`. + /// + /// If the point is outside the cuboid, the returned point will be on the surface of the cuboid. + /// Otherwise, it will be inside the cuboid and returned as is. + #[inline(always)] + pub fn closest_point(&self, point: Vec3) -> Vec3 { + // Clamp point coordinates to the cuboid + point.clamp(-self.half_size, self.half_size) + } +} + +impl Measured3d for Cuboid { /// Get the surface area of the cuboid #[inline(always)] - pub fn area(&self) -> f32 { + fn area(&self) -> f32 { 8.0 * (self.half_size.x * self.half_size.y + self.half_size.y * self.half_size.z + self.half_size.x * self.half_size.z) @@ -370,19 +384,9 @@ impl Cuboid { /// Get the volume of the cuboid #[inline(always)] - pub fn volume(&self) -> f32 { + fn volume(&self) -> f32 { 8.0 * self.half_size.x * self.half_size.y * self.half_size.z } - - /// Finds the point on the cuboid that is closest to the given `point`. - /// - /// If the point is outside the cuboid, the returned point will be on the surface of the cuboid. - /// Otherwise, it will be inside the cuboid and returned as is. - #[inline(always)] - pub fn closest_point(&self, point: Vec3) -> Vec3 { - // Clamp point coordinates to the cuboid - point.clamp(-self.half_size, self.half_size) - } } /// A cylinder primitive @@ -437,16 +441,18 @@ impl Cylinder { pub fn base_area(&self) -> f32 { PI * self.radius.powi(2) } +} +impl Measured3d for Cylinder { /// Get the total surface area of the cylinder #[inline(always)] - pub fn area(&self) -> f32 { + fn area(&self) -> f32 { 2.0 * PI * self.radius * (self.radius + 2.0 * self.half_height) } /// Get the volume of the cylinder #[inline(always)] - pub fn volume(&self) -> f32 { + fn volume(&self) -> f32 { self.base_area() * 2.0 * self.half_height } } @@ -492,17 +498,19 @@ impl Capsule3d { half_height: self.half_length, } } +} +impl Measured3d for Capsule3d { /// Get the surface area of the capsule #[inline(always)] - pub fn area(&self) -> f32 { + fn area(&self) -> f32 { // Modified version of 2pi * r * (2r + h) 4.0 * PI * self.radius * (self.radius + self.half_length) } /// Get the volume of the capsule #[inline(always)] - pub fn volume(&self) -> f32 { + fn volume(&self) -> f32 { // Modified version of pi * r^2 * (4/3 * r + a) let diameter = self.radius * 2.0; PI * self.radius * diameter * (diameter / 3.0 + self.half_length) @@ -520,6 +528,16 @@ pub struct Cone { } impl Primitive3d for Cone {} +impl Default for Cone { + /// Returns the default [`Cone`] with a base radius of `0.5` and a height of `1.0`. + fn default() -> Self { + Self { + radius: 0.5, + height: 1.0, + } + } +} + impl Cone { /// Get the base of the cone as a [`Circle`] #[inline(always)] @@ -550,16 +568,18 @@ impl Cone { pub fn base_area(&self) -> f32 { PI * self.radius.powi(2) } +} +impl Measured3d for Cone { /// Get the total surface area of the cone #[inline(always)] - pub fn area(&self) -> f32 { + fn area(&self) -> f32 { self.base_area() + self.lateral_area() } /// Get the volume of the cone #[inline(always)] - pub fn volume(&self) -> f32 { + fn volume(&self) -> f32 { (self.base_area() * self.height) / 3.0 } } @@ -681,18 +701,20 @@ impl Torus { std::cmp::Ordering::Less => TorusKind::Spindle, } } +} +impl Measured3d for Torus { /// Get the surface area of the torus. Note that this only produces /// the expected result when the torus has a ring and isn't self-intersecting #[inline(always)] - pub fn area(&self) -> f32 { + fn area(&self) -> f32 { 4.0 * PI.powi(2) * self.major_radius * self.minor_radius } /// Get the volume of the torus. Note that this only produces /// the expected result when the torus has a ring and isn't self-intersecting #[inline(always)] - pub fn volume(&self) -> f32 { + fn volume(&self) -> f32 { 2.0 * PI.powi(2) * self.major_radius * self.minor_radius.powi(2) } } @@ -729,22 +751,6 @@ impl Triangle3d { } } - /// Get the area of the triangle. - #[inline(always)] - pub fn area(&self) -> f32 { - let [a, b, c] = self.vertices; - let ab = b - a; - let ac = c - a; - ab.cross(ac).length() / 2.0 - } - - /// Get the perimeter of the triangle. - #[inline(always)] - pub fn perimeter(&self) -> f32 { - let [a, b, c] = self.vertices; - a.distance(b) + b.distance(c) + c.distance(a) - } - /// Get the normal of the triangle in the direction of the right-hand rule, assuming /// the vertices are ordered in a counter-clockwise direction. /// @@ -835,6 +841,24 @@ impl Triangle3d { } } +impl Measured2d for Triangle3d { + /// Get the area of the triangle. + #[inline(always)] + fn area(&self) -> f32 { + let [a, b, c] = self.vertices; + let ab = b - a; + let ac = c - a; + ab.cross(ac).length() / 2.0 + } + + /// Get the perimeter of the triangle. + #[inline(always)] + fn perimeter(&self) -> f32 { + let [a, b, c] = self.vertices; + a.distance(b) + b.distance(c) + c.distance(a) + } +} + /// A tetrahedron primitive. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -868,28 +892,6 @@ impl Tetrahedron { } } - /// Get the surface area of the tetrahedron. - #[inline(always)] - pub fn area(&self) -> f32 { - let [a, b, c, d] = self.vertices; - let ab = b - a; - let ac = c - a; - let ad = d - a; - let bc = c - b; - let bd = d - b; - (ab.cross(ac).length() - + ab.cross(ad).length() - + ac.cross(ad).length() - + bc.cross(bd).length()) - / 2.0 - } - - /// Get the volume of the tetrahedron. - #[inline(always)] - pub fn volume(&self) -> f32 { - self.signed_volume().abs() - } - /// Get the signed volume of the tetrahedron. /// /// If it's negative, the normal vector of the face defined by @@ -915,6 +917,70 @@ impl Tetrahedron { } } +impl Measured3d for Tetrahedron { + /// Get the surface area of the tetrahedron. + #[inline(always)] + fn area(&self) -> f32 { + let [a, b, c, d] = self.vertices; + let ab = b - a; + let ac = c - a; + let ad = d - a; + let bc = c - b; + let bd = d - b; + (ab.cross(ac).length() + + ab.cross(ad).length() + + ac.cross(ad).length() + + bc.cross(bd).length()) + / 2.0 + } + + /// Get the volume of the tetrahedron. + #[inline(always)] + fn volume(&self) -> f32 { + self.signed_volume().abs() + } +} + +/// A 3D shape representing an extruded 2D `base_shape`. +/// +/// Extruding a shape effectively "thickens" a 2D shapes, +/// creating a shape with the same cross-section over the entire depth. +/// +/// The resulting volumes are prisms. +/// For example, a triangle becomes a triangular prism, while a circle becomes a cylinder. +#[doc(alias = "Prism")] +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct Extrusion { + /// The base shape of the extrusion + pub base_shape: T, + /// Half of the depth of the extrusion + pub half_depth: f32, +} +impl Primitive3d for Extrusion {} + +impl Extrusion { + /// Create a new `Extrusion` from a given `base_shape` and `depth` + pub fn new(base_shape: T, depth: f32) -> Self { + Self { + base_shape, + half_depth: depth / 2., + } + } +} + +impl Measured3d for Extrusion { + /// Get the surface area of the extrusion + fn area(&self) -> f32 { + 2. * (self.base_shape.area() + self.half_depth * self.base_shape.perimeter()) + } + + /// Get the volume of the extrusion + fn volume(&self) -> f32 { + 2. * self.base_shape.area() * self.half_depth + } +} + #[cfg(test)] mod tests { // Reference values were computed by hand and/or with external tools @@ -1146,4 +1212,22 @@ mod tests { let degenerate = Triangle3d::new(Vec3::NEG_ONE, Vec3::ZERO, Vec3::ONE); assert!(degenerate.is_degenerate(), "did not find degenerate"); } + + #[test] + fn extrusion_math() { + let circle = Circle::new(0.75); + let cylinder = Extrusion::new(circle, 2.5); + assert_eq!(cylinder.area(), 15.315264, "incorrect surface area"); + assert_eq!(cylinder.volume(), 4.417865, "incorrect volume"); + + let annulus = crate::primitives::Annulus::new(0.25, 1.375); + let tube = Extrusion::new(annulus, 0.333); + assert_eq!(tube.area(), 14.886437, "incorrect surface area"); + assert_eq!(tube.volume(), 1.9124937, "incorrect volume"); + + let polygon = crate::primitives::RegularPolygon::new(3.8, 7); + let regular_prism = Extrusion::new(polygon, 1.25); + assert_eq!(regular_prism.area(), 107.8808, "incorrect surface area"); + assert_eq!(regular_prism.volume(), 49.392204, "incorrect volume"); + } } diff --git a/crates/bevy_math/src/primitives/mod.rs b/crates/bevy_math/src/primitives/mod.rs index 8fda6924ec5e8..460e635867ecb 100644 --- a/crates/bevy_math/src/primitives/mod.rs +++ b/crates/bevy_math/src/primitives/mod.rs @@ -29,3 +29,21 @@ pub enum WindingOrder { #[doc(alias("Degenerate", "Collinear"))] Invalid, } + +/// A trait for getting measurements of 2D shapes +pub trait Measured2d { + /// Get the perimeter of the shape + fn perimeter(&self) -> f32; + + /// Get the area of the shape + fn area(&self) -> f32; +} + +/// A trait for getting measurements of 3D shapes +pub trait Measured3d { + /// Get the surface area of the shape + fn area(&self) -> f32; + + /// Get the volume of the shape + fn volume(&self) -> f32; +} diff --git a/crates/bevy_mikktspace/Cargo.toml b/crates/bevy_mikktspace/Cargo.toml index e11ecdc163e48..aec04e9505d47 100644 --- a/crates/bevy_mikktspace/Cargo.toml +++ b/crates/bevy_mikktspace/Cargo.toml @@ -13,6 +13,7 @@ homepage = "https://bevyengine.org" repository = "https://github.com/bevyengine/bevy" license = "Zlib AND (MIT OR Apache-2.0)" keywords = ["bevy", "3D", "graphics", "algorithm", "tangent"] +rust-version = "1.76.0" [dependencies] glam = "0.27" diff --git a/crates/bevy_mikktspace/README.md b/crates/bevy_mikktspace/README.md index 7ac119d674ce2..cb497c981aa99 100644 --- a/crates/bevy_mikktspace/README.md +++ b/crates/bevy_mikktspace/README.md @@ -10,7 +10,7 @@ This is a fork of [https://github.com/gltf-rs/mikktspace](https://github.com/glt Port of the [Mikkelsen Tangent Space Algorithm](https://en.blender.org/index.php/Dev:Shading/Tangent_Space_Normal_Maps) reference implementation. -Requires at least Rust 1.52.1. +Requires at least Rust 1.76.0. ## Examples diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index adcc4beea33d7..3af9f643484bf 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["bevy"] webgl = [] webgpu = [] pbr_transmission_textures = [] +pbr_multi_layer_material_textures = [] shader_format_glsl = ["bevy_render/shader_format_glsl"] trace = ["bevy_render/trace"] ios_simulator = ["bevy_render/ios_simulator"] diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 8c5078f1fe245..efe9ae4aa2f79 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -34,6 +34,7 @@ mod pbr_material; mod prepass; mod render; mod ssao; +mod volumetric_fog; use bevy_color::{Color, LinearRgba}; use std::marker::PhantomData; @@ -50,6 +51,7 @@ pub use pbr_material::*; pub use prepass::*; pub use render::*; pub use ssao::*; +pub use volumetric_fog::*; pub mod prelude { #[doc(hidden)] @@ -81,6 +83,8 @@ pub mod graph { /// Label for the screen space ambient occlusion render node. ScreenSpaceAmbientOcclusion, DeferredLightingPass, + /// Label for the volumetric lighting pass. + VolumetricFog, /// Label for the compute shader instance data building pass. GpuPreprocess, } @@ -314,6 +318,7 @@ impl Plugin for PbrPlugin { GpuMeshPreprocessPlugin { use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder, }, + VolumetricFogPlugin, )) .configure_sets( PostUpdate, diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 0cbe8afab8bab..c0b82602454ff 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -26,6 +26,7 @@ use crate::*; mod ambient_light; pub use ambient_light::AmbientLight; + mod point_light; pub use point_light::PointLight; mod spot_light; @@ -1006,19 +1007,24 @@ pub(crate) fn point_light_order( } // Sort lights by -// - those with shadows enabled first, so that the index can be used to render at most `directional_light_shadow_maps_count` -// directional light shadows -// - then by entity as a stable key to ensure that a consistent set of lights are chosen if the light count limit is exceeded. +// - those with volumetric (and shadows) enabled first, so that the volumetric +// lighting pass can quickly find the volumetric lights; +// - then those with shadows enabled second, so that the index can be used to +// render at most `directional_light_shadow_maps_count` directional light +// shadows; +// - then by entity as a stable key to ensure that a consistent set of lights +// are chosen if the light count limit is exceeded. pub(crate) fn directional_light_order( - (entity_1, shadows_enabled_1): (&Entity, &bool), - (entity_2, shadows_enabled_2): (&Entity, &bool), + (entity_1, volumetric_1, shadows_enabled_1): (&Entity, &bool, &bool), + (entity_2, volumetric_2, shadows_enabled_2): (&Entity, &bool, &bool), ) -> std::cmp::Ordering { - shadows_enabled_2 - .cmp(shadows_enabled_1) // shadow casters before non-casters + volumetric_2 + .cmp(volumetric_1) // volumetric before shadows + .then_with(|| shadows_enabled_2.cmp(shadows_enabled_1)) // shadow casters before non-casters .then_with(|| entity_1.cmp(entity_2)) // stable } -#[derive(Clone, Copy)] +#[derive(Clone)] // data required for assigning lights to clusters pub(crate) struct PointLightAssignmentData { entity: Entity, @@ -1108,7 +1114,7 @@ pub(crate) fn assign_lights_to_clusters( shadows_enabled: point_light.shadows_enabled, range: point_light.range, spot_light_angle: None, - render_layers: maybe_layers.copied().unwrap_or_default(), + render_layers: maybe_layers.unwrap_or_default().clone(), } }, ), @@ -1125,7 +1131,7 @@ pub(crate) fn assign_lights_to_clusters( 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(), + render_layers: maybe_layers.unwrap_or_default().clone(), } }, ), @@ -1199,7 +1205,7 @@ pub(crate) fn assign_lights_to_clusters( mut visible_lights, ) in &mut views { - let view_layers = maybe_layers.copied().unwrap_or_default(); + let view_layers = maybe_layers.unwrap_or_default(); let clusters = clusters.into_inner(); if matches!(config, ClusterConfig::None) { @@ -1926,7 +1932,7 @@ pub fn check_light_mesh_visibility( continue; } - let view_mask = maybe_view_mask.copied().unwrap_or_default(); + let view_mask = maybe_view_mask.unwrap_or_default(); for ( entity, @@ -1942,8 +1948,8 @@ pub fn check_light_mesh_visibility( continue; } - let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); - if !view_mask.intersects(&entity_mask) { + let entity_mask = maybe_entity_mask.unwrap_or_default(); + if !view_mask.intersects(entity_mask) { continue; } @@ -2016,7 +2022,7 @@ pub fn check_light_mesh_visibility( continue; } - let view_mask = maybe_view_mask.copied().unwrap_or_default(); + let view_mask = maybe_view_mask.unwrap_or_default(); let light_sphere = Sphere { center: Vec3A::from(transform.translation()), radius: point_light.range, @@ -2036,8 +2042,8 @@ pub fn check_light_mesh_visibility( continue; } - let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); - if !view_mask.intersects(&entity_mask) { + let entity_mask = maybe_entity_mask.unwrap_or_default(); + if !view_mask.intersects(entity_mask) { continue; } @@ -2091,7 +2097,7 @@ pub fn check_light_mesh_visibility( continue; } - let view_mask = maybe_view_mask.copied().unwrap_or_default(); + let view_mask = maybe_view_mask.unwrap_or_default(); let light_sphere = Sphere { center: Vec3A::from(transform.translation()), radius: point_light.range, @@ -2111,8 +2117,8 @@ pub fn check_light_mesh_visibility( continue; } - let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); - if !view_mask.intersects(&entity_mask) { + let entity_mask = maybe_entity_mask.unwrap_or_default(); + if !view_mask.intersects(entity_mask) { continue; } diff --git a/crates/bevy_pbr/src/light/point_light.rs b/crates/bevy_pbr/src/light/point_light.rs index f90556f2ffbbb..9ca85cd4db167 100644 --- a/crates/bevy_pbr/src/light/point_light.rs +++ b/crates/bevy_pbr/src/light/point_light.rs @@ -5,7 +5,7 @@ use super::*; /// Real-world values for `intensity` (luminous power in lumens) based on the electrical power /// consumption of the type of real-world light are: /// -/// | Luminous Power (lumen) (i.e. the intensity member) | Incandescent non-halogen (Watts) | Incandescent halogen (Watts) | Compact fluorescent (Watts) | LED (Watts | +/// | Luminous Power (lumen) (i.e. the intensity member) | Incandescent non-halogen (Watts) | Incandescent halogen (Watts) | Compact fluorescent (Watts) | LED (Watts) | /// |------|-----|----|--------|-------| /// | 200 | 25 | | 3-5 | 3 | /// | 450 | 40 | 29 | 9-11 | 5-8 | @@ -20,12 +20,26 @@ use super::*; #[derive(Component, Debug, Clone, Copy, Reflect)] #[reflect(Component, Default)] pub struct PointLight { + /// The color of this light source. pub color: Color, /// Luminous power in lumens, representing the amount of light emitted by this source in all directions. pub intensity: f32, + /// Cut-off for the light's area-of-effect. Fragments outside this range will not be affected by + /// this light at all, so it's important to tune this together with `intensity` to prevent hard + /// lighting cut-offs. pub range: f32, + /// Simulates a light source coming from a spherical volume with the given radius. Only affects + /// the size of specular highlights created by this light. Because of this, large values may not + /// produce the intended result -- for example, light radius does not affect shadow softness or + /// diffuse lighting. pub radius: f32, + /// Whether this light casts shadows. pub shadows_enabled: bool, + /// A bias used when sampling shadow maps to avoid "shadow-acne", or false shadow occlusions + /// that happen as a result of shadow-map fragments not mapping 1:1 to screen-space fragments. + /// Too high of a depth bias can lead to shadows detaching from their casters, or + /// "peter-panning". This bias can be tuned together with `shadow_normal_bias` to correct shadow + /// artifacts for a given scene. pub shadow_depth_bias: f32, /// A bias applied along the direction of the fragment's surface normal. It is scaled to the /// shadow map's texel size so that it can be small close to the camera and gets larger further diff --git a/crates/bevy_pbr/src/light_probe/environment_map.wgsl b/crates/bevy_pbr/src/light_probe/environment_map.wgsl index 2c8390f83ebbe..7b9945a8a684c 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.wgsl +++ b/crates/bevy_pbr/src/light_probe/environment_map.wgsl @@ -3,6 +3,9 @@ #import bevy_pbr::light_probe::query_light_probe #import bevy_pbr::mesh_view_bindings as bindings #import bevy_pbr::mesh_view_bindings::light_probes +#import bevy_pbr::lighting::{ + F_Schlick_vec, LayerLightingInput, LightingInput, LAYER_BASE, LAYER_CLEARCOAT +} struct EnvironmentMapLight { diffuse: vec3, @@ -21,12 +24,16 @@ struct EnvironmentMapRadiances { #ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY fn compute_radiances( - perceptual_roughness: f32, - N: vec3, - R: vec3, + input: ptr, + layer: u32, world_position: vec3, found_diffuse_indirect: bool, ) -> EnvironmentMapRadiances { + // Unpack. + let perceptual_roughness = (*input).layers[layer].perceptual_roughness; + let N = (*input).layers[layer].N; + let R = (*input).layers[layer].R; + var radiances: EnvironmentMapRadiances; // Search for a reflection probe that contains the fragment. @@ -69,12 +76,16 @@ fn compute_radiances( #else // MULTIPLE_LIGHT_PROBES_IN_ARRAY fn compute_radiances( - perceptual_roughness: f32, - N: vec3, - R: vec3, + input: ptr, + layer: u32, world_position: vec3, found_diffuse_indirect: bool, ) -> EnvironmentMapRadiances { + // Unpack. + let perceptual_roughness = (*input).layers[layer].perceptual_roughness; + let N = (*input).layers[layer].N; + let R = (*input).layers[layer].R; + var radiances: EnvironmentMapRadiances; if (light_probes.view_cubemap_index < 0) { @@ -109,26 +120,53 @@ fn compute_radiances( #endif // MULTIPLE_LIGHT_PROBES_IN_ARRAY +#ifdef STANDARD_MATERIAL_CLEARCOAT + +// Adds the environment map light from the clearcoat layer to that of the base +// layer. +fn environment_map_light_clearcoat( + out: ptr, + input: ptr, + found_diffuse_indirect: bool, +) { + // Unpack. + let world_position = (*input).P; + let clearcoat_NdotV = (*input).layers[LAYER_CLEARCOAT].NdotV; + let clearcoat_strength = (*input).clearcoat_strength; + + // Calculate the Fresnel term `Fc` for the clearcoat layer. + // 0.04 is a hardcoded value for F0 from the Filament spec. + let clearcoat_F0 = vec3(0.04); + let Fc = F_Schlick_vec(clearcoat_F0, 1.0, clearcoat_NdotV) * clearcoat_strength; + let inv_Fc = 1.0 - Fc; + + let clearcoat_radiances = compute_radiances( + input, LAYER_CLEARCOAT, world_position, found_diffuse_indirect); + + // Composite the clearcoat layer on top of the existing one. + // These formulas are from Filament: + // + (*out).diffuse *= inv_Fc; + (*out).specular = (*out).specular * inv_Fc * inv_Fc + clearcoat_radiances.radiance * Fc; +} + +#endif // STANDARD_MATERIAL_CLEARCOAT + fn environment_map_light( - perceptual_roughness: f32, - roughness: f32, - diffuse_color: vec3, - NdotV: f32, - f_ab: vec2, - N: vec3, - R: vec3, - F0: vec3, - world_position: vec3, + input: ptr, found_diffuse_indirect: bool, ) -> EnvironmentMapLight { + // Unpack. + let roughness = (*input).layers[LAYER_BASE].roughness; + let diffuse_color = (*input).diffuse_color; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let F_ab = (*input).F_ab; + let F0 = (*input).F0_; + let world_position = (*input).P; + var out: EnvironmentMapLight; - let radiances = compute_radiances( - perceptual_roughness, - N, - R, - world_position, - found_diffuse_indirect); + let radiances = compute_radiances(input, LAYER_BASE, world_position, found_diffuse_indirect); if (all(radiances.irradiance == vec3(0.0)) && all(radiances.radiance == vec3(0.0))) { out.diffuse = vec3(0.0); out.specular = vec3(0.0); @@ -144,7 +182,7 @@ fn environment_map_light( // Useful reference: https://bruop.github.io/ibl let Fr = max(vec3(1.0 - roughness), F0) - F0; let kS = F0 + Fr * pow(1.0 - NdotV, 5.0); - let Ess = f_ab.x + f_ab.y; + let Ess = F_ab.x + F_ab.y; let FssEss = kS * Ess * specular_occlusion; let Ems = 1.0 - Ess; let Favg = F0 + (1.0 - F0) / 21.0; @@ -153,7 +191,6 @@ fn environment_map_light( let Edss = 1.0 - (FssEss + FmsEms); let kD = diffuse_color * Edss; - if (!found_diffuse_indirect) { out.diffuse = (FmsEms + kD) * radiances.irradiance; } else { @@ -161,5 +198,10 @@ fn environment_map_light( } out.specular = FssEss * radiances.radiance; + +#ifdef STANDARD_MATERIAL_CLEARCOAT + environment_map_light_clearcoat(&out, input, found_diffuse_indirect); +#endif // STANDARD_MATERIAL_CLEARCOAT + return out; } diff --git a/crates/bevy_pbr/src/meshlet/cull_meshlets.wgsl b/crates/bevy_pbr/src/meshlet/cull_meshlets.wgsl index 2e04f3332b20d..abfd9aed55db1 100644 --- a/crates/bevy_pbr/src/meshlet/cull_meshlets.wgsl +++ b/crates/bevy_pbr/src/meshlet/cull_meshlets.wgsl @@ -1,14 +1,14 @@ #import bevy_pbr::meshlet_bindings::{ - meshlet_thread_meshlet_ids, + meshlet_cluster_meshlet_ids, meshlet_bounding_spheres, - meshlet_thread_instance_ids, + meshlet_cluster_instance_ids, meshlet_instance_uniforms, meshlet_second_pass_candidates, depth_pyramid, view, previous_view, should_cull_instance, - meshlet_is_second_pass_candidate, + cluster_is_second_pass_candidate, meshlets, draw_indirect_args, draw_triangle_buffer, @@ -21,7 +21,7 @@ /// the instance, frustum, and LOD tests in the first pass, but were not visible last frame according to the occlusion culling. @compute -@workgroup_size(128, 1, 1) // 128 threads per workgroup, 1 instanced meshlet per thread +@workgroup_size(128, 1, 1) // 128 threads per workgroup, 1 cluster per thread fn cull_meshlets( @builtin(workgroup_id) workgroup_id: vec3, @builtin(num_workgroups) num_workgroups: vec3, @@ -29,21 +29,21 @@ fn cull_meshlets( ) { // Calculate the cluster ID for this thread let cluster_id = local_invocation_id.x + 128u * dot(workgroup_id, vec3(num_workgroups.x * num_workgroups.x, num_workgroups.x, 1u)); - if cluster_id >= arrayLength(&meshlet_thread_meshlet_ids) { return; } + if cluster_id >= arrayLength(&meshlet_cluster_meshlet_ids) { return; } #ifdef MESHLET_SECOND_CULLING_PASS - if !meshlet_is_second_pass_candidate(cluster_id) { return; } + if !cluster_is_second_pass_candidate(cluster_id) { return; } #endif // Check for instance culling - let instance_id = meshlet_thread_instance_ids[cluster_id]; + let instance_id = meshlet_cluster_instance_ids[cluster_id]; #ifdef MESHLET_FIRST_CULLING_PASS if should_cull_instance(instance_id) { return; } #endif // Calculate world-space culling bounding sphere for the cluster let instance_uniform = meshlet_instance_uniforms[instance_id]; - let meshlet_id = meshlet_thread_meshlet_ids[cluster_id]; + let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id]; let model = affine3_to_square(instance_uniform.model); let model_scale = max(length(model[0]), max(length(model[1]), length(model[2]))); let bounding_spheres = meshlet_bounding_spheres[meshlet_id]; diff --git a/crates/bevy_pbr/src/meshlet/fill_cluster_buffers.wgsl b/crates/bevy_pbr/src/meshlet/fill_cluster_buffers.wgsl new file mode 100644 index 0000000000000..89e64de0c197b --- /dev/null +++ b/crates/bevy_pbr/src/meshlet/fill_cluster_buffers.wgsl @@ -0,0 +1,42 @@ +#import bevy_pbr::meshlet_bindings::{ + cluster_count, + meshlet_instance_meshlet_counts_prefix_sum, + meshlet_instance_meshlet_slice_starts, + meshlet_cluster_instance_ids, + meshlet_cluster_meshlet_ids, +} + +@compute +@workgroup_size(128, 1, 1) // 128 threads per workgroup, 1 cluster per thread +fn fill_cluster_buffers( + @builtin(workgroup_id) workgroup_id: vec3, + @builtin(num_workgroups) num_workgroups: vec3, + @builtin(local_invocation_id) local_invocation_id: vec3 +) { + // Calculate the cluster ID for this thread + let cluster_id = local_invocation_id.x + 128u * dot(workgroup_id, vec3(num_workgroups.x * num_workgroups.x, num_workgroups.x, 1u)); + if cluster_id >= cluster_count { return; } + + // Binary search to find the instance this cluster belongs to + var left = 0u; + var right = arrayLength(&meshlet_instance_meshlet_counts_prefix_sum) - 1u; + while left <= right { + let mid = (left + right) / 2u; + if meshlet_instance_meshlet_counts_prefix_sum[mid] <= cluster_id { + left = mid + 1u; + } else { + right = mid - 1u; + } + } + let instance_id = right; + + // Find the meshlet ID for this cluster within the instance's MeshletMesh + let meshlet_id_local = cluster_id - meshlet_instance_meshlet_counts_prefix_sum[instance_id]; + + // Find the overall meshlet ID in the global meshlet buffer + let meshlet_id = meshlet_id_local + meshlet_instance_meshlet_slice_starts[instance_id]; + + // Write results to buffers + meshlet_cluster_instance_ids[cluster_id] = instance_id; + meshlet_cluster_meshlet_ids[cluster_id] = meshlet_id; +} diff --git a/crates/bevy_pbr/src/meshlet/gpu_scene.rs b/crates/bevy_pbr/src/meshlet/gpu_scene.rs index 4cffa6da714bf..a986260003c71 100644 --- a/crates/bevy_pbr/src/meshlet/gpu_scene.rs +++ b/crates/bevy_pbr/src/meshlet/gpu_scene.rs @@ -31,7 +31,7 @@ use std::{ iter, mem::size_of, ops::{DerefMut, Range}, - sync::Arc, + sync::{atomic::AtomicBool, Arc}, }; /// Create and queue for uploading to the GPU [`MeshUniform`] components for @@ -91,17 +91,14 @@ pub fn extract_meshlet_meshes( } for ( - instance_index, - ( - instance, - handle, - transform, - previous_transform, - render_layers, - not_shadow_receiver, - not_shadow_caster, - ), - ) in instances_query.iter().enumerate() + instance, + handle, + transform, + previous_transform, + render_layers, + not_shadow_receiver, + not_shadow_caster, + ) in &instances_query { // Skip instances with an unloaded MeshletMesh asset if asset_server.is_managed(handle.id()) @@ -117,7 +114,6 @@ pub fn extract_meshlet_meshes( not_shadow_caster, handle, &mut assets, - instance_index as u32, ); // Build a MeshUniform for each instance @@ -235,12 +231,12 @@ pub fn prepare_meshlet_per_frame_resources( &render_queue, ); upload_storage_buffer( - &mut gpu_scene.thread_instance_ids, + &mut gpu_scene.instance_meshlet_counts_prefix_sum, &render_device, &render_queue, ); upload_storage_buffer( - &mut gpu_scene.thread_meshlet_ids, + &mut gpu_scene.instance_meshlet_slice_starts, &render_device, &render_queue, ); @@ -248,6 +244,34 @@ pub fn prepare_meshlet_per_frame_resources( // Early submission for GPU data uploads to start while the render graph records commands render_queue.submit([]); + let needed_buffer_size = 4 * gpu_scene.scene_meshlet_count as u64; + match &mut gpu_scene.cluster_instance_ids { + Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(), + slot => { + let buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_cluster_instance_ids"), + size: needed_buffer_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }); + *slot = Some(buffer.clone()); + buffer + } + }; + match &mut gpu_scene.cluster_meshlet_ids { + Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(), + slot => { + let buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_cluster_meshlet_ids"), + size: needed_buffer_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }); + *slot = Some(buffer.clone()); + buffer + } + }; + let needed_buffer_size = 4 * gpu_scene.scene_triangle_count; let visibility_buffer_draw_triangle_buffer = match &mut gpu_scene.visibility_buffer_draw_triangle_buffer { @@ -456,18 +480,44 @@ pub fn prepare_meshlet_view_bind_groups( render_device: Res, mut commands: Commands, ) { - let (Some(view_uniforms), Some(previous_view_uniforms)) = ( + let ( + Some(cluster_instance_ids), + Some(cluster_meshlet_ids), + Some(view_uniforms), + Some(previous_view_uniforms), + ) = ( + gpu_scene.cluster_instance_ids.as_ref(), + gpu_scene.cluster_meshlet_ids.as_ref(), view_uniforms.uniforms.binding(), previous_view_uniforms.uniforms.binding(), - ) else { + ) + else { return; }; + let first_node = Arc::new(AtomicBool::new(true)); + + // TODO: Some of these bind groups can be reused across multiple views for (view_entity, view_resources, view_depth) in &views { let entries = BindGroupEntries::sequential(( - gpu_scene.thread_meshlet_ids.binding().unwrap(), + gpu_scene + .instance_meshlet_counts_prefix_sum + .binding() + .unwrap(), + gpu_scene.instance_meshlet_slice_starts.binding().unwrap(), + cluster_instance_ids.as_entire_binding(), + cluster_meshlet_ids.as_entire_binding(), + )); + let fill_cluster_buffers = render_device.create_bind_group( + "meshlet_fill_cluster_buffers", + &gpu_scene.fill_cluster_buffers_bind_group_layout, + &entries, + ); + + let entries = BindGroupEntries::sequential(( + cluster_meshlet_ids.as_entire_binding(), gpu_scene.meshlet_bounding_spheres.binding(), - gpu_scene.thread_instance_ids.binding().unwrap(), + cluster_instance_ids.as_entire_binding(), gpu_scene.instance_uniforms.binding().unwrap(), view_resources.instance_visibility.as_entire_binding(), view_resources @@ -491,9 +541,9 @@ pub fn prepare_meshlet_view_bind_groups( ); let entries = BindGroupEntries::sequential(( - gpu_scene.thread_meshlet_ids.binding().unwrap(), + cluster_meshlet_ids.as_entire_binding(), gpu_scene.meshlet_bounding_spheres.binding(), - gpu_scene.thread_instance_ids.binding().unwrap(), + cluster_instance_ids.as_entire_binding(), gpu_scene.instance_uniforms.binding().unwrap(), view_resources.instance_visibility.as_entire_binding(), view_resources @@ -539,12 +589,12 @@ pub fn prepare_meshlet_view_bind_groups( .collect(); let entries = BindGroupEntries::sequential(( - gpu_scene.thread_meshlet_ids.binding().unwrap(), + cluster_meshlet_ids.as_entire_binding(), gpu_scene.meshlets.binding(), gpu_scene.indices.binding(), gpu_scene.vertex_ids.binding(), gpu_scene.vertex_data.binding(), - gpu_scene.thread_instance_ids.binding().unwrap(), + cluster_instance_ids.as_entire_binding(), gpu_scene.instance_uniforms.binding().unwrap(), gpu_scene.instance_material_ids.binding().unwrap(), view_resources @@ -581,12 +631,12 @@ pub fn prepare_meshlet_view_bind_groups( .map(|visibility_buffer| { let entries = BindGroupEntries::sequential(( &visibility_buffer.default_view, - gpu_scene.thread_meshlet_ids.binding().unwrap(), + cluster_meshlet_ids.as_entire_binding(), gpu_scene.meshlets.binding(), gpu_scene.indices.binding(), gpu_scene.vertex_ids.binding(), gpu_scene.vertex_data.binding(), - gpu_scene.thread_instance_ids.binding().unwrap(), + cluster_instance_ids.as_entire_binding(), gpu_scene.instance_uniforms.binding().unwrap(), )); render_device.create_bind_group( @@ -597,6 +647,8 @@ pub fn prepare_meshlet_view_bind_groups( }); commands.entity(view_entity).insert(MeshletViewBindGroups { + first_node: Arc::clone(&first_node), + fill_cluster_buffers, culling_first, culling_second, downsample_depth, @@ -629,12 +681,15 @@ pub struct MeshletGpuScene { /// Per-view per-instance visibility bit. Used for [`RenderLayers`] and [`NotShadowCaster`] support. view_instance_visibility: EntityHashMap>>, instance_material_ids: StorageBuffer>, - thread_instance_ids: StorageBuffer>, - thread_meshlet_ids: StorageBuffer>, + instance_meshlet_counts_prefix_sum: StorageBuffer>, + instance_meshlet_slice_starts: StorageBuffer>, + cluster_instance_ids: Option, + cluster_meshlet_ids: Option, second_pass_candidates_buffer: Option, previous_depth_pyramids: EntityHashMap, visibility_buffer_draw_triangle_buffer: Option, + fill_cluster_buffers_bind_group_layout: BindGroupLayout, culling_bind_group_layout: BindGroupLayout, visibility_buffer_raster_bind_group_layout: BindGroupLayout, downsample_depth_bind_group_layout: BindGroupLayout, @@ -675,21 +730,35 @@ impl FromWorld for MeshletGpuScene { buffer.set_label(Some("meshlet_instance_material_ids")); buffer }, - thread_instance_ids: { + instance_meshlet_counts_prefix_sum: { let mut buffer = StorageBuffer::default(); - buffer.set_label(Some("meshlet_thread_instance_ids")); + buffer.set_label(Some("meshlet_instance_meshlet_counts_prefix_sum")); buffer }, - thread_meshlet_ids: { + instance_meshlet_slice_starts: { let mut buffer = StorageBuffer::default(); - buffer.set_label(Some("meshlet_thread_meshlet_ids")); + buffer.set_label(Some("meshlet_instance_meshlet_slice_starts")); buffer }, + cluster_instance_ids: None, + cluster_meshlet_ids: None, second_pass_candidates_buffer: None, previous_depth_pyramids: EntityHashMap::default(), visibility_buffer_draw_triangle_buffer: None, // TODO: Buffer min sizes + fill_cluster_buffers_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_fill_cluster_buffers_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), culling_bind_group_layout: render_device.create_bind_group_layout( "meshlet_culling_bind_group_layout", &BindGroupLayoutEntries::sequential( @@ -784,8 +853,8 @@ impl MeshletGpuScene { .for_each(|b| b.get_mut().clear()); self.instance_uniforms.get_mut().clear(); self.instance_material_ids.get_mut().clear(); - self.thread_instance_ids.get_mut().clear(); - self.thread_meshlet_ids.get_mut().clear(); + self.instance_meshlet_counts_prefix_sum.get_mut().clear(); + self.instance_meshlet_slice_starts.get_mut().clear(); // TODO: Remove unused entries for view_instance_visibility and previous_depth_pyramids } @@ -796,7 +865,6 @@ impl MeshletGpuScene { not_shadow_caster: bool, handle: &Handle, assets: &mut Assets, - instance_index: u32, ) { let queue_meshlet_mesh = |asset_id: &AssetId| { let meshlet_mesh = assets.remove_untracked(*asset_id).expect( @@ -833,11 +901,6 @@ impl MeshletGpuScene { ) }; - // Append instance data for this frame - self.instances - .push((instance, render_layers, not_shadow_caster)); - self.instance_material_ids.get_mut().push(0); - // If the MeshletMesh asset has not been uploaded to the GPU yet, queue it for uploading let ([_, _, _, meshlets_slice, _], triangle_count) = self .meshlet_mesh_slices @@ -848,14 +911,19 @@ impl MeshletGpuScene { let meshlets_slice = (meshlets_slice.start as u32 / size_of::() as u32) ..(meshlets_slice.end as u32 / size_of::() as u32); + // Append instance data for this frame + self.instances + .push((instance, render_layers, not_shadow_caster)); + self.instance_material_ids.get_mut().push(0); + self.instance_meshlet_counts_prefix_sum + .get_mut() + .push(self.scene_meshlet_count); + self.instance_meshlet_slice_starts + .get_mut() + .push(meshlets_slice.start); + self.scene_meshlet_count += meshlets_slice.end - meshlets_slice.start; self.scene_triangle_count += triangle_count; - - // Append per-cluster data for this frame - self.thread_instance_ids - .get_mut() - .extend(std::iter::repeat(instance_index).take(meshlets_slice.len())); - self.thread_meshlet_ids.get_mut().extend(meshlets_slice); } /// Get the depth value for use with the material depth texture for a given [`Material`] asset. @@ -873,6 +941,10 @@ impl MeshletGpuScene { self.material_ids_present_in_scene.contains(material_id) } + pub fn fill_cluster_buffers_bind_group_layout(&self) -> BindGroupLayout { + self.fill_cluster_buffers_bind_group_layout.clone() + } + pub fn culling_bind_group_layout(&self) -> BindGroupLayout { self.culling_bind_group_layout.clone() } @@ -912,6 +984,8 @@ pub struct MeshletViewResources { #[derive(Component)] pub struct MeshletViewBindGroups { + pub first_node: Arc, + pub fill_cluster_buffers: BindGroup, pub culling_first: BindGroup, pub culling_second: BindGroup, pub downsample_depth: Box<[BindGroup]>, diff --git a/crates/bevy_pbr/src/meshlet/material_draw_nodes.rs b/crates/bevy_pbr/src/meshlet/material_draw_nodes.rs index bbe1676bbe076..e327eadfcc9bf 100644 --- a/crates/bevy_pbr/src/meshlet/material_draw_nodes.rs +++ b/crates/bevy_pbr/src/meshlet/material_draw_nodes.rs @@ -116,8 +116,8 @@ impl ViewNode for MeshletMainOpaquePass3dNode { pipeline_cache.get_render_pipeline(*material_pipeline_id) { let x = *material_id * 3; - render_pass.set_bind_group(2, material_bind_group, &[]); render_pass.set_render_pipeline(material_pipeline); + render_pass.set_bind_group(2, material_bind_group, &[]); render_pass.draw(x..(x + 3), 0..1); } } @@ -237,8 +237,8 @@ impl ViewNode for MeshletPrepassNode { pipeline_cache.get_render_pipeline(*material_pipeline_id) { let x = *material_id * 3; - render_pass.set_bind_group(2, material_bind_group, &[]); render_pass.set_render_pipeline(material_pipeline); + render_pass.set_bind_group(2, material_bind_group, &[]); render_pass.draw(x..(x + 3), 0..1); } } @@ -363,8 +363,8 @@ impl ViewNode for MeshletDeferredGBufferPrepassNode { pipeline_cache.get_render_pipeline(*material_pipeline_id) { let x = *material_id * 3; - render_pass.set_bind_group(2, material_bind_group, &[]); render_pass.set_render_pipeline(material_pipeline); + render_pass.set_bind_group(2, material_bind_group, &[]); render_pass.draw(x..(x + 3), 0..1); } } diff --git a/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl b/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl index 2ca98b5d41bac..a3f18cbc9b29e 100644 --- a/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl +++ b/crates/bevy_pbr/src/meshlet/meshlet_bindings.wgsl @@ -51,14 +51,22 @@ struct DrawIndirectArgs { first_instance: u32, } +#ifdef MESHLET_FILL_CLUSTER_BUFFERS_PASS +var cluster_count: u32; +@group(0) @binding(0) var meshlet_instance_meshlet_counts_prefix_sum: array; // Per entity instance +@group(0) @binding(1) var meshlet_instance_meshlet_slice_starts: array; // Per entity instance +@group(0) @binding(2) var meshlet_cluster_instance_ids: array; // Per cluster +@group(0) @binding(3) var meshlet_cluster_meshlet_ids: array; // Per cluster +#endif + #ifdef MESHLET_CULLING_PASS -@group(0) @binding(0) var meshlet_thread_meshlet_ids: array; // Per cluster (instance of a meshlet) -@group(0) @binding(1) var meshlet_bounding_spheres: array; // Per asset meshlet -@group(0) @binding(2) var meshlet_thread_instance_ids: array; // Per cluster (instance of a meshlet) +@group(0) @binding(0) var meshlet_cluster_meshlet_ids: array; // Per cluster +@group(0) @binding(1) var meshlet_bounding_spheres: array; // Per meshlet +@group(0) @binding(2) var meshlet_cluster_instance_ids: array; // Per cluster @group(0) @binding(3) var meshlet_instance_uniforms: array; // Per entity instance @group(0) @binding(4) var meshlet_view_instance_visibility: array; // 1 bit per entity instance, packed as a bitmask -@group(0) @binding(5) var meshlet_second_pass_candidates: array>; // 1 bit per cluster (instance of a meshlet), packed as a bitmask -@group(0) @binding(6) var meshlets: array; // Per asset meshlet +@group(0) @binding(5) var meshlet_second_pass_candidates: array>; // 1 bit per cluster , packed as a bitmask +@group(0) @binding(6) var meshlets: array; // Per meshlet @group(0) @binding(7) var draw_indirect_args: DrawIndirectArgs; // Single object shared between all workgroups/meshlets/triangles @group(0) @binding(8) var draw_triangle_buffer: array; // Single object shared between all workgroups/meshlets/triangles @group(0) @binding(9) var depth_pyramid: texture_2d; // From the end of the last frame for the first culling pass, and from the first raster pass for the second culling pass @@ -71,7 +79,7 @@ fn should_cull_instance(instance_id: u32) -> bool { return bool(extractBits(packed_visibility, bit_offset, 1u)); } -fn meshlet_is_second_pass_candidate(cluster_id: u32) -> bool { +fn cluster_is_second_pass_candidate(cluster_id: u32) -> bool { let packed_candidates = meshlet_second_pass_candidates[cluster_id / 32u]; let bit_offset = cluster_id % 32u; return bool(extractBits(packed_candidates, bit_offset, 1u)); @@ -79,12 +87,12 @@ fn meshlet_is_second_pass_candidate(cluster_id: u32) -> bool { #endif #ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS -@group(0) @binding(0) var meshlet_thread_meshlet_ids: array; // Per cluster (instance of a meshlet) -@group(0) @binding(1) var meshlets: array; // Per asset meshlet -@group(0) @binding(2) var meshlet_indices: array; // Many per asset meshlet -@group(0) @binding(3) var meshlet_vertex_ids: array; // Many per asset meshlet -@group(0) @binding(4) var meshlet_vertex_data: array; // Many per asset meshlet -@group(0) @binding(5) var meshlet_thread_instance_ids: array; // Per cluster (instance of a meshlet) +@group(0) @binding(0) var meshlet_cluster_meshlet_ids: array; // Per cluster +@group(0) @binding(1) var meshlets: array; // Per meshlet +@group(0) @binding(2) var meshlet_indices: array; // Many per meshlet +@group(0) @binding(3) var meshlet_vertex_ids: array; // Many per meshlet +@group(0) @binding(4) var meshlet_vertex_data: array; // Many per meshlet +@group(0) @binding(5) var meshlet_cluster_instance_ids: array; // Per cluster @group(0) @binding(6) var meshlet_instance_uniforms: array; // Per entity instance @group(0) @binding(7) var meshlet_instance_material_ids: array; // Per entity instance @group(0) @binding(8) var draw_triangle_buffer: array; // Single object shared between all workgroups/meshlets/triangles @@ -99,12 +107,12 @@ fn get_meshlet_index(index_id: u32) -> u32 { #ifdef MESHLET_MESH_MATERIAL_PASS @group(1) @binding(0) var meshlet_visibility_buffer: texture_2d; // Generated from the meshlet raster passes -@group(1) @binding(1) var meshlet_thread_meshlet_ids: array; // Per cluster (instance of a meshlet) -@group(1) @binding(2) var meshlets: array; // Per asset meshlet -@group(1) @binding(3) var meshlet_indices: array; // Many per asset meshlet -@group(1) @binding(4) var meshlet_vertex_ids: array; // Many per asset meshlet -@group(1) @binding(5) var meshlet_vertex_data: array; // Many per asset meshlet -@group(1) @binding(6) var meshlet_thread_instance_ids: array; // Per cluster (instance of a meshlet) +@group(1) @binding(1) var meshlet_cluster_meshlet_ids: array; // Per cluster +@group(1) @binding(2) var meshlets: array; // Per meshlet +@group(1) @binding(3) var meshlet_indices: array; // Many per meshlet +@group(1) @binding(4) var meshlet_vertex_ids: array; // Many per meshlet +@group(1) @binding(5) var meshlet_vertex_data: array; // Many per meshlet +@group(1) @binding(6) var meshlet_cluster_instance_ids: array; // Per cluster @group(1) @binding(7) var meshlet_instance_uniforms: array; // Per entity instance fn get_meshlet_index(index_id: u32) -> u32 { diff --git a/crates/bevy_pbr/src/meshlet/mod.rs b/crates/bevy_pbr/src/meshlet/mod.rs index 00c7629bcba3f..69255f9084cf1 100644 --- a/crates/bevy_pbr/src/meshlet/mod.rs +++ b/crates/bevy_pbr/src/meshlet/mod.rs @@ -49,7 +49,8 @@ use self::{ }, pipelines::{ MeshletPipelines, MESHLET_COPY_MATERIAL_DEPTH_SHADER_HANDLE, MESHLET_CULLING_SHADER_HANDLE, - MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE, MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, + MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE, MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE, + MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE, }, visibility_buffer_raster_node::MeshletVisibilityBufferRasterPassNode, }; @@ -74,6 +75,8 @@ use bevy_ecs::{ use bevy_render::{ render_graph::{RenderGraphApp, ViewNodeRunner}, render_resource::{Shader, TextureUsages}, + renderer::RenderDevice, + settings::WgpuFeatures, view::{ check_visibility, prepare_view_targets, InheritedVisibility, Msaa, ViewVisibility, Visibility, VisibilitySystems, @@ -105,7 +108,7 @@ const MESHLET_MESH_MATERIAL_SHADER_HANDLE: Handle = /// /// This plugin is not compatible with [`Msaa`], and adding this plugin will disable it. /// -/// This plugin does not work on the WebGL2 backend. +/// This plugin does not work on WASM. /// /// ![A render of the Stanford dragon as a `MeshletMesh`](https://raw.githubusercontent.com/bevyengine/bevy/main/crates/bevy_pbr/src/meshlet/meshlet_preview.png) pub struct MeshletPlugin; @@ -124,6 +127,12 @@ impl Plugin for MeshletPlugin { "visibility_buffer_resolve.wgsl", Shader::from_wgsl ); + load_internal_asset!( + app, + MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE, + "fill_cluster_buffers.wgsl", + Shader::from_wgsl + ); load_internal_asset!( app, MESHLET_CULLING_SHADER_HANDLE, @@ -169,6 +178,15 @@ impl Plugin for MeshletPlugin { return; }; + if !render_app + .world() + .resource::() + .features() + .contains(WgpuFeatures::PUSH_CONSTANTS) + { + panic!("MeshletPlugin can't be used. GPU lacks support: WgpuFeatures::PUSH_CONSTANTS is not supported."); + } + render_app .add_render_graph_node::( Core3d, diff --git a/crates/bevy_pbr/src/meshlet/pipelines.rs b/crates/bevy_pbr/src/meshlet/pipelines.rs index bb62c6bdf5020..551efbe176f19 100644 --- a/crates/bevy_pbr/src/meshlet/pipelines.rs +++ b/crates/bevy_pbr/src/meshlet/pipelines.rs @@ -9,16 +9,19 @@ use bevy_ecs::{ }; use bevy_render::render_resource::*; -pub const MESHLET_CULLING_SHADER_HANDLE: Handle = Handle::weak_from_u128(4325134235233421); +pub const MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE: Handle = + Handle::weak_from_u128(4325134235233421); +pub const MESHLET_CULLING_SHADER_HANDLE: Handle = Handle::weak_from_u128(5325134235233421); pub const MESHLET_DOWNSAMPLE_DEPTH_SHADER_HANDLE: Handle = - Handle::weak_from_u128(5325134235233421); -pub const MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE: Handle = Handle::weak_from_u128(6325134235233421); -pub const MESHLET_COPY_MATERIAL_DEPTH_SHADER_HANDLE: Handle = +pub const MESHLET_VISIBILITY_BUFFER_RASTER_SHADER_HANDLE: Handle = Handle::weak_from_u128(7325134235233421); +pub const MESHLET_COPY_MATERIAL_DEPTH_SHADER_HANDLE: Handle = + Handle::weak_from_u128(8325134235233421); #[derive(Resource)] pub struct MeshletPipelines { + fill_cluster_buffers: CachedComputePipelineId, cull_first: CachedComputePipelineId, cull_second: CachedComputePipelineId, downsample_depth: CachedRenderPipelineId, @@ -31,6 +34,8 @@ pub struct MeshletPipelines { impl FromWorld for MeshletPipelines { fn from_world(world: &mut World) -> Self { let gpu_scene = world.resource::(); + let fill_cluster_buffers_bind_group_layout = + gpu_scene.fill_cluster_buffers_bind_group_layout(); let cull_layout = gpu_scene.culling_bind_group_layout(); let downsample_depth_layout = gpu_scene.downsample_depth_bind_group_layout(); let visibility_buffer_layout = gpu_scene.visibility_buffer_raster_bind_group_layout(); @@ -38,6 +43,20 @@ impl FromWorld for MeshletPipelines { let pipeline_cache = world.resource_mut::(); Self { + fill_cluster_buffers: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some("meshlet_fill_cluster_buffers_pipeline".into()), + layout: vec![fill_cluster_buffers_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE, + shader_defs: vec!["MESHLET_FILL_CLUSTER_BUFFERS_PASS".into()], + entry_point: "fill_cluster_buffers".into(), + }, + ), + cull_first: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: Some("meshlet_culling_first_pipeline".into()), layout: vec![cull_layout.clone()], @@ -242,6 +261,7 @@ impl MeshletPipelines { pub fn get( world: &World, ) -> Option<( + &ComputePipeline, &ComputePipeline, &ComputePipeline, &RenderPipeline, @@ -253,6 +273,7 @@ impl MeshletPipelines { let pipeline_cache = world.get_resource::()?; let pipeline = world.get_resource::()?; Some(( + pipeline_cache.get_compute_pipeline(pipeline.fill_cluster_buffers)?, pipeline_cache.get_compute_pipeline(pipeline.cull_first)?, pipeline_cache.get_compute_pipeline(pipeline.cull_second)?, pipeline_cache.get_render_pipeline(pipeline.downsample_depth)?, diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_raster.wgsl b/crates/bevy_pbr/src/meshlet/visibility_buffer_raster.wgsl index e2c716de162e7..b72079b7f1065 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_raster.wgsl +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_raster.wgsl @@ -1,10 +1,10 @@ #import bevy_pbr::{ meshlet_bindings::{ - meshlet_thread_meshlet_ids, + meshlet_cluster_meshlet_ids, meshlets, meshlet_vertex_ids, meshlet_vertex_data, - meshlet_thread_instance_ids, + meshlet_cluster_instance_ids, meshlet_instance_uniforms, meshlet_instance_material_ids, draw_triangle_buffer, @@ -42,12 +42,12 @@ fn vertex(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { let cluster_id = packed_ids >> 6u; let triangle_id = extractBits(packed_ids, 0u, 6u); let index_id = (triangle_id * 3u) + (vertex_index % 3u); - let meshlet_id = meshlet_thread_meshlet_ids[cluster_id]; + let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id]; let meshlet = meshlets[meshlet_id]; let index = get_meshlet_index(meshlet.start_index_id + index_id); let vertex_id = meshlet_vertex_ids[meshlet.start_vertex_id + index]; let vertex = unpack_meshlet_vertex(meshlet_vertex_data[vertex_id]); - let instance_id = meshlet_thread_instance_ids[cluster_id]; + let instance_id = meshlet_cluster_instance_ids[cluster_id]; let instance_uniform = meshlet_instance_uniforms[instance_id]; let model = affine3_to_square(instance_uniform.model); diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs b/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs index 54303af71c74a..f3ffb1865ed50 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs @@ -15,6 +15,7 @@ use bevy_render::{ renderer::RenderContext, view::{ViewDepthTexture, ViewUniformOffset}, }; +use std::sync::atomic::Ordering; /// Rasterize meshlets into a depth buffer, and optional visibility buffer + material depth buffer for shading passes. pub struct MeshletVisibilityBufferRasterPassNode { @@ -72,6 +73,7 @@ impl Node for MeshletVisibilityBufferRasterPassNode { }; let Some(( + fill_cluster_buffers_pipeline, culling_first_pipeline, culling_second_pipeline, downsample_depth_pipeline, @@ -84,9 +86,14 @@ impl Node for MeshletVisibilityBufferRasterPassNode { return Ok(()); }; - let culling_workgroups = (meshlet_view_resources.scene_meshlet_count.div_ceil(128) as f32) - .cbrt() - .ceil() as u32; + let first_node = meshlet_view_bind_groups + .first_node + .fetch_and(false, Ordering::SeqCst); + + let thread_per_cluster_workgroups = + (meshlet_view_resources.scene_meshlet_count.div_ceil(128) as f32) + .cbrt() + .ceil() as u32; render_context .command_encoder() @@ -96,6 +103,15 @@ impl Node for MeshletVisibilityBufferRasterPassNode { 0, None, ); + if first_node { + fill_cluster_buffers_pass( + render_context, + &meshlet_view_bind_groups.fill_cluster_buffers, + fill_cluster_buffers_pipeline, + thread_per_cluster_workgroups, + meshlet_view_resources.scene_meshlet_count, + ); + } cull_pass( "culling_first", render_context, @@ -103,7 +119,7 @@ impl Node for MeshletVisibilityBufferRasterPassNode { view_offset, previous_view_offset, culling_first_pipeline, - culling_workgroups, + thread_per_cluster_workgroups, ); raster_pass( true, @@ -129,7 +145,7 @@ impl Node for MeshletVisibilityBufferRasterPassNode { view_offset, previous_view_offset, culling_second_pipeline, - culling_workgroups, + thread_per_cluster_workgroups, ); raster_pass( false, @@ -191,7 +207,7 @@ impl Node for MeshletVisibilityBufferRasterPassNode { view_offset, previous_view_offset, culling_first_pipeline, - culling_workgroups, + thread_per_cluster_workgroups, ); raster_pass( true, @@ -217,7 +233,7 @@ impl Node for MeshletVisibilityBufferRasterPassNode { view_offset, previous_view_offset, culling_second_pipeline, - culling_workgroups, + thread_per_cluster_workgroups, ); raster_pass( false, @@ -243,6 +259,29 @@ impl Node for MeshletVisibilityBufferRasterPassNode { } } +// TODO: Reuse same compute pass as cull_pass +fn fill_cluster_buffers_pass( + render_context: &mut RenderContext, + fill_cluster_buffers_bind_group: &BindGroup, + fill_cluster_buffers_pass_pipeline: &ComputePipeline, + fill_cluster_buffers_pass_workgroups: u32, + cluster_count: u32, +) { + let command_encoder = render_context.command_encoder(); + let mut cull_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("fill_cluster_buffers"), + timestamp_writes: None, + }); + cull_pass.set_pipeline(fill_cluster_buffers_pass_pipeline); + cull_pass.set_push_constants(0, &cluster_count.to_le_bytes()); + cull_pass.set_bind_group(0, fill_cluster_buffers_bind_group, &[]); + cull_pass.dispatch_workgroups( + fill_cluster_buffers_pass_workgroups, + fill_cluster_buffers_pass_workgroups, + fill_cluster_buffers_pass_workgroups, + ); +} + fn cull_pass( label: &'static str, render_context: &mut RenderContext, @@ -257,12 +296,12 @@ fn cull_pass( label: Some(label), timestamp_writes: None, }); + cull_pass.set_pipeline(culling_pipeline); cull_pass.set_bind_group( 0, culling_bind_group, &[view_offset.offset, previous_view_offset.offset], ); - cull_pass.set_pipeline(culling_pipeline); cull_pass.dispatch_workgroups(culling_workgroups, culling_workgroups, culling_workgroups); } @@ -327,12 +366,12 @@ fn raster_pass( draw_pass.set_camera_viewport(viewport); } + draw_pass.set_render_pipeline(visibility_buffer_raster_pipeline); draw_pass.set_bind_group( 0, &meshlet_view_bind_groups.visibility_buffer_raster, &[view_offset.offset], ); - draw_pass.set_render_pipeline(visibility_buffer_raster_pipeline); draw_pass.draw_indirect(visibility_buffer_draw_indirect_args, 0); } @@ -363,8 +402,8 @@ fn downsample_depth( }; let mut downsample_pass = render_context.begin_tracked_render_pass(downsample_pass); - downsample_pass.set_bind_group(0, &meshlet_view_bind_groups.downsample_depth[i], &[]); downsample_pass.set_render_pipeline(downsample_depth_pipeline); + downsample_pass.set_bind_group(0, &meshlet_view_bind_groups.downsample_depth[i], &[]); downsample_pass.draw(0..3, 0..1); } @@ -400,8 +439,8 @@ fn copy_material_depth_pass( copy_pass.set_camera_viewport(viewport); } - copy_pass.set_bind_group(0, copy_material_depth_bind_group, &[]); copy_pass.set_render_pipeline(copy_material_depth_pipeline); + copy_pass.set_bind_group(0, copy_material_depth_bind_group, &[]); copy_pass.draw(0..3, 0..1); } } diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl b/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl index 947c9d49be99c..7f8e50573e109 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl @@ -3,11 +3,11 @@ #import bevy_pbr::{ meshlet_bindings::{ meshlet_visibility_buffer, - meshlet_thread_meshlet_ids, + meshlet_cluster_meshlet_ids, meshlets, meshlet_vertex_ids, meshlet_vertex_data, - meshlet_thread_instance_ids, + meshlet_cluster_instance_ids, meshlet_instance_uniforms, get_meshlet_index, unpack_meshlet_vertex, @@ -95,11 +95,11 @@ struct VertexOutput { /// Load the visibility buffer texture and resolve it into a VertexOutput. fn resolve_vertex_output(frag_coord: vec4) -> VertexOutput { - let vbuffer = textureLoad(meshlet_visibility_buffer, vec2(frag_coord.xy), 0).r; - let cluster_id = vbuffer >> 6u; - let meshlet_id = meshlet_thread_meshlet_ids[cluster_id]; + let packed_ids = textureLoad(meshlet_visibility_buffer, vec2(frag_coord.xy), 0).r; + let cluster_id = packed_ids >> 6u; + let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id]; let meshlet = meshlets[meshlet_id]; - let triangle_id = extractBits(vbuffer, 0u, 6u); + let triangle_id = extractBits(packed_ids, 0u, 6u); let index_ids = meshlet.start_index_id + vec3(triangle_id * 3u) + vec3(0u, 1u, 2u); let indices = meshlet.start_vertex_id + vec3(get_meshlet_index(index_ids.x), get_meshlet_index(index_ids.y), get_meshlet_index(index_ids.z)); let vertex_ids = vec3(meshlet_vertex_ids[indices.x], meshlet_vertex_ids[indices.y], meshlet_vertex_ids[indices.z]); @@ -107,13 +107,14 @@ fn resolve_vertex_output(frag_coord: vec4) -> VertexOutput { let vertex_2 = unpack_meshlet_vertex(meshlet_vertex_data[vertex_ids.y]); let vertex_3 = unpack_meshlet_vertex(meshlet_vertex_data[vertex_ids.z]); - let instance_id = meshlet_thread_instance_ids[cluster_id]; + let instance_id = meshlet_cluster_instance_ids[cluster_id]; let instance_uniform = meshlet_instance_uniforms[instance_id]; let model = affine3_to_square(instance_uniform.model); let world_position_1 = mesh_position_local_to_world(model, vec4(vertex_1.position, 1.0)); let world_position_2 = mesh_position_local_to_world(model, vec4(vertex_2.position, 1.0)); let world_position_3 = mesh_position_local_to_world(model, vec4(vertex_3.position, 1.0)); + let clip_position_1 = position_world_to_clip(world_position_1.xyz); let clip_position_2 = position_world_to_clip(world_position_2.xyz); let clip_position_3 = position_world_to_clip(world_position_3.xyz); diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index 59e6d63fffa3e..05d01e3902dd0 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -10,6 +10,18 @@ use bitflags::bitflags; use crate::deferred::DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID; use crate::*; +/// An enum to define which UV attribute to use for a texture. +/// It is used for every texture in the [`StandardMaterial`]. +/// It only supports two UV attributes, [`Mesh::ATTRIBUTE_UV_0`] and [`Mesh::ATTRIBUTE_UV_1`]. +/// The default is [`UvChannel::Uv0`]. +#[derive(Reflect, Default, Debug, Clone, PartialEq, Eq)] +#[reflect(Default, Debug)] +pub enum UvChannel { + #[default] + Uv0, + Uv1, +} + /// A material with "standard" properties used in PBR lighting /// Standard property values with pictures here /// . @@ -29,6 +41,11 @@ pub struct StandardMaterial { /// Defaults to [`Color::WHITE`]. pub base_color: Color, + /// The UV channel to use for the [`StandardMaterial::base_color_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub base_color_channel: UvChannel, + /// The texture component of the material's color before lighting. /// The actual pre-lighting color is `base_color * this_texture`. /// @@ -73,6 +90,11 @@ pub struct StandardMaterial { /// it just adds a value to the color seen on screen. pub emissive: Color, + /// The UV channel to use for the [`StandardMaterial::emissive_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub emissive_channel: UvChannel, + /// The emissive map, multiplies pixels with [`emissive`] /// to get the final "emitting" color of a surface. /// @@ -114,6 +136,11 @@ pub struct StandardMaterial { /// color as `metallic * metallic_texture_value`. pub metallic: f32, + /// The UV channel to use for the [`StandardMaterial::metallic_roughness_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub metallic_roughness_channel: UvChannel, + /// Metallic and roughness maps, stored as a single texture. /// /// The blue channel contains metallic values, @@ -170,6 +197,12 @@ pub struct StandardMaterial { #[doc(alias = "translucency")] pub diffuse_transmission: f32, + /// The UV channel to use for the [`StandardMaterial::diffuse_transmission_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_transmission_textures")] + pub diffuse_transmission_channel: UvChannel, + /// A map that modulates diffuse transmission via its alpha channel. Multiplied by [`StandardMaterial::diffuse_transmission`] /// to obtain the final result. /// @@ -205,6 +238,12 @@ pub struct StandardMaterial { #[doc(alias = "refraction")] pub specular_transmission: f32, + /// The UV channel to use for the [`StandardMaterial::specular_transmission_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_transmission_textures")] + pub specular_transmission_channel: UvChannel, + /// A map that modulates specular transmission via its red channel. Multiplied by [`StandardMaterial::specular_transmission`] /// to obtain the final result. /// @@ -228,6 +267,12 @@ pub struct StandardMaterial { #[doc(alias = "thin_walled")] pub thickness: f32, + /// The UV channel to use for the [`StandardMaterial::thickness_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_transmission_textures")] + pub thickness_channel: UvChannel, + /// A map that modulates thickness via its green channel. Multiplied by [`StandardMaterial::thickness`] /// to obtain the final result. /// @@ -294,6 +339,11 @@ pub struct StandardMaterial { #[doc(alias = "extinction_color")] pub attenuation_color: Color, + /// The UV channel to use for the [`StandardMaterial::normal_map_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub normal_map_channel: UvChannel, + /// Used to fake the lighting of bumps and dents on a material. /// /// A typical usage would be faking cobblestones on a flat plane mesh in 3D. @@ -323,6 +373,11 @@ pub struct StandardMaterial { /// it to right-handed conventions. pub flip_normal_map_y: bool, + /// The UV channel to use for the [`StandardMaterial::occlusion_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub occlusion_channel: UvChannel, + /// Specifies the level of exposure to ambient light. /// /// This is usually generated and stored automatically ("baked") by 3D-modelling software. @@ -338,6 +393,78 @@ pub struct StandardMaterial { #[dependency] pub occlusion_texture: Option>, + /// An extra thin translucent layer on top of the main PBR layer. This is + /// typically used for painted surfaces. + /// + /// This value specifies the strength of the layer, which affects how + /// visible the clearcoat layer will be. + /// + /// Defaults to zero, specifying no clearcoat layer. + pub clearcoat: f32, + + /// The UV channel to use for the [`StandardMaterial::clearcoat_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_channel: UvChannel, + + /// An image texture that specifies the strength of the clearcoat layer in + /// the red channel. Values sampled from this texture are multiplied by the + /// main [`StandardMaterial::clearcoat`] factor. + /// + /// As this is a non-color map, it must not be loaded as sRGB. + #[texture(19)] + #[sampler(20)] + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_texture: Option>, + + /// The roughness of the clearcoat material. This is specified in exactly + /// the same way as the [`StandardMaterial::perceptual_roughness`]. + /// + /// If the [`StandardMaterial::clearcoat`] value if zero, this has no + /// effect. + /// + /// Defaults to 0.5. + pub clearcoat_perceptual_roughness: f32, + + /// The UV channel to use for the [`StandardMaterial::clearcoat_roughness_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_roughness_channel: UvChannel, + + /// An image texture that specifies the roughness of the clearcoat level in + /// the green channel. Values from this texture are multiplied by the main + /// [`StandardMaterial::clearcoat_perceptual_roughness`] factor. + /// + /// As this is a non-color map, it must not be loaded as sRGB. + #[texture(21)] + #[sampler(22)] + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_roughness_texture: Option>, + + /// The UV channel to use for the [`StandardMaterial::clearcoat_normal_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_normal_channel: UvChannel, + + /// An image texture that specifies a normal map that is to be applied to + /// the clearcoat layer. This can be used to simulate, for example, + /// scratches on an outer layer of varnish. Normal maps are in the same + /// format as [`StandardMaterial::normal_map_texture`]. + /// + /// Note that, if a clearcoat normal map isn't specified, the main normal + /// map, if any, won't be applied to the clearcoat. If you want a normal map + /// that applies to both the main materal and to the clearcoat, specify it + /// in both [`StandardMaterial::normal_map_texture`] and this field. + /// + /// As this is a non-color map, it must not be loaded as sRGB. + #[texture(23)] + #[sampler(24)] + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_normal_texture: Option>, + /// Support two-sided lighting by automatically flipping the normals for "back" faces /// within the PBR lighting shader. /// @@ -553,13 +680,16 @@ impl Default for StandardMaterial { // White because it gets multiplied with texture values if someone uses // a texture. base_color: Color::WHITE, + base_color_channel: UvChannel::Uv0, base_color_texture: None, emissive: Color::BLACK, + emissive_channel: UvChannel::Uv0, emissive_texture: None, // Matches Blender's default roughness. perceptual_roughness: 0.5, // Metallic should generally be set to 0.0 or 1.0. metallic: 0.0, + metallic_roughness_channel: UvChannel::Uv0, metallic_roughness_texture: None, // Minimum real-world reflectance is 2%, most materials between 2-5% // Expressed in a linear scale and equivalent to 4% reflectance see @@ -567,18 +697,40 @@ impl Default for StandardMaterial { reflectance: 0.5, diffuse_transmission: 0.0, #[cfg(feature = "pbr_transmission_textures")] + diffuse_transmission_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_transmission_textures")] diffuse_transmission_texture: None, specular_transmission: 0.0, #[cfg(feature = "pbr_transmission_textures")] + specular_transmission_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_transmission_textures")] specular_transmission_texture: None, thickness: 0.0, #[cfg(feature = "pbr_transmission_textures")] + thickness_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_transmission_textures")] thickness_texture: None, ior: 1.5, attenuation_color: Color::WHITE, attenuation_distance: f32::INFINITY, + occlusion_channel: UvChannel::Uv0, occlusion_texture: None, + normal_map_channel: UvChannel::Uv0, normal_map_texture: None, + clearcoat: 0.0, + clearcoat_perceptual_roughness: 0.5, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_texture: None, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_texture: None, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_texture: None, flip_normal_map_y: false, double_sided: false, cull_mode: Some(Face::Back), @@ -641,6 +793,9 @@ bitflags::bitflags! { const THICKNESS_TEXTURE = 1 << 11; const DIFFUSE_TRANSMISSION_TEXTURE = 1 << 12; const ATTENUATION_ENABLED = 1 << 13; + const CLEARCOAT_TEXTURE = 1 << 14; + const CLEARCOAT_ROUGHNESS_TEXTURE = 1 << 15; + const CLEARCOAT_NORMAL_TEXTURE = 1 << 16; const ALPHA_MODE_RESERVED_BITS = Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS; // ← Bitmask reserving bits for the `AlphaMode` const ALPHA_MODE_OPAQUE = 0 << Self::ALPHA_MODE_SHIFT_BITS; // ← Values are just sequential values bitshifted into const ALPHA_MODE_MASK = 1 << Self::ALPHA_MODE_SHIFT_BITS; // the bitmask, and can range from 0 to 7. @@ -690,6 +845,8 @@ pub struct StandardMaterialUniform { pub ior: f32, /// How far light travels through the volume underneath the material surface before being absorbed pub attenuation_distance: f32, + pub clearcoat: f32, + pub clearcoat_perceptual_roughness: f32, /// The [`StandardMaterialFlags`] accessible in the `wgsl` shader. pub flags: u32, /// When the alpha mode mask flag is set, any base color alpha above this cutoff means fully opaque, @@ -753,6 +910,20 @@ impl AsBindGroupShaderType for StandardMaterial { flags |= StandardMaterialFlags::DIFFUSE_TRANSMISSION_TEXTURE; } } + + #[cfg(feature = "pbr_multi_layer_material_textures")] + { + if self.clearcoat_texture.is_some() { + flags |= StandardMaterialFlags::CLEARCOAT_TEXTURE; + } + if self.clearcoat_roughness_texture.is_some() { + flags |= StandardMaterialFlags::CLEARCOAT_ROUGHNESS_TEXTURE; + } + if self.clearcoat_normal_texture.is_some() { + flags |= StandardMaterialFlags::CLEARCOAT_NORMAL_TEXTURE; + } + } + let has_normal_map = self.normal_map_texture.is_some(); if has_normal_map { let normal_map_id = self.normal_map_texture.as_ref().map(|h| h.id()).unwrap(); @@ -799,6 +970,8 @@ impl AsBindGroupShaderType for StandardMaterial { roughness: self.perceptual_roughness, metallic: self.metallic, reflectance: self.reflectance, + clearcoat: self.clearcoat, + clearcoat_perceptual_roughness: self.clearcoat_perceptual_roughness, diffuse_transmission: self.diffuse_transmission, specular_transmission: self.specular_transmission, thickness: self.thickness, @@ -829,6 +1002,19 @@ bitflags! { const RELIEF_MAPPING = 0x08; const DIFFUSE_TRANSMISSION = 0x10; const SPECULAR_TRANSMISSION = 0x20; + const CLEARCOAT = 0x40; + const CLEARCOAT_NORMAL_MAP = 0x80; + const BASE_COLOR_UV = 0x00100; + const EMISSIVE_UV = 0x00200; + const METALLIC_ROUGHNESS_UV = 0x00400; + const OCCLUSION_UV = 0x00800; + const SPECULAR_TRANSMISSION_UV = 0x01000; + const THICKNESS_UV = 0x02000; + const DIFFUSE_TRANSMISSION_UV = 0x04000; + const NORMAL_MAP_UV = 0x08000; + const CLEARCOAT_UV = 0x10000; + const CLEARCOAT_ROUGHNESS_UV = 0x20000; + const CLEARCOAT_NORMAL_UV = 0x40000; const DEPTH_BIAS = 0xffffffff_00000000; } } @@ -865,6 +1051,67 @@ impl From<&StandardMaterial> for StandardMaterialKey { StandardMaterialKey::SPECULAR_TRANSMISSION, material.specular_transmission > 0.0, ); + + key.set(StandardMaterialKey::CLEARCOAT, material.clearcoat > 0.0); + + #[cfg(feature = "pbr_multi_layer_material_textures")] + key.set( + StandardMaterialKey::CLEARCOAT_NORMAL_MAP, + material.clearcoat > 0.0 && material.clearcoat_normal_texture.is_some(), + ); + + key.set( + StandardMaterialKey::BASE_COLOR_UV, + material.base_color_channel != UvChannel::Uv0, + ); + + key.set( + StandardMaterialKey::EMISSIVE_UV, + material.emissive_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::METALLIC_ROUGHNESS_UV, + material.metallic_roughness_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::OCCLUSION_UV, + material.occlusion_channel != UvChannel::Uv0, + ); + #[cfg(feature = "pbr_transmission_textures")] + { + key.set( + StandardMaterialKey::SPECULAR_TRANSMISSION_UV, + material.specular_transmission_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::THICKNESS_UV, + material.thickness_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::DIFFUSE_TRANSMISSION_UV, + material.diffuse_transmission_channel != UvChannel::Uv0, + ); + } + key.set( + StandardMaterialKey::NORMAL_MAP_UV, + material.normal_map_channel != UvChannel::Uv0, + ); + #[cfg(feature = "pbr_multi_layer_material_textures")] + { + key.set( + StandardMaterialKey::CLEARCOAT_UV, + material.clearcoat_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::CLEARCOAT_ROUGHNESS_UV, + material.clearcoat_roughness_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::CLEARCOAT_NORMAL_UV, + material.clearcoat_normal_channel != UvChannel::Uv0, + ); + } + key.insert(StandardMaterialKey::from_bits_retain( (material.depth_bias as u64) << STANDARD_MATERIAL_KEY_DEPTH_BIAS_SHIFT, )); @@ -941,38 +1188,81 @@ impl Material for StandardMaterial { if let Some(fragment) = descriptor.fragment.as_mut() { let shader_defs = &mut fragment.shader_defs; - if key - .bind_group_data - .contains(StandardMaterialKey::NORMAL_MAP) - { - shader_defs.push("STANDARD_MATERIAL_NORMAL_MAP".into()); - } - if key - .bind_group_data - .contains(StandardMaterialKey::RELIEF_MAPPING) - { - shader_defs.push("RELIEF_MAPPING".into()); - } - - if key - .bind_group_data - .contains(StandardMaterialKey::DIFFUSE_TRANSMISSION) - { - shader_defs.push("STANDARD_MATERIAL_DIFFUSE_TRANSMISSION".into()); - } - - if key - .bind_group_data - .contains(StandardMaterialKey::SPECULAR_TRANSMISSION) - { - shader_defs.push("STANDARD_MATERIAL_SPECULAR_TRANSMISSION".into()); - } - - if key.bind_group_data.intersects( - StandardMaterialKey::DIFFUSE_TRANSMISSION - | StandardMaterialKey::SPECULAR_TRANSMISSION, - ) { - shader_defs.push("STANDARD_MATERIAL_SPECULAR_OR_DIFFUSE_TRANSMISSION".into()); + for (flags, shader_def) in [ + ( + StandardMaterialKey::NORMAL_MAP, + "STANDARD_MATERIAL_NORMAL_MAP", + ), + (StandardMaterialKey::RELIEF_MAPPING, "RELIEF_MAPPING"), + ( + StandardMaterialKey::DIFFUSE_TRANSMISSION, + "STANDARD_MATERIAL_DIFFUSE_TRANSMISSION", + ), + ( + StandardMaterialKey::SPECULAR_TRANSMISSION, + "STANDARD_MATERIAL_SPECULAR_TRANSMISSION", + ), + ( + StandardMaterialKey::DIFFUSE_TRANSMISSION + | StandardMaterialKey::SPECULAR_TRANSMISSION, + "STANDARD_MATERIAL_DIFFUSE_OR_SPECULAR_TRANSMISSION", + ), + ( + StandardMaterialKey::CLEARCOAT, + "STANDARD_MATERIAL_CLEARCOAT", + ), + ( + StandardMaterialKey::CLEARCOAT_NORMAL_MAP, + "STANDARD_MATERIAL_CLEARCOAT_NORMAL_MAP", + ), + ( + StandardMaterialKey::BASE_COLOR_UV, + "STANDARD_MATERIAL_BASE_COLOR_UV_B", + ), + ( + StandardMaterialKey::EMISSIVE_UV, + "STANDARD_MATERIAL_EMISSIVE_UV_B", + ), + ( + StandardMaterialKey::METALLIC_ROUGHNESS_UV, + "STANDARD_MATERIAL_METALLIC_ROUGHNESS_UV_B", + ), + ( + StandardMaterialKey::OCCLUSION_UV, + "STANDARD_MATERIAL_OCCLUSION_UV_B", + ), + ( + StandardMaterialKey::SPECULAR_TRANSMISSION_UV, + "STANDARD_MATERIAL_SPECULAR_TRANSMISSION_UV_B", + ), + ( + StandardMaterialKey::THICKNESS_UV, + "STANDARD_MATERIAL_THICKNESS_UV_B", + ), + ( + StandardMaterialKey::DIFFUSE_TRANSMISSION_UV, + "STANDARD_MATERIAL_DIFFUSE_TRANSMISSION_UV_B", + ), + ( + StandardMaterialKey::NORMAL_MAP_UV, + "STANDARD_MATERIAL_NORMAL_MAP_UV_B", + ), + ( + StandardMaterialKey::CLEARCOAT_UV, + "STANDARD_MATERIAL_CLEARCOAT_UV_B", + ), + ( + StandardMaterialKey::CLEARCOAT_ROUGHNESS_UV, + "STANDARD_MATERIAL_CLEARCOAT_ROUGHNESS_UV_B", + ), + ( + StandardMaterialKey::CLEARCOAT_NORMAL_UV, + "STANDARD_MATERIAL_CLEARCOAT_NORMAL_UV_B", + ), + ] { + if key.bind_group_data.intersects(flags) { + shader_defs.push(shader_def.into()); + } } } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index f4e6b59c74bac..7a1b831f8ec15 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -394,10 +394,12 @@ where if layout.0.contains(Mesh::ATTRIBUTE_UV_0) { shader_defs.push("VERTEX_UVS".into()); + shader_defs.push("VERTEX_UVS_A".into()); vertex_attributes.push(Mesh::ATTRIBUTE_UV_0.at_shader_location(1)); } if layout.0.contains(Mesh::ATTRIBUTE_UV_1) { + shader_defs.push("VERTEX_UVS".into()); shader_defs.push("VERTEX_UVS_B".into()); vertex_attributes.push(Mesh::ATTRIBUTE_UV_1.at_shader_location(2)); } diff --git a/crates/bevy_pbr/src/prepass/prepass.wgsl b/crates/bevy_pbr/src/prepass/prepass.wgsl index 89e98fb3139ff..0113618d3b13c 100644 --- a/crates/bevy_pbr/src/prepass/prepass.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass.wgsl @@ -5,6 +5,7 @@ skinning, morph, mesh_view_bindings::view, + view_transformations::position_world_to_clip, } #ifdef DEFERRED_PREPASS @@ -50,15 +51,16 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput { var model = mesh_functions::get_model_matrix(vertex_no_morph.instance_index); #endif // SKINNED - out.position = mesh_functions::mesh_position_local_to_clip(model, vec4(vertex.position, 1.0)); + out.world_position = mesh_functions::mesh_position_local_to_world(model, vec4(vertex.position, 1.0)); + out.position = position_world_to_clip(out.world_position.xyz); #ifdef DEPTH_CLAMP_ORTHO out.clip_position_unclamped = out.position; out.position.z = min(out.position.z, 1.0); #endif // DEPTH_CLAMP_ORTHO -#ifdef VERTEX_UVS +#ifdef VERTEX_UVS_A out.uv = vertex.uv; -#endif // VERTEX_UVS +#endif // VERTEX_UVS_A #ifdef VERTEX_UVS_B out.uv_b = vertex.uv_b; @@ -91,8 +93,6 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput { out.color = vertex.color; #endif - out.world_position = mesh_functions::mesh_position_local_to_world(model, vec4(vertex.position, 1.0)); - #ifdef MOTION_VECTOR_PREPASS // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. // See https://github.com/gfx-rs/naga/issues/2416 diff --git a/crates/bevy_pbr/src/prepass/prepass_io.wgsl b/crates/bevy_pbr/src/prepass/prepass_io.wgsl index 5abcfc1aedddb..bd05de978c829 100644 --- a/crates/bevy_pbr/src/prepass/prepass_io.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass_io.wgsl @@ -6,7 +6,7 @@ struct Vertex { @builtin(instance_index) instance_index: u32, @location(0) position: vec3, -#ifdef VERTEX_UVS +#ifdef VERTEX_UVS_A @location(1) uv: vec2, #endif @@ -40,7 +40,7 @@ struct VertexOutput { // and `frag coord` when used as a fragment stage input @builtin(position) position: vec4, -#ifdef VERTEX_UVS +#ifdef VERTEX_UVS_A @location(0) uv: vec2, #endif diff --git a/crates/bevy_pbr/src/render/clustered_forward.wgsl b/crates/bevy_pbr/src/render/clustered_forward.wgsl index ae142f2eb5485..56525248963e3 100644 --- a/crates/bevy_pbr/src/render/clustered_forward.wgsl +++ b/crates/bevy_pbr/src/render/clustered_forward.wgsl @@ -2,7 +2,12 @@ #import bevy_pbr::{ mesh_view_bindings as bindings, - utils::{PI_2, hsv_to_rgb, rand_f}, + utils::rand_f, +} + +#import bevy_render::{ + color_operations::hsv_to_rgb, + maths::PI_2, } // NOTE: Keep in sync with bevy_pbr/src/light.rs diff --git a/crates/bevy_pbr/src/render/fog.rs b/crates/bevy_pbr/src/render/fog.rs index 16ae9a1007d98..1421ddcd6daab 100644 --- a/crates/bevy_pbr/src/render/fog.rs +++ b/crates/bevy_pbr/src/render/fog.rs @@ -1,6 +1,6 @@ use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Handle}; -use bevy_color::LinearRgba; +use bevy_color::{ColorToComponents, LinearRgba}; use bevy_ecs::prelude::*; use bevy_math::{Vec3, Vec4}; use bevy_render::{ @@ -66,24 +66,27 @@ pub fn prepare_fog( match &fog.falloff { FogFalloff::Linear { start, end } => GpuFog { mode: GPU_FOG_MODE_LINEAR, - base_color: LinearRgba::from(fog.color).into(), - directional_light_color: LinearRgba::from(fog.directional_light_color).into(), + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), directional_light_exponent: fog.directional_light_exponent, be: Vec3::new(*start, *end, 0.0), ..Default::default() }, FogFalloff::Exponential { density } => GpuFog { mode: GPU_FOG_MODE_EXPONENTIAL, - base_color: LinearRgba::from(fog.color).into(), - directional_light_color: LinearRgba::from(fog.directional_light_color).into(), + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), directional_light_exponent: fog.directional_light_exponent, be: Vec3::new(*density, 0.0, 0.0), ..Default::default() }, FogFalloff::ExponentialSquared { density } => GpuFog { mode: GPU_FOG_MODE_EXPONENTIAL_SQUARED, - base_color: LinearRgba::from(fog.color).into(), - directional_light_color: LinearRgba::from(fog.directional_light_color).into(), + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), directional_light_exponent: fog.directional_light_exponent, be: Vec3::new(*density, 0.0, 0.0), ..Default::default() @@ -93,8 +96,9 @@ pub fn prepare_fog( inscattering, } => GpuFog { mode: GPU_FOG_MODE_ATMOSPHERIC, - base_color: LinearRgba::from(fog.color).into(), - directional_light_color: LinearRgba::from(fog.directional_light_color).into(), + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), directional_light_exponent: fog.directional_light_exponent, be: *extinction, bi: *inscattering, diff --git a/crates/bevy_pbr/src/render/forward_io.wgsl b/crates/bevy_pbr/src/render/forward_io.wgsl index 68378945d13bb..99f2ecced7628 100644 --- a/crates/bevy_pbr/src/render/forward_io.wgsl +++ b/crates/bevy_pbr/src/render/forward_io.wgsl @@ -8,7 +8,7 @@ struct Vertex { #ifdef VERTEX_NORMALS @location(1) normal: vec3, #endif -#ifdef VERTEX_UVS +#ifdef VERTEX_UVS_A @location(2) uv: vec2, #endif #ifdef VERTEX_UVS_B @@ -35,7 +35,7 @@ struct VertexOutput { @builtin(position) position: vec4, @location(0) world_position: vec4, @location(1) world_normal: vec3, -#ifdef VERTEX_UVS +#ifdef VERTEX_UVS_A @location(2) uv: vec2, #endif #ifdef VERTEX_UVS_B diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 74e340c01a33e..1e9709ab00a78 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -46,6 +46,7 @@ pub struct ExtractedDirectionalLight { pub illuminance: f32, pub transform: GlobalTransform, pub shadows_enabled: bool, + pub volumetric: bool, pub shadow_depth_bias: f32, pub shadow_normal_bias: f32, pub cascade_shadow_config: CascadeShadowConfig, @@ -173,7 +174,7 @@ pub struct GpuDirectionalLight { num_cascades: u32, cascades_overlap_proportion: f32, depth_texture_base_index: u32, - render_layers: u32, + skip: u32, } // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! @@ -181,6 +182,7 @@ bitflags::bitflags! { #[repr(transparent)] struct DirectionalLightFlags: u32 { const SHADOWS_ENABLED = 1 << 0; + const VOLUMETRIC = 1 << 1; const NONE = 0; const UNINITIALIZED = 0xFFFF; } @@ -346,6 +348,7 @@ pub fn extract_lights( &GlobalTransform, &ViewVisibility, Option<&RenderLayers>, + Option<&VolumetricLight>, ), Without, >, @@ -468,6 +471,7 @@ pub fn extract_lights( transform, view_visibility, maybe_layers, + volumetric_light, ) in &directional_lights { if !view_visibility.get() { @@ -481,6 +485,7 @@ pub fn extract_lights( color: directional_light.color.into(), illuminance: directional_light.illuminance, transform: *transform, + volumetric: volumetric_light.is_some(), shadows_enabled: directional_light.shadows_enabled, shadow_depth_bias: directional_light.shadow_depth_bias, // The factor of SQRT_2 is for the worst-case diagonal offset @@ -488,7 +493,7 @@ pub fn extract_lights( cascade_shadow_config: cascade_config.clone(), cascades: cascades.cascades.clone(), frusta: frusta.frusta.clone(), - render_layers: maybe_layers.copied().unwrap_or_default(), + render_layers: maybe_layers.unwrap_or_default().clone(), }, render_visible_entities, )); @@ -684,7 +689,12 @@ pub fn prepare_lights( mut global_light_meta: ResMut, mut light_meta: ResMut, views: Query< - (Entity, &ExtractedView, &ExtractedClusterConfig), + ( + Entity, + &ExtractedView, + &ExtractedClusterConfig, + Option<&RenderLayers>, + ), With>, >, ambient_light: Res, @@ -772,6 +782,13 @@ pub fn prepare_lights( .count() .min(max_texture_cubes); + let directional_volumetric_enabled_count = directional_lights + .iter() + .take(MAX_DIRECTIONAL_LIGHTS) + .filter(|(_, light)| light.volumetric) + .count() + .min(max_texture_array_layers / MAX_CASCADES_PER_LIGHT); + let directional_shadow_enabled_count = directional_lights .iter() .take(MAX_DIRECTIONAL_LIGHTS) @@ -806,13 +823,17 @@ pub fn prepare_lights( }); // Sort lights by - // - those with shadows enabled first, so that the index can be used to render at most `directional_light_shadow_maps_count` - // directional light shadows - // - then by entity as a stable key to ensure that a consistent set of lights are chosen if the light count limit is exceeded. + // - those with volumetric (and shadows) enabled first, so that the + // volumetric lighting pass can quickly find the volumetric lights; + // - then those with shadows enabled second, so that the index can be used + // to render at most `directional_light_shadow_maps_count` directional light + // shadows + // - then by entity as a stable key to ensure that a consistent set of + // lights are chosen if the light count limit is exceeded. directional_lights.sort_by(|(entity_1, light_1), (entity_2, light_2)| { directional_light_order( - (entity_1, &light_1.shadows_enabled), - (entity_2, &light_2.shadows_enabled), + (entity_1, &light_1.volumetric, &light_1.shadows_enabled), + (entity_2, &light_2.volumetric, &light_2.shadows_enabled), ) }); @@ -893,7 +914,14 @@ pub fn prepare_lights( { let mut flags = DirectionalLightFlags::NONE; - // Lights are sorted, shadow enabled lights are first + // Lights are sorted, volumetric and shadow enabled lights are first + if light.volumetric + && light.shadows_enabled + && (index < directional_volumetric_enabled_count) + { + flags |= DirectionalLightFlags::VOLUMETRIC; + } + // Shadow enabled lights are second if light.shadows_enabled && (index < directional_shadow_enabled_count) { flags |= DirectionalLightFlags::SHADOWS_ENABLED; } @@ -904,20 +932,21 @@ pub fn prepare_lights( .len() .min(MAX_CASCADES_PER_LIGHT); gpu_directional_lights[index] = GpuDirectionalLight { + // Set to true later when necessary. + skip: 0u32, // Filled in later. cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT], // premultiply color by illuminance // we don't use the alpha at all, so no reason to multiply only [0..3] color: Vec4::from_slice(&light.color.to_f32_array()) * light.illuminance, // direction is negated to be ready for N.L - dir_to_light: light.transform.back(), + dir_to_light: light.transform.back().into(), flags: flags.bits(), shadow_depth_bias: light.shadow_depth_bias, shadow_normal_bias: light.shadow_normal_bias, 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; @@ -930,7 +959,7 @@ pub fn prepare_lights( .write_buffer(&render_device, &render_queue); // set up light data for each view - for (entity, extracted_view, clusters) in &views { + for (entity, extracted_view, clusters, maybe_layers) in &views { let point_light_depth_texture = texture_cache.get( &render_device, TextureDescriptor { @@ -1128,11 +1157,25 @@ pub fn prepare_lights( // directional lights let mut directional_depth_texture_array_index = 0u32; + let view_layers = maybe_layers.unwrap_or_default(); for (light_index, &(light_entity, light)) in directional_lights .iter() .enumerate() - .take(directional_shadow_enabled_count) + .take(MAX_DIRECTIONAL_LIGHTS) { + let gpu_light = &mut gpu_lights.directional_lights[light_index]; + + // Check if the light intersects with the view. + if !view_layers.intersects(&light.render_layers) { + gpu_light.skip = 1u32; + continue; + } + + // Only deal with cascades when shadows are enabled. + if (gpu_light.flags & DirectionalLightFlags::SHADOWS_ENABLED.bits()) == 0u32 { + continue; + } + let cascades = light .cascades .get(&entity) diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 199f6752cfea0..c554a6489838c 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -33,15 +33,13 @@ use bevy_render::{ texture::{BevyDefault, DefaultImageSampler, ImageSampler, TextureFormatPixelInfo}, view::{ prepare_view_targets, GpuCulling, RenderVisibilityRanges, ViewTarget, ViewUniformOffset, - ViewVisibility, VisibilityRange, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT, + ViewVisibility, VisibilityRange, }, Extract, }; use bevy_transform::components::GlobalTransform; use bevy_utils::{tracing::error, tracing::warn, Entry, HashMap, Parallel}; -#[cfg(debug_assertions)] -use bevy_utils::warn_once; use bytemuck::{Pod, Zeroable}; use nonmax::{NonMaxU16, NonMaxU32}; use static_assertions::const_assert_eq; @@ -234,6 +232,7 @@ impl Plugin for MeshRenderPlugin { render_app .insert_resource(indirect_parameters_buffer) + .init_resource::() .init_resource::(); } @@ -1032,9 +1031,11 @@ fn collect_meshes_for_gpu_building( } } +/// All data needed to construct a pipeline for rendering 3D meshes. #[derive(Resource, Clone)] pub struct MeshPipeline { - view_layouts: [MeshPipelineViewLayout; MeshPipelineViewLayoutKey::COUNT], + /// A reference to all the mesh pipeline view layouts. + pub view_layouts: MeshPipelineViewLayouts, // This dummy white texture is to be used in place of optional StandardMaterial textures pub dummy_white_gpu_image: GpuImage, pub clustered_forward_buffer_binding_type: BufferBindingType, @@ -1065,18 +1066,13 @@ impl FromWorld for MeshPipeline { Res, Res, Res, + Res, )> = SystemState::new(world); - let (render_device, default_sampler, render_queue) = system_state.get_mut(world); + let (render_device, default_sampler, render_queue, view_layouts) = + system_state.get_mut(world); + let clustered_forward_buffer_binding_type = render_device .get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT); - let visibility_ranges_buffer_binding_type = render_device - .get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT); - - let view_layouts = generate_view_layouts( - &render_device, - clustered_forward_buffer_binding_type, - visibility_ranges_buffer_binding_type, - ); // A 1x1x1 'all 1.0' texture to use as a dummy texture to use in place of optional StandardMaterial textures let dummy_white_gpu_image = { @@ -1113,7 +1109,7 @@ impl FromWorld for MeshPipeline { }; MeshPipeline { - view_layouts, + view_layouts: view_layouts.clone(), clustered_forward_buffer_binding_type, dummy_white_gpu_image, mesh_layouts: MeshLayouts::new(&render_device), @@ -1141,16 +1137,7 @@ impl MeshPipeline { } pub fn get_view_layout(&self, layout_key: MeshPipelineViewLayoutKey) -> &BindGroupLayout { - let index = layout_key.bits() as usize; - let layout = &self.view_layouts[index]; - - #[cfg(debug_assertions)] - if layout.texture_count > MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES { - // Issue our own warning here because Naga's error message is a bit cryptic in this situation - warn_once!("Too many textures in mesh pipeline view layout, this might cause us to hit `wgpu::Limits::max_sampled_textures_per_shader_stage` in some environments."); - } - - &layout.bind_group_layout + self.view_layouts.get_view_layout(layout_key) } } @@ -1543,10 +1530,12 @@ impl SpecializedMeshPipeline for MeshPipeline { if layout.0.contains(Mesh::ATTRIBUTE_UV_0) { shader_defs.push("VERTEX_UVS".into()); + shader_defs.push("VERTEX_UVS_A".into()); vertex_attributes.push(Mesh::ATTRIBUTE_UV_0.at_shader_location(2)); } if layout.0.contains(Mesh::ATTRIBUTE_UV_1) { + shader_defs.push("VERTEX_UVS".into()); shader_defs.push("VERTEX_UVS_B".into()); vertex_attributes.push(Mesh::ATTRIBUTE_UV_1.at_shader_location(3)); } @@ -1564,6 +1553,9 @@ impl SpecializedMeshPipeline for MeshPipeline { if cfg!(feature = "pbr_transmission_textures") { shader_defs.push("PBR_TRANSMISSION_TEXTURES_SUPPORTED".into()); } + if cfg!(feature = "pbr_multi_layer_material_textures") { + shader_defs.push("PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED".into()); + } let mut bind_group_layout = vec![self.get_view_layout(key.into()).clone()]; diff --git a/crates/bevy_pbr/src/render/mesh.wgsl b/crates/bevy_pbr/src/render/mesh.wgsl index 03dad7813e953..dce832304d91f 100644 --- a/crates/bevy_pbr/src/render/mesh.wgsl +++ b/crates/bevy_pbr/src/render/mesh.wgsl @@ -63,10 +63,9 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput { out.position = position_world_to_clip(out.world_position.xyz); #endif -#ifdef VERTEX_UVS +#ifdef VERTEX_UVS_A out.uv = vertex.uv; #endif - #ifdef VERTEX_UVS_B out.uv_b = vertex.uv_b; #endif diff --git a/crates/bevy_pbr/src/render/mesh_functions.wgsl b/crates/bevy_pbr/src/render/mesh_functions.wgsl index c100f16f6ffe9..ce8e9701271a5 100644 --- a/crates/bevy_pbr/src/render/mesh_functions.wgsl +++ b/crates/bevy_pbr/src/render/mesh_functions.wgsl @@ -1,7 +1,11 @@ #define_import_path bevy_pbr::mesh_functions #import bevy_pbr::{ - mesh_view_bindings::{view, visibility_ranges}, + mesh_view_bindings::{ + view, + visibility_ranges, + VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE + }, mesh_bindings::mesh, mesh_types::MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT, view_transformations::position_world_to_clip, @@ -90,8 +94,16 @@ fn mesh_tangent_local_to_world(model: mat4x4, vertex_tangent: vec4, in // camera distance to determine the dithering level. #ifdef VISIBILITY_RANGE_DITHER fn get_visibility_range_dither_level(instance_index: u32, world_position: vec4) -> i32 { +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 + // If we're using a storage buffer, then the length is variable. + let visibility_buffer_array_len = arrayLength(&visibility_ranges); +#else // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 + // If we're using a uniform buffer, then the length is constant + let visibility_buffer_array_len = VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE; +#endif // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 + let visibility_buffer_index = mesh[instance_index].flags & 0xffffu; - if (visibility_buffer_index > arrayLength(&visibility_ranges)) { + if (visibility_buffer_index > visibility_buffer_array_len) { return -16; } diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index db2aa1831a6fa..c937d364937a0 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -1,4 +1,4 @@ -use std::{array, num::NonZeroU64}; +use std::{array, num::NonZeroU64, sync::Arc}; use bevy_core_pipeline::{ core_3d::ViewTransmissionTexture, @@ -7,10 +7,12 @@ use bevy_core_pipeline::{ get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts, }, }; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, entity::Entity, - system::{Commands, Query, Res}, + system::{Commands, Query, Res, Resource}, + world::{FromWorld, World}, }; use bevy_math::Vec4; use bevy_render::{ @@ -19,13 +21,20 @@ use bevy_render::{ render_resource::{binding_types::*, *}, renderer::RenderDevice, texture::{BevyDefault, FallbackImage, FallbackImageMsaa, FallbackImageZero, GpuImage}, - view::{Msaa, RenderVisibilityRanges, ViewUniform, ViewUniforms}, + view::{ + Msaa, RenderVisibilityRanges, ViewUniform, ViewUniforms, + VISIBILITY_RANGES_STORAGE_BUFFER_COUNT, + }, }; #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] use bevy_render::render_resource::binding_types::texture_cube; +#[cfg(debug_assertions)] +use bevy_utils::warn_once; use environment_map::EnvironmentMapLight; +#[cfg(debug_assertions)] +use crate::MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES; use crate::{ environment_map::{self, RenderViewEnvironmentMapBindGroupEntries}, irradiance_volume::{ @@ -35,6 +44,7 @@ use crate::{ prepass, FogMeta, GlobalLightMeta, GpuFog, GpuLights, GpuPointLights, LightMeta, LightProbesBuffer, LightProbesUniform, MeshPipeline, MeshPipelineKey, RenderViewLightProbes, ScreenSpaceAmbientOcclusionTextures, ShadowSamplers, ViewClusterBindings, ViewShadowBindings, + CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, }; #[derive(Clone)] @@ -330,6 +340,66 @@ fn layout_entries( entries.to_vec() } +/// Stores the view layouts for every combination of pipeline keys. +/// +/// This is wrapped in an [`Arc`] so that it can be efficiently cloned and +/// placed inside specializable pipeline types. +#[derive(Resource, Clone, Deref, DerefMut)] +pub struct MeshPipelineViewLayouts( + pub Arc<[MeshPipelineViewLayout; MeshPipelineViewLayoutKey::COUNT]>, +); + +impl FromWorld for MeshPipelineViewLayouts { + fn from_world(world: &mut World) -> Self { + // Generates all possible view layouts for the mesh pipeline, based on all combinations of + // [`MeshPipelineViewLayoutKey`] flags. + + let render_device = world.resource::(); + + let clustered_forward_buffer_binding_type = render_device + .get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT); + let visibility_ranges_buffer_binding_type = render_device + .get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT); + + Self(Arc::new(array::from_fn(|i| { + let key = MeshPipelineViewLayoutKey::from_bits_truncate(i as u32); + let entries = layout_entries( + clustered_forward_buffer_binding_type, + visibility_ranges_buffer_binding_type, + key, + render_device, + ); + #[cfg(debug_assertions)] + let texture_count: usize = entries + .iter() + .filter(|entry| matches!(entry.ty, BindingType::Texture { .. })) + .count(); + + MeshPipelineViewLayout { + bind_group_layout: render_device + .create_bind_group_layout(key.label().as_str(), &entries), + #[cfg(debug_assertions)] + texture_count, + } + }))) + } +} + +impl MeshPipelineViewLayouts { + pub fn get_view_layout(&self, layout_key: MeshPipelineViewLayoutKey) -> &BindGroupLayout { + let index = layout_key.bits() as usize; + let layout = &self[index]; + + #[cfg(debug_assertions)] + if layout.texture_count > MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES { + // Issue our own warning here because Naga's error message is a bit cryptic in this situation + warn_once!("Too many textures in mesh pipeline view layout, this might cause us to hit `wgpu::Limits::max_sampled_textures_per_shader_stage` in some environments."); + } + + &layout.bind_group_layout + } +} + /// Generates all possible view layouts for the mesh pipeline, based on all combinations of /// [`MeshPipelineViewLayoutKey`] flags. pub fn generate_view_layouts( @@ -436,7 +506,7 @@ pub fn prepare_mesh_view_bind_groups( .map(|t| &t.screen_space_ambient_occlusion_texture.default_view) .unwrap_or(&fallback_ssao); - let layout = &mesh_pipeline.get_view_layout( + let layout = &mesh_pipeline.view_layouts.get_view_layout( MeshPipelineViewLayoutKey::from(*msaa) | MeshPipelineViewLayoutKey::from(prepass_textures), ); diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index 4fd1f2327d024..b8e74c60b8b43 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -35,10 +35,11 @@ @group(0) @binding(10) var fog: types::Fog; @group(0) @binding(11) var light_probes: types::LightProbes; +const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u; #if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 @group(0) @binding(12) var visibility_ranges: array>; #else -@group(0) @binding(12) var visibility_ranges: array>; +@group(0) @binding(12) var visibility_ranges: array, VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE>; #endif @group(0) @binding(13) var screen_space_ambient_occlusion_texture: texture_2d; diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index 76e43eed2d541..f517daec4d6b4 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -33,10 +33,11 @@ struct DirectionalLight { num_cascades: u32, cascades_overlap_proportion: f32, depth_texture_base_index: u32, - render_layers: u32, + skip: u32, }; const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; +const DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 2u; struct Lights { // NOTE: this array size must be kept in sync with the constants defined in bevy_pbr/src/render/light.rs diff --git a/crates/bevy_pbr/src/render/pbr_bindings.wgsl b/crates/bevy_pbr/src/render/pbr_bindings.wgsl index 2f26e010b0614..e51c22a7eab2b 100644 --- a/crates/bevy_pbr/src/render/pbr_bindings.wgsl +++ b/crates/bevy_pbr/src/render/pbr_bindings.wgsl @@ -23,3 +23,11 @@ @group(2) @binding(17) var diffuse_transmission_texture: texture_2d; @group(2) @binding(18) var diffuse_transmission_sampler: sampler; #endif +#ifdef PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED +@group(2) @binding(19) var clearcoat_texture: texture_2d; +@group(2) @binding(20) var clearcoat_sampler: sampler; +@group(2) @binding(21) var clearcoat_roughness_texture: texture_2d; +@group(2) @binding(22) var clearcoat_roughness_sampler: sampler; +@group(2) @binding(23) var clearcoat_normal_texture: texture_2d; +@group(2) @binding(24) var clearcoat_normal_sampler: sampler; +#endif diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index d64e78b47ea25..b2bb252f871f6 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -2,6 +2,7 @@ #import bevy_pbr::{ pbr_functions, + pbr_functions::SampleBias, pbr_bindings, pbr_types, prepass_utils, @@ -79,9 +80,26 @@ fn pbr_input_from_standard_material( // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" let NdotV = max(dot(pbr_input.N, pbr_input.V), 0.0001); + // Fill in the sample bias so we can sample from textures. + var bias: SampleBias; +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv = in.ddx_uv; + bias.ddy_uv = in.ddy_uv; +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias = view.mip_bias; +#endif // MESHLET_MESH_MATERIAL_PASS + #ifdef VERTEX_UVS let uv_transform = pbr_bindings::material.uv_transform; +#ifdef VERTEX_UVS_A var uv = (uv_transform * vec3(in.uv, 1.0)).xy; +#endif + +#ifdef VERTEX_UVS_B + var uv_b = (uv_transform * vec3(in.uv_b, 1.0)).xy; +#else + var uv_b = uv; +#endif #ifdef VERTEX_TANGENTS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT) != 0u) { @@ -91,6 +109,7 @@ fn pbr_input_from_standard_material( let B = in.world_tangent.w * cross(N, T); // Transform V from fragment to camera in world space to tangent space. let Vt = vec3(dot(V, T), dot(V, B), dot(V, N)); +#ifdef VERTEX_UVS_A uv = parallaxed_uv( pbr_bindings::material.parallax_depth_scale, pbr_bindings::material.max_parallax_layer_count, @@ -101,15 +120,36 @@ fn pbr_input_from_standard_material( // about. -Vt, ); +#endif + +#ifdef VERTEX_UVS_B + uv_b = parallaxed_uv( + pbr_bindings::material.parallax_depth_scale, + pbr_bindings::material.max_parallax_layer_count, + pbr_bindings::material.max_relief_mapping_search_steps, + uv_b, + // Flip the direction of Vt to go toward the surface to make the + // parallax mapping algorithm easier to understand and reason + // about. + -Vt, + ); +#else + uv_b = uv; +#endif } #endif // VERTEX_TANGENTS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) { -#ifdef MESHLET_MESH_MATERIAL_PASS - pbr_input.material.base_color *= textureSampleGrad(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, uv, in.ddx_uv, in.ddy_uv); + pbr_input.material.base_color *= pbr_functions::sample_texture( + pbr_bindings::base_color_texture, + pbr_bindings::base_color_sampler, +#ifdef STANDARD_MATERIAL_BASE_COLOR_UV_B + uv_b, #else - pbr_input.material.base_color *= textureSampleBias(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, uv, view.mip_bias); + uv, #endif + bias, + ); #ifdef ALPHA_TO_COVERAGE // Sharpen alpha edges. @@ -142,11 +182,16 @@ fn pbr_input_from_standard_material( var emissive: vec4 = pbr_bindings::material.emissive; #ifdef VERTEX_UVS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) { -#ifdef MESHLET_MESH_MATERIAL_PASS - emissive = vec4(emissive.rgb * textureSampleGrad(pbr_bindings::emissive_texture, pbr_bindings::emissive_sampler, uv, in.ddx_uv, in.ddy_uv).rgb, 1.0); + emissive = vec4(emissive.rgb * pbr_functions::sample_texture( + pbr_bindings::emissive_texture, + pbr_bindings::emissive_sampler, +#ifdef STANDARD_MATERIAL_EMISSIVE_UV_B + uv_b, #else - emissive = vec4(emissive.rgb * textureSampleBias(pbr_bindings::emissive_texture, pbr_bindings::emissive_sampler, uv, view.mip_bias).rgb, 1.0); + uv, #endif + bias, + ).rgb, 1.0); } #endif pbr_input.material.emissive = emissive; @@ -157,11 +202,16 @@ fn pbr_input_from_standard_material( let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); #ifdef VERTEX_UVS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) { -#ifdef MESHLET_MESH_MATERIAL_PASS - let metallic_roughness = textureSampleGrad(pbr_bindings::metallic_roughness_texture, pbr_bindings::metallic_roughness_sampler, uv, in.ddx_uv, in.ddy_uv); + let metallic_roughness = pbr_functions::sample_texture( + pbr_bindings::metallic_roughness_texture, + pbr_bindings::metallic_roughness_sampler, +#ifdef STANDARD_MATERIAL_METALLIC_ROUGHNESS_UV_B + uv_b, #else - let metallic_roughness = textureSampleBias(pbr_bindings::metallic_roughness_texture, pbr_bindings::metallic_roughness_sampler, uv, view.mip_bias); + uv, #endif + bias, + ); // Sampling from GLTF standard channels for now metallic *= metallic_roughness.b; perceptual_roughness *= metallic_roughness.g; @@ -170,27 +220,79 @@ fn pbr_input_from_standard_material( pbr_input.material.metallic = metallic; pbr_input.material.perceptual_roughness = perceptual_roughness; + // Clearcoat factor + pbr_input.material.clearcoat = pbr_bindings::material.clearcoat; +#ifdef VERTEX_UVS +#ifdef PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED + if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_CLEARCOAT_TEXTURE_BIT) != 0u) { + pbr_input.material.clearcoat *= pbr_functions::sample_texture( + pbr_bindings::clearcoat_texture, + pbr_bindings::clearcoat_sampler, +#ifdef STANDARD_MATERIAL_CLEARCOAT_UV_B + uv_b, +#else + uv, +#endif + bias, + ).r; + } +#endif // PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED +#endif // VERTEX_UVS + + // Clearcoat roughness + pbr_input.material.clearcoat_perceptual_roughness = pbr_bindings::material.clearcoat_perceptual_roughness; +#ifdef VERTEX_UVS +#ifdef PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED + if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_CLEARCOAT_ROUGHNESS_TEXTURE_BIT) != 0u) { + pbr_input.material.clearcoat_perceptual_roughness *= pbr_functions::sample_texture( + pbr_bindings::clearcoat_roughness_texture, + pbr_bindings::clearcoat_roughness_sampler, +#ifdef STANDARD_MATERIAL_CLEARCOAT_ROUGHNESS_UV_B + uv_b, +#else + uv, +#endif + bias, + ).g; + } +#endif // PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED +#endif // VERTEX_UVS + var specular_transmission: f32 = pbr_bindings::material.specular_transmission; +#ifdef VERTEX_UVS #ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_SPECULAR_TRANSMISSION_TEXTURE_BIT) != 0u) { -#ifdef MESHLET_MESH_MATERIAL_PASS - specular_transmission *= textureSampleGrad(pbr_bindings::specular_transmission_texture, pbr_bindings::specular_transmission_sampler, uv, in.ddx_uv, in.ddy_uv).r; + specular_transmission *= pbr_functions::sample_texture( + pbr_bindings::specular_transmission_texture, + pbr_bindings::specular_transmission_sampler, +#ifdef STANDARD_MATERIAL_SPECULAR_TRANSMISSION_UV_B + uv_b, #else - specular_transmission *= textureSampleBias(pbr_bindings::specular_transmission_texture, pbr_bindings::specular_transmission_sampler, uv, view.mip_bias).r; + uv, #endif + bias, + ).r; } +#endif #endif pbr_input.material.specular_transmission = specular_transmission; var thickness: f32 = pbr_bindings::material.thickness; +#ifdef VERTEX_UVS #ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_THICKNESS_TEXTURE_BIT) != 0u) { -#ifdef MESHLET_MESH_MATERIAL_PASS - thickness *= textureSampleGrad(pbr_bindings::thickness_texture, pbr_bindings::thickness_sampler, uv, in.ddx_uv, in.ddy_uv).g; + thickness *= pbr_functions::sample_texture( + pbr_bindings::thickness_texture, + pbr_bindings::thickness_sampler, +#ifdef STANDARD_MATERIAL_THICKNESS_UV_B + uv_b, #else - thickness *= textureSampleBias(pbr_bindings::thickness_texture, pbr_bindings::thickness_sampler, uv, view.mip_bias).g; + uv, #endif + bias, + ).g; } +#endif #endif // scale thickness, accounting for non-uniform scaling (e.g. a “squished” mesh) // TODO: Meshlet support @@ -202,14 +304,21 @@ fn pbr_input_from_standard_material( pbr_input.material.thickness = thickness; var diffuse_transmission = pbr_bindings::material.diffuse_transmission; +#ifdef VERTEX_UVS #ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DIFFUSE_TRANSMISSION_TEXTURE_BIT) != 0u) { -#ifdef MESHLET_MESH_MATERIAL_PASS - diffuse_transmission *= textureSampleGrad(pbr_bindings::diffuse_transmission_texture, pbr_bindings::diffuse_transmission_sampler, uv, in.ddx_uv, in.ddy_uv).a; + diffuse_transmission *= pbr_functions::sample_texture( + pbr_bindings::diffuse_transmission_texture, + pbr_bindings::diffuse_transmission_sampler, +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION_UV_B + uv_b, #else - diffuse_transmission *= textureSampleBias(pbr_bindings::diffuse_transmission_texture, pbr_bindings::diffuse_transmission_sampler, uv, view.mip_bias).a; + uv, #endif + bias, + ).a; } +#endif #endif pbr_input.material.diffuse_transmission = diffuse_transmission; @@ -217,11 +326,16 @@ fn pbr_input_from_standard_material( var specular_occlusion: f32 = 1.0; #ifdef VERTEX_UVS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) { -#ifdef MESHLET_MESH_MATERIAL_PASS - diffuse_occlusion = vec3(textureSampleGrad(pbr_bindings::occlusion_texture, pbr_bindings::occlusion_sampler, uv, in.ddx_uv, in.ddy_uv).r); + diffuse_occlusion *= pbr_functions::sample_texture( + pbr_bindings::occlusion_texture, + pbr_bindings::occlusion_sampler, +#ifdef STANDARD_MATERIAL_OCCLUSION_UV_B + uv_b, #else - diffuse_occlusion = vec3(textureSampleBias(pbr_bindings::occlusion_texture, pbr_bindings::occlusion_sampler, uv, view.mip_bias).r); + uv, #endif + bias, + ).r; } #endif #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION @@ -237,26 +351,75 @@ fn pbr_input_from_standard_material( // N (normal vector) #ifndef LOAD_PREPASS_NORMALS + + pbr_input.N = normalize(pbr_input.world_normal); + pbr_input.clearcoat_N = pbr_input.N; + +#ifdef VERTEX_UVS +#ifdef VERTEX_TANGENTS + +#ifdef STANDARD_MATERIAL_NORMAL_MAP + + let Nt = pbr_functions::sample_texture( + pbr_bindings::normal_map_texture, + pbr_bindings::normal_map_sampler, +#ifdef STANDARD_MATERIAL_NORMAL_MAP_UV_B + uv_b, +#else + uv, +#endif + bias, + ).rgb; + pbr_input.N = pbr_functions::apply_normal_mapping( pbr_bindings::material.flags, pbr_input.world_normal, double_sided, is_front, -#ifdef VERTEX_TANGENTS -#ifdef STANDARD_MATERIAL_NORMAL_MAP in.world_tangent, -#endif -#endif -#ifdef VERTEX_UVS - uv, -#endif + Nt, view.mip_bias, -#ifdef MESHLET_MESH_MATERIAL_PASS - in.ddx_uv, - in.ddy_uv, -#endif ); + +#endif // STANDARD_MATERIAL_NORMAL_MAP + +#ifdef STANDARD_MATERIAL_CLEARCOAT + + // Note: `KHR_materials_clearcoat` specifies that, if there's no + // clearcoat normal map, we must set the normal to the mesh's normal, + // and not to the main layer's bumped normal. + +#ifdef STANDARD_MATERIAL_CLEARCOAT_NORMAL_MAP + + let clearcoat_Nt = pbr_functions::sample_texture( + pbr_bindings::clearcoat_normal_texture, + pbr_bindings::clearcoat_normal_sampler, +#ifdef STANDARD_MATERIAL_CLEARCOAT_NORMAL_UV_B + uv_b, +#else + uv, #endif + bias, + ).rgb; + + pbr_input.clearcoat_N = pbr_functions::apply_normal_mapping( + pbr_bindings::material.flags, + pbr_input.world_normal, + double_sided, + is_front, + in.world_tangent, + clearcoat_Nt, + view.mip_bias, + ); + +#endif // STANDARD_MATERIAL_CLEARCOAT_NORMAL_MAP + +#endif // STANDARD_MATERIAL_CLEARCOAT + +#endif // VERTEX_TANGENTS +#endif // VERTEX_UVS + +#endif // LOAD_PREPASS_NORMALS // TODO: Meshlet support #ifdef LIGHTMAP diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 9f6b5fb240a2a..f816c604360e4 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -6,14 +6,23 @@ mesh_view_bindings as view_bindings, mesh_view_types, lighting, + lighting::{LAYER_BASE, LAYER_CLEARCOAT}, transmission, clustered_forward as clustering, shadows, ambient, irradiance_volume, mesh_types::{MESH_FLAGS_SHADOW_RECEIVER_BIT, MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT}, - utils::E, } +#import bevy_render::maths::E + +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::VertexOutput +#else ifdef PREPASS_PIPELINE +#import bevy_pbr::prepass_io::VertexOutput +#else // PREPASS_PIPELINE +#import bevy_pbr::forward_io::VertexOutput +#endif // PREPASS_PIPELINE #ifdef ENVIRONMENT_MAP #import bevy_pbr::environment_map @@ -21,6 +30,18 @@ #import bevy_core_pipeline::tonemapping::{screen_space_dither, powsafe, tone_mapping} +// Biasing info needed to sample from a texture when calling `sample_texture`. +// How this is done depends on whether we're rendering meshlets or regular +// meshes. +struct SampleBias { +#ifdef MESHLET_MESH_MATERIAL_PASS + ddx_uv: vec2, + ddy_uv: vec2, +#else // MESHLET_MESH_MATERIAL_PASS + mip_bias: f32, +#endif // MESHLET_MESH_MATERIAL_PASS +} + // This is the standard 4x4 ordered dithering pattern from [1]. // // We can't use `array, 4>` because they can't be indexed dynamically @@ -97,6 +118,21 @@ fn alpha_discard(material: pbr_types::StandardMaterial, output_color: vec4) return color; } +// Samples a texture using the appropriate biasing metric for the type of mesh +// in use (mesh vs. meshlet). +fn sample_texture( + texture: texture_2d, + samp: sampler, + uv: vec2, + bias: SampleBias, +) -> vec4 { +#ifdef MESHLET_MESH_MATERIAL_PASS + return textureSampleGrad(texture, samp, uv, bias.ddx_uv, bias.ddy_uv); +#else + return textureSampleBias(texture, samp, uv, bias.mip_bias); +#endif +} + fn prepare_world_normal( world_normal: vec3, double_sided: bool, @@ -118,19 +154,9 @@ fn apply_normal_mapping( world_normal: vec3, double_sided: bool, is_front: bool, -#ifdef VERTEX_TANGENTS -#ifdef STANDARD_MATERIAL_NORMAL_MAP world_tangent: vec4, -#endif -#endif -#ifdef VERTEX_UVS - uv: vec2, -#endif + in_Nt: vec3, mip_bias: f32, -#ifdef MESHLET_MESH_MATERIAL_PASS - ddx_uv: vec2, - ddy_uv: vec2, -#endif ) -> vec3 { // NOTE: The mikktspace method of normal mapping explicitly requires that the world normal NOT // be re-normalized in the fragment shader. This is primarily to match the way mikktspace @@ -140,26 +166,15 @@ fn apply_normal_mapping( // http://www.mikktspace.com/ var N: vec3 = world_normal; -#ifdef VERTEX_TANGENTS -#ifdef STANDARD_MATERIAL_NORMAL_MAP // NOTE: The mikktspace method of normal mapping explicitly requires that these NOT be // normalized nor any Gram-Schmidt applied to ensure the vertex normal is orthogonal to the // vertex tangent! Do not change this code unless you really know what you are doing. // http://www.mikktspace.com/ var T: vec3 = world_tangent.xyz; var B: vec3 = world_tangent.w * cross(N, T); -#endif -#endif -#ifdef VERTEX_TANGENTS -#ifdef VERTEX_UVS -#ifdef STANDARD_MATERIAL_NORMAL_MAP // Nt is the tangent-space normal. -#ifdef MESHLET_MESH_MATERIAL_PASS - var Nt = textureSampleGrad(pbr_bindings::normal_map_texture, pbr_bindings::normal_map_sampler, uv, ddx_uv, ddy_uv).rgb; -#else - var Nt = textureSampleBias(pbr_bindings::normal_map_texture, pbr_bindings::normal_map_sampler, uv, mip_bias).rgb; -#endif + var Nt = in_Nt; if (standard_material_flags & pbr_types::STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP) != 0u { // Only use the xy components and derive z for 2-component normal maps. Nt = vec3(Nt.rg * 2.0 - 1.0, 0.0); @@ -182,9 +197,6 @@ fn apply_normal_mapping( // unless you really know what you are doing. // http://www.mikktspace.com/ N = Nt.x * T + Nt.y * B + Nt.z * N; -#endif -#endif -#endif return normalize(N); } @@ -231,11 +243,18 @@ fn apply_pbr_lighting( // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" let NdotV = max(dot(in.N, in.V), 0.0001); + let R = reflect(-in.V, in.N); - // Remapping [0,1] reflectance to F0 - // See https://google.github.io/filament/Filament.html#materialsystem/parameterization/remapping - let reflectance = in.material.reflectance; - let F0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + output_color.rgb * metallic; +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Do the above calculations again for the clearcoat layer. Remember that + // the clearcoat can have its own roughness and its own normal. + let clearcoat = in.material.clearcoat; + let clearcoat_perceptual_roughness = in.material.clearcoat_perceptual_roughness; + let clearcoat_roughness = lighting::perceptualRoughnessToRoughness(clearcoat_perceptual_roughness); + let clearcoat_N = in.clearcoat_N; + let clearcoat_NdotV = max(dot(clearcoat_N, in.V), 0.0001); + let clearcoat_R = reflect(-in.V, clearcoat_N); +#endif // STANDARD_MATERIAL_CLEARCOAT // Diffuse strength is inversely related to metallicity, specular and diffuse transmission let diffuse_color = output_color.rgb * (1.0 - metallic) * (1.0 - specular_transmission) * (1.0 - diffuse_transmission); @@ -246,15 +265,58 @@ fn apply_pbr_lighting( // Calculate the world position of the second Lambertian lobe used for diffuse transmission, by subtracting material thickness let diffuse_transmissive_lobe_world_position = in.world_position - vec4(in.world_normal, 0.0) * thickness; - let R = reflect(-in.V, in.N); - - let f_ab = lighting::F_AB(perceptual_roughness, NdotV); + let F0 = lighting::F0(in.material.reflectance, metallic, output_color.rgb); + let F_ab = lighting::F_AB(perceptual_roughness, NdotV); var direct_light: vec3 = vec3(0.0); // Transmitted Light (Specular and Diffuse) var transmitted_light: vec3 = vec3(0.0); + // Pack all the values into a structure. + var lighting_input: lighting::LightingInput; + lighting_input.layers[LAYER_BASE].NdotV = NdotV; + lighting_input.layers[LAYER_BASE].N = in.N; + lighting_input.layers[LAYER_BASE].R = R; + lighting_input.layers[LAYER_BASE].perceptual_roughness = perceptual_roughness; + lighting_input.layers[LAYER_BASE].roughness = roughness; + lighting_input.P = in.world_position.xyz; + lighting_input.V = in.V; + lighting_input.diffuse_color = diffuse_color; + lighting_input.F0_ = F0; + lighting_input.F_ab = F_ab; +#ifdef STANDARD_MATERIAL_CLEARCOAT + lighting_input.layers[LAYER_CLEARCOAT].NdotV = clearcoat_NdotV; + lighting_input.layers[LAYER_CLEARCOAT].N = clearcoat_N; + lighting_input.layers[LAYER_CLEARCOAT].R = clearcoat_R; + lighting_input.layers[LAYER_CLEARCOAT].perceptual_roughness = clearcoat_perceptual_roughness; + lighting_input.layers[LAYER_CLEARCOAT].roughness = clearcoat_roughness; + lighting_input.clearcoat_strength = clearcoat; +#endif // STANDARD_MATERIAL_CLEARCOAT + + // And do the same for transmissive if we need to. +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + var transmissive_lighting_input: lighting::LightingInput; + transmissive_lighting_input.layers[LAYER_BASE].NdotV = 1.0; + transmissive_lighting_input.layers[LAYER_BASE].N = -in.N; + transmissive_lighting_input.layers[LAYER_BASE].R = vec3(0.0); + transmissive_lighting_input.layers[LAYER_BASE].perceptual_roughness = 1.0; + transmissive_lighting_input.layers[LAYER_BASE].roughness = 1.0; + transmissive_lighting_input.P = diffuse_transmissive_lobe_world_position.xyz; + transmissive_lighting_input.V = -in.V; + transmissive_lighting_input.diffuse_color = diffuse_transmissive_color; + transmissive_lighting_input.F0_ = vec3(0.0); + transmissive_lighting_input.F_ab = vec2(0.1); +#ifdef STANDARD_MATERIAL_CLEARCOAT + transmissive_lighting_input.layers[LAYER_CLEARCOAT].NdotV = 0.0; + transmissive_lighting_input.layers[LAYER_CLEARCOAT].N = vec3(0.0); + transmissive_lighting_input.layers[LAYER_CLEARCOAT].R = vec3(0.0); + transmissive_lighting_input.layers[LAYER_CLEARCOAT].perceptual_roughness = 0.0; + transmissive_lighting_input.layers[LAYER_CLEARCOAT].roughness = 0.0; + transmissive_lighting_input.clearcoat_strength = 0.0; +#endif // STANDARD_MATERIAL_CLEARCOAT +#endif // STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + let view_z = dot(vec4( view_bindings::view.inverse_view[0].z, view_bindings::view.inverse_view[1].z, @@ -272,7 +334,8 @@ fn apply_pbr_lighting( && (view_bindings::point_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { shadow = shadows::fetch_point_shadow(light_id, in.world_position, in.world_normal); } - let light_contrib = lighting::point_light(in.world_position.xyz, light_id, roughness, NdotV, in.N, in.V, R, F0, f_ab, diffuse_color); + + let light_contrib = lighting::point_light(light_id, &lighting_input); direct_light += light_contrib * shadow; #ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION @@ -283,14 +346,16 @@ fn apply_pbr_lighting( // roughness = 1.0; // NdotV = 1.0; // R = vec3(0.0) // doesn't really matter - // f_ab = vec2(0.1) + // F_ab = vec2(0.1) // F0 = vec3(0.0) var transmitted_shadow: f32 = 1.0; if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) && (view_bindings::point_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { transmitted_shadow = shadows::fetch_point_shadow(light_id, diffuse_transmissive_lobe_world_position, -in.world_normal); } - let transmitted_light_contrib = lighting::point_light(diffuse_transmissive_lobe_world_position.xyz, light_id, 1.0, 1.0, -in.N, -in.V, vec3(0.0), vec3(0.0), vec2(0.1), diffuse_transmissive_color); + + let transmitted_light_contrib = + lighting::point_light(light_id, &transmissive_lighting_input); transmitted_light += transmitted_light_contrib * transmitted_shadow; #endif } @@ -304,7 +369,8 @@ fn apply_pbr_lighting( && (view_bindings::point_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { shadow = shadows::fetch_spot_shadow(light_id, in.world_position, in.world_normal); } - let light_contrib = lighting::spot_light(in.world_position.xyz, light_id, roughness, NdotV, in.N, in.V, R, F0, f_ab, diffuse_color); + + let light_contrib = lighting::spot_light(light_id, &lighting_input); direct_light += light_contrib * shadow; #ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION @@ -315,14 +381,16 @@ fn apply_pbr_lighting( // roughness = 1.0; // NdotV = 1.0; // R = vec3(0.0) // doesn't really matter - // f_ab = vec2(0.1) + // F_ab = vec2(0.1) // F0 = vec3(0.0) var transmitted_shadow: f32 = 1.0; if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) && (view_bindings::point_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { transmitted_shadow = shadows::fetch_spot_shadow(light_id, diffuse_transmissive_lobe_world_position, -in.world_normal); } - let transmitted_light_contrib = lighting::spot_light(diffuse_transmissive_lobe_world_position.xyz, light_id, 1.0, 1.0, -in.N, -in.V, vec3(0.0), vec3(0.0), vec2(0.1), diffuse_transmissive_color); + + let transmitted_light_contrib = + lighting::spot_light(light_id, &transmissive_lighting_input); transmitted_light += transmitted_light_contrib * transmitted_shadow; #endif } @@ -330,10 +398,10 @@ 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` + // check if this light should be skipped, which occurs if this light does not intersect with the view + // note point and spot lights aren't skippable, 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 { + if (*light).skip != 0u { continue; } @@ -342,7 +410,9 @@ fn apply_pbr_lighting( && (view_bindings::lights.directional_lights[i].flags & mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { shadow = shadows::fetch_directional_shadow(i, in.world_position, in.world_normal, view_z); } - var light_contrib = lighting::directional_light(i, roughness, NdotV, in.N, in.V, R, F0, f_ab, diffuse_color); + + var light_contrib = lighting::directional_light(i, &lighting_input); + #ifdef DIRECTIONAL_LIGHT_SHADOW_MAP_DEBUG_CASCADES light_contrib = shadows::cascade_debug_visualization(light_contrib, i, view_z); #endif @@ -356,14 +426,16 @@ fn apply_pbr_lighting( // roughness = 1.0; // NdotV = 1.0; // R = vec3(0.0) // doesn't really matter - // f_ab = vec2(0.1) + // F_ab = vec2(0.1) // F0 = vec3(0.0) var transmitted_shadow: f32 = 1.0; if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) && (view_bindings::lights.directional_lights[i].flags & mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { transmitted_shadow = shadows::fetch_directional_shadow(i, diffuse_transmissive_lobe_world_position, -in.world_normal, view_z); } - let transmitted_light_contrib = lighting::directional_light(i, 1.0, 1.0, -in.N, -in.V, vec3(0.0), vec3(0.0), vec2(0.1), diffuse_transmissive_color); + + let transmitted_light_contrib = + lighting::directional_light(i, &transmissive_lighting_input); transmitted_light += transmitted_light_contrib * transmitted_shadow; #endif } @@ -413,17 +485,8 @@ fn apply_pbr_lighting( // Note that up until this point, we have only accumulated diffuse light. // This call is the first call that can accumulate specular light. #ifdef ENVIRONMENT_MAP - let environment_light = environment_map::environment_map_light( - perceptual_roughness, - roughness, - diffuse_color, - NdotV, - f_ab, - in.N, - R, - F0, - in.world_position.xyz, - any(indirect_light != vec3(0.0f))); + let environment_light = + environment_map::environment_map_light(&lighting_input, any(indirect_light != vec3(0.0f))); indirect_light += environment_light.diffuse * diffuse_occlusion + environment_light.specular * specular_occlusion; @@ -432,7 +495,7 @@ fn apply_pbr_lighting( // light in the call to `specular_transmissive_light()` below var specular_transmitted_environment_light = vec3(0.0); -#ifdef STANDARD_MATERIAL_SPECULAR_OR_DIFFUSE_TRANSMISSION +#ifdef STANDARD_MATERIAL_DIFFUSE_OR_SPECULAR_TRANSMISSION // NOTE: We use the diffuse transmissive color, inverted normal and view vectors, // and the following simplified values for the transmitted environment light contribution // approximation: @@ -451,24 +514,37 @@ fn apply_pbr_lighting( refract(in.V, -in.N, 1.0 / ior) * thickness // add refracted vector scaled by thickness, towards exit point ); // normalize to find exit point view vector - let transmitted_environment_light = bevy_pbr::environment_map::environment_map_light( - perceptual_roughness, - roughness, - vec3(1.0), - 1.0, - f_ab, - -in.N, - T, - vec3(1.0), - in.world_position.xyz, - false); + var transmissive_environment_light_input: lighting::LightingInput; + transmissive_environment_light_input.diffuse_color = vec3(1.0); + transmissive_environment_light_input.layers[LAYER_BASE].NdotV = 1.0; + transmissive_environment_light_input.P = in.world_position.xyz; + transmissive_environment_light_input.layers[LAYER_BASE].N = -in.N; + transmissive_environment_light_input.V = in.V; + transmissive_environment_light_input.layers[LAYER_BASE].R = T; + transmissive_environment_light_input.layers[LAYER_BASE].perceptual_roughness = perceptual_roughness; + transmissive_environment_light_input.layers[LAYER_BASE].roughness = roughness; + transmissive_environment_light_input.F0_ = vec3(1.0); + transmissive_environment_light_input.F_ab = vec2(0.1); +#ifdef STANDARD_MATERIAL_CLEARCOAT + // No clearcoat. + transmissive_environment_light_input.clearcoat_strength = 0.0; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].NdotV = 0.0; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].N = in.N; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].R = vec3(0.0); + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].perceptual_roughness = 0.0; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].roughness = 0.0; +#endif // STANDARD_MATERIAL_CLEARCOAT + + let transmitted_environment_light = + environment_map::environment_map_light(&transmissive_environment_light_input, false); + #ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION transmitted_light += transmitted_environment_light.diffuse * diffuse_transmissive_color; #endif #ifdef STANDARD_MATERIAL_SPECULAR_TRANSMISSION specular_transmitted_environment_light = transmitted_environment_light.specular * specular_transmissive_color; #endif -#endif // STANDARD_MATERIAL_SPECULAR_OR_DIFFUSE_TRANSMISSION +#endif // STANDARD_MATERIAL_DIFFUSE_OR_SPECULAR_TRANSMISSION #else // If there's no environment map light, there's no transmitted environment // light specular component, so we can just hardcode it to zero. @@ -478,7 +554,15 @@ fn apply_pbr_lighting( // Ambient light (indirect) indirect_light += ambient::ambient_light(in.world_position, in.N, in.V, NdotV, diffuse_color, F0, perceptual_roughness, diffuse_occlusion); - let emissive_light = emissive.rgb * output_color.a; + var emissive_light = emissive.rgb * output_color.a; + + // "The clearcoat layer is on top of emission in the layering stack. + // Consequently, the emission is darkened by the Fresnel term." + // + // +#ifdef STANDARD_MATERIAL_CLEARCOAT + emissive_light = emissive_light * (0.04 + (1.0 - 0.04) * pow(1.0 - clearcoat_NdotV, 5.0)); +#endif #ifdef STANDARD_MATERIAL_SPECULAR_TRANSMISSION transmitted_light += transmission::specular_transmissive_light(in.world_position, in.frag_coord.xyz, view_z, in.N, in.V, F0, ior, thickness, perceptual_roughness, specular_transmissive_color, specular_transmitted_environment_light).rgb; diff --git a/crates/bevy_pbr/src/render/pbr_lighting.wgsl b/crates/bevy_pbr/src/render/pbr_lighting.wgsl index bc279ca594ad3..ae629f6699f9d 100644 --- a/crates/bevy_pbr/src/render/pbr_lighting.wgsl +++ b/crates/bevy_pbr/src/render/pbr_lighting.wgsl @@ -1,10 +1,13 @@ #define_import_path bevy_pbr::lighting #import bevy_pbr::{ - utils::PI, mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE, mesh_view_bindings as view_bindings, } +#import bevy_render::maths::PI + +const LAYER_BASE: u32 = 0; +const LAYER_CLEARCOAT: u32 = 1; // From the Filament design doc // https://google.github.io/filament/Filament.html#table_symbols @@ -40,6 +43,69 @@ // // The above integration needs to be approximated. +// Input to a lighting function for a single layer (either the base layer or the +// clearcoat layer). +struct LayerLightingInput { + // The normal vector. + N: vec3, + // The reflected vector. + R: vec3, + // The normal vector ⋅ the view vector. + NdotV: f32, + + // The perceptual roughness of the layer. + perceptual_roughness: f32, + // The roughness of the layer. + roughness: f32, +} + +// Input to a lighting function (`point_light`, `spot_light`, +// `directional_light`). +struct LightingInput { +#ifdef STANDARD_MATERIAL_CLEARCOAT + layers: array, +#else // STANDARD_MATERIAL_CLEARCOAT + layers: array, +#endif // STANDARD_MATERIAL_CLEARCOAT + + // The world-space position. + P: vec3, + // The vector to the light. + V: vec3, + + // The diffuse color of the material. + diffuse_color: vec3, + + // Specular reflectance at the normal incidence angle. + // + // This should be read F₀, but due to Naga limitations we can't name it that. + F0_: vec3, + // Constants for the BRDF approximation. + // + // See `EnvBRDFApprox` in + // . + // What we call `F_ab` they call `AB`. + F_ab: vec2, + +#ifdef STANDARD_MATERIAL_CLEARCOAT + // The strength of the clearcoat layer. + clearcoat_strength: f32, +#endif // STANDARD_MATERIAL_CLEARCOAT +} + +// Values derived from the `LightingInput` for both diffuse and specular lights. +struct DerivedLightingInput { + // The half-vector between L, the incident light vector, and V, the view + // vector. + H: vec3, + // The normal vector ⋅ the incident light vector. + NdotL: f32, + // The normal vector ⋅ the half-vector. + NdotH: f32, + // The incident light vector ⋅ the half-vector. + LdotH: f32, +} + // distanceAttenuation is simply the square falloff of light intensity // combined with a smooth attenuation at the edge of the light radius // @@ -59,10 +125,10 @@ fn getDistanceAttenuation(distanceSquare: f32, inverseRangeSquared: f32) -> f32 // Simple implementation, has precision problems when using fp16 instead of fp32 // see https://google.github.io/filament/Filament.html#listing_speculardfp16 -fn D_GGX(roughness: f32, NoH: f32, h: vec3) -> f32 { - let oneMinusNoHSquared = 1.0 - NoH * NoH; - let a = NoH * roughness; - let k = roughness / (oneMinusNoHSquared + a * a); +fn D_GGX(roughness: f32, NdotH: f32, h: vec3) -> f32 { + let oneMinusNdotHSquared = 1.0 - NdotH * NdotH; + let a = NdotH * roughness; + let k = roughness / (oneMinusNdotHSquared + a * a); let d = k * k * (1.0 / PI); return d; } @@ -74,62 +140,141 @@ fn D_GGX(roughness: f32, NoH: f32, h: vec3) -> f32 { // where // V(v,l,α) = 0.5 / { n⋅l sqrt((n⋅v)^2 (1−α2) + α2) + n⋅v sqrt((n⋅l)^2 (1−α2) + α2) } // Note the two sqrt's, that may be slow on mobile, see https://google.github.io/filament/Filament.html#listing_approximatedspecularv -fn V_SmithGGXCorrelated(roughness: f32, NoV: f32, NoL: f32) -> f32 { +fn V_SmithGGXCorrelated(roughness: f32, NdotV: f32, NdotL: f32) -> f32 { let a2 = roughness * roughness; - let lambdaV = NoL * sqrt((NoV - a2 * NoV) * NoV + a2); - let lambdaL = NoV * sqrt((NoL - a2 * NoL) * NoL + a2); + let lambdaV = NdotL * sqrt((NdotV - a2 * NdotV) * NdotV + a2); + let lambdaL = NdotV * sqrt((NdotL - a2 * NdotL) * NdotL + a2); let v = 0.5 / (lambdaV + lambdaL); return v; } +// A simpler, but nonphysical, alternative to Smith-GGX. We use this for +// clearcoat, per the Filament spec. +// +// https://google.github.io/filament/Filament.html#materialsystem/clearcoatmodel#toc4.9.1 +fn V_Kelemen(LdotH: f32) -> f32 { + return 0.25 / (LdotH * LdotH); +} + // Fresnel function // see https://google.github.io/filament/Filament.html#citation-schlick94 // F_Schlick(v,h,f_0,f_90) = f_0 + (f_90 − f_0) (1 − v⋅h)^5 -fn F_Schlick_vec(f0: vec3, f90: f32, VoH: f32) -> vec3 { +fn F_Schlick_vec(f0: vec3, f90: f32, VdotH: f32) -> vec3 { // not using mix to keep the vec3 and float versions identical - return f0 + (f90 - f0) * pow(1.0 - VoH, 5.0); + return f0 + (f90 - f0) * pow(1.0 - VdotH, 5.0); } -fn F_Schlick(f0: f32, f90: f32, VoH: f32) -> f32 { +fn F_Schlick(f0: f32, f90: f32, VdotH: f32) -> f32 { // not using mix to keep the vec3 and float versions identical - return f0 + (f90 - f0) * pow(1.0 - VoH, 5.0); + return f0 + (f90 - f0) * pow(1.0 - VdotH, 5.0); } -fn fresnel(f0: vec3, LoH: f32) -> vec3 { +fn fresnel(f0: vec3, LdotH: f32) -> vec3 { // f_90 suitable for ambient occlusion // see https://google.github.io/filament/Filament.html#lighting/occlusion let f90 = saturate(dot(f0, vec3(50.0 * 0.33))); - return F_Schlick_vec(f0, f90, LoH); + return F_Schlick_vec(f0, f90, LdotH); } // Specular BRDF // https://google.github.io/filament/Filament.html#materialsystem/specularbrdf +// N, V, and L must all be normalized. +fn derive_lighting_input(N: vec3, V: vec3, L: vec3) -> DerivedLightingInput { + var input: DerivedLightingInput; + var H: vec3 = normalize(L + V); + input.H = H; + input.NdotL = saturate(dot(N, L)); + input.NdotH = saturate(dot(N, H)); + input.LdotH = saturate(dot(L, H)); + return input; +} + +// Returns L in the `xyz` components and the specular intensity in the `w` component. +fn compute_specular_layer_values_for_point_light( + input: ptr, + layer: u32, + V: vec3, + light_to_frag: vec3, + light_position_radius: f32, +) -> vec4 { + // Unpack. + let R = (*input).layers[layer].R; + let a = (*input).layers[layer].roughness; + + // Representative Point Area Lights. + // see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16 + let centerToRay = dot(light_to_frag, R) * R - light_to_frag; + let closestPoint = light_to_frag + centerToRay * saturate( + light_position_radius * inverseSqrt(dot(centerToRay, centerToRay))); + let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint)); + let normalizationFactor = a / saturate(a + (light_position_radius * 0.5 * LspecLengthInverse)); + let intensity = normalizationFactor * normalizationFactor; + + let L: vec3 = closestPoint * LspecLengthInverse; // normalize() equivalent? + return vec4(L, intensity); +} + // Cook-Torrance approximation of the microfacet model integration using Fresnel law F to model f_m // f_r(v,l) = { D(h,α) G(v,l,α) F(v,h,f0) } / { 4 (n⋅v) (n⋅l) } fn specular( - f0: vec3, - roughness: f32, - h: vec3, - NoV: f32, - NoL: f32, - NoH: f32, - LoH: f32, - specularIntensity: f32, - f_ab: vec2 + input: ptr, + derived_input: ptr, + specular_intensity: f32, ) -> vec3 { - let D = D_GGX(roughness, NoH, h); - let V = V_SmithGGXCorrelated(roughness, NoV, NoL); - let F = fresnel(f0, LoH); - - var Fr = (specularIntensity * D * V) * F; - - // Multiscattering approximation: https://google.github.io/filament/Filament.html#listing_energycompensationimpl - Fr *= 1.0 + f0 * (1.0 / f_ab.x - 1.0); - + // Unpack. + let roughness = (*input).layers[LAYER_BASE].roughness; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let F0 = (*input).F0_; + let F_ab = (*input).F_ab; + let H = (*derived_input).H; + let NdotL = (*derived_input).NdotL; + let NdotH = (*derived_input).NdotH; + let LdotH = (*derived_input).LdotH; + + // Calculate distribution. + let D = D_GGX(roughness, NdotH, H); + // Calculate visibility. + let V = V_SmithGGXCorrelated(roughness, NdotV, NdotL); + // Calculate the Fresnel term. + let F = fresnel(F0, LdotH); + + // Calculate the specular light. + // Multiscattering approximation: + // + var Fr = (specular_intensity * D * V) * F; + Fr *= 1.0 + F0 * (1.0 / F_ab.x - 1.0); return Fr; } +// Calculates the specular light for the clearcoat layer. Returns Fc, the +// Fresnel term, in the first channel, and Frc, the specular clearcoat light, in +// the second channel. +// +// +fn specular_clearcoat( + input: ptr, + derived_input: ptr, + clearcoat_strength: f32, + specular_intensity: f32, +) -> vec2 { + // Unpack. + let roughness = (*input).layers[LAYER_CLEARCOAT].roughness; + let H = (*derived_input).H; + let NdotH = (*derived_input).NdotH; + let LdotH = (*derived_input).LdotH; + + // Calculate distribution. + let Dc = D_GGX(roughness, NdotH, H); + // Calculate visibility. + let Vc = V_Kelemen(LdotH); + // Calculate the Fresnel term. + let Fc = F_Schlick(0.04, 1.0, LdotH) * clearcoat_strength; + // Calculate the specular light. + let Frc = (specular_intensity * Dc * Vc) * Fc; + return vec2(Fc, Frc); +} + // Diffuse BRDF // https://google.github.io/filament/Filament.html#materialsystem/diffusebrdf // fd(v,l) = σ/π * 1 / { |n⋅v||n⋅l| } ∫Ω D(m,α) G(v,l,m) (v⋅m) (l⋅m) dm @@ -144,26 +289,41 @@ fn specular( // Disney approximation // See https://google.github.io/filament/Filament.html#citation-burley12 // minimal quality difference -fn Fd_Burley(roughness: f32, NoV: f32, NoL: f32, LoH: f32) -> f32 { - let f90 = 0.5 + 2.0 * roughness * LoH * LoH; - let lightScatter = F_Schlick(1.0, f90, NoL); - let viewScatter = F_Schlick(1.0, f90, NoV); +fn Fd_Burley( + input: ptr, + derived_input: ptr, +) -> f32 { + // Unpack. + let roughness = (*input).layers[LAYER_BASE].roughness; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let NdotL = (*derived_input).NdotL; + let LdotH = (*derived_input).LdotH; + + let f90 = 0.5 + 2.0 * roughness * LdotH * LdotH; + let lightScatter = F_Schlick(1.0, f90, NdotL); + let viewScatter = F_Schlick(1.0, f90, NdotV); return lightScatter * viewScatter * (1.0 / PI); } +// Remapping [0,1] reflectance to F0 +// See https://google.github.io/filament/Filament.html#materialsystem/parameterization/remapping +fn F0(reflectance: f32, metallic: f32, color: vec3) -> vec3 { + return 0.16 * reflectance * reflectance * (1.0 - metallic) + color * metallic; +} + // Scale/bias approximation // https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile // TODO: Use a LUT (more accurate) -fn F_AB(perceptual_roughness: f32, NoV: f32) -> vec2 { +fn F_AB(perceptual_roughness: f32, NdotV: f32) -> vec2 { let c0 = vec4(-1.0, -0.0275, -0.572, 0.022); let c1 = vec4(1.0, 0.0425, 1.04, -0.04); let r = perceptual_roughness * c0 + c1; - let a004 = min(r.x * r.x, exp2(-9.28 * NoV)) * r.x + r.y; + let a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y; return vec2(-1.04, 1.04) * a004 + r.zw; } -fn EnvBRDFApprox(f0: vec3, f_ab: vec2) -> vec3 { - return f0 * f_ab.x + f_ab.y; +fn EnvBRDFApprox(F0: vec3, F_ab: vec2) -> vec3 { + return F0 * F_ab.x + F_ab.y; } fn perceptualRoughnessToRoughness(perceptualRoughness: f32) -> f32 { @@ -174,50 +334,69 @@ fn perceptualRoughnessToRoughness(perceptualRoughness: f32) -> f32 { return clampedPerceptualRoughness * clampedPerceptualRoughness; } -fn point_light( - world_position: vec3, - light_id: u32, - roughness: f32, - NdotV: f32, - N: vec3, - V: vec3, - R: vec3, - F0: vec3, - f_ab: vec2, - diffuseColor: vec3 -) -> vec3 { +fn point_light(light_id: u32, input: ptr) -> vec3 { + // Unpack. + let diffuse_color = (*input).diffuse_color; + let P = (*input).P; + let N = (*input).layers[LAYER_BASE].N; + let V = (*input).V; + let light = &view_bindings::point_lights.data[light_id]; - let light_to_frag = (*light).position_radius.xyz - world_position.xyz; + let light_to_frag = (*light).position_radius.xyz - P; let distance_square = dot(light_to_frag, light_to_frag); let rangeAttenuation = getDistanceAttenuation(distance_square, (*light).color_inverse_square_range.w); - // Specular. - // Representative Point Area Lights. - // see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16 - let a = roughness; - let centerToRay = dot(light_to_frag, R) * R - light_to_frag; - let closestPoint = light_to_frag + centerToRay * saturate((*light).position_radius.w * inverseSqrt(dot(centerToRay, centerToRay))); - let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint)); - let normalizationFactor = a / saturate(a + ((*light).position_radius.w * 0.5 * LspecLengthInverse)); - let specularIntensity = normalizationFactor * normalizationFactor; - - var L: vec3 = closestPoint * LspecLengthInverse; // normalize() equivalent? - var H: vec3 = normalize(L + V); - var NoL: f32 = saturate(dot(N, L)); - var NoH: f32 = saturate(dot(N, H)); - var LoH: f32 = saturate(dot(L, H)); - - let specular_light = specular(F0, roughness, H, NdotV, NoL, NoH, LoH, specularIntensity, f_ab); + // Base layer + + let specular_L_intensity = compute_specular_layer_values_for_point_light( + input, + LAYER_BASE, + V, + light_to_frag, + (*light).position_radius.w, + ); + var specular_derived_input = derive_lighting_input(N, V, specular_L_intensity.xyz); + + let specular_intensity = specular_L_intensity.w; + let specular_light = specular(input, &specular_derived_input, specular_intensity); + + // Clearcoat + +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Unpack. + let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N; + let clearcoat_strength = (*input).clearcoat_strength; + + // Perform specular input calculations again for the clearcoat layer. We + // can't reuse the above because the clearcoat normal might be different + // from the main layer normal. + let clearcoat_specular_L_intensity = compute_specular_layer_values_for_point_light( + input, + LAYER_CLEARCOAT, + V, + light_to_frag, + (*light).position_radius.w, + ); + var clearcoat_specular_derived_input = + derive_lighting_input(clearcoat_N, V, clearcoat_specular_L_intensity.xyz); + + // Calculate the specular light. + let clearcoat_specular_intensity = clearcoat_specular_L_intensity.w; + let Fc_Frc = specular_clearcoat( + input, + &clearcoat_specular_derived_input, + clearcoat_strength, + clearcoat_specular_intensity + ); + let inv_Fc = 1.0 - Fc_Frc.r; // Inverse Fresnel term. + let Frc = Fc_Frc.g; // Clearcoat light. +#endif // STANDARD_MATERIAL_CLEARCOAT // Diffuse. - // Comes after specular since its NoL is used in the lighting equation. - L = normalize(light_to_frag); - H = normalize(L + V); - NoL = saturate(dot(N, L)); - NoH = saturate(dot(N, H)); - LoH = saturate(dot(L, H)); - - let diffuse = diffuseColor * Fd_Burley(roughness, NdotV, NoL, LoH); + // Comes after specular since its N⋅L is used in the lighting equation. + let L = normalize(light_to_frag); + var derived_input = derive_lighting_input(N, V, L); + let diffuse = diffuse_color * Fd_Burley(input, &derived_input); // See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminanceEquation // Lout = f(v,l) Φ / { 4 π d^2 }⟨n⋅l⟩ @@ -232,23 +411,23 @@ fn point_light( // NOTE: (*light).color.rgb is premultiplied with (*light).intensity / 4 π (which would be the luminous intensity) on the CPU - return ((diffuse + specular_light) * (*light).color_inverse_square_range.rgb) * (rangeAttenuation * NoL); + var color: vec3; +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Account for the Fresnel term from the clearcoat darkening the main layer. + // + // + color = (diffuse + specular_light * inv_Fc) * inv_Fc + Frc; +#else // STANDARD_MATERIAL_CLEARCOAT + color = diffuse + specular_light; +#endif // STANDARD_MATERIAL_CLEARCOAT + + return color * (*light).color_inverse_square_range.rgb * + (rangeAttenuation * derived_input.NdotL); } -fn spot_light( - world_position: vec3, - light_id: u32, - roughness: f32, - NdotV: f32, - N: vec3, - V: vec3, - R: vec3, - F0: vec3, - f_ab: vec2, - diffuseColor: vec3 -) -> vec3 { +fn spot_light(light_id: u32, input: ptr) -> vec3 { // reuse the point light calculations - let point_light = point_light(world_position, light_id, roughness, NdotV, N, V, R, F0, f_ab, diffuseColor); + let point_light = point_light(light_id, input); let light = &view_bindings::point_lights.data[light_id]; @@ -258,7 +437,7 @@ fn spot_light( if ((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u { spot_dir.y = -spot_dir.y; } - let light_to_frag = (*light).position_radius.xyz - world_position.xyz; + let light_to_frag = (*light).position_radius.xyz - (*input).P.xyz; // calculate attenuation based on filament formula https://google.github.io/filament/Filament.html#listing_glslpunctuallight // spot_scale and spot_offset have been precomputed @@ -270,19 +449,48 @@ fn spot_light( return point_light * spot_attenuation; } -fn directional_light(light_id: u32, roughness: f32, NdotV: f32, normal: vec3, view: vec3, R: vec3, F0: vec3, f_ab: vec2, diffuseColor: vec3) -> vec3 { +fn directional_light(light_id: u32, input: ptr) -> vec3 { + // Unpack. + let diffuse_color = (*input).diffuse_color; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let N = (*input).layers[LAYER_BASE].N; + let V = (*input).V; + let roughness = (*input).layers[LAYER_BASE].roughness; + let light = &view_bindings::lights.directional_lights[light_id]; let incident_light = (*light).direction_to_light.xyz; - - let half_vector = normalize(incident_light + view); - let NoL = saturate(dot(normal, incident_light)); - let NoH = saturate(dot(normal, half_vector)); - let LoH = saturate(dot(incident_light, half_vector)); - - let diffuse = diffuseColor * Fd_Burley(roughness, NdotV, NoL, LoH); - let specularIntensity = 1.0; - let specular_light = specular(F0, roughness, half_vector, NdotV, NoL, NoH, LoH, specularIntensity, f_ab); - - return (specular_light + diffuse) * (*light).color.rgb * NoL; + var derived_input = derive_lighting_input(N, V, incident_light); + + let diffuse = diffuse_color * Fd_Burley(input, &derived_input); + + let specular_light = specular(input, &derived_input, 1.0); + +#ifdef STANDARD_MATERIAL_CLEARCOAT + let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N; + let clearcoat_strength = (*input).clearcoat_strength; + + // Perform specular input calculations again for the clearcoat layer. We + // can't reuse the above because the clearcoat normal might be different + // from the main layer normal. + var derived_clearcoat_input = derive_lighting_input(clearcoat_N, V, incident_light); + + let Fc_Frc = + specular_clearcoat(input, &derived_clearcoat_input, clearcoat_strength, 1.0); + let inv_Fc = 1.0 - Fc_Frc.r; + let Frc = Fc_Frc.g; +#endif // STANDARD_MATERIAL_CLEARCOAT + + var color: vec3; +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Account for the Fresnel term from the clearcoat darkening the main layer. + // + // + color = (diffuse + specular_light * inv_Fc) * inv_Fc * derived_input.NdotL + + Frc * derived_clearcoat_input.NdotL; +#else // STANDARD_MATERIAL_CLEARCOAT + color = (diffuse + specular_light) * derived_input.NdotL; +#endif // STANDARD_MATERIAL_CLEARCOAT + + return color * (*light).color.rgb; } diff --git a/crates/bevy_pbr/src/render/pbr_prepass.wgsl b/crates/bevy_pbr/src/render/pbr_prepass.wgsl index c77d71ebca16d..93032dfb96b78 100644 --- a/crates/bevy_pbr/src/render/pbr_prepass.wgsl +++ b/crates/bevy_pbr/src/render/pbr_prepass.wgsl @@ -1,8 +1,10 @@ #import bevy_pbr::{ pbr_prepass_functions, + pbr_bindings, pbr_bindings::material, pbr_types, pbr_functions, + pbr_functions::SampleBias, prepass_io, mesh_view_bindings::view, } @@ -45,26 +47,48 @@ fn fragment( is_front, ); - let normal = pbr_functions::apply_normal_mapping( + var normal = world_normal; + +#ifdef VERTEX_UVS +#ifdef VERTEX_TANGENTS +#ifdef STANDARD_MATERIAL_NORMAL_MAP + +#ifdef STANDARD_MATERIAL_NORMAL_MAP_UV_B + let uv = (material.uv_transform * vec3(in.uv_b, 1.0)).xy; +#else + let uv = (material.uv_transform * vec3(in.uv, 1.0)).xy; +#endif + + // Fill in the sample bias so we can sample from textures. + var bias: SampleBias; +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv = in.ddx_uv; + bias.ddy_uv = in.ddy_uv; +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias = view.mip_bias; +#endif // MESHLET_MESH_MATERIAL_PASS + + let Nt = pbr_functions::sample_texture( + pbr_bindings::normal_map_texture, + pbr_bindings::normal_map_sampler, + uv, + bias, + ).rgb; + + normal = pbr_functions::apply_normal_mapping( material.flags, world_normal, double_sided, is_front, -#ifdef VERTEX_TANGENTS -#ifdef STANDARD_MATERIAL_NORMAL_MAP in.world_tangent, -#endif // STANDARD_MATERIAL_NORMAL_MAP -#endif // VERTEX_TANGENTS -#ifdef VERTEX_UVS - in.uv, -#endif // VERTEX_UVS + Nt, view.mip_bias, -#ifdef MESHLET_MESH_MATERIAL_PASS - in.ddx_uv, - in.ddy_uv, -#endif // MESHLET_MESH_MATERIAL_PASS ); +#endif // STANDARD_MATERIAL_NORMAL_MAP +#endif // VERTEX_TANGENTS +#endif // VERTEX_UVS + out.normal = vec4(normal * 0.5 + vec3(0.5), 1.0); } else { out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0); diff --git a/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl b/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl index 4fcaa33e89547..3dd9babda6d82 100644 --- a/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl @@ -18,8 +18,18 @@ fn prepass_alpha_discard(in: VertexOutput) { var output_color: vec4 = pbr_bindings::material.base_color; #ifdef VERTEX_UVS +#ifdef VERTEX_UVS_A + var uv = in.uv; +#else + var uv = in.uv_b; +#endif +#ifdef VERTEX_UVS_B + if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_UV_BIT) != 0u) { + uv = in.uv_b; + } +#endif let uv_transform = pbr_bindings::material.uv_transform; - let uv = (uv_transform * vec3(in.uv, 1.0)).xy; + uv = (uv_transform * vec3(uv, 1.0)).xy; if (pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u { output_color = output_color * textureSampleBias(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, uv, view.mip_bias); } diff --git a/crates/bevy_pbr/src/render/pbr_transmission.wgsl b/crates/bevy_pbr/src/render/pbr_transmission.wgsl index 65f84c86bbcab..4a48260ae6bea 100644 --- a/crates/bevy_pbr/src/render/pbr_transmission.wgsl +++ b/crates/bevy_pbr/src/render/pbr_transmission.wgsl @@ -3,11 +3,13 @@ #import bevy_pbr::{ lighting, prepass_utils, - utils::{PI, interleaved_gradient_noise}, + utils::interleaved_gradient_noise, utils, mesh_view_bindings as view_bindings, }; +#import bevy_render::maths::PI + #import bevy_core_pipeline::tonemapping::{ approximate_inverse_tone_mapping }; diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index 98d0e569184c7..aaa133fa5ddb3 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -15,6 +15,8 @@ struct StandardMaterial { thickness: f32, ior: f32, attenuation_distance: f32, + clearcoat: f32, + clearcoat_perceptual_roughness: f32, // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32, alpha_cutoff: f32, @@ -44,6 +46,9 @@ const STANDARD_MATERIAL_FLAGS_SPECULAR_TRANSMISSION_TEXTURE_BIT: u32 = 1024u; const STANDARD_MATERIAL_FLAGS_THICKNESS_TEXTURE_BIT: u32 = 2048u; const STANDARD_MATERIAL_FLAGS_DIFFUSE_TRANSMISSION_TEXTURE_BIT: u32 = 4096u; const STANDARD_MATERIAL_FLAGS_ATTENUATION_ENABLED_BIT: u32 = 8192u; +const STANDARD_MATERIAL_FLAGS_CLEARCOAT_TEXTURE_BIT: u32 = 16384u; +const STANDARD_MATERIAL_FLAGS_CLEARCOAT_ROUGHNESS_TEXTURE_BIT: u32 = 32768u; +const STANDARD_MATERIAL_FLAGS_CLEARCOAT_NORMAL_TEXTURE_BIT: u32 = 65536u; const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 3758096384u; // (0b111u32 << 29) const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u; // (0u32 << 29) const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 536870912u; // (1u32 << 29) @@ -72,6 +77,8 @@ fn standard_material_new() -> StandardMaterial { material.ior = 1.5; material.attenuation_distance = 1.0; material.attenuation_color = vec4(1.0, 1.0, 1.0, 1.0); + material.clearcoat = 0.0; + material.clearcoat_perceptual_roughness = 0.0; material.flags = STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE; material.alpha_cutoff = 0.5; material.parallax_depth_scale = 0.1; @@ -101,6 +108,7 @@ struct PbrInput { // view world position V: vec3, lightmap_light: vec3, + clearcoat_N: vec3, is_orthographic: bool, flags: u32, }; diff --git a/crates/bevy_pbr/src/render/shadow_sampling.wgsl b/crates/bevy_pbr/src/render/shadow_sampling.wgsl index 0ced974ebfeda..ec155cf3fcb77 100644 --- a/crates/bevy_pbr/src/render/shadow_sampling.wgsl +++ b/crates/bevy_pbr/src/render/shadow_sampling.wgsl @@ -2,10 +2,10 @@ #import bevy_pbr::{ mesh_view_bindings as view_bindings, - utils::{PI, interleaved_gradient_noise}, + utils::interleaved_gradient_noise, utils, } -#import bevy_render::maths::orthonormalize +#import bevy_render::maths::{orthonormalize, PI} // Do the lookup, using HW 2x2 PCF and comparison fn sample_shadow_map_hardware(light_local: vec2, depth: f32, array_index: i32) -> f32 { diff --git a/crates/bevy_pbr/src/render/shadows.wgsl b/crates/bevy_pbr/src/render/shadows.wgsl index bec8b90d262d0..21b25f7f3aebf 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -3,10 +3,14 @@ #import bevy_pbr::{ mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE, mesh_view_bindings as view_bindings, - utils::{hsv_to_rgb, PI_2}, shadow_sampling::{SPOT_SHADOW_TEXEL_SIZE, sample_shadow_cubemap, sample_shadow_map} } +#import bevy_render::{ + color_operations::hsv_to_rgb, + maths::PI_2 +} + const flip_z: vec3 = vec3(1.0, 1.0, -1.0); fn fetch_point_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3) -> f32 { @@ -109,24 +113,27 @@ fn get_cascade_index(light_id: u32, view_z: f32) -> u32 { return (*light).num_cascades; } -fn sample_directional_cascade(light_id: u32, cascade_index: u32, frag_position: vec4, surface_normal: vec3) -> f32 { +// Converts from world space to the uv position in the light's shadow map. +// +// The depth is stored in the return value's z coordinate. If the return value's +// w coordinate is 0.0, then we landed outside the shadow map entirely. +fn world_to_directional_light_local( + light_id: u32, + cascade_index: u32, + offset_position: vec4 +) -> vec4 { let light = &view_bindings::lights.directional_lights[light_id]; let cascade = &(*light).cascades[cascade_index]; - // The normal bias is scaled to the texel size. - let normal_offset = (*light).shadow_normal_bias * (*cascade).texel_size * surface_normal.xyz; - let depth_offset = (*light).shadow_depth_bias * (*light).direction_to_light.xyz; - let offset_position = vec4(frag_position.xyz + normal_offset + depth_offset, frag_position.w); - let offset_position_clip = (*cascade).view_projection * offset_position; if (offset_position_clip.w <= 0.0) { - return 1.0; + return vec4(0.0); } let offset_position_ndc = offset_position_clip.xyz / offset_position_clip.w; // No shadow outside the orthographic projection volume if (any(offset_position_ndc.xy < vec2(-1.0)) || offset_position_ndc.z < 0.0 || any(offset_position_ndc > vec3(1.0))) { - return 1.0; + return vec4(0.0); } // compute texture coordinates for shadow lookup, compensating for the Y-flip difference @@ -136,8 +143,25 @@ fn sample_directional_cascade(light_id: u32, cascade_index: u32, frag_position: let depth = offset_position_ndc.z; + return vec4(light_local, depth, 1.0); +} + +fn sample_directional_cascade(light_id: u32, cascade_index: u32, frag_position: vec4, surface_normal: vec3) -> f32 { + let light = &view_bindings::lights.directional_lights[light_id]; + let cascade = &(*light).cascades[cascade_index]; + + // The normal bias is scaled to the texel size. + let normal_offset = (*light).shadow_normal_bias * (*cascade).texel_size * surface_normal.xyz; + let depth_offset = (*light).shadow_depth_bias * (*light).direction_to_light.xyz; + let offset_position = vec4(frag_position.xyz + normal_offset + depth_offset, frag_position.w); + + let light_local = world_to_directional_light_local(light_id, cascade_index, offset_position); + if (light_local.w == 0.0) { + return 1.0; + } + let array_index = i32((*light).depth_texture_base_index + cascade_index); - return sample_shadow_map(light_local, depth, array_index, (*cascade).texel_size); + return sample_shadow_map(light_local.xy, light_local.z, array_index, (*cascade).texel_size); } fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3, view_z: f32) -> f32 { diff --git a/crates/bevy_pbr/src/render/utils.wgsl b/crates/bevy_pbr/src/render/utils.wgsl index 057c2f734cdfa..dbee28181586c 100644 --- a/crates/bevy_pbr/src/render/utils.wgsl +++ b/crates/bevy_pbr/src/render/utils.wgsl @@ -2,55 +2,6 @@ #import bevy_pbr::rgb9e5 -const PI: f32 = 3.141592653589793; // π -const PI_2: f32 = 6.283185307179586; // 2π -const HALF_PI: f32 = 1.57079632679; // π/2 -const FRAC_PI_3: f32 = 1.0471975512; // π/3 -const E: f32 = 2.718281828459045; // exp(1) - -// Converts HSV to RGB. -// -// Input: H ∈ [0, 2π), S ∈ [0, 1], V ∈ [0, 1]. -// Output: R ∈ [0, 1], G ∈ [0, 1], B ∈ [0, 1]. -// -// -fn hsv_to_rgb(hsv: vec3) -> vec3 { - let n = vec3(5.0, 3.0, 1.0); - let k = (n + hsv.x / FRAC_PI_3) % 6.0; - return hsv.z - hsv.z * hsv.y * max(vec3(0.0), min(k, min(4.0 - k, vec3(1.0)))); -} - -// Converts RGB to HSV. -// -// Input: R ∈ [0, 1], G ∈ [0, 1], B ∈ [0, 1]. -// Output: H ∈ [0, 2π), S ∈ [0, 1], V ∈ [0, 1]. -// -// -fn rgb_to_hsv(rgb: vec3) -> vec3 { - let x_max = max(rgb.r, max(rgb.g, rgb.b)); // i.e. V - let x_min = min(rgb.r, min(rgb.g, rgb.b)); - let c = x_max - x_min; // chroma - - var swizzle = vec3(0.0); - if (x_max == rgb.r) { - swizzle = vec3(rgb.gb, 0.0); - } else if (x_max == rgb.g) { - swizzle = vec3(rgb.br, 2.0); - } else { - swizzle = vec3(rgb.rg, 4.0); - } - - let h = FRAC_PI_3 * (((swizzle.x - swizzle.y) / c + swizzle.z) % 6.0); - - // Avoid division by zero. - var s = 0.0; - if (x_max > 0.0) { - s = c / x_max; - } - - return vec3(h, s, x_max); -} - // Generates a random u32 in range [0, u32::MAX]. // // `state` is a mutable reference to a u32 used as the seed. diff --git a/crates/bevy_pbr/src/ssao/gtao.wgsl b/crates/bevy_pbr/src/ssao/gtao.wgsl index be5fea01ee230..1fded0b53aa8e 100644 --- a/crates/bevy_pbr/src/ssao/gtao.wgsl +++ b/crates/bevy_pbr/src/ssao/gtao.wgsl @@ -5,13 +5,12 @@ // Source code heavily based on XeGTAO v1.30 from Intel // https://github.com/GameTechDev/XeGTAO/blob/0d177ce06bfa642f64d8af4de1197ad1bcb862d4/Source/Rendering/Shaders/XeGTAO.hlsli -#import bevy_pbr::{ - gtao_utils::fast_acos, - utils::{PI, HALF_PI}, -} +#import bevy_pbr::gtao_utils::fast_acos + #import bevy_render::{ view::View, globals::Globals, + maths::{PI, HALF_PI}, } @group(0) @binding(0) var preprocessed_depth: texture_2d; diff --git a/crates/bevy_pbr/src/ssao/gtao_utils.wgsl b/crates/bevy_pbr/src/ssao/gtao_utils.wgsl index f081393edb395..32c46e1d1d95c 100644 --- a/crates/bevy_pbr/src/ssao/gtao_utils.wgsl +++ b/crates/bevy_pbr/src/ssao/gtao_utils.wgsl @@ -1,6 +1,6 @@ #define_import_path bevy_pbr::gtao_utils -#import bevy_pbr::utils::{PI, HALF_PI} +#import bevy_render::maths::{PI, HALF_PI} // Approximates single-bounce ambient occlusion to multi-bounce ambient occlusion // https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf#page=78 diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index 136552090ee6f..9c43854b94810 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -153,13 +153,13 @@ pub struct ScreenSpaceAmbientOcclusionBundle { /// Doing so greatly reduces SSAO noise. /// /// SSAO is not supported on `WebGL2`, and is not currently supported on `WebGPU` or `DirectX12`. -#[derive(Component, ExtractComponent, Reflect, PartialEq, Eq, Hash, Clone, Default)] +#[derive(Component, ExtractComponent, Reflect, PartialEq, Eq, Hash, Clone, Default, Debug)] #[reflect(Component)] pub struct ScreenSpaceAmbientOcclusionSettings { pub quality_level: ScreenSpaceAmbientOcclusionQualityLevel, } -#[derive(Reflect, PartialEq, Eq, Hash, Clone, Copy, Default)] +#[derive(Reflect, PartialEq, Eq, Hash, Clone, Copy, Default, Debug)] pub enum ScreenSpaceAmbientOcclusionQualityLevel { Low, Medium, diff --git a/crates/bevy_pbr/src/volumetric_fog/mod.rs b/crates/bevy_pbr/src/volumetric_fog/mod.rs new file mode 100644 index 0000000000000..7d7b611a614cc --- /dev/null +++ b/crates/bevy_pbr/src/volumetric_fog/mod.rs @@ -0,0 +1,647 @@ +//! Volumetric fog and volumetric lighting, also known as light shafts or god +//! rays. +//! +//! This module implements a more physically-accurate, but slower, form of fog +//! than the [`crate::fog`] module does. Notably, this *volumetric fog* allows +//! for light beams from directional lights to shine through, creating what is +//! known as *light shafts* or *god rays*. +//! +//! To add volumetric fog to a scene, add [`VolumetricFogSettings`] to the +//! camera, and add [`VolumetricLight`] to directional lights that you wish to +//! be volumetric. [`VolumetricFogSettings`] feature numerous settings that +//! allow you to define the accuracy of the simulation, as well as the look of +//! the fog. Currently, only interaction with directional lights that have +//! shadow maps is supported. Note that the overhead of the effect scales +//! directly with the number of directional lights in use, so apply +//! [`VolumetricLight`] sparingly for the best results. +//! +//! The overall algorithm, which is implemented as a postprocessing effect, is a +//! combination of the techniques described in [Scratchapixel] and [this blog +//! post]. It uses raymarching in screen space, transformed into shadow map +//! space for sampling and combined with physically-based modeling of absorption +//! and scattering. Bevy employs the widely-used [Henyey-Greenstein phase +//! function] to model asymmetry; this essentially allows light shafts to fade +//! into and out of existence as the user views them. +//! +//! [Scratchapixel]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html +//! +//! [this blog post]: https://www.alexandre-pestana.com/volumetric-lights/ +//! +//! [Henyey-Greenstein phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction + +use bevy_app::{App, Plugin}; +use bevy_asset::{load_internal_asset, Handle}; +use bevy_color::Color; +use bevy_core_pipeline::{ + core_3d::{ + graph::{Core3d, Node3d}, + prepare_core_3d_depth_textures, Camera3d, + }, + fullscreen_vertex_shader::fullscreen_shader_vertex_state, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{Has, QueryItem, With}, + schedule::IntoSystemConfigs as _, + system::{lifetimeless::Read, Commands, Query, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_math::Vec3; +use bevy_render::{ + render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_resource::{ + binding_types::{ + sampler, texture_2d, texture_depth_2d, texture_depth_2d_multisampled, uniform_buffer, + }, + BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, + ColorTargetState, ColorWrites, DynamicUniformBuffer, FilterMode, FragmentState, + MultisampleState, Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, + RenderPassDescriptor, RenderPipelineDescriptor, Sampler, SamplerBindingType, + SamplerDescriptor, Shader, ShaderStages, ShaderType, SpecializedRenderPipeline, + SpecializedRenderPipelines, TextureFormat, TextureSampleType, TextureUsages, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + texture::BevyDefault, + view::{ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniformOffset}, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, +}; +use bevy_utils::prelude::default; + +use crate::{ + graph::NodePbr, MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup, + ViewFogUniformOffset, ViewLightProbesUniformOffset, ViewLightsUniformOffset, +}; + +/// The volumetric fog shader. +pub const VOLUMETRIC_FOG_HANDLE: Handle = Handle::weak_from_u128(17400058287583986650); + +/// A plugin that implements volumetric fog. +pub struct VolumetricFogPlugin; + +/// Add this component to a [`DirectionalLight`] with a shadow map +/// (`shadows_enabled: true`) to make volumetric fog interact with it. +/// +/// This allows the light to generate light shafts/god rays. +#[derive(Clone, Copy, Component, Default, Debug)] +pub struct VolumetricLight; + +/// When placed on a [`Camera3d`], enables volumetric fog and volumetric +/// lighting, also known as light shafts or god rays. +#[derive(Clone, Copy, Component, Debug)] +pub struct VolumetricFogSettings { + /// The color of the fog. + /// + /// Note that the fog must be lit by a [`VolumetricLight`] or ambient light + /// in order for this color to appear. + /// + /// Defaults to white. + pub fog_color: Color, + + /// Color of the ambient light. + /// + /// This is separate from Bevy's [`crate::light::AmbientLight`] because an + /// [`EnvironmentMapLight`] is still considered an ambient light for the + /// purposes of volumetric fog. If you're using a + /// [`crate::EnvironmentMapLight`], for best results, this should be a good + /// approximation of the average color of the environment map. + /// + /// Defaults to white. + pub ambient_color: Color, + + /// The brightness of the ambient light. + /// + /// If there's no ambient light, set this to 0. + /// + /// Defaults to 0.1. + pub ambient_intensity: f32, + + /// The number of raymarching steps to perform. + /// + /// Higher values produce higher-quality results with less banding, but + /// reduce performance. + /// + /// The default value is 64. + pub step_count: u32, + + /// The maximum distance that Bevy will trace a ray for, in world space. + /// + /// You can think of this as the radius of a sphere of fog surrounding the + /// camera. It has to be capped to a finite value or else there would be an + /// infinite amount of fog, which would result in completely-opaque areas + /// where the skybox would be. + /// + /// The default value is 25. + pub max_depth: f32, + + /// The absorption coefficient, which measures what fraction of light is + /// absorbed by the fog at each step. + /// + /// Increasing this value makes the fog darker. + /// + /// The default value is 0.3. + pub absorption: f32, + + /// The scattering coefficient, which measures the fraction of light that's + /// scattered toward, and away from, the viewer. + /// + /// The default value is 0.3. + pub scattering: f32, + + /// The density of fog, which measures how dark the fog is. + /// + /// The default value is 0.1. + pub density: f32, + + /// Measures the fraction of light that's scattered *toward* the camera, as opposed to *away* from the camera. + /// + /// Increasing this value makes light shafts become more prominent when the + /// camera is facing toward their source and less prominent when the camera + /// is facing away. Essentially, a high value here means the light shafts + /// will fade into view as the camera focuses on them and fade away when the + /// camera is pointing away. + /// + /// The default value is 0.8. + pub scattering_asymmetry: f32, + + /// Applies a nonphysical color to the light. + /// + /// This can be useful for artistic purposes but is nonphysical. + /// + /// The default value is white. + pub light_tint: Color, + + /// Scales the light by a fixed fraction. + /// + /// This can be useful for artistic purposes but is nonphysical. + /// + /// The default value is 1.0, which results in no adjustment. + pub light_intensity: f32, +} + +/// The GPU pipeline for the volumetric fog postprocessing effect. +#[derive(Resource)] +pub struct VolumetricFogPipeline { + /// A reference to the shared set of mesh pipeline view layouts. + mesh_view_layouts: MeshPipelineViewLayouts, + /// The view bind group when multisample antialiasing isn't in use. + volumetric_view_bind_group_layout_no_msaa: BindGroupLayout, + /// The view bind group when multisample antialiasing is in use. + volumetric_view_bind_group_layout_msaa: BindGroupLayout, + /// The sampler that we use to sample the postprocessing input. + color_sampler: Sampler, +} + +#[derive(Component, Deref, DerefMut)] +pub struct ViewVolumetricFogPipeline(pub CachedRenderPipelineId); + +/// The node in the render graph, part of the postprocessing stack, that +/// implements volumetric fog. +#[derive(Default)] +pub struct VolumetricFogNode; + +/// Identifies a single specialization of the volumetric fog shader. +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +pub struct VolumetricFogPipelineKey { + /// The layout of the view, which is needed for the raymarching. + mesh_pipeline_view_key: MeshPipelineViewLayoutKey, + /// Whether the view has high dynamic range. + hdr: bool, +} + +/// The same as [`VolumetricFogSettings`], but formatted for the GPU. +#[derive(ShaderType)] +pub struct VolumetricFogUniform { + fog_color: Vec3, + light_tint: Vec3, + ambient_color: Vec3, + ambient_intensity: f32, + step_count: u32, + max_depth: f32, + absorption: f32, + scattering: f32, + density: f32, + scattering_asymmetry: f32, + light_intensity: f32, +} + +/// Specifies the offset within the [`VolumetricFogUniformBuffer`] of the +/// [`VolumetricFogUniform`] for a specific view. +#[derive(Component, Deref, DerefMut)] +pub struct ViewVolumetricFogUniformOffset(u32); + +/// The GPU buffer that stores the [`VolumetricFogUniform`] data. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct VolumetricFogUniformBuffer(pub DynamicUniformBuffer); + +impl Plugin for VolumetricFogPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + VOLUMETRIC_FOG_HANDLE, + "volumetric_fog.wgsl", + Shader::from_wgsl + ); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::>() + .init_resource::() + .add_systems(ExtractSchedule, extract_volumetric_fog) + .add_systems( + Render, + ( + prepare_volumetric_fog_pipelines.in_set(RenderSet::Prepare), + prepare_volumetric_fog_uniforms.in_set(RenderSet::Prepare), + prepare_view_depth_textures_for_volumetric_fog + .in_set(RenderSet::Prepare) + .before(prepare_core_3d_depth_textures), + ), + ); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::() + .add_render_graph_node::>( + Core3d, + NodePbr::VolumetricFog, + ) + .add_render_graph_edges( + Core3d, + // Volumetric fog is a postprocessing effect. Run it after the + // main pass but before bloom. + (Node3d::EndMainPass, NodePbr::VolumetricFog, Node3d::Bloom), + ); + } +} + +impl Default for VolumetricFogSettings { + fn default() -> Self { + Self { + step_count: 64, + max_depth: 25.0, + absorption: 0.3, + scattering: 0.3, + density: 0.1, + scattering_asymmetry: 0.5, + fog_color: Color::WHITE, + // Matches `AmbientLight` defaults. + ambient_color: Color::WHITE, + ambient_intensity: 0.1, + light_tint: Color::WHITE, + light_intensity: 1.0, + } + } +} + +impl FromWorld for VolumetricFogPipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let mesh_view_layouts = world.resource::(); + + // Create the bind group layout entries common to both the MSAA and + // non-MSAA bind group layouts. + let base_bind_group_layout_entries = &*BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + // `volumetric_fog` + uniform_buffer::(true), + // `color_texture` + texture_2d(TextureSampleType::Float { filterable: true }), + // `color_sampler` + sampler(SamplerBindingType::Filtering), + ), + ); + + // Because `texture_depth_2d` and `texture_depth_2d_multisampled` are + // different types, we need to make separate bind group layouts for + // each. + + let mut bind_group_layout_entries_no_msaa = base_bind_group_layout_entries.to_vec(); + bind_group_layout_entries_no_msaa.extend_from_slice(&BindGroupLayoutEntries::with_indices( + ShaderStages::FRAGMENT, + ((3, texture_depth_2d()),), + )); + let volumetric_view_bind_group_layout_no_msaa = render_device.create_bind_group_layout( + "volumetric lighting view bind group layout", + &bind_group_layout_entries_no_msaa, + ); + + let mut bind_group_layout_entries_msaa = base_bind_group_layout_entries.to_vec(); + bind_group_layout_entries_msaa.extend_from_slice(&BindGroupLayoutEntries::with_indices( + ShaderStages::FRAGMENT, + ((3, texture_depth_2d_multisampled()),), + )); + let volumetric_view_bind_group_layout_msaa = render_device.create_bind_group_layout( + "volumetric lighting view bind group layout (multisampled)", + &bind_group_layout_entries_msaa, + ); + + let color_sampler = render_device.create_sampler(&SamplerDescriptor { + label: Some("volumetric lighting color sampler"), + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + compare: None, + ..default() + }); + + VolumetricFogPipeline { + mesh_view_layouts: mesh_view_layouts.clone(), + volumetric_view_bind_group_layout_no_msaa, + volumetric_view_bind_group_layout_msaa, + color_sampler, + } + } +} + +/// Extracts [`VolumetricFogSettings`] and [`VolumetricLight`]s from the main +/// world to the render world. +pub fn extract_volumetric_fog( + mut commands: Commands, + view_targets: Extract>, + volumetric_lights: Extract>, +) { + if volumetric_lights.is_empty() { + return; + } + + for (view_target, volumetric_fog_settings) in view_targets.iter() { + commands + .get_or_spawn(view_target) + .insert(*volumetric_fog_settings); + } + + for (entity, volumetric_light) in volumetric_lights.iter() { + commands.get_or_spawn(entity).insert(*volumetric_light); + } +} + +impl ViewNode for VolumetricFogNode { + type ViewQuery = ( + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + ); + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + ( + view_target, + view_depth_texture, + view_volumetric_lighting_pipeline, + view_uniform_offset, + view_lights_offset, + view_fog_offset, + view_light_probes_offset, + view_volumetric_lighting_uniform_buffer_offset, + view_bind_group, + ): QueryItem<'w, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let volumetric_lighting_pipeline = world.resource::(); + let volumetric_lighting_uniform_buffer = world.resource::(); + let msaa = world.resource::(); + + // Fetch the uniform buffer and binding. + let (Some(pipeline), Some(volumetric_lighting_uniform_buffer_binding)) = ( + pipeline_cache.get_render_pipeline(**view_volumetric_lighting_pipeline), + volumetric_lighting_uniform_buffer.binding(), + ) else { + return Ok(()); + }; + + let postprocess = view_target.post_process_write(); + + // Create the bind group for the view. + // + // TODO: Cache this. + let volumetric_view_bind_group_layout = match *msaa { + Msaa::Off => &volumetric_lighting_pipeline.volumetric_view_bind_group_layout_no_msaa, + _ => &volumetric_lighting_pipeline.volumetric_view_bind_group_layout_msaa, + }; + let volumetric_view_bind_group = render_context.render_device().create_bind_group( + None, + volumetric_view_bind_group_layout, + &BindGroupEntries::sequential(( + volumetric_lighting_uniform_buffer_binding, + postprocess.source, + &volumetric_lighting_pipeline.color_sampler, + view_depth_texture.view(), + )), + ); + + let render_pass_descriptor = RenderPassDescriptor { + label: Some("volumetric lighting pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: postprocess.destination, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&render_pass_descriptor); + + render_pass.set_pipeline(pipeline); + render_pass.set_bind_group( + 0, + &view_bind_group.value, + &[ + view_uniform_offset.offset, + view_lights_offset.offset, + view_fog_offset.offset, + **view_light_probes_offset, + ], + ); + render_pass.set_bind_group( + 1, + &volumetric_view_bind_group, + &[**view_volumetric_lighting_uniform_buffer_offset], + ); + render_pass.draw(0..3, 0..1); + + Ok(()) + } +} + +impl SpecializedRenderPipeline for VolumetricFogPipeline { + type Key = VolumetricFogPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mesh_view_layout = self + .mesh_view_layouts + .get_view_layout(key.mesh_pipeline_view_key); + + // We always use hardware 2x2 filtering for sampling the shadow map; the + // more accurate versions with percentage-closer filtering aren't worth + // the overhead. + let mut shader_defs = vec!["SHADOW_FILTER_METHOD_HARDWARE_2X2".into()]; + + // We need a separate layout for MSAA and non-MSAA. + let volumetric_view_bind_group_layout = if key + .mesh_pipeline_view_key + .contains(MeshPipelineViewLayoutKey::MULTISAMPLED) + { + shader_defs.push("MULTISAMPLED".into()); + self.volumetric_view_bind_group_layout_msaa.clone() + } else { + self.volumetric_view_bind_group_layout_no_msaa.clone() + }; + + RenderPipelineDescriptor { + label: Some("volumetric lighting pipeline".into()), + layout: vec![mesh_view_layout.clone(), volumetric_view_bind_group_layout], + push_constant_ranges: vec![], + vertex: fullscreen_shader_vertex_state(), + primitive: PrimitiveState::default(), + depth_stencil: None, + multisample: MultisampleState::default(), + fragment: Some(FragmentState { + shader: VOLUMETRIC_FOG_HANDLE, + shader_defs, + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: if key.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + } + } +} + +/// Specializes volumetric fog pipelines for all views with that effect enabled. +pub fn prepare_volumetric_fog_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + volumetric_lighting_pipeline: Res, + view_targets: Query< + ( + Entity, + &ExtractedView, + Has, + Has, + Has, + Has, + ), + With, + >, + msaa: Res, +) { + for (entity, view, normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass) in + view_targets.iter() + { + // Create a mesh pipeline view layout key corresponding to the view. + let mut mesh_pipeline_view_key = MeshPipelineViewLayoutKey::from(*msaa); + mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::NORMAL_PREPASS, normal_prepass); + mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::DEPTH_PREPASS, depth_prepass); + mesh_pipeline_view_key.set( + MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS, + motion_vector_prepass, + ); + mesh_pipeline_view_key.set( + MeshPipelineViewLayoutKey::DEFERRED_PREPASS, + deferred_prepass, + ); + + // Specialize the pipeline. + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &volumetric_lighting_pipeline, + VolumetricFogPipelineKey { + mesh_pipeline_view_key, + hdr: view.hdr, + }, + ); + + commands + .entity(entity) + .insert(ViewVolumetricFogPipeline(pipeline_id)); + } +} + +/// A system that converts [`VolumetricFogSettings`] +pub fn prepare_volumetric_fog_uniforms( + mut commands: Commands, + mut volumetric_lighting_uniform_buffer: ResMut, + view_targets: Query<(Entity, &VolumetricFogSettings)>, + render_device: Res, + render_queue: Res, +) { + let Some(mut writer) = volumetric_lighting_uniform_buffer.get_writer( + view_targets.iter().len(), + &render_device, + &render_queue, + ) else { + return; + }; + + for (entity, volumetric_fog_settings) in view_targets.iter() { + let offset = writer.write(&VolumetricFogUniform { + fog_color: Vec3::from_slice( + &volumetric_fog_settings.fog_color.linear().to_f32_array()[0..3], + ), + light_tint: Vec3::from_slice( + &volumetric_fog_settings.light_tint.linear().to_f32_array()[0..3], + ), + ambient_color: Vec3::from_slice( + &volumetric_fog_settings + .ambient_color + .linear() + .to_f32_array()[0..3], + ), + ambient_intensity: volumetric_fog_settings.ambient_intensity, + step_count: volumetric_fog_settings.step_count, + max_depth: volumetric_fog_settings.max_depth, + absorption: volumetric_fog_settings.absorption, + scattering: volumetric_fog_settings.scattering, + density: volumetric_fog_settings.density, + scattering_asymmetry: volumetric_fog_settings.scattering_asymmetry, + light_intensity: volumetric_fog_settings.light_intensity, + }); + + commands + .entity(entity) + .insert(ViewVolumetricFogUniformOffset(offset)); + } +} + +/// A system that marks all view depth textures as readable in shaders. +/// +/// The volumetric lighting pass needs to do this, and it doesn't happen by +/// default. +pub fn prepare_view_depth_textures_for_volumetric_fog( + mut view_targets: Query<&mut Camera3d, With>, +) { + for mut camera in view_targets.iter_mut() { + camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits(); + } +} diff --git a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl new file mode 100644 index 0000000000000..0ea6c18f7c46b --- /dev/null +++ b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl @@ -0,0 +1,218 @@ +// A postprocessing shader that implements volumetric fog via raymarching and +// sampling directional light shadow maps. +// +// The overall approach is a combination of the volumetric rendering in [1] and +// the shadow map raymarching in [2]. First, we sample the depth buffer to +// determine how long our ray is. Then we do a raymarch, with physically-based +// calculations at each step to determine how much light was absorbed, scattered +// out, and scattered in. To determine in-scattering, we sample the shadow map +// for the light to determine whether the point was in shadow or not. +// +// [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html +// +// [2]: http://www.alexandre-pestana.com/volumetric-lights/ + +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +#import bevy_pbr::mesh_view_bindings::{lights, view} +#import bevy_pbr::mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT +#import bevy_pbr::shadow_sampling::sample_shadow_map_hardware +#import bevy_pbr::shadows::{get_cascade_index, world_to_directional_light_local} +#import bevy_pbr::view_transformations::{ + frag_coord_to_ndc, + position_ndc_to_view, + position_ndc_to_world +} + +// The GPU version of [`VolumetricFogSettings`]. See the comments in +// `volumetric_fog/mod.rs` for descriptions of the fields here. +struct VolumetricFog { + fog_color: vec3, + light_tint: vec3, + ambient_color: vec3, + ambient_intensity: f32, + step_count: u32, + max_depth: f32, + absorption: f32, + scattering: f32, + density: f32, + scattering_asymmetry: f32, + light_intensity: f32, +} + +@group(1) @binding(0) var volumetric_fog: VolumetricFog; +@group(1) @binding(1) var color_texture: texture_2d; +@group(1) @binding(2) var color_sampler: sampler; + +#ifdef MULTISAMPLED +@group(1) @binding(3) var depth_texture: texture_depth_multisampled_2d; +#else +@group(1) @binding(3) var depth_texture: texture_depth_2d; +#endif + +// 1 / (4π) +const FRAC_4_PI: f32 = 0.07957747154594767; + +// The common Henyey-Greenstein asymmetric phase function [1] [2]. +// +// This determines how much light goes toward the viewer as opposed to away from +// the viewer. From a visual point of view, it controls how the light shafts +// appear and disappear as the camera looks at the light source. +// +// [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/ray-marching-get-it-right.html +// +// [2]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction +fn henyey_greenstein(neg_LdotV: f32) -> f32 { + let g = volumetric_fog.scattering_asymmetry; + let denom = 1.0 + g * g - 2.0 * g * neg_LdotV; + return FRAC_4_PI * (1.0 - g * g) / (denom * sqrt(denom)); +} + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + // Unpack the `volumetric_fog` settings. + let fog_color = volumetric_fog.fog_color; + let ambient_color = volumetric_fog.ambient_color; + let ambient_intensity = volumetric_fog.ambient_intensity; + let step_count = volumetric_fog.step_count; + let max_depth = volumetric_fog.max_depth; + let absorption = volumetric_fog.absorption; + let scattering = volumetric_fog.scattering; + let density = volumetric_fog.density; + let light_tint = volumetric_fog.light_tint; + let light_intensity = volumetric_fog.light_intensity; + + let exposure = view.exposure; + + // Sample the depth. If this is multisample, just use sample 0; this is + // approximate but good enough. + let frag_coord = in.position; + let depth = textureLoad(depth_texture, vec2(frag_coord.xy), 0); + + // Starting at the end depth, which we got above, figure out how long the + // ray we want to trace is and the length of each increment. + let end_depth = min( + max_depth, + -position_ndc_to_view(frag_coord_to_ndc(vec4(in.position.xy, depth, 1.0))).z + ); + let step_size = end_depth / f32(step_count); + + let directional_light_count = lights.n_directional_lights; + + // Calculate the ray origin (`Ro`) and the ray direction (`Rd`) in NDC, + // view, and world coordinates. + let Rd_ndc = vec3(frag_coord_to_ndc(in.position).xy, 1.0); + let Rd_view = normalize(position_ndc_to_view(Rd_ndc)); + let Ro_world = view.world_position; + let Rd_world = normalize(position_ndc_to_world(Rd_ndc) - Ro_world); + + // Use Beer's law [1] [2] to calculate the maximum amount of light that each + // directional light could contribute, and modulate that value by the light + // tint and fog color. (The actual value will in turn be modulated by the + // phase according to the Henyey-Greenstein formula.) + // + // We use a bit of a hack here. Conceptually, directional lights are + // infinitely far away. But, if we modeled exactly that, then directional + // lights would never contribute any light to the fog, because an + // infinitely-far directional light combined with an infinite amount of fog + // would result in complete absorption of the light. So instead we pretend + // that the directional light is `max_depth` units away and do the + // calculation in those terms. Because the fake distance to the directional + // light is a constant, this lets us perform the calculation once up here + // instead of marching secondary rays toward the light during the + // raymarching step, which improves performance dramatically. + // + // [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html + // + // [2]: https://en.wikipedia.org/wiki/Beer%E2%80%93Lambert_law + let light_attenuation = exp(-density * max_depth * (absorption + scattering)); + let light_factors_per_step = fog_color * light_tint * light_attenuation * scattering * + density * step_size * light_intensity * exposure; + + // Use Beer's law again to accumulate the ambient light all along the path. + var accumulated_color = exp(-end_depth * (absorption + scattering)) * ambient_color * + ambient_intensity; + + // Pre-calculate absorption (amount of light absorbed by the fog) and + // out-scattering (amount of light the fog scattered away). This is the same + // amount for every step. + let sample_attenuation = exp(-step_size * density * (absorption + scattering)); + + // This is the amount of the background that shows through. We're actually + // going to recompute this over and over again for each directional light, + // coming up with the same values each time. + var background_alpha = 1.0; + + for (var light_index = 0u; light_index < directional_light_count; light_index += 1u) { + // Volumetric lights are all sorted first, so the first time we come to + // a non-volumetric light, we know we've seen them all. + let light = &lights.directional_lights[light_index]; + if (((*light).flags & DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT) == 0) { + break; + } + + // Offset the depth value by the bias. + let depth_offset = (*light).shadow_depth_bias * (*light).direction_to_light.xyz; + + // Compute phase, which determines the fraction of light that's + // scattered toward the camera instead of away from it. + let neg_LdotV = dot(normalize((*light).direction_to_light.xyz), Rd_world); + let phase = henyey_greenstein(neg_LdotV); + + // Modulate the factor we calculated above by the phase, fog color, + // light color, light tint. + let light_color_per_step = (*light).color.rgb * phase * light_factors_per_step; + + // Reset `background_alpha` for a new raymarch. + background_alpha = 1.0; + + // Start raymarching. + for (var step = 0u; step < step_count; step += 1u) { + // As an optimization, break if we've gotten too dark. + if (background_alpha < 0.001) { + break; + } + + // Calculate where we are in the ray. + let P_world = Ro_world + Rd_world * f32(step) * step_size; + let P_view = Rd_view * f32(step) * step_size; + + // Process absorption and out-scattering. + background_alpha *= sample_attenuation; + + // Compute in-scattering (amount of light other fog particles + // scattered into this ray). This is where any directional light is + // scattered in. + + // Prepare to sample the shadow map. + let cascade_index = get_cascade_index(light_index, P_view.z); + let light_local = world_to_directional_light_local( + light_index, + cascade_index, + vec4(P_world + depth_offset, 1.0) + ); + + // If we're outside the shadow map entirely, local light attenuation + // is zero. + var local_light_attenuation = f32(light_local.w != 0.0); + + // Otherwise, sample the shadow map to determine whether, and by how + // much, this sample is in the light. + if (local_light_attenuation != 0.0) { + let cascade = &(*light).cascades[cascade_index]; + let array_index = i32((*light).depth_texture_base_index + cascade_index); + local_light_attenuation = + sample_shadow_map_hardware(light_local.xy, light_local.z, array_index); + } + + if (local_light_attenuation != 0.0) { + // Accumulate the light. + accumulated_color += light_color_per_step * local_light_attenuation * + background_alpha; + } + } + } + + // We're done! Blend between the source color and the lit fog color. + let source = textureSample(color_texture, color_sampler, in.uv); + return vec4(source.rgb * background_alpha + accumulated_color, source.a); +} diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 212463d3e3b45..f23f2387656bd 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -7,6 +7,7 @@ homepage = "https://bevyengine.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] +rust-version = "1.76.0" [features] default = ["smallvec"] @@ -25,7 +26,7 @@ documentation = ["bevy_reflect_derive/documentation"] bevy_math = { path = "../bevy_math", version = "0.14.0-dev", features = [ "serialize", ], optional = true } -bevy_reflect_derive = { path = "bevy_reflect_derive", version = "0.14.0-dev" } +bevy_reflect_derive = { path = "derive", version = "0.14.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } bevy_ptr = { path = "../bevy_ptr", version = "0.14.0-dev" } diff --git a/crates/bevy_reflect/compile_fail/.gitignore b/crates/bevy_reflect/compile_fail/.gitignore new file mode 100644 index 0000000000000..ea8c4bf7f35f6 --- /dev/null +++ b/crates/bevy_reflect/compile_fail/.gitignore @@ -0,0 +1 @@ +/target diff --git a/crates/bevy_reflect_compile_fail_tests/Cargo.toml b/crates/bevy_reflect/compile_fail/Cargo.toml similarity index 56% rename from crates/bevy_reflect_compile_fail_tests/Cargo.toml rename to crates/bevy_reflect/compile_fail/Cargo.toml index 4acf5dd05a426..2e8d542e2a8e4 100644 --- a/crates/bevy_reflect_compile_fail_tests/Cargo.toml +++ b/crates/bevy_reflect/compile_fail/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bevy_reflect_compile_fail_tests" +name = "bevy_reflect_compile_fail" edition = "2021" description = "Compile fail tests for Bevy Engine's reflection system" homepage = "https://bevyengine.org" @@ -8,11 +8,10 @@ license = "MIT OR Apache-2.0" publish = false [dependencies] -# ui_test dies if we don't specify the version. See oli-obk/ui_test#211 -bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" } +bevy_reflect = { path = "../" } [dev-dependencies] -bevy_compile_test_utils = { path = "../bevy_compile_test_utils" } +compile_fail_utils = { path = "../../../tools/compile_fail_utils" } [[test]] name = "derive" diff --git a/crates/bevy_reflect_compile_fail_tests/README.md b/crates/bevy_reflect/compile_fail/README.md similarity index 69% rename from crates/bevy_reflect_compile_fail_tests/README.md rename to crates/bevy_reflect/compile_fail/README.md index 79f0ee24ec5bd..1d7da41daa09c 100644 --- a/crates/bevy_reflect_compile_fail_tests/README.md +++ b/crates/bevy_reflect/compile_fail/README.md @@ -4,6 +4,6 @@ This crate is separate from `bevy_reflect` and not part of the Bevy workspace in Bevy. The tests assert on the exact compiler errors and can easily fail for new Rust versions due to updated compiler errors (e.g. changes in spans). -The `CI` workflow executes these tests on the stable rust toolchain (see [tools/ci](../../tools/ci/src/main.rs)). +The `CI` workflow executes these tests on the stable rust toolchain (see [tools/ci](../../../tools/ci/src/main.rs)). -For information on writing tests see [bevy_compile_test_utils/README.md](../bevy_compile_test_utils/README.md). +For information on writing tests see [compile_fail_utils/README.md](../../../tools/compile_fail_utils/README.md). diff --git a/crates/bevy_reflect_compile_fail_tests/src/lib.rs b/crates/bevy_reflect/compile_fail/src/lib.rs similarity index 100% rename from crates/bevy_reflect_compile_fail_tests/src/lib.rs rename to crates/bevy_reflect/compile_fail/src/lib.rs diff --git a/crates/bevy_reflect/compile_fail/tests/derive.rs b/crates/bevy_reflect/compile_fail/tests/derive.rs new file mode 100644 index 0000000000000..1b1922254ceed --- /dev/null +++ b/crates/bevy_reflect/compile_fail/tests/derive.rs @@ -0,0 +1,3 @@ +fn main() -> compile_fail_utils::ui_test::Result<()> { + compile_fail_utils::test("tests/reflect_derive") +} diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/bounds_pass.rs b/crates/bevy_reflect/compile_fail/tests/reflect_derive/bounds_pass.rs similarity index 100% rename from crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/bounds_pass.rs rename to crates/bevy_reflect/compile_fail/tests/reflect_derive/bounds_pass.rs diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/custom_where_fail.rs b/crates/bevy_reflect/compile_fail/tests/reflect_derive/custom_where_fail.rs similarity index 100% rename from crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/custom_where_fail.rs rename to crates/bevy_reflect/compile_fail/tests/reflect_derive/custom_where_fail.rs diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/custom_where_fail.stderr b/crates/bevy_reflect/compile_fail/tests/reflect_derive/custom_where_fail.stderr similarity index 100% rename from crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/custom_where_fail.stderr rename to crates/bevy_reflect/compile_fail/tests/reflect_derive/custom_where_fail.stderr diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/custom_where_pass.rs b/crates/bevy_reflect/compile_fail/tests/reflect_derive/custom_where_pass.rs similarity index 100% rename from crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/custom_where_pass.rs rename to crates/bevy_reflect/compile_fail/tests/reflect_derive/custom_where_pass.rs diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/from_reflect_fail.rs b/crates/bevy_reflect/compile_fail/tests/reflect_derive/from_reflect_fail.rs similarity index 100% rename from crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/from_reflect_fail.rs rename to crates/bevy_reflect/compile_fail/tests/reflect_derive/from_reflect_fail.rs diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/from_reflect_fail.stderr b/crates/bevy_reflect/compile_fail/tests/reflect_derive/from_reflect_fail.stderr similarity index 100% rename from crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/from_reflect_fail.stderr rename to crates/bevy_reflect/compile_fail/tests/reflect_derive/from_reflect_fail.stderr diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/from_reflect_pass.rs b/crates/bevy_reflect/compile_fail/tests/reflect_derive/from_reflect_pass.rs similarity index 100% rename from crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/from_reflect_pass.rs rename to crates/bevy_reflect/compile_fail/tests/reflect_derive/from_reflect_pass.rs diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics_fail.rs b/crates/bevy_reflect/compile_fail/tests/reflect_derive/generics_fail.rs similarity index 100% rename from crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics_fail.rs rename to crates/bevy_reflect/compile_fail/tests/reflect_derive/generics_fail.rs diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics_fail.stderr b/crates/bevy_reflect/compile_fail/tests/reflect_derive/generics_fail.stderr similarity index 100% rename from crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics_fail.stderr rename to crates/bevy_reflect/compile_fail/tests/reflect_derive/generics_fail.stderr diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics_structs_pass.rs b/crates/bevy_reflect/compile_fail/tests/reflect_derive/generics_structs_pass.rs similarity index 100% rename from crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics_structs_pass.rs rename to crates/bevy_reflect/compile_fail/tests/reflect_derive/generics_structs_pass.rs diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/lifetimes_pass.rs b/crates/bevy_reflect/compile_fail/tests/reflect_derive/lifetimes_pass.rs similarity index 100% rename from crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/lifetimes_pass.rs rename to crates/bevy_reflect/compile_fail/tests/reflect_derive/lifetimes_pass.rs diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/lifetimes_pass.stderr b/crates/bevy_reflect/compile_fail/tests/reflect_derive/lifetimes_pass.stderr similarity index 100% rename from crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/lifetimes_pass.stderr rename to crates/bevy_reflect/compile_fail/tests/reflect_derive/lifetimes_pass.stderr diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/nested_pass.rs b/crates/bevy_reflect/compile_fail/tests/reflect_derive/nested_pass.rs similarity index 100% rename from crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/nested_pass.rs rename to crates/bevy_reflect/compile_fail/tests/reflect_derive/nested_pass.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/Cargo.toml b/crates/bevy_reflect/derive/Cargo.toml similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/Cargo.toml rename to crates/bevy_reflect/derive/Cargo.toml diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/container_attributes.rs b/crates/bevy_reflect/derive/src/container_attributes.rs similarity index 85% rename from crates/bevy_reflect/bevy_reflect_derive/src/container_attributes.rs rename to crates/bevy_reflect/derive/src/container_attributes.rs index 9d236d9b5b8a8..e87e565940074 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/container_attributes.rs +++ b/crates/bevy_reflect/derive/src/container_attributes.rs @@ -92,24 +92,6 @@ impl FromReflectAttrs { .map(|lit| lit.value()) .unwrap_or(true) } - - /// Merges this [`FromReflectAttrs`] with another. - pub fn merge(&mut self, other: FromReflectAttrs) -> Result<(), syn::Error> { - if let Some(new) = other.auto_derive { - if let Some(existing) = &self.auto_derive { - if existing.value() != new.value() { - return Err(syn::Error::new( - new.span(), - format!("`{FROM_REFLECT_ATTR}` already set to {}", existing.value()), - )); - } - } else { - self.auto_derive = Some(new); - } - } - - Ok(()) - } } /// A collection of attributes used for deriving `TypePath` via the `Reflect` derive. @@ -133,24 +115,6 @@ impl TypePathAttrs { .map(|lit| lit.value()) .unwrap_or(true) } - - /// Merges this [`TypePathAttrs`] with another. - pub fn merge(&mut self, other: TypePathAttrs) -> Result<(), syn::Error> { - if let Some(new) = other.auto_derive { - if let Some(existing) = &self.auto_derive { - if existing.value() != new.value() { - return Err(syn::Error::new( - new.span(), - format!("`{TYPE_PATH_ATTR}` already set to {}", existing.value()), - )); - } - } else { - self.auto_derive = Some(new); - } - } - - Ok(()) - } } /// A collection of traits that have been registered for a reflected type. @@ -231,14 +195,16 @@ impl ContainerAttributes { /// /// # Example /// - `Hash, Debug(custom_debug), MyTrait` - pub fn parse_terminated(input: ParseStream, trait_: ReflectTraitToImpl) -> syn::Result { - let mut this = Self::default(); - + pub fn parse_terminated( + &mut self, + input: ParseStream, + trait_: ReflectTraitToImpl, + ) -> syn::Result<()> { terminated_parser(Token![,], |stream| { - this.parse_container_attribute(stream, trait_) + self.parse_container_attribute(stream, trait_) })(input)?; - Ok(this) + Ok(()) } /// Parse the contents of a `#[reflect(...)]` attribute into a [`ContainerAttributes`] instance. @@ -246,8 +212,12 @@ impl ContainerAttributes { /// # Example /// - `#[reflect(Hash, Debug(custom_debug), MyTrait)]` /// - `#[reflect(no_field_bounds)]` - pub fn parse_meta_list(meta: &MetaList, trait_: ReflectTraitToImpl) -> syn::Result { - meta.parse_args_with(|stream: ParseStream| Self::parse_terminated(stream, trait_)) + pub fn parse_meta_list( + &mut self, + meta: &MetaList, + trait_: ReflectTraitToImpl, + ) -> syn::Result<()> { + meta.parse_args_with(|stream: ParseStream| self.parse_terminated(stream, trait_)) } /// Parse a single container attribute. @@ -392,7 +362,7 @@ impl ContainerAttributes { trait_: ReflectTraitToImpl, ) -> syn::Result<()> { let pair = input.parse::()?; - let value = extract_bool(&pair.value, |lit| { + let extracted_bool = extract_bool(&pair.value, |lit| { // Override `lit` if this is a `FromReflect` derive. // This typically means a user is opting out of the default implementation // from the `Reflect` derive and using the `FromReflect` derive directly instead. @@ -401,7 +371,16 @@ impl ContainerAttributes { .unwrap_or_else(|| lit.clone()) })?; - self.from_reflect_attrs.auto_derive = Some(value); + if let Some(existing) = &self.from_reflect_attrs.auto_derive { + if existing.value() != extracted_bool.value() { + return Err(syn::Error::new( + extracted_bool.span(), + format!("`{FROM_REFLECT_ATTR}` already set to {}", existing.value()), + )); + } + } else { + self.from_reflect_attrs.auto_derive = Some(extracted_bool); + } Ok(()) } @@ -416,7 +395,7 @@ impl ContainerAttributes { trait_: ReflectTraitToImpl, ) -> syn::Result<()> { let pair = input.parse::()?; - let value = extract_bool(&pair.value, |lit| { + let extracted_bool = extract_bool(&pair.value, |lit| { // Override `lit` if this is a `FromReflect` derive. // This typically means a user is opting out of the default implementation // from the `Reflect` derive and using the `FromReflect` derive directly instead. @@ -425,7 +404,16 @@ impl ContainerAttributes { .unwrap_or_else(|| lit.clone()) })?; - self.type_path_attrs.auto_derive = Some(value); + if let Some(existing) = &self.type_path_attrs.auto_derive { + if existing.value() != extracted_bool.value() { + return Err(syn::Error::new( + extracted_bool.span(), + format!("`{TYPE_PATH_ATTR}` already set to {}", existing.value()), + )); + } + } else { + self.type_path_attrs.auto_derive = Some(extracted_bool); + } Ok(()) } @@ -530,50 +518,6 @@ impl ContainerAttributes { pub fn no_field_bounds(&self) -> bool { self.no_field_bounds } - - /// Merges the trait implementations of this [`ContainerAttributes`] with another one. - /// - /// An error is returned if the two [`ContainerAttributes`] have conflicting implementations. - pub fn merge(&mut self, other: ContainerAttributes) -> Result<(), syn::Error> { - // Destructuring is used to help ensure that all fields are merged - let Self { - debug, - hash, - partial_eq, - from_reflect_attrs, - type_path_attrs, - custom_where, - no_field_bounds, - idents, - } = self; - - debug.merge(other.debug)?; - hash.merge(other.hash)?; - partial_eq.merge(other.partial_eq)?; - from_reflect_attrs.merge(other.from_reflect_attrs)?; - type_path_attrs.merge(other.type_path_attrs)?; - - Self::merge_custom_where(custom_where, other.custom_where); - - *no_field_bounds |= other.no_field_bounds; - - for ident in other.idents { - add_unique_ident(idents, ident)?; - } - Ok(()) - } - - fn merge_custom_where(this: &mut Option, other: Option) { - match (this, other) { - (Some(this), Some(other)) => { - this.predicates.extend(other.predicates); - } - (this @ None, Some(other)) => { - *this = Some(other); - } - _ => {} - } - } } /// Adds an identifier to a vector of identifiers if it is not already present. diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs b/crates/bevy_reflect/derive/src/derive_data.rs similarity index 98% rename from crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs rename to crates/bevy_reflect/derive/src/derive_data.rs index 48625ec3ca283..3c45b5ee3be15 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs +++ b/crates/bevy_reflect/derive/src/derive_data.rs @@ -183,7 +183,7 @@ impl<'a> ReflectDerive<'a> { input: &'a DeriveInput, provenance: ReflectProvenance, ) -> Result { - let mut traits = ContainerAttributes::default(); + let mut container_attributes = ContainerAttributes::default(); // Should indicate whether `#[reflect_value]` was used. let mut reflect_mode = None; // Should indicate whether `#[type_path = "..."]` was used. @@ -205,9 +205,7 @@ impl<'a> ReflectDerive<'a> { } reflect_mode = Some(ReflectMode::Normal); - let new_traits = - ContainerAttributes::parse_meta_list(meta_list, provenance.trait_)?; - traits.merge(new_traits)?; + container_attributes.parse_meta_list(meta_list, provenance.trait_)?; } Meta::List(meta_list) if meta_list.path.is_ident(REFLECT_VALUE_ATTRIBUTE_NAME) => { if !matches!(reflect_mode, None | Some(ReflectMode::Value)) { @@ -218,9 +216,7 @@ impl<'a> ReflectDerive<'a> { } reflect_mode = Some(ReflectMode::Value); - let new_traits = - ContainerAttributes::parse_meta_list(meta_list, provenance.trait_)?; - traits.merge(new_traits)?; + container_attributes.parse_meta_list(meta_list, provenance.trait_)?; } Meta::Path(path) if path.is_ident(REFLECT_VALUE_ATTRIBUTE_NAME) => { if !matches!(reflect_mode, None | Some(ReflectMode::Value)) { @@ -296,7 +292,7 @@ impl<'a> ReflectDerive<'a> { generics: &input.generics, }; - let meta = ReflectMeta::new(type_path, traits); + let meta = ReflectMeta::new(type_path, container_attributes); if provenance.source == ReflectImplSource::ImplRemoteType && meta.type_path_attrs().should_auto_derive() @@ -439,7 +435,7 @@ impl<'a> ReflectMeta<'a> { Self { docs, ..self } } - /// The registered reflect traits on this struct. + /// The registered reflect attributes on this struct. pub fn attrs(&self) -> &ContainerAttributes { &self.attrs } diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/documentation.rs b/crates/bevy_reflect/derive/src/documentation.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/documentation.rs rename to crates/bevy_reflect/derive/src/documentation.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/enum_utility.rs b/crates/bevy_reflect/derive/src/enum_utility.rs similarity index 76% rename from crates/bevy_reflect/bevy_reflect_derive/src/enum_utility.rs rename to crates/bevy_reflect/derive/src/enum_utility.rs index 36109bbd30c3f..c69a4ec4e245a 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/enum_utility.rs +++ b/crates/bevy_reflect/derive/src/enum_utility.rs @@ -21,7 +21,7 @@ pub(crate) struct EnumVariantConstructors { pub(crate) fn get_variant_constructors( reflect_enum: &ReflectEnum, ref_value: &Ident, - can_panic: bool, + return_apply_error: bool, ) -> EnumVariantConstructors { let bevy_reflect_path = reflect_enum.meta().bevy_reflect_path(); let variant_count = reflect_enum.variants().len(); @@ -50,21 +50,6 @@ pub(crate) fn get_variant_constructors( _ => quote! { #FQDefault::default() } } } else { - let (resolve_error, resolve_missing) = if can_panic { - let field_ref_str = match &field_ident { - Member::Named(ident) => format!("the field `{ident}`"), - Member::Unnamed(index) => format!("the field at index {}", index.index) - }; - let ty = field.data.ty.to_token_stream(); - - let on_error = format!("{field_ref_str} should be of type `{ty}`"); - let on_missing = format!("{field_ref_str} is required but could not be found"); - - (quote!(.expect(#on_error)), quote!(.expect(#on_missing))) - } else { - (quote!(?), quote!(?)) - }; - let field_accessor = match &field.data.ident { Some(ident) => { let name = ident.to_string(); @@ -74,6 +59,31 @@ pub(crate) fn get_variant_constructors( }; reflect_index += 1; + let (resolve_error, resolve_missing) = if return_apply_error { + let field_ref_str = match &field_ident { + Member::Named(ident) => format!("{ident}"), + Member::Unnamed(index) => format!(".{}", index.index) + }; + let ty = field.data.ty.to_token_stream(); + + ( + quote!(.ok_or(#bevy_reflect_path::ApplyError::MismatchedTypes { + // The unwrap won't panic. By this point the #field_accessor would have been invoked once and any failure to + // access the given field handled by the `resolve_missing` code bellow. + from_type: ::core::convert::Into::into( + #bevy_reflect_path::DynamicTypePath::reflect_type_path(#FQOption::unwrap(#field_accessor)) + ), + to_type: ::core::convert::Into::into(<#ty as #bevy_reflect_path::TypePath>::type_path()) + })?), + quote!(.ok_or(#bevy_reflect_path::ApplyError::MissingEnumField { + variant_name: ::core::convert::Into::into(#name), + field_name: ::core::convert::Into::into(#field_ref_str) + })?) + ) + } else { + (quote!(?), quote!(?)) + }; + match &field.attrs.default { DefaultBehavior::Func(path) => quote! { if let #FQOption::Some(field) = #field_accessor { diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/field_attributes.rs b/crates/bevy_reflect/derive/src/field_attributes.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/field_attributes.rs rename to crates/bevy_reflect/derive/src/field_attributes.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/from_reflect.rs b/crates/bevy_reflect/derive/src/from_reflect.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/from_reflect.rs rename to crates/bevy_reflect/derive/src/from_reflect.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/enums.rs b/crates/bevy_reflect/derive/src/impls/enums.rs similarity index 91% rename from crates/bevy_reflect/bevy_reflect_derive/src/impls/enums.rs rename to crates/bevy_reflect/derive/src/impls/enums.rs index 056a293ec91be..8293da7157785 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/impls/enums.rs +++ b/crates/bevy_reflect/derive/src/impls/enums.rs @@ -230,7 +230,7 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream } #[inline] - fn apply(&mut self, #ref_value: &dyn #bevy_reflect_path::Reflect) { + fn try_apply(&mut self, #ref_value: &dyn #bevy_reflect_path::Reflect) -> #FQResult<(), #bevy_reflect_path::ApplyError> { if let #bevy_reflect_path::ReflectRef::Enum(#ref_value) = #bevy_reflect_path::Reflect::reflect_ref(#ref_value) { if #bevy_reflect_path::Enum::variant_name(self) == #bevy_reflect_path::Enum::variant_name(#ref_value) { // Same variant -> just update fields @@ -238,12 +238,16 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream #bevy_reflect_path::VariantType::Struct => { for field in #bevy_reflect_path::Enum::iter_fields(#ref_value) { let name = field.name().unwrap(); - #bevy_reflect_path::Enum::field_mut(self, name).map(|v| v.apply(field.value())); + if let #FQOption::Some(v) = #bevy_reflect_path::Enum::field_mut(self, name) { + #bevy_reflect_path::Reflect::try_apply(v, field.value())?; + } } } #bevy_reflect_path::VariantType::Tuple => { for (index, field) in ::core::iter::Iterator::enumerate(#bevy_reflect_path::Enum::iter_fields(#ref_value)) { - #bevy_reflect_path::Enum::field_at_mut(self, index).map(|v| v.apply(field.value())); + if let #FQOption::Some(v) = #bevy_reflect_path::Enum::field_at_mut(self, index) { + #bevy_reflect_path::Reflect::try_apply(v, field.value())?; + } } } _ => {} @@ -254,12 +258,25 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream #(#variant_names => { *self = #variant_constructors })* - name => panic!("variant with name `{}` does not exist on enum `{}`", name, ::type_path()), + name => { + return #FQResult::Err( + #bevy_reflect_path::ApplyError::UnknownVariant { + enum_name: ::core::convert::Into::into(#bevy_reflect_path::DynamicTypePath::reflect_type_path(self)), + variant_name: ::core::convert::Into::into(name), + } + ); + } } } } else { - panic!("`{}` is not an enum", #bevy_reflect_path::DynamicTypePath::reflect_type_path(#ref_value)); + return #FQResult::Err( + #bevy_reflect_path::ApplyError::MismatchedKinds { + from_kind: #bevy_reflect_path::Reflect::reflect_kind(#ref_value), + to_kind: #bevy_reflect_path::ReflectKind::Enum, + } + ); } + #FQResult::Ok(()) } fn reflect_kind(&self) -> #bevy_reflect_path::ReflectKind { diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/mod.rs b/crates/bevy_reflect/derive/src/impls/mod.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/impls/mod.rs rename to crates/bevy_reflect/derive/src/impls/mod.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/structs.rs b/crates/bevy_reflect/derive/src/impls/structs.rs similarity index 91% rename from crates/bevy_reflect/bevy_reflect_derive/src/impls/structs.rs rename to crates/bevy_reflect/derive/src/impls/structs.rs index 40441aafbcd7e..f51f0b2de4d23 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/impls/structs.rs +++ b/crates/bevy_reflect/derive/src/impls/structs.rs @@ -208,29 +208,37 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS } #[inline] - fn apply(&mut self, value: &dyn #bevy_reflect_path::Reflect) { + fn try_apply(&mut self, value: &dyn #bevy_reflect_path::Reflect) -> #FQResult<(), #bevy_reflect_path::ApplyError> { if let #bevy_reflect_path::ReflectRef::Struct(struct_value) = #bevy_reflect_path::Reflect::reflect_ref(value) { for (i, value) in ::core::iter::Iterator::enumerate(#bevy_reflect_path::Struct::iter_fields(struct_value)) { let name = #bevy_reflect_path::Struct::name_at(struct_value, i).unwrap(); - #bevy_reflect_path::Struct::field_mut(self, name).map(|v| v.apply(value)); + if let #FQOption::Some(v) = #bevy_reflect_path::Struct::field_mut(self, name) { + #bevy_reflect_path::Reflect::try_apply(v, value)?; + } } } else { - panic!("Attempted to apply non-struct type to struct type."); + return #FQResult::Err( + #bevy_reflect_path::ApplyError::MismatchedKinds { + from_kind: #bevy_reflect_path::Reflect::reflect_kind(value), + to_kind: #bevy_reflect_path::ReflectKind::Struct + } + ); } + #FQResult::Ok(()) } - + #[inline] fn reflect_kind(&self) -> #bevy_reflect_path::ReflectKind { #bevy_reflect_path::ReflectKind::Struct } - + #[inline] fn reflect_ref(&self) -> #bevy_reflect_path::ReflectRef { #bevy_reflect_path::ReflectRef::Struct(self) } - + #[inline] fn reflect_mut(&mut self) -> #bevy_reflect_path::ReflectMut { #bevy_reflect_path::ReflectMut::Struct(self) } - + #[inline] fn reflect_owned(self: #FQBox) -> #bevy_reflect_path::ReflectOwned { #bevy_reflect_path::ReflectOwned::Struct(self) } diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/tuple_structs.rs b/crates/bevy_reflect/derive/src/impls/tuple_structs.rs similarity index 89% rename from crates/bevy_reflect/bevy_reflect_derive/src/impls/tuple_structs.rs rename to crates/bevy_reflect/derive/src/impls/tuple_structs.rs index 659261466cff5..255928cf97c6e 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/impls/tuple_structs.rs +++ b/crates/bevy_reflect/derive/src/impls/tuple_structs.rs @@ -112,11 +112,11 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: _ => #FQOption::None, } } - + #[inline] fn field_len(&self) -> usize { #field_count } - + #[inline] fn iter_fields(&self) -> #bevy_reflect_path::TupleStructFieldIter { #bevy_reflect_path::TupleStructFieldIter::new(self) } @@ -177,28 +177,36 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: } #[inline] - fn apply(&mut self, value: &dyn #bevy_reflect_path::Reflect) { + fn try_apply(&mut self, value: &dyn #bevy_reflect_path::Reflect) -> #FQResult<(), #bevy_reflect_path::ApplyError> { if let #bevy_reflect_path::ReflectRef::TupleStruct(struct_value) = #bevy_reflect_path::Reflect::reflect_ref(value) { for (i, value) in ::core::iter::Iterator::enumerate(#bevy_reflect_path::TupleStruct::iter_fields(struct_value)) { - #bevy_reflect_path::TupleStruct::field_mut(self, i).map(|v| v.apply(value)); + if let #FQOption::Some(v) = #bevy_reflect_path::TupleStruct::field_mut(self, i) { + #bevy_reflect_path::Reflect::try_apply(v, value)?; + } } } else { - panic!("Attempted to apply non-TupleStruct type to TupleStruct type."); + return #FQResult::Err( + #bevy_reflect_path::ApplyError::MismatchedKinds { + from_kind: #bevy_reflect_path::Reflect::reflect_kind(value), + to_kind: #bevy_reflect_path::ReflectKind::TupleStruct, + } + ); } + #FQResult::Ok(()) } - + #[inline] fn reflect_kind(&self) -> #bevy_reflect_path::ReflectKind { #bevy_reflect_path::ReflectKind::TupleStruct } - + #[inline] fn reflect_ref(&self) -> #bevy_reflect_path::ReflectRef { #bevy_reflect_path::ReflectRef::TupleStruct(self) } - + #[inline] fn reflect_mut(&mut self) -> #bevy_reflect_path::ReflectMut { #bevy_reflect_path::ReflectMut::TupleStruct(self) } - + #[inline] fn reflect_owned(self: #FQBox) -> #bevy_reflect_path::ReflectOwned { #bevy_reflect_path::ReflectOwned::TupleStruct(self) } diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/typed.rs b/crates/bevy_reflect/derive/src/impls/typed.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/impls/typed.rs rename to crates/bevy_reflect/derive/src/impls/typed.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/values.rs b/crates/bevy_reflect/derive/src/impls/values.rs similarity index 84% rename from crates/bevy_reflect/bevy_reflect_derive/src/impls/values.rs rename to crates/bevy_reflect/derive/src/impls/values.rs index cb9162cc4a734..c0e7b2d4fee44 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/impls/values.rs +++ b/crates/bevy_reflect/derive/src/impls/values.rs @@ -85,14 +85,20 @@ pub(crate) fn impl_value(meta: &ReflectMeta) -> proc_macro2::TokenStream { #FQBox::new(#FQClone::clone(self)) } - #[inline] - fn apply(&mut self, value: &dyn #bevy_reflect_path::Reflect) { - let value = #bevy_reflect_path::Reflect::as_any(value); - if let #FQOption::Some(value) = ::downcast_ref::(value) { + #[inline] + fn try_apply(&mut self, value: &dyn #bevy_reflect_path::Reflect) -> #FQResult<(), #bevy_reflect_path::ApplyError> { + let any = #bevy_reflect_path::Reflect::as_any(value); + if let #FQOption::Some(value) = ::downcast_ref::(any) { *self = #FQClone::clone(value); } else { - panic!("Value is not {}.", ::type_path()); + return #FQResult::Err( + #bevy_reflect_path::ApplyError::MismatchedTypes { + from_type: ::core::convert::Into::into(#bevy_reflect_path::DynamicTypePath::reflect_type_path(value)), + to_type: ::core::convert::Into::into(::type_path()), + } + ); } + #FQResult::Ok(()) } #[inline] @@ -101,18 +107,22 @@ pub(crate) fn impl_value(meta: &ReflectMeta) -> proc_macro2::TokenStream { #FQResult::Ok(()) } + #[inline] fn reflect_kind(&self) -> #bevy_reflect_path::ReflectKind { #bevy_reflect_path::ReflectKind::Value } + #[inline] fn reflect_ref(&self) -> #bevy_reflect_path::ReflectRef { #bevy_reflect_path::ReflectRef::Value(self) } + #[inline] fn reflect_mut(&mut self) -> #bevy_reflect_path::ReflectMut { #bevy_reflect_path::ReflectMut::Value(self) } + #[inline] fn reflect_owned(self: #FQBox) -> #bevy_reflect_path::ReflectOwned { #bevy_reflect_path::ReflectOwned::Value(self) } diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/lib.rs b/crates/bevy_reflect/derive/src/lib.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/lib.rs rename to crates/bevy_reflect/derive/src/lib.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/reflect_value.rs b/crates/bevy_reflect/derive/src/reflect_value.rs similarity index 91% rename from crates/bevy_reflect/bevy_reflect_derive/src/reflect_value.rs rename to crates/bevy_reflect/derive/src/reflect_value.rs index e924401a93351..6faa0e8752d45 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/reflect_value.rs +++ b/crates/bevy_reflect/derive/src/reflect_value.rs @@ -55,7 +55,11 @@ impl ReflectValueDef { if input.peek(Paren) { let content; parenthesized!(content in input); - traits = Some(ContainerAttributes::parse_terminated(&content, trait_)?); + traits = Some({ + let mut attrs = ContainerAttributes::default(); + attrs.parse_terminated(&content, trait_)?; + attrs + }); } Ok(ReflectValueDef { attrs, diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/registration.rs b/crates/bevy_reflect/derive/src/registration.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/registration.rs rename to crates/bevy_reflect/derive/src/registration.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/serialization.rs b/crates/bevy_reflect/derive/src/serialization.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/serialization.rs rename to crates/bevy_reflect/derive/src/serialization.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/trait_reflection.rs b/crates/bevy_reflect/derive/src/trait_reflection.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/trait_reflection.rs rename to crates/bevy_reflect/derive/src/trait_reflection.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/type_path.rs b/crates/bevy_reflect/derive/src/type_path.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/type_path.rs rename to crates/bevy_reflect/derive/src/type_path.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/utility.rs b/crates/bevy_reflect/derive/src/utility.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/utility.rs rename to crates/bevy_reflect/derive/src/utility.rs diff --git a/crates/bevy_reflect/src/array.rs b/crates/bevy_reflect/src/array.rs index 0b704a21f77e9..f5f1158c20f11 100644 --- a/crates/bevy_reflect/src/array.rs +++ b/crates/bevy_reflect/src/array.rs @@ -1,6 +1,6 @@ use crate::{ - self as bevy_reflect, utility::reflect_hasher, Reflect, ReflectKind, ReflectMut, ReflectOwned, - ReflectRef, TypeInfo, TypePath, TypePathTable, + self as bevy_reflect, utility::reflect_hasher, ApplyError, Reflect, ReflectKind, ReflectMut, + ReflectOwned, ReflectRef, TypeInfo, TypePath, TypePathTable, }; use bevy_reflect_derive::impl_type_path; use std::{ @@ -262,6 +262,10 @@ impl Reflect for DynamicArray { array_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + array_try_apply(self, value) + } + #[inline] fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; @@ -374,7 +378,7 @@ impl<'a> Iterator for ArrayIter<'a> { #[inline] fn next(&mut self) -> Option { let value = self.array.get(self.index); - self.index += 1; + self.index += value.is_some() as usize; value } @@ -421,6 +425,38 @@ pub fn array_apply(array: &mut A, reflect: &dyn Reflect) { } } +/// Tries to apply the reflected [array](Array) data to the given [array](Array) and +/// returns a Result. +/// +/// # Errors +/// +/// * Returns an [`ApplyError::DifferentSize`] if the two arrays have differing lengths. +/// * Returns an [`ApplyError::MismatchedKinds`] if the reflected value is not a +/// [valid array](ReflectRef::Array). +/// * Returns any error that is generated while applying elements to each other. +/// +#[inline] +pub fn array_try_apply(array: &mut A, reflect: &dyn Reflect) -> Result<(), ApplyError> { + if let ReflectRef::Array(reflect_array) = reflect.reflect_ref() { + if array.len() != reflect_array.len() { + return Err(ApplyError::DifferentSize { + from_size: reflect_array.len(), + to_size: array.len(), + }); + } + for (i, value) in reflect_array.iter().enumerate() { + let v = array.get_mut(i).unwrap(); + v.try_apply(value)?; + } + } else { + return Err(ApplyError::MismatchedKinds { + from_kind: reflect.reflect_kind(), + to_kind: ReflectKind::Array, + }); + } + Ok(()) +} + /// Compares two [arrays](Array) (one concrete and one reflected) to see if they /// are equal. /// @@ -467,3 +503,32 @@ pub fn array_debug(dyn_array: &dyn Array, f: &mut std::fmt::Formatter<'_>) -> st } debug.finish() } +#[cfg(test)] +mod tests { + use crate::{Reflect, ReflectRef}; + #[test] + fn next_index_increment() { + const SIZE: usize = if cfg!(debug_assertions) { + 4 + } else { + // If compiled in release mode, verify we dont overflow + usize::MAX + }; + + let b = Box::new([(); SIZE]).into_reflect(); + + let ReflectRef::Array(array) = b.reflect_ref() else { + panic!("Not an array..."); + }; + + let mut iter = array.iter(); + iter.index = SIZE - 1; + assert!(iter.next().is_some()); + + // When None we should no longer increase index + assert!(iter.next().is_none()); + assert!(iter.index == SIZE); + assert!(iter.next().is_none()); + assert!(iter.index == SIZE); + } +} diff --git a/crates/bevy_reflect/src/enums/dynamic_enum.rs b/crates/bevy_reflect/src/enums/dynamic_enum.rs index d01c351448ae0..7d0bdccc1289b 100644 --- a/crates/bevy_reflect/src/enums/dynamic_enum.rs +++ b/crates/bevy_reflect/src/enums/dynamic_enum.rs @@ -1,9 +1,9 @@ use bevy_reflect_derive::impl_type_path; use crate::{ - self as bevy_reflect, enum_debug, enum_hash, enum_partial_eq, DynamicStruct, DynamicTuple, - Enum, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, Struct, Tuple, TypeInfo, - VariantFieldIter, VariantType, + self as bevy_reflect, enum_debug, enum_hash, enum_partial_eq, ApplyError, DynamicStruct, + DynamicTuple, Enum, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, Struct, Tuple, + TypeInfo, VariantFieldIter, VariantType, }; use std::any::Any; use std::fmt::Formatter; @@ -324,7 +324,7 @@ impl Reflect for DynamicEnum { } #[inline] - fn apply(&mut self, value: &dyn Reflect) { + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { if let ReflectRef::Enum(value) = value.reflect_ref() { if Enum::variant_name(self) == value.variant_name() { // Same variant -> just update fields @@ -333,14 +333,14 @@ impl Reflect for DynamicEnum { for field in value.iter_fields() { let name = field.name().unwrap(); if let Some(v) = Enum::field_mut(self, name) { - v.apply(field.value()); + v.try_apply(field.value())?; } } } VariantType::Tuple => { for (index, field) in value.iter_fields().enumerate() { if let Some(v) = Enum::field_at_mut(self, index) { - v.apply(field.value()); + v.try_apply(field.value())?; } } } @@ -369,8 +369,12 @@ impl Reflect for DynamicEnum { self.set_variant(value.variant_name(), dyn_variant); } } else { - panic!("`{}` is not an enum", value.reflect_type_path()); + return Err(ApplyError::MismatchedKinds { + from_kind: value.reflect_kind(), + to_kind: ReflectKind::Enum, + }); } + Ok(()) } #[inline] diff --git a/crates/bevy_reflect/src/enums/enum_trait.rs b/crates/bevy_reflect/src/enums/enum_trait.rs index e6cb09e83ab9c..66029923da25b 100644 --- a/crates/bevy_reflect/src/enums/enum_trait.rs +++ b/crates/bevy_reflect/src/enums/enum_trait.rs @@ -280,7 +280,7 @@ impl<'a> Iterator for VariantFieldIter<'a> { Some(VariantField::Struct(name, self.container.field(name)?)) } }; - self.index += 1; + self.index += value.is_some() as usize; value } @@ -307,8 +307,63 @@ impl<'a> VariantField<'a> { } pub fn value(&self) -> &'a dyn Reflect { - match self { - Self::Struct(.., value) | Self::Tuple(value) => *value, + match *self { + Self::Struct(_, value) | Self::Tuple(value) => value, + } + } +} + +// Tests that need access to internal fields have to go here rather than in mod.rs +#[cfg(test)] +mod tests { + use crate as bevy_reflect; + use crate::*; + + #[derive(Reflect, Debug, PartialEq)] + enum MyEnum { + A, + B(usize, i32), + C { foo: f32, bar: bool }, + } + #[test] + fn next_index_increment() { + // unit enums always return none, so index should stay at 0 + let unit_enum = MyEnum::A; + let mut iter = unit_enum.iter_fields(); + let size = iter.len(); + for _ in 0..2 { + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); + } + // tuple enums we iter over each value (unnamed fields), stop after that + let tuple_enum = MyEnum::B(0, 1); + let mut iter = tuple_enum.iter_fields(); + let size = iter.len(); + for _ in 0..2 { + let prev_index = iter.index; + assert!(iter.next().is_some()); + assert_eq!(prev_index, iter.index - 1); + } + for _ in 0..2 { + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); + } + + // struct enums, we iterate over each field in the struct + let struct_enum = MyEnum::C { + foo: 0., + bar: false, + }; + let mut iter = struct_enum.iter_fields(); + let size = iter.len(); + for _ in 0..2 { + let prev_index = iter.index; + assert!(iter.next().is_some()); + assert_eq!(prev_index, iter.index - 1); + } + for _ in 0..2 { + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); } } } diff --git a/crates/bevy_reflect/src/enums/mod.rs b/crates/bevy_reflect/src/enums/mod.rs index b78af06f86729..2cb77af571fcd 100644 --- a/crates/bevy_reflect/src/enums/mod.rs +++ b/crates/bevy_reflect/src/enums/mod.rs @@ -283,7 +283,9 @@ mod tests { } #[test] - #[should_panic(expected = "`bevy_reflect::DynamicTuple` is not an enum")] + #[should_panic( + expected = "called `Result::unwrap()` on an `Err` value: MismatchedKinds { from_kind: Tuple, to_kind: Enum }" + )] fn applying_non_enum_should_panic() { let mut value = MyEnum::B(0, 0); let mut dyn_tuple = DynamicTuple::default(); @@ -291,6 +293,38 @@ mod tests { value.apply(&dyn_tuple); } + #[test] + fn enum_try_apply_should_detect_type_mismatch() { + #[derive(Reflect, Debug, PartialEq)] + enum MyEnumAnalogue { + A(u32), + B(usize, usize), + C { foo: f32, bar: u8 }, + } + + let mut target = MyEnumAnalogue::A(0); + + // === Tuple === // + let result = target.try_apply(&MyEnum::B(0, 1)); + assert!( + matches!(result, Err(ApplyError::MismatchedTypes { .. })), + "`result` was {result:?}" + ); + + // === Struct === // + target = MyEnumAnalogue::C { foo: 0.0, bar: 1 }; + let result = target.try_apply(&MyEnum::C { + foo: 1.0, + bar: true, + }); + assert!( + matches!(result, Err(ApplyError::MismatchedTypes { .. })), + "`result` was {result:?}" + ); + // Type mismatch should occur after partial application. + assert_eq!(target, MyEnumAnalogue::C { foo: 1.0, bar: 1 }); + } + #[test] fn should_skip_ignored_fields() { #[derive(Reflect, Debug, PartialEq)] diff --git a/crates/bevy_reflect/src/impls/math/cubic_splines.rs b/crates/bevy_reflect/src/impls/math/cubic_splines.rs new file mode 100644 index 0000000000000..2d14f1b6248b1 --- /dev/null +++ b/crates/bevy_reflect/src/impls/math/cubic_splines.rs @@ -0,0 +1,89 @@ +use crate as bevy_reflect; +use bevy_math::{cubic_splines::*, VectorSpace}; +use bevy_reflect::std_traits::ReflectDefault; +use bevy_reflect_derive::impl_reflect; + +impl_reflect!( + #[reflect(Debug)] + #[type_path = "bevy_math::cubic_splines"] + struct CubicBezier { + control_points: Vec<[P; 4]>, + } +); + +impl_reflect!( + #[reflect(Debug)] + #[type_path = "bevy_math::cubic_splines"] + struct CubicHermite { + control_points: Vec<(P, P)>, + } +); + +impl_reflect!( + #[reflect(Debug)] + #[type_path = "bevy_math::cubic_splines"] + struct CubicCardinalSpline { + tension: f32, + control_points: Vec

, + } +); + +impl_reflect!( + #[reflect(Debug)] + #[type_path = "bevy_math::cubic_splines"] + struct CubicBSpline { + control_points: Vec

, + } +); + +impl_reflect!( + #[reflect(Debug)] + #[type_path = "bevy_math::cubic_splines"] + struct CubicNurbs { + control_points: Vec

, + weights: Vec, + knots: Vec, + } +); + +impl_reflect!( + #[reflect(Debug)] + #[type_path = "bevy_math::cubic_splines"] + struct LinearSpline { + points: Vec

, + } +); + +impl_reflect!( + #[reflect(Debug, Default)] + #[type_path = "bevy_math::cubic_splines"] + struct CubicSegment { + coeff: [P; 4], + } +); + +impl_reflect!( + #[reflect(Debug)] + #[type_path = "bevy_math::cubic_splines"] + struct CubicCurve { + segments: Vec>, + } +); + +impl_reflect!( + #[reflect(Debug, Default)] + #[type_path = "bevy_math::cubic_splines"] + struct RationalSegment { + coeff: [P; 4], + weight_coeff: [f32; 4], + knot_span: f32, + } +); + +impl_reflect!( + #[reflect(Debug)] + #[type_path = "bevy_math::cubic_splines"] + struct RationalCurve { + segments: Vec>, + } +); diff --git a/crates/bevy_reflect/src/impls/smallvec.rs b/crates/bevy_reflect/src/impls/smallvec.rs index b27c7a1e1ae9f..3943ac269bb86 100644 --- a/crates/bevy_reflect/src/impls/smallvec.rs +++ b/crates/bevy_reflect/src/impls/smallvec.rs @@ -5,9 +5,9 @@ use std::any::Any; use crate::utility::GenericTypeInfoCell; use crate::{ - self as bevy_reflect, FromReflect, FromType, GetTypeRegistration, List, ListInfo, ListIter, - Reflect, ReflectFromPtr, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, TypePath, - TypeRegistration, Typed, + self as bevy_reflect, ApplyError, FromReflect, FromType, GetTypeRegistration, List, ListInfo, + ListIter, Reflect, ReflectFromPtr, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, + TypePath, TypeRegistration, Typed, }; impl List for SmallVec @@ -113,6 +113,10 @@ where crate::list_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + crate::list_try_apply(self, value) + } + fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs index 55bdd75e36fd9..bcee1b51ee6e8 100644 --- a/crates/bevy_reflect/src/impls/std.rs +++ b/crates/bevy_reflect/src/impls/std.rs @@ -1,15 +1,15 @@ use crate::std_traits::ReflectDefault; -use crate::{self as bevy_reflect, ReflectFromPtr, ReflectFromReflect, ReflectOwned, TypeRegistry}; -use crate::{ - impl_type_path, map_apply, map_partial_eq, Array, ArrayInfo, ArrayIter, DynamicMap, - FromReflect, FromType, GetTypeRegistration, List, ListInfo, ListIter, Map, MapInfo, MapIter, - Reflect, ReflectDeserialize, ReflectKind, ReflectMut, ReflectRef, ReflectSerialize, TypeInfo, - TypePath, TypeRegistration, Typed, ValueInfo, -}; - use crate::utility::{ reflect_hasher, GenericTypeInfoCell, GenericTypePathCell, NonGenericTypeInfoCell, }; +use crate::{ + self as bevy_reflect, impl_type_path, map_apply, map_partial_eq, map_try_apply, ApplyError, + Array, ArrayInfo, ArrayIter, DynamicMap, DynamicTypePath, FromReflect, FromType, + GetTypeRegistration, List, ListInfo, ListIter, Map, MapInfo, MapIter, Reflect, + ReflectDeserialize, ReflectFromPtr, ReflectFromReflect, ReflectKind, ReflectMut, ReflectOwned, + ReflectRef, ReflectSerialize, TypeInfo, TypePath, TypeRegistration, TypeRegistry, Typed, + ValueInfo, +}; use bevy_reflect_derive::{impl_reflect, impl_reflect_value}; use std::fmt; use std::{ @@ -314,6 +314,10 @@ macro_rules! impl_reflect_for_veclike { crate::list_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + crate::list_try_apply(self, value) + } + fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) @@ -540,6 +544,10 @@ macro_rules! impl_reflect_for_hashmap { map_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + map_try_apply(self, value) + } + fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) @@ -765,6 +773,10 @@ where map_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + map_try_apply(self, value) + } + fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) @@ -909,6 +921,11 @@ impl Reflect for [T crate::array_apply(self, value); } + #[inline] + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + crate::array_try_apply(self, value) + } + #[inline] fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; @@ -1063,13 +1080,18 @@ impl Reflect for Cow<'static, str> { self } - fn apply(&mut self, value: &dyn Reflect) { - let value = value.as_any(); - if let Some(value) = value.downcast_ref::() { + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + let any = value.as_any(); + if let Some(value) = any.downcast_ref::() { self.clone_from(value); } else { - panic!("Value is not a {}.", Self::type_path()); + return Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + // If we invoke the reflect_type_path on self directly the borrow checker complains that the lifetime of self must outlive 'static + to_type: Self::type_path().into(), + }); } + Ok(()) } fn set(&mut self, value: Box) -> Result<(), Box> { @@ -1253,6 +1275,10 @@ impl Reflect for Cow<'s crate::list_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + crate::list_try_apply(self, value) + } + fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) @@ -1349,13 +1375,17 @@ impl Reflect for &'static str { self } - fn apply(&mut self, value: &dyn Reflect) { - let value = value.as_any(); - if let Some(&value) = value.downcast_ref::() { + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + let any = value.as_any(); + if let Some(&value) = any.downcast_ref::() { *self = value; } else { - panic!("Value is not a {}.", Self::type_path()); + return Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: Self::type_path().into(), + }); } + Ok(()) } fn set(&mut self, value: Box) -> Result<(), Box> { @@ -1451,12 +1481,16 @@ impl Reflect for &'static Path { self } - fn apply(&mut self, value: &dyn Reflect) { - let value = value.as_any(); - if let Some(&value) = value.downcast_ref::() { + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + let any = value.as_any(); + if let Some(&value) = any.downcast_ref::() { *self = value; + Ok(()) } else { - panic!("Value is not a {}.", Self::type_path()); + Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: ::reflect_type_path(self).into(), + }) } } @@ -1552,12 +1586,16 @@ impl Reflect for Cow<'static, Path> { self } - fn apply(&mut self, value: &dyn Reflect) { - let value = value.as_any(); - if let Some(value) = value.downcast_ref::() { + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + let any = value.as_any(); + if let Some(value) = any.downcast_ref::() { self.clone_from(value); + Ok(()) } else { - panic!("Value is not a {}.", Self::type_path()); + Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: ::reflect_type_path(self).into(), + }) } } @@ -1789,14 +1827,14 @@ mod tests { // === None on None === // let patch = None::; - let mut value = None; + let mut value = None::; Reflect::apply(&mut value, &patch); assert_eq!(patch, value, "None apply onto None"); // === Some on None === // let patch = Some(Foo(123)); - let mut value = None; + let mut value = None::; Reflect::apply(&mut value, &patch); assert_eq!(patch, value, "Some apply onto None"); diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index 073fb5000bfee..aa4918de51097 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -172,7 +172,7 @@ //! ## Patching //! //! These dynamic types come in handy when needing to apply multiple changes to another type. -//! This is known as "patching" and is done using the [`Reflect::apply`] method. +//! This is known as "patching" and is done using the [`Reflect::apply`] and [`Reflect::try_apply`] methods. //! //! ``` //! # use bevy_reflect::{DynamicEnum, Reflect}; @@ -493,6 +493,7 @@ mod impls { #[cfg(feature = "bevy_math")] mod math { + mod cubic_splines; mod direction; mod primitives2d; mod primitives3d; @@ -613,6 +614,54 @@ mod tests { use crate::serde::{ReflectDeserializer, ReflectSerializer}; use crate::utility::GenericTypePathCell; + #[test] + fn try_apply_should_detect_kinds() { + #[derive(Reflect, Debug)] + struct Struct { + a: u32, + b: f32, + } + + #[derive(Reflect, Debug)] + enum Enum { + A, + B(u32), + } + + let mut struct_target = Struct { + a: 0xDEADBEEF, + b: 3.14, + }; + + let mut enum_target = Enum::A; + + let array_src = [8, 0, 8]; + + let result = struct_target.try_apply(&enum_target); + assert!( + matches!( + result, + Err(ApplyError::MismatchedKinds { + from_kind: ReflectKind::Enum, + to_kind: ReflectKind::Struct + }) + ), + "result was {result:?}" + ); + + let result = enum_target.try_apply(&array_src); + assert!( + matches!( + result, + Err(ApplyError::MismatchedKinds { + from_kind: ReflectKind::Array, + to_kind: ReflectKind::Enum + }) + ), + "result was {result:?}" + ); + } + #[test] fn reflect_struct() { #[derive(Reflect)] diff --git a/crates/bevy_reflect/src/list.rs b/crates/bevy_reflect/src/list.rs index bc423132c02ea..5b786ee76a991 100644 --- a/crates/bevy_reflect/src/list.rs +++ b/crates/bevy_reflect/src/list.rs @@ -6,8 +6,8 @@ use bevy_reflect_derive::impl_type_path; use crate::utility::reflect_hasher; use crate::{ - self as bevy_reflect, FromReflect, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, - TypeInfo, TypePath, TypePathTable, + self as bevy_reflect, ApplyError, FromReflect, Reflect, ReflectKind, ReflectMut, ReflectOwned, + ReflectRef, TypeInfo, TypePath, TypePathTable, }; /// A trait used to power [list-like] operations via [reflection]. @@ -312,6 +312,10 @@ impl Reflect for DynamicList { list_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + list_try_apply(self, value) + } + #[inline] fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; @@ -401,7 +405,7 @@ impl<'a> Iterator for ListIter<'a> { #[inline] fn next(&mut self) -> Option { let value = self.list.get(self.index); - self.index += 1; + self.index += value.is_some() as usize; value } @@ -436,19 +440,40 @@ pub fn list_hash(list: &L) -> Option { /// This function panics if `b` is not a list. #[inline] pub fn list_apply(a: &mut L, b: &dyn Reflect) { + if let Err(err) = list_try_apply(a, b) { + panic!("{err}"); + } +} + +/// Tries to apply the elements of `b` to the corresponding elements of `a` and +/// returns a Result. +/// +/// If the length of `b` is greater than that of `a`, the excess elements of `b` +/// are cloned and appended to `a`. +/// +/// # Errors +/// +/// This function returns an [`ApplyError::MismatchedKinds`] if `b` is not a list or if +/// applying elements to each other fails. +#[inline] +pub fn list_try_apply(a: &mut L, b: &dyn Reflect) -> Result<(), ApplyError> { if let ReflectRef::List(list_value) = b.reflect_ref() { for (i, value) in list_value.iter().enumerate() { if i < a.len() { if let Some(v) = a.get_mut(i) { - v.apply(value); + v.try_apply(value)?; } } else { - a.push(value.clone_value()); + List::push(a, value.clone_value()); } } } else { - panic!("Attempted to apply a non-list type to a list type."); + return Err(ApplyError::MismatchedKinds { + from_kind: b.reflect_kind(), + to_kind: ReflectKind::List, + }); } + Ok(()) } /// Compares a [`List`] with a [`Reflect`] value. @@ -508,6 +533,7 @@ pub fn list_debug(dyn_list: &dyn List, f: &mut Formatter<'_>) -> std::fmt::Resul #[cfg(test)] mod tests { use super::DynamicList; + use crate::{Reflect, ReflectRef}; use std::assert_eq; #[test] @@ -522,4 +548,29 @@ mod tests { assert_eq!(index, value); } } + + #[test] + fn next_index_increment() { + const SIZE: usize = if cfg!(debug_assertions) { + 4 + } else { + // If compiled in release mode, verify we dont overflow + usize::MAX + }; + let b = Box::new(vec![(); SIZE]).into_reflect(); + + let ReflectRef::List(list) = b.reflect_ref() else { + panic!("Not a list..."); + }; + + let mut iter = list.iter(); + iter.index = SIZE - 1; + assert!(iter.next().is_some()); + + // When None we should no longer increase index + assert!(iter.next().is_none()); + assert!(iter.index == SIZE); + assert!(iter.next().is_none()); + assert!(iter.index == SIZE); + } } diff --git a/crates/bevy_reflect/src/map.rs b/crates/bevy_reflect/src/map.rs index 41ef9e575bb4f..5b182c68a973a 100644 --- a/crates/bevy_reflect/src/map.rs +++ b/crates/bevy_reflect/src/map.rs @@ -5,8 +5,8 @@ use bevy_reflect_derive::impl_type_path; use bevy_utils::{Entry, HashMap}; use crate::{ - self as bevy_reflect, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, - TypePath, TypePathTable, + self as bevy_reflect, ApplyError, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, + TypeInfo, TypePath, TypePathTable, }; /// A trait used to power [map-like] operations via [reflection]. @@ -345,6 +345,10 @@ impl Reflect for DynamicMap { map_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + map_try_apply(self, value) + } + fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) @@ -413,7 +417,7 @@ impl<'a> Iterator for MapIter<'a> { fn next(&mut self) -> Option { let value = self.map.get_at(self.index); - self.index += 1; + self.index += value.is_some() as usize; value } @@ -502,17 +506,37 @@ pub fn map_debug(dyn_map: &dyn Map, f: &mut Formatter<'_>) -> std::fmt::Result { /// This function panics if `b` is not a reflected map. #[inline] pub fn map_apply(a: &mut M, b: &dyn Reflect) { + if let Err(err) = map_try_apply(a, b) { + panic!("{err}"); + } +} + +/// Tries to apply the elements of reflected map `b` to the corresponding elements of map `a` +/// and returns a Result. +/// +/// If a key from `b` does not exist in `a`, the value is cloned and inserted. +/// +/// # Errors +/// +/// This function returns an [`ApplyError::MismatchedKinds`] if `b` is not a reflected map or if +/// applying elements to each other fails. +#[inline] +pub fn map_try_apply(a: &mut M, b: &dyn Reflect) -> Result<(), ApplyError> { if let ReflectRef::Map(map_value) = b.reflect_ref() { for (key, b_value) in map_value.iter() { if let Some(a_value) = a.get_mut(key) { - a_value.apply(b_value); + a_value.try_apply(b_value)?; } else { a.insert_boxed(key.clone_value(), b_value.clone_value()); } } } else { - panic!("Attempted to apply a non-map type to a map type."); + return Err(ApplyError::MismatchedKinds { + from_kind: b.reflect_kind(), + to_kind: ReflectKind::Map, + }); } + Ok(()) } #[cfg(test)] @@ -594,4 +618,27 @@ mod tests { assert!(map.get_at(2).is_none()); } + + #[test] + fn next_index_increment() { + let values = ["first", "last"]; + let mut map = DynamicMap::default(); + map.insert(0usize, values[0]); + map.insert(1usize, values[1]); + + let mut iter = map.iter(); + let size = iter.len(); + + for _ in 0..2 { + let prev_index = iter.index; + assert!(iter.next().is_some()); + assert_eq!(prev_index, iter.index - 1); + } + + // When None we should no longer increase index + for _ in 0..2 { + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); + } + } } diff --git a/crates/bevy_reflect/src/reflect.rs b/crates/bevy_reflect/src/reflect.rs index 9a50decb7240b..43108e61a4278 100644 --- a/crates/bevy_reflect/src/reflect.rs +++ b/crates/bevy_reflect/src/reflect.rs @@ -8,6 +8,8 @@ use std::{ fmt::Debug, }; +use thiserror::Error; + use crate::utility::NonGenericTypeInfoCell; macro_rules! impl_reflect_enum { @@ -99,6 +101,42 @@ pub enum ReflectOwned { } impl_reflect_enum!(ReflectOwned); +/// A enumeration of all error outcomes that might happen when running [`try_apply`](Reflect::try_apply). +#[derive(Error, Debug)] +pub enum ApplyError { + #[error("attempted to apply `{from_kind}` to `{to_kind}`")] + /// Attempted to apply the wrong [kind](ReflectKind) to a type, e.g. a struct to a enum. + MismatchedKinds { + from_kind: ReflectKind, + to_kind: ReflectKind, + }, + + #[error("enum variant `{variant_name}` doesn't have a field named `{field_name}`")] + /// Enum variant that we tried to apply to was missing a field. + MissingEnumField { + variant_name: Box, + field_name: Box, + }, + + #[error("`{from_type}` is not `{to_type}`")] + /// Tried to apply incompatible types. + MismatchedTypes { + from_type: Box, + to_type: Box, + }, + + #[error("attempted to apply type with {from_size} size to a type with {to_size} size")] + /// Attempted to apply to types with mismatched sizez, e.g. a [u8; 4] to [u8; 3]. + DifferentSize { from_size: usize, to_size: usize }, + + #[error("variant with name `{variant_name}` does not exist on enum `{enum_name}`")] + /// The enum we tried to apply to didn't contain a variant with the give name. + UnknownVariant { + enum_name: Box, + variant_name: Box, + }, +} + /// A zero-sized enumuration of the "kinds" of a reflected type. /// /// A [`ReflectKind`] is obtained via [`Reflect::reflect_kind`], @@ -217,7 +255,20 @@ pub trait Reflect: DynamicTypePath + Any + Send + Sync { /// - If `T` is any complex type and the corresponding fields or elements of /// `self` and `value` are not of the same type. /// - If `T` is a value type and `self` cannot be downcast to `T` - fn apply(&mut self, value: &dyn Reflect); + fn apply(&mut self, value: &dyn Reflect) { + Reflect::try_apply(self, value).unwrap(); + } + + /// Tries to [`apply`](Reflect::apply) a reflected value to this value. + /// + /// Functions the same as the [`apply`](Reflect::apply) function but returns an error instead of + /// panicking. + /// + /// # Handling Errors + /// + /// This function may leave `self` in a partially mutated state if a error was encountered on the way. + /// consider maintaining a cloned instance of this data you can switch to if a error is encountered. + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError>; /// Performs a type-checked assignment of a reflected value to this value. /// diff --git a/crates/bevy_reflect/src/struct_trait.rs b/crates/bevy_reflect/src/struct_trait.rs index d26ea6adb5f42..5d134034691d6 100644 --- a/crates/bevy_reflect/src/struct_trait.rs +++ b/crates/bevy_reflect/src/struct_trait.rs @@ -1,6 +1,6 @@ use crate::{ - self as bevy_reflect, NamedField, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, - TypeInfo, TypePath, TypePathTable, + self as bevy_reflect, ApplyError, NamedField, Reflect, ReflectKind, ReflectMut, ReflectOwned, + ReflectRef, TypeInfo, TypePath, TypePathTable, }; use bevy_reflect_derive::impl_type_path; use bevy_utils::HashMap; @@ -204,7 +204,7 @@ impl<'a> Iterator for FieldIter<'a> { fn next(&mut self) -> Option { let value = self.struct_val.field_at(self.index); - self.index += 1; + self.index += value.is_some() as usize; value } @@ -420,19 +420,24 @@ impl Reflect for DynamicStruct { self } - fn apply(&mut self, value: &dyn Reflect) { + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { if let ReflectRef::Struct(struct_value) = value.reflect_ref() { for (i, value) in struct_value.iter_fields().enumerate() { let name = struct_value.name_at(i).unwrap(); if let Some(v) = self.field_mut(name) { - v.apply(value); + v.try_apply(value)?; } } } else { - panic!("Attempted to apply non-struct type to struct type."); + return Err(ApplyError::MismatchedKinds { + from_kind: value.reflect_kind(), + to_kind: ReflectKind::Struct, + }); } + Ok(()) } + #[inline] fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) @@ -557,3 +562,31 @@ pub fn struct_debug(dyn_struct: &dyn Struct, f: &mut Formatter<'_>) -> std::fmt: } debug.finish() } + +#[cfg(test)] +mod tests { + use crate as bevy_reflect; + use crate::*; + #[derive(Reflect, Default)] + struct MyStruct { + a: (), + b: (), + c: (), + } + #[test] + fn next_index_increment() { + let my_struct = MyStruct::default(); + let mut iter = my_struct.iter_fields(); + iter.index = iter.len() - 1; + let prev_index = iter.index; + assert!(iter.next().is_some()); + assert_eq!(prev_index, iter.index - 1); + + // When None we should no longer increase index + let prev_index = iter.index; + assert!(iter.next().is_none()); + assert_eq!(prev_index, iter.index); + assert!(iter.next().is_none()); + assert_eq!(prev_index, iter.index); + } +} diff --git a/crates/bevy_reflect/src/tuple.rs b/crates/bevy_reflect/src/tuple.rs index de6af437df4bb..cf111edcdfb99 100644 --- a/crates/bevy_reflect/src/tuple.rs +++ b/crates/bevy_reflect/src/tuple.rs @@ -2,9 +2,9 @@ use bevy_reflect_derive::impl_type_path; use bevy_utils::all_tuples; use crate::{ - self as bevy_reflect, utility::GenericTypePathCell, FromReflect, GetTypeRegistration, Reflect, - ReflectMut, ReflectOwned, ReflectRef, TypeInfo, TypePath, TypeRegistration, TypeRegistry, - Typed, UnnamedField, + self as bevy_reflect, utility::GenericTypePathCell, ApplyError, FromReflect, + GetTypeRegistration, Reflect, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, TypePath, + TypeRegistration, TypeRegistry, Typed, UnnamedField, }; use crate::{ReflectKind, TypePathTable}; use std::any::{Any, TypeId}; @@ -75,7 +75,7 @@ impl<'a> Iterator for TupleFieldIter<'a> { fn next(&mut self) -> Option { let value = self.tuple.field(self.index); - self.index += 1; + self.index += value.is_some() as usize; value } @@ -369,6 +369,10 @@ impl Reflect for DynamicTuple { Box::new(self.clone_dynamic()) } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + tuple_try_apply(self, value) + } + fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { tuple_partial_eq(self, value) } @@ -394,15 +398,33 @@ impl_type_path!((in bevy_reflect) DynamicTuple); /// This function panics if `b` is not a tuple. #[inline] pub fn tuple_apply(a: &mut T, b: &dyn Reflect) { + if let Err(err) = tuple_try_apply(a, b) { + panic!("{err}"); + } +} + +/// Tries to apply the elements of `b` to the corresponding elements of `a` and +/// returns a Result. +/// +/// # Errors +/// +/// This function returns an [`ApplyError::MismatchedKinds`] if `b` is not a tuple or if +/// applying elements to each other fails. +#[inline] +pub fn tuple_try_apply(a: &mut T, b: &dyn Reflect) -> Result<(), ApplyError> { if let ReflectRef::Tuple(tuple) = b.reflect_ref() { for (i, value) in tuple.iter_fields().enumerate() { if let Some(v) = a.field_mut(i) { - v.apply(value); + v.try_apply(value)?; } } } else { - panic!("Attempted to apply non-Tuple type to Tuple type."); + return Err(ApplyError::MismatchedKinds { + from_kind: b.reflect_kind(), + to_kind: ReflectKind::Tuple, + }); } + Ok(()) } /// Compares a [`Tuple`] with a [`Reflect`] value. @@ -545,6 +567,10 @@ macro_rules! impl_reflect_tuple { crate::tuple_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + crate::tuple_try_apply(self, value) + } + fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) @@ -683,3 +709,24 @@ macro_rules! impl_type_path_tuple { } all_tuples!(impl_type_path_tuple, 0, 12, P); + +#[cfg(test)] +mod tests { + use super::Tuple; + + #[test] + fn next_index_increment() { + let mut iter = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11).iter_fields(); + let size = iter.len(); + iter.index = size - 1; + let prev_index = iter.index; + assert!(iter.next().is_some()); + assert_eq!(prev_index, iter.index - 1); + + // When None we should no longer increase index + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); + } +} diff --git a/crates/bevy_reflect/src/tuple_struct.rs b/crates/bevy_reflect/src/tuple_struct.rs index c8699b506e040..8aeb103984029 100644 --- a/crates/bevy_reflect/src/tuple_struct.rs +++ b/crates/bevy_reflect/src/tuple_struct.rs @@ -1,8 +1,8 @@ use bevy_reflect_derive::impl_type_path; use crate::{ - self as bevy_reflect, DynamicTuple, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, - Tuple, TypeInfo, TypePath, TypePathTable, UnnamedField, + self as bevy_reflect, ApplyError, DynamicTuple, Reflect, ReflectKind, ReflectMut, ReflectOwned, + ReflectRef, Tuple, TypeInfo, TypePath, TypePathTable, UnnamedField, }; use std::any::{Any, TypeId}; use std::fmt::{Debug, Formatter}; @@ -155,7 +155,7 @@ impl<'a> Iterator for TupleStructFieldIter<'a> { fn next(&mut self) -> Option { let value = self.tuple_struct.field(self.index); - self.index += 1; + self.index += value.is_some() as usize; value } @@ -329,18 +329,23 @@ impl Reflect for DynamicTupleStruct { self } - fn apply(&mut self, value: &dyn Reflect) { + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { if let ReflectRef::TupleStruct(tuple_struct) = value.reflect_ref() { for (i, value) in tuple_struct.iter_fields().enumerate() { if let Some(v) = self.field_mut(i) { - v.apply(value); + v.try_apply(value)?; } } } else { - panic!("Attempted to apply non-TupleStruct type to TupleStruct type."); + return Err(ApplyError::MismatchedKinds { + from_kind: value.reflect_kind(), + to_kind: ReflectKind::TupleStruct, + }); } + Ok(()) } + #[inline] fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) @@ -371,6 +376,7 @@ impl Reflect for DynamicTupleStruct { Box::new(self.clone_dynamic()) } + #[inline] fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { tuple_struct_partial_eq(self, value) } @@ -469,3 +475,26 @@ pub fn tuple_struct_debug( } debug.finish() } + +#[cfg(test)] +mod tests { + use crate as bevy_reflect; + use crate::*; + #[derive(Reflect)] + struct Ts(u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8); + #[test] + fn next_index_increment() { + let mut iter = Ts(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11).iter_fields(); + let size = iter.len(); + iter.index = size - 1; + let prev_index = iter.index; + assert!(iter.next().is_some()); + assert_eq!(prev_index, iter.index - 1); + + // When None we should no longer increase index + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); + } +} diff --git a/crates/bevy_reflect/src/type_info.rs b/crates/bevy_reflect/src/type_info.rs index 6b61b71fdf14e..c64131a7b825f 100644 --- a/crates/bevy_reflect/src/type_info.rs +++ b/crates/bevy_reflect/src/type_info.rs @@ -25,7 +25,7 @@ use std::fmt::Debug; /// /// ``` /// # use std::any::Any; -/// # use bevy_reflect::{DynamicTypePath, NamedField, Reflect, ReflectMut, ReflectOwned, ReflectRef, StructInfo, TypeInfo, TypePath, ValueInfo}; +/// # use bevy_reflect::{DynamicTypePath, NamedField, Reflect, ReflectMut, ReflectOwned, ReflectRef, StructInfo, TypeInfo, TypePath, ValueInfo, ApplyError}; /// # use bevy_reflect::utility::NonGenericTypeInfoCell; /// use bevy_reflect::Typed; /// @@ -60,7 +60,7 @@ use std::fmt::Debug; /// # fn into_reflect(self: Box) -> Box { todo!() } /// # fn as_reflect(&self) -> &dyn Reflect { todo!() } /// # fn as_reflect_mut(&mut self) -> &mut dyn Reflect { todo!() } -/// # fn apply(&mut self, value: &dyn Reflect) { todo!() } +/// # fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { todo!() } /// # fn set(&mut self, value: Box) -> Result<(), Box> { todo!() } /// # fn reflect_ref(&self) -> ReflectRef { todo!() } /// # fn reflect_mut(&mut self) -> ReflectMut { todo!() } diff --git a/crates/bevy_reflect/src/type_path.rs b/crates/bevy_reflect/src/type_path.rs index 9d446cc81088e..d6e6a4ad44ef4 100644 --- a/crates/bevy_reflect/src/type_path.rs +++ b/crates/bevy_reflect/src/type_path.rs @@ -147,22 +147,27 @@ pub trait DynamicTypePath { } impl DynamicTypePath for T { + #[inline] fn reflect_type_path(&self) -> &str { Self::type_path() } + #[inline] fn reflect_short_type_path(&self) -> &str { Self::short_type_path() } + #[inline] fn reflect_type_ident(&self) -> Option<&str> { Self::type_ident() } + #[inline] fn reflect_crate_name(&self) -> Option<&str> { Self::crate_name() } + #[inline] fn reflect_module_path(&self) -> Option<&str> { Self::module_path() } diff --git a/crates/bevy_reflect/src/utility.rs b/crates/bevy_reflect/src/utility.rs index df860d194c373..86dcbbc175462 100644 --- a/crates/bevy_reflect/src/utility.rs +++ b/crates/bevy_reflect/src/utility.rs @@ -49,7 +49,7 @@ mod sealed { /// /// ``` /// # use std::any::Any; -/// # use bevy_reflect::{DynamicTypePath, NamedField, Reflect, ReflectMut, ReflectOwned, ReflectRef, StructInfo, Typed, TypeInfo, TypePath}; +/// # use bevy_reflect::{DynamicTypePath, NamedField, Reflect, ReflectMut, ReflectOwned, ReflectRef, StructInfo, Typed, TypeInfo, TypePath, ApplyError}; /// use bevy_reflect::utility::NonGenericTypeInfoCell; /// /// struct Foo { @@ -78,7 +78,7 @@ mod sealed { /// # fn into_reflect(self: Box) -> Box { todo!() } /// # fn as_reflect(&self) -> &dyn Reflect { todo!() } /// # fn as_reflect_mut(&mut self) -> &mut dyn Reflect { todo!() } -/// # fn apply(&mut self, value: &dyn Reflect) { todo!() } +/// # fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { todo!() } /// # fn set(&mut self, value: Box) -> Result<(), Box> { todo!() } /// # fn reflect_ref(&self) -> ReflectRef { todo!() } /// # fn reflect_mut(&mut self) -> ReflectMut { todo!() } @@ -130,7 +130,7 @@ impl Default for NonGenericTypeCell { /// /// ``` /// # use std::any::Any; -/// # use bevy_reflect::{DynamicTypePath, Reflect, ReflectMut, ReflectOwned, ReflectRef, TupleStructInfo, Typed, TypeInfo, TypePath, UnnamedField}; +/// # use bevy_reflect::{DynamicTypePath, Reflect, ReflectMut, ReflectOwned, ReflectRef, TupleStructInfo, Typed, TypeInfo, TypePath, UnnamedField, ApplyError}; /// use bevy_reflect::utility::GenericTypeInfoCell; /// /// struct Foo(T); @@ -157,7 +157,7 @@ impl Default for NonGenericTypeCell { /// # fn into_reflect(self: Box) -> Box { todo!() } /// # fn as_reflect(&self) -> &dyn Reflect { todo!() } /// # fn as_reflect_mut(&mut self) -> &mut dyn Reflect { todo!() } -/// # fn apply(&mut self, value: &dyn Reflect) { todo!() } +/// # fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { todo!() } /// # fn set(&mut self, value: Box) -> Result<(), Box> { todo!() } /// # fn reflect_ref(&self) -> ReflectRef { todo!() } /// # fn reflect_mut(&mut self) -> ReflectMut { todo!() } diff --git a/crates/bevy_reflect_compile_fail_tests/tests/derive.rs b/crates/bevy_reflect_compile_fail_tests/tests/derive.rs deleted file mode 100644 index 14ea5803ecff6..0000000000000 --- a/crates/bevy_reflect_compile_fail_tests/tests/derive.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() -> bevy_compile_test_utils::ui_test::Result<()> { - bevy_compile_test_utils::test("tests/reflect_derive") -} diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 82d16c933733a..a22597b662d78 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -18,7 +18,7 @@ bmp = ["image/bmp"] webp = ["image/webp"] dds = ["ddsfile"] pnm = ["image/pnm"] -multi-threaded = ["bevy_tasks/multi-threaded"] +multi_threaded = ["bevy_tasks/multi_threaded"] shader_format_glsl = ["naga/glsl-in", "naga/wgsl-out", "naga_oil/glsl"] shader_format_spirv = ["wgpu/spirv", "naga/spv-in", "naga/spv-out"] @@ -100,7 +100,7 @@ profiling = { version = "1", features = [ ], optional = true } async-channel = "2.2.0" nonmax = "0.5" -smallvec = "1.11" +smallvec = { version = "1.11", features = ["const_new"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Omit the `glsl` feature in non-WebAssembly by default. diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index e407629e7b058..d0d9a4a6e1cd1 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -146,8 +146,8 @@ impl Default for Exposure { } } -/// Parameters based on physical camera characteristics for calculating -/// EV100 values for use with [`Exposure`]. +/// Parameters based on physical camera characteristics for calculating EV100 +/// values for use with [`Exposure`]. This is also used for depth of field. #[derive(Clone, Copy)] pub struct PhysicalCameraParameters { /// @@ -156,6 +156,15 @@ pub struct PhysicalCameraParameters { pub shutter_speed_s: f32, /// pub sensitivity_iso: f32, + /// The height of the [image sensor format] in meters. + /// + /// Focal length is derived from the FOV and this value. The default is + /// 18.66mm, matching the [Super 35] format, which is popular in cinema. + /// + /// [image sensor format]: https://en.wikipedia.org/wiki/Image_sensor_format + /// + /// [Super 35]: https://en.wikipedia.org/wiki/Super_35 + pub sensor_height: f32, } impl PhysicalCameraParameters { @@ -173,6 +182,7 @@ impl Default for PhysicalCameraParameters { aperture_f_stops: 1.0, shutter_speed_s: 1.0 / 125.0, sensitivity_iso: 100.0, + sensor_height: 0.01866, } } } @@ -916,7 +926,7 @@ pub fn extract_cameras( } if let Some(render_layers) = render_layers { - commands.insert(*render_layers); + commands.insert(render_layers.clone()); } if let Some(perspective) = projection { diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index 2ea1dc5535fcd..c8495ffbf39d1 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -70,6 +70,9 @@ pub struct CameraUpdateSystem; /// to recompute the camera projection matrix of the [`Camera`] component attached to /// the same entity as the component implementing this trait. /// +/// Use the plugins [`CameraProjectionPlugin`] and `bevy::pbr::PbrProjectionPlugin` to setup the +/// systems for your [`CameraProjection`] implementation. +/// /// [`Camera`]: crate::camera::Camera pub trait CameraProjection { fn get_projection_matrix(&self) -> Mat4; diff --git a/crates/bevy_render/src/color_operations.wgsl b/crates/bevy_render/src/color_operations.wgsl new file mode 100644 index 0000000000000..b68ad2a3db57f --- /dev/null +++ b/crates/bevy_render/src/color_operations.wgsl @@ -0,0 +1,47 @@ +#define_import_path bevy_render::color_operations + +#import bevy_render::maths::FRAC_PI_3 + +// Converts HSV to RGB. +// +// Input: H ∈ [0, 2π), S ∈ [0, 1], V ∈ [0, 1]. +// Output: R ∈ [0, 1], G ∈ [0, 1], B ∈ [0, 1]. +// +// +fn hsv_to_rgb(hsv: vec3) -> vec3 { + let n = vec3(5.0, 3.0, 1.0); + let k = (n + hsv.x / FRAC_PI_3) % 6.0; + return hsv.z - hsv.z * hsv.y * max(vec3(0.0), min(k, min(4.0 - k, vec3(1.0)))); +} + +// Converts RGB to HSV. +// +// Input: R ∈ [0, 1], G ∈ [0, 1], B ∈ [0, 1]. +// Output: H ∈ [0, 2π), S ∈ [0, 1], V ∈ [0, 1]. +// +// +fn rgb_to_hsv(rgb: vec3) -> vec3 { + let x_max = max(rgb.r, max(rgb.g, rgb.b)); // i.e. V + let x_min = min(rgb.r, min(rgb.g, rgb.b)); + let c = x_max - x_min; // chroma + + var swizzle = vec3(0.0); + if (x_max == rgb.r) { + swizzle = vec3(rgb.gb, 0.0); + } else if (x_max == rgb.g) { + swizzle = vec3(rgb.br, 2.0); + } else { + swizzle = vec3(rgb.rg, 4.0); + } + + let h = FRAC_PI_3 * (((swizzle.x - swizzle.y) / c + swizzle.z) % 6.0); + + // Avoid division by zero. + var s = 0.0; + if (x_max > 0.0) { + s = c / x_max; + } + + return vec3(h, s, x_max); +} + diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 60d8ef648aa77..dea5f7bb7d38f 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -46,7 +46,7 @@ pub mod prelude { mesh::{morph::MorphWeights, primitives::Meshable, Mesh}, render_resource::Shader, spatial_bundle::SpatialBundle, - texture::{Image, ImagePlugin}, + texture::{image_texture_conversion::IntoDynamicImageError, Image, ImagePlugin}, view::{InheritedVisibility, Msaa, ViewVisibility, Visibility, VisibilityBundle}, ExtractSchedule, }; @@ -96,7 +96,7 @@ use std::{ pub struct RenderPlugin { pub render_creation: RenderCreation, /// If `true`, disables asynchronous pipeline compilation. - /// This has no effect on macOS, Wasm, iOS, or without the `multi-threaded` feature. + /// This has no effect on macOS, Wasm, iOS, or without the `multi_threaded` feature. pub synchronous_pipeline_compilation: bool, } @@ -236,6 +236,8 @@ pub struct RenderApp; pub const INSTANCE_INDEX_SHADER_HANDLE: Handle = Handle::weak_from_u128(10313207077636615845); pub const MATHS_SHADER_HANDLE: Handle = Handle::weak_from_u128(10665356303104593376); +pub const COLOR_OPERATIONS_SHADER_HANDLE: Handle = + Handle::weak_from_u128(1844674407370955161); impl Plugin for RenderPlugin { /// Initializes the renderer, sets up the [`RenderSet`] and creates the rendering sub-app. @@ -359,6 +361,12 @@ impl Plugin for RenderPlugin { fn finish(&self, app: &mut App) { load_internal_asset!(app, MATHS_SHADER_HANDLE, "maths.wgsl", Shader::from_wgsl); + load_internal_asset!( + app, + COLOR_OPERATIONS_SHADER_HANDLE, + "color_operations.wgsl", + Shader::from_wgsl + ); if let Some(future_renderer_resources) = app.world_mut().remove_resource::() { diff --git a/crates/bevy_render/src/maths.wgsl b/crates/bevy_render/src/maths.wgsl index 720e6bac46a08..29ea0a896566a 100644 --- a/crates/bevy_render/src/maths.wgsl +++ b/crates/bevy_render/src/maths.wgsl @@ -1,5 +1,11 @@ #define_import_path bevy_render::maths +const PI: f32 = 3.141592653589793; // π +const PI_2: f32 = 6.283185307179586; // 2π +const HALF_PI: f32 = 1.57079632679; // π/2 +const FRAC_PI_3: f32 = 1.0471975512; // π/3 +const E: f32 = 2.718281828459045; // exp(1) + fn affine2_to_square(affine: mat3x2) -> mat3x3 { return mat3x3( vec3(affine[0].xy, 0.0), diff --git a/crates/bevy_render/src/mesh/mesh/mod.rs b/crates/bevy_render/src/mesh/mesh/mod.rs index 90e23fbcd8849..a58556d827d97 100644 --- a/crates/bevy_render/src/mesh/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mesh/mod.rs @@ -544,15 +544,44 @@ impl Mesh { } /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of a mesh. + /// If the mesh is indexed, this defaults to smooth normals. Otherwise, it defaults to flat + /// normals. /// /// # Panics /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. /// - /// FIXME: The should handle more cases since this is called as a part of gltf + /// FIXME: This should handle more cases since this is called as a part of gltf + /// mesh loading where we can't really blame users for loading meshes that might + /// not conform to the limitations here! + pub fn compute_normals(&mut self) { + assert!( + matches!(self.primitive_topology, PrimitiveTopology::TriangleList), + "`compute_normals` can only work on `TriangleList`s" + ); + if self.indices().is_none() { + self.compute_flat_normals(); + } else { + self.compute_smooth_normals(); + } + } + + /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of a mesh. + /// + /// # Panics + /// Panics if [`Indices`] are set or [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. + /// Consider calling [`Mesh::duplicate_vertices`] or exporting your mesh with normal + /// attributes. + /// + /// FIXME: This should handle more cases since this is called as a part of gltf /// mesh loading where we can't really blame users for loading meshes that might /// not conform to the limitations here! pub fn compute_flat_normals(&mut self) { + assert!( + self.indices().is_none(), + "`compute_flat_normals` can't work on indexed geometry. Consider calling either `Mesh::compute_smooth_normals` or `Mesh::duplicate_vertices` followed by `Mesh::compute_flat_normals`." + ); assert!( matches!(self.primitive_topology, PrimitiveTopology::TriangleList), "`compute_flat_normals` can only work on `TriangleList`s" @@ -564,67 +593,114 @@ impl Mesh { .as_float3() .expect("`Mesh::ATTRIBUTE_POSITION` vertex attributes should be of type `float3`"); - match self.indices() { - Some(indices) => { - let mut count: usize = 0; - let mut corners = [0_usize; 3]; - let mut normals = vec![[0.0f32; 3]; positions.len()]; - let mut adjacency_counts = vec![0_usize; positions.len()]; - - for i in indices.iter() { - corners[count % 3] = i; - count += 1; - if count % 3 == 0 { - let normal = face_normal( - positions[corners[0]], - positions[corners[1]], - positions[corners[2]], - ); - for corner in corners { - normals[corner] = - (Vec3::from(normal) + Vec3::from(normals[corner])).into(); - adjacency_counts[corner] += 1; - } - } - } + let normals: Vec<_> = positions + .chunks_exact(3) + .map(|p| face_normal(p[0], p[1], p[2])) + .flat_map(|normal| [normal; 3]) + .collect(); - // average (smooth) normals for shared vertices... - // TODO: support different methods of weighting the average - for i in 0..normals.len() { - let count = adjacency_counts[i]; - if count > 0 { - normals[i] = (Vec3::from(normals[i]) / (count as f32)).normalize().into(); - } - } + self.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); + } - self.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); - } - None => { - let normals: Vec<_> = positions - .chunks_exact(3) - .map(|p| face_normal(p[0], p[1], p[2])) - .flat_map(|normal| [normal; 3]) - .collect(); - - self.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); + /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of an indexed mesh, smoothing normals for shared + /// vertices. + /// + /// # Panics + /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. + /// Panics if the mesh does not have indices defined. + /// + /// FIXME: This should handle more cases since this is called as a part of gltf + /// mesh loading where we can't really blame users for loading meshes that might + /// not conform to the limitations here! + pub fn compute_smooth_normals(&mut self) { + assert!( + matches!(self.primitive_topology, PrimitiveTopology::TriangleList), + "`compute_smooth_normals` can only work on `TriangleList`s" + ); + assert!( + self.indices().is_some(), + "`compute_smooth_normals` can only work on indexed meshes" + ); + + let positions = self + .attribute(Mesh::ATTRIBUTE_POSITION) + .unwrap() + .as_float3() + .expect("`Mesh::ATTRIBUTE_POSITION` vertex attributes should be of type `float3`"); + + let mut normals = vec![Vec3::ZERO; positions.len()]; + let mut adjacency_counts = vec![0_usize; positions.len()]; + + self.indices() + .unwrap() + .iter() + .collect::>() + .chunks_exact(3) + .for_each(|face| { + let [a, b, c] = [face[0], face[1], face[2]]; + let normal = Vec3::from(face_normal(positions[a], positions[b], positions[c])); + [a, b, c].iter().for_each(|pos| { + normals[*pos] += normal; + adjacency_counts[*pos] += 1; + }); + }); + + // average (smooth) normals for shared vertices... + // TODO: support different methods of weighting the average + for i in 0..normals.len() { + let count = adjacency_counts[i]; + if count > 0 { + normals[i] = (normals[i] / (count as f32)).normalize(); } } + + self.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); + } + + /// Consumes the mesh and returns a mesh with calculated [`Mesh::ATTRIBUTE_NORMAL`]. + /// If the mesh is indexed, this defaults to smooth normals. Otherwise, it defaults to flat + /// normals. + /// + /// (Alternatively, you can use [`Mesh::compute_normals`] to mutate an existing mesh in-place) + /// + /// # Panics + /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. + #[must_use] + pub fn with_computed_normals(mut self) -> Self { + self.compute_normals(); + self } /// Consumes the mesh and returns a mesh with calculated [`Mesh::ATTRIBUTE_NORMAL`]. /// - /// (Alternatively, you can use [`Mesh::with_computed_flat_normals`] to mutate an existing mesh in-place) + /// (Alternatively, you can use [`Mesh::compute_flat_normals`] to mutate an existing mesh in-place) /// /// # Panics - /// Panics if [`Indices`] are set or [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3` or - /// if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. - /// Consider calling [`Mesh::with_duplicated_vertices`] or export your mesh with normal attributes. + /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. + /// Panics if the mesh has indices defined #[must_use] pub fn with_computed_flat_normals(mut self) -> Self { self.compute_flat_normals(); self } + /// Consumes the mesh and returns a mesh with calculated [`Mesh::ATTRIBUTE_NORMAL`]. + /// + /// (Alternatively, you can use [`Mesh::compute_smooth_normals`] to mutate an existing mesh in-place) + /// + /// # Panics + /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. + /// Panics if the mesh does not have indices defined. + #[must_use] + pub fn with_computed_smooth_normals(mut self) -> Self { + self.compute_smooth_normals(); + self + } + /// Generate tangents for the mesh using the `mikktspace` algorithm. /// /// Sets the [`Mesh::ATTRIBUTE_TANGENT`] attribute if successful. diff --git a/crates/bevy_render/src/mesh/primitives/dim3/cone.rs b/crates/bevy_render/src/mesh/primitives/dim3/cone.rs new file mode 100644 index 0000000000000..58b49af5ed48a --- /dev/null +++ b/crates/bevy_render/src/mesh/primitives/dim3/cone.rs @@ -0,0 +1,247 @@ +use bevy_math::{primitives::Cone, Vec3}; +use wgpu::PrimitiveTopology; + +use crate::{ + mesh::{Indices, Mesh, Meshable}, + render_asset::RenderAssetUsages, +}; + +/// A builder used for creating a [`Mesh`] with a [`Cone`] shape. +#[derive(Clone, Copy, Debug)] +pub struct ConeMeshBuilder { + /// The [`Cone`] shape. + pub cone: Cone, + /// The number of vertices used for the base of the cone. + /// + /// The default is `32`. + pub resolution: u32, +} + +impl Default for ConeMeshBuilder { + fn default() -> Self { + Self { + cone: Cone::default(), + resolution: 32, + } + } +} + +impl ConeMeshBuilder { + /// Creates a new [`ConeMeshBuilder`] from a given radius, height, + /// and number of vertices used for the base of the cone. + #[inline] + pub const fn new(radius: f32, height: f32, resolution: u32) -> Self { + Self { + cone: Cone { radius, height }, + resolution, + } + } + + /// Sets the number of vertices used for the base of the cone. + #[inline] + pub const fn resolution(mut self, resolution: u32) -> Self { + self.resolution = resolution; + self + } + + /// Builds a [`Mesh`] based on the configuration in `self`. + pub fn build(&self) -> Mesh { + let half_height = self.cone.height / 2.0; + + // `resolution` vertices for the base, `resolution` vertices for the bottom of the lateral surface, + // and one vertex for the tip. + let num_vertices = self.resolution as usize * 2 + 1; + let num_indices = self.resolution as usize * 6 - 6; + + let mut positions = Vec::with_capacity(num_vertices); + let mut normals = Vec::with_capacity(num_vertices); + let mut uvs = Vec::with_capacity(num_vertices); + let mut indices = Vec::with_capacity(num_indices); + + // Tip + positions.push([0.0, half_height, 0.0]); + + // The tip doesn't have a singular normal that works correctly. + // We use an invalid normal here so that it becomes NaN in the fragment shader + // and doesn't affect the overall shading. This might seem hacky, but it's one of + // the only ways to get perfectly smooth cones without creases or other shading artefacts. + // + // Note that this requires that normals are not normalized in the vertex shader, + // as that would make the entire triangle invalid and make the cone appear as black. + normals.push([0.0, 0.0, 0.0]); + + // The UVs of the cone are in polar coordinates, so it's like projecting a circle texture from above. + // The center of the texture is at the center of the lateral surface, at the tip of the cone. + uvs.push([0.5, 0.5]); + + // Now we build the lateral surface, the side of the cone. + + // The vertex normals will be perpendicular to the surface. + // + // Here we get the slope of a normal and use it for computing + // the multiplicative inverse of the length of a vector in the direction + // of the normal. This allows us to normalize vertex normals efficiently. + let normal_slope = self.cone.radius / self.cone.height; + // Equivalent to Vec2::new(1.0, slope).length().recip() + let normalization_factor = (1.0 + normal_slope * normal_slope).sqrt().recip(); + + // How much the angle changes at each step + let step_theta = std::f32::consts::TAU / self.resolution as f32; + + // Add vertices for the bottom of the lateral surface. + for segment in 0..self.resolution { + let theta = segment as f32 * step_theta; + let (sin, cos) = theta.sin_cos(); + + // The vertex normal perpendicular to the side + let normal = Vec3::new(cos, normal_slope, sin) * normalization_factor; + + positions.push([self.cone.radius * cos, -half_height, self.cone.radius * sin]); + normals.push(normal.to_array()); + uvs.push([0.5 + cos * 0.5, 0.5 + sin * 0.5]); + } + + // Add indices for the lateral surface. Each triangle is formed by the tip + // and two vertices at the base. + for j in 1..self.resolution { + indices.extend_from_slice(&[0, j + 1, j]); + } + + // Close the surface with a triangle between the tip, first base vertex, and last base vertex. + indices.extend_from_slice(&[0, 1, self.resolution]); + + // Now we build the actual base of the cone. + + let index_offset = positions.len() as u32; + + // Add base vertices. + for i in 0..self.resolution { + let theta = i as f32 * step_theta; + let (sin, cos) = theta.sin_cos(); + + positions.push([cos * self.cone.radius, -half_height, sin * self.cone.radius]); + normals.push([0.0, -1.0, 0.0]); + uvs.push([0.5 * (cos + 1.0), 1.0 - 0.5 * (sin + 1.0)]); + } + + // Add base indices. + for i in 1..(self.resolution - 1) { + indices.extend_from_slice(&[index_offset, index_offset + i, index_offset + i + 1]); + } + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(Indices::U32(indices)) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } +} + +impl Meshable for Cone { + type Output = ConeMeshBuilder; + + fn mesh(&self) -> Self::Output { + ConeMeshBuilder { + cone: *self, + ..Default::default() + } + } +} + +impl From for Mesh { + fn from(cone: Cone) -> Self { + cone.mesh().build() + } +} + +impl From for Mesh { + fn from(cone: ConeMeshBuilder) -> Self { + cone.build() + } +} + +#[cfg(test)] +mod tests { + use bevy_math::{primitives::Cone, Vec2}; + + use crate::mesh::{Mesh, Meshable, VertexAttributeValues}; + + /// Rounds floats to handle floating point error in tests. + fn round_floats(points: &mut [[f32; N]]) { + for point in points.iter_mut() { + for coord in point.iter_mut() { + let round = (*coord * 100.0).round() / 100.0; + if (*coord - round).abs() < 0.00001 { + *coord = round; + } + } + } + } + + #[test] + fn cone_mesh() { + let mut mesh = Cone { + radius: 0.5, + height: 1.0, + } + .mesh() + .resolution(4) + .build(); + + let Some(VertexAttributeValues::Float32x3(mut positions)) = + mesh.remove_attribute(Mesh::ATTRIBUTE_POSITION) + else { + panic!("Expected positions f32x3"); + }; + let Some(VertexAttributeValues::Float32x3(mut normals)) = + mesh.remove_attribute(Mesh::ATTRIBUTE_NORMAL) + else { + panic!("Expected normals f32x3"); + }; + + round_floats(&mut positions); + round_floats(&mut normals); + + // Vertex positions + assert_eq!( + [ + // Tip + [0.0, 0.5, 0.0], + // Lateral surface + [0.5, -0.5, 0.0], + [0.0, -0.5, 0.5], + [-0.5, -0.5, 0.0], + [0.0, -0.5, -0.5], + // Base + [0.5, -0.5, 0.0], + [0.0, -0.5, 0.5], + [-0.5, -0.5, 0.0], + [0.0, -0.5, -0.5], + ], + &positions[..] + ); + + // Vertex normals + let [x, y] = Vec2::new(0.5, -1.0).perp().normalize().to_array(); + assert_eq!( + &[ + // Tip + [0.0, 0.0, 0.0], + // Lateral surface + [x, y, 0.0], + [0.0, y, x], + [-x, y, 0.0], + [0.0, y, -x], + // Base + [0.0, -1.0, 0.0], + [0.0, -1.0, 0.0], + [0.0, -1.0, 0.0], + [0.0, -1.0, 0.0], + ], + &normals[..] + ); + } +} diff --git a/crates/bevy_render/src/mesh/primitives/dim3/mod.rs b/crates/bevy_render/src/mesh/primitives/dim3/mod.rs index 3f98557f70a72..82b418f6588f4 100644 --- a/crates/bevy_render/src/mesh/primitives/dim3/mod.rs +++ b/crates/bevy_render/src/mesh/primitives/dim3/mod.rs @@ -1,4 +1,5 @@ mod capsule; +mod cone; mod cuboid; mod cylinder; mod plane; @@ -7,6 +8,7 @@ mod torus; pub(crate) mod triangle3d; pub use capsule::*; +pub use cone::*; pub use cylinder::*; pub use plane::*; pub use sphere::*; diff --git a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs index 4d4553399ab41..bc576bcc5aaaf 100644 --- a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs @@ -408,6 +408,15 @@ pub mod binding_types { .into_bind_group_layout_entry_builder() } + pub fn texture_1d(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder { + BindingType::Texture { + sample_type, + view_dimension: TextureViewDimension::D1, + multisampled: false, + } + .into_bind_group_layout_entry_builder() + } + pub fn texture_2d(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder { BindingType::Texture { sample_type, diff --git a/crates/bevy_render/src/render_resource/buffer_vec.rs b/crates/bevy_render/src/render_resource/buffer_vec.rs index 15874e4743b76..4db7bacd3e01a 100644 --- a/crates/bevy_render/src/render_resource/buffer_vec.rs +++ b/crates/bevy_render/src/render_resource/buffer_vec.rs @@ -43,7 +43,7 @@ pub struct RawBufferVec { item_size: usize, buffer_usage: BufferUsages, label: Option, - label_changed: bool, + changed: bool, } impl RawBufferVec { @@ -55,7 +55,7 @@ impl RawBufferVec { item_size: std::mem::size_of::(), buffer_usage, label: None, - label_changed: false, + changed: false, } } @@ -93,7 +93,7 @@ impl RawBufferVec { let label = label.map(str::to_string); if label != self.label { - self.label_changed = true; + self.changed = true; } self.label = label; @@ -115,16 +115,16 @@ impl RawBufferVec { /// the `RawBufferVec` was created, the buffer on the [`RenderDevice`] /// is marked as [`BufferUsages::COPY_DST`](BufferUsages). pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) { - if capacity > self.capacity || self.label_changed { + let size = self.item_size * capacity; + if capacity > self.capacity || (self.changed && size > 0) { self.capacity = capacity; - let size = self.item_size * capacity; self.buffer = Some(device.create_buffer(&wgpu::BufferDescriptor { label: self.label.as_deref(), size: size as BufferAddress, usage: BufferUsages::COPY_DST | self.buffer_usage, mapped_at_creation: false, })); - self.label_changed = false; + self.changed = false; } } diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 377e24ce7ec35..678be9b0bd76e 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -982,7 +982,7 @@ impl PipelineCache { #[cfg(all( not(target_arch = "wasm32"), not(target_os = "macos"), - feature = "multi-threaded" + feature = "multi_threaded" ))] fn create_pipeline_task( task: impl Future> + Send + 'static, @@ -1001,7 +1001,7 @@ fn create_pipeline_task( #[cfg(any( target_arch = "wasm32", target_os = "macos", - not(feature = "multi-threaded") + not(feature = "multi_threaded") ))] fn create_pipeline_task( task: impl Future> + Send + 'static, diff --git a/crates/bevy_render/src/render_resource/storage_buffer.rs b/crates/bevy_render/src/render_resource/storage_buffer.rs index d30a002c88d4a..1b244f90327ac 100644 --- a/crates/bevy_render/src/render_resource/storage_buffer.rs +++ b/crates/bevy_render/src/render_resource/storage_buffer.rs @@ -8,6 +8,8 @@ use encase::{ }; use wgpu::{util::BufferInitDescriptor, BindingResource, BufferBinding, BufferUsages}; +use super::IntoBinding; + /// Stores data to be transferred to the GPU and made accessible to shaders as a storage buffer. /// /// Storage buffers can be made available to shaders in some combination of read/write mode, and can store large amounts of data. @@ -138,6 +140,16 @@ impl StorageBuffer { } } +impl<'a, T: ShaderType + WriteInto> IntoBinding<'a> for &'a StorageBuffer { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + self.buffer() + .expect("Failed to get buffer") + .as_entire_buffer_binding() + .into_binding() + } +} + /// Stores data to be transferred to the GPU and made accessible to shaders as a dynamic storage buffer. /// /// Dynamic storage buffers can be made available to shaders in some combination of read/write mode, and can store large amounts @@ -238,7 +250,7 @@ impl DynamicStorageBuffer { let capacity = self.buffer.as_deref().map(wgpu::Buffer::size).unwrap_or(0); let size = self.scratch.as_ref().len() as u64; - if capacity < size || self.changed { + if capacity < size || (self.changed && size > 0) { self.buffer = Some(device.create_buffer_with_data(&BufferInitDescriptor { label: self.label.as_deref(), usage: self.buffer_usage, @@ -256,3 +268,10 @@ impl DynamicStorageBuffer { self.scratch.set_offset(0); } } + +impl<'a, T: ShaderType + WriteInto> IntoBinding<'a> for &'a DynamicStorageBuffer { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + self.binding().unwrap() + } +} diff --git a/crates/bevy_render/src/render_resource/uniform_buffer.rs b/crates/bevy_render/src/render_resource/uniform_buffer.rs index f71cdc1cebb22..de4d84c94a06e 100644 --- a/crates/bevy_render/src/render_resource/uniform_buffer.rs +++ b/crates/bevy_render/src/render_resource/uniform_buffer.rs @@ -295,7 +295,7 @@ impl DynamicUniformBuffer { .checked_mul(max_count as u64) .unwrap(); - if capacity < size || self.changed { + if capacity < size || (self.changed && size > 0) { let buffer = device.create_buffer(&BufferDescriptor { label: self.label.as_deref(), usage: self.buffer_usage, @@ -336,7 +336,7 @@ impl DynamicUniformBuffer { let capacity = self.buffer.as_deref().map(wgpu::Buffer::size).unwrap_or(0); let size = self.scratch.as_ref().len() as u64; - if capacity < size || self.changed { + if capacity < size || (self.changed && size > 0) { self.buffer = Some(device.create_buffer_with_data(&BufferInitDescriptor { label: self.label.as_deref(), usage: self.buffer_usage, diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 83e934e78da72..0a6dcc5e0020c 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -36,7 +36,7 @@ use std::{ }, }; use wgpu::{ - Extent3d, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp, + BufferUsages, Extent3d, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, }; @@ -114,7 +114,7 @@ impl Plugin for ViewPlugin { )); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.init_resource::().add_systems( + render_app.add_systems( Render, ( prepare_view_targets @@ -127,6 +127,12 @@ impl Plugin for ViewPlugin { ); } } + + fn finish(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.init_resource::(); + } + } } /// Configuration resource for [Multi-Sample Anti-Aliasing](https://en.wikipedia.org/wiki/Multisample_anti-aliasing). @@ -412,14 +418,27 @@ pub struct ViewUniform { frustum: [Vec4; 6], color_grading: ColorGradingUniform, mip_bias: f32, - render_layers: u32, } -#[derive(Resource, Default)] +#[derive(Resource)] pub struct ViewUniforms { pub uniforms: DynamicUniformBuffer, } +impl FromWorld for ViewUniforms { + fn from_world(world: &mut World) -> Self { + let mut uniforms = DynamicUniformBuffer::default(); + uniforms.set_label(Some("view_uniforms_buffer")); + + let render_device = world.resource::(); + if render_device.limits().max_storage_buffers_per_shader_stage > 0 { + uniforms.add_usages(BufferUsages::STORAGE); + } + + Self { uniforms } + } +} + #[derive(Component)] pub struct ViewUniformOffset { pub offset: u32, @@ -695,7 +714,6 @@ pub fn prepare_view_uniforms( Option<&Frustum>, Option<&TemporalJitter>, Option<&MipBias>, - Option<&RenderLayers>, )>, ) { let view_iter = views.iter(); @@ -707,16 +725,7 @@ pub fn prepare_view_uniforms( else { return; }; - for ( - entity, - extracted_camera, - extracted_view, - frustum, - temporal_jitter, - mip_bias, - maybe_layers, - ) in &views - { + for (entity, extracted_camera, extracted_view, frustum, temporal_jitter, mip_bias) in &views { let viewport = extracted_view.viewport.as_vec4(); let unjittered_projection = extracted_view.projection; let mut projection = unjittered_projection; @@ -759,7 +768,6 @@ pub fn prepare_view_uniforms( frustum, color_grading: extracted_view.color_grading.clone().into(), mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0, - render_layers: maybe_layers.copied().unwrap_or_default().bits(), }), }; diff --git a/crates/bevy_render/src/view/view.wgsl b/crates/bevy_render/src/view/view.wgsl index 6cf0b75a48f0a..4537a09428391 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -28,5 +28,4 @@ struct View { frustum: array, 6>, color_grading: ColorGrading, mip_bias: f32, - render_layers: u32, }; diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index f23a01e441f60..b05451b8ca240 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -429,7 +429,7 @@ pub fn check_visibility( continue; } - let view_mask = maybe_view_mask.copied().unwrap_or_default(); + let view_mask = maybe_view_mask.unwrap_or_default(); visible_aabb_query.par_iter_mut().for_each_init( || thread_queues.borrow_local_mut(), @@ -451,8 +451,8 @@ pub fn check_visibility( return; } - let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); - if !view_mask.intersects(&entity_mask) { + let entity_mask = maybe_entity_mask.unwrap_or_default(); + if !view_mask.intersects(entity_mask) { return; } diff --git a/crates/bevy_render/src/view/visibility/range.rs b/crates/bevy_render/src/view/visibility/range.rs index 62485e42bd8d0..d1e1d6546fa14 100644 --- a/crates/bevy_render/src/view/visibility/range.rs +++ b/crates/bevy_render/src/view/visibility/range.rs @@ -19,7 +19,7 @@ use bevy_reflect::Reflect; use bevy_transform::components::GlobalTransform; use bevy_utils::{prelude::default, EntityHashMap, HashMap}; use nonmax::NonMaxU16; -use wgpu::BufferUsages; +use wgpu::{BufferBindingType, BufferUsages}; use crate::{ camera::Camera, @@ -38,6 +38,11 @@ use super::{check_visibility, VisibilitySystems, WithMesh}; /// buffer slot. pub const VISIBILITY_RANGES_STORAGE_BUFFER_COUNT: u32 = 4; +/// The size of the visibility ranges buffer in elements (not bytes) when fewer +/// than 6 storage buffers are available and we're forced to use a uniform +/// buffer instead (most notably, on WebGL 2). +const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: usize = 64; + /// A plugin that enables [`VisibilityRange`]s, which allow entities to be /// hidden or shown based on distance to the camera. pub struct VisibilityRangePlugin; @@ -424,9 +429,33 @@ pub fn write_render_visibility_ranges( return; } - // If the buffer is empty, push *something* so that we allocate it. - if render_visibility_ranges.buffer.is_empty() { - render_visibility_ranges.buffer.push(default()); + // Mess with the length of the buffer to meet API requirements if necessary. + match render_device.get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT) + { + // If we're using a uniform buffer, we must have *exactly* + // `VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE` elements. + BufferBindingType::Uniform + if render_visibility_ranges.buffer.len() > VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE => + { + render_visibility_ranges + .buffer + .truncate(VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE); + } + BufferBindingType::Uniform + if render_visibility_ranges.buffer.len() < VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE => + { + while render_visibility_ranges.buffer.len() < VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE { + render_visibility_ranges.buffer.push(default()); + } + } + + // Otherwise, if we're using a storage buffer, just ensure there's + // something in the buffer, or else it won't get allocated. + BufferBindingType::Storage { .. } if render_visibility_ranges.buffer.is_empty() => { + render_visibility_ranges.buffer.push(default()); + } + + _ => {} } // Schedule the write. diff --git a/crates/bevy_render/src/view/visibility/render_layers.rs b/crates/bevy_render/src/view/visibility/render_layers.rs index 2a0e5da7d89fc..9cfcb0798d06b 100644 --- a/crates/bevy_render/src/view/visibility/render_layers.rs +++ b/crates/bevy_render/src/view/visibility/render_layers.rs @@ -1,11 +1,12 @@ use bevy_ecs::prelude::{Component, ReflectComponent}; use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; +use smallvec::SmallVec; -type LayerMask = u32; +pub const DEFAULT_LAYERS: &RenderLayers = &RenderLayers::layer(0); /// An identifier for a rendering layer. -pub type Layer = u8; +pub type Layer = usize; /// Describes which rendering layers an entity belongs to. /// @@ -20,9 +21,15 @@ pub type Layer = u8; /// An entity with this component without any layers is invisible. /// /// Entities without this component belong to layer `0`. -#[derive(Component, Copy, Clone, Reflect, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Component, Clone, Reflect, PartialEq, Eq, PartialOrd, Ord)] #[reflect(Component, Default, PartialEq)] -pub struct RenderLayers(LayerMask); +pub struct RenderLayers(SmallVec<[u64; 1]>); + +impl Default for &RenderLayers { + fn default() -> Self { + DEFAULT_LAYERS + } +} impl std::fmt::Debug for RenderLayers { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -41,27 +48,25 @@ impl FromIterator for RenderLayers { impl Default for RenderLayers { /// By default, this structure includes layer `0`, which represents the first layer. fn default() -> Self { - RenderLayers::layer(0) + let (_, bit) = Self::layer_info(0); + RenderLayers(SmallVec::from_const([bit])) } } impl RenderLayers { - /// The total number of layers supported. - pub const TOTAL_LAYERS: usize = std::mem::size_of::() * 8; - /// Create a new `RenderLayers` belonging to the given layer. pub const fn layer(n: Layer) -> Self { - RenderLayers(0).with(n) - } - - /// Create a new `RenderLayers` that belongs to all layers. - pub const fn all() -> Self { - RenderLayers(u32::MAX) + let (buffer_index, bit) = Self::layer_info(n); + assert!( + buffer_index < 1, + "layer is out of bounds for const construction" + ); + RenderLayers(SmallVec::from_const([bit])) } /// Create a new `RenderLayers` that belongs to no layers. pub const fn none() -> Self { - RenderLayers(0) + RenderLayers(SmallVec::from_const([0])) } /// Create a `RenderLayers` from a list of layers. @@ -72,33 +77,28 @@ impl RenderLayers { /// Add the given layer. /// /// This may be called multiple times to allow an entity to belong - /// to multiple rendering layers. The maximum layer is `TOTAL_LAYERS - 1`. - /// - /// # Panics - /// Panics when called with a layer greater than `TOTAL_LAYERS - 1`. + /// to multiple rendering layers. #[must_use] - pub const fn with(mut self, layer: Layer) -> Self { - assert!((layer as usize) < Self::TOTAL_LAYERS); - self.0 |= 1 << layer; + pub fn with(mut self, layer: Layer) -> Self { + let (buffer_index, bit) = Self::layer_info(layer); + self.extend_buffer(buffer_index + 1); + self.0[buffer_index] |= bit; self } /// Removes the given rendering layer. - /// - /// # Panics - /// Panics when called with a layer greater than `TOTAL_LAYERS - 1`. #[must_use] - pub const fn without(mut self, layer: Layer) -> Self { - assert!((layer as usize) < Self::TOTAL_LAYERS); - self.0 &= !(1 << layer); + pub fn without(mut self, layer: Layer) -> Self { + let (buffer_index, bit) = Self::layer_info(layer); + if buffer_index < self.0.len() { + self.0[buffer_index] &= !bit; + } self } /// Get an iterator of the layers. - pub fn iter(&self) -> impl Iterator { - let total: Layer = std::convert::TryInto::try_into(Self::TOTAL_LAYERS).unwrap(); - let mask = *self; - (0..total).filter(move |g| RenderLayers::layer(*g).intersects(&mask)) + pub fn iter(&self) -> impl Iterator + '_ { + self.0.iter().copied().zip(0..).flat_map(Self::iter_layers) } /// Determine if a `RenderLayers` intersects another. @@ -108,40 +108,95 @@ impl RenderLayers { /// A `RenderLayers` with no layers will not match any other /// `RenderLayers`, even another with no layers. pub fn intersects(&self, other: &RenderLayers) -> bool { - (self.0 & other.0) > 0 + // Check for the common case where the view layer and entity layer + // both point towards our default layer. + if self.0.as_ptr() == other.0.as_ptr() { + return true; + } + + for (self_layer, other_layer) in self.0.iter().zip(other.0.iter()) { + if (*self_layer & *other_layer) != 0 { + return true; + } + } + + false } /// get the bitmask representation of the contained layers - pub fn bits(&self) -> u32 { - self.0 + pub fn bits(&self) -> &[u64] { + self.0.as_slice() + } + + const fn layer_info(layer: usize) -> (usize, u64) { + let buffer_index = layer / 64; + let bit_index = layer % 64; + let bit = 1u64 << bit_index; + + (buffer_index, bit) + } + + fn extend_buffer(&mut self, other_len: usize) { + let new_size = std::cmp::max(self.0.len(), other_len); + self.0.reserve_exact(new_size - self.0.len()); + self.0.resize(new_size, 0u64); + } + + fn iter_layers(buffer_and_offset: (u64, usize)) -> impl Iterator + 'static { + let (mut buffer, mut layer) = buffer_and_offset; + layer *= 64; + std::iter::from_fn(move || { + if buffer == 0 { + return None; + } + let next = buffer.trailing_zeros() + 1; + buffer >>= next; + layer += next as usize; + Some(layer - 1) + }) } } #[cfg(test)] mod rendering_mask_tests { use super::{Layer, RenderLayers}; + use smallvec::SmallVec; #[test] fn rendering_mask_sanity() { + let layer_0 = RenderLayers::layer(0); + assert_eq!(layer_0.0.len(), 1, "layer 0 is one buffer"); + assert_eq!(layer_0.0[0], 1, "layer 0 is mask 1"); + let layer_1 = RenderLayers::layer(1); + assert_eq!(layer_1.0.len(), 1, "layer 1 is one buffer"); + assert_eq!(layer_1.0[0], 2, "layer 1 is mask 2"); + let layer_0_1 = RenderLayers::layer(0).with(1); + assert_eq!(layer_0_1.0.len(), 1, "layer 0 + 1 is one buffer"); + assert_eq!(layer_0_1.0[0], 3, "layer 0 + 1 is mask 3"); + let layer_0_1_without_0 = layer_0_1.without(0); assert_eq!( - RenderLayers::TOTAL_LAYERS, - 32, - "total layers is what we think it is" + layer_0_1_without_0.0.len(), + 1, + "layer 0 + 1 - 0 is one buffer" ); - assert_eq!(RenderLayers::layer(0).0, 1, "layer 0 is mask 1"); - assert_eq!(RenderLayers::layer(1).0, 2, "layer 1 is mask 2"); - assert_eq!(RenderLayers::layer(0).with(1).0, 3, "layer 0 + 1 is mask 3"); + assert_eq!(layer_0_1_without_0.0[0], 2, "layer 0 + 1 - 0 is mask 2"); + let layer_0_2345 = RenderLayers::layer(0).with(2345); + assert_eq!(layer_0_2345.0.len(), 37, "layer 0 + 2345 is 37 buffers"); + assert_eq!(layer_0_2345.0[0], 1, "layer 0 + 2345 is mask 1"); assert_eq!( - RenderLayers::layer(0).with(1).without(0).0, - 2, - "layer 0 + 1 - 0 is mask 2" + layer_0_2345.0[36], 2199023255552, + "layer 0 + 2345 is mask 2199023255552" + ); + assert!( + layer_0_2345.intersects(&layer_0), + "layer 0 + 2345 intersects 0" ); assert!( RenderLayers::layer(1).intersects(&RenderLayers::layer(1)), "layers match like layers" ); assert!( - RenderLayers::layer(0).intersects(&RenderLayers(1)), + RenderLayers::layer(0).intersects(&RenderLayers(SmallVec::from_const([1]))), "a layer of 0 means the mask is just 1 bit" ); @@ -162,7 +217,7 @@ mod rendering_mask_tests { "masks with differing layers do not match" ); assert!( - !RenderLayers(0).intersects(&RenderLayers(0)), + !RenderLayers::none().intersects(&RenderLayers::none()), "empty masks don't match" ); assert_eq!( @@ -182,5 +237,10 @@ mod rendering_mask_tests { >::from_iter(vec![0, 1, 2]), "from_layers and from_iter are equivalent" ); + + let tricky_layers = vec![0, 5, 17, 55, 999, 1025, 1026]; + let layers = RenderLayers::from_layers(&tricky_layers); + let out = layers.iter().collect::>(); + assert_eq!(tricky_layers, out, "tricky layers roundtrip"); } } diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index 762771a41edd5..cf347586e6429 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -12,7 +12,7 @@ use bevy_ecs::{entity::EntityHashMap, prelude::*}; use bevy_utils::warn_once; use bevy_utils::{default, tracing::debug, HashSet}; use bevy_window::{ - CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosed, + CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing, }; use std::{ num::NonZeroU32, @@ -117,7 +117,7 @@ impl DerefMut for ExtractedWindows { fn extract_windows( mut extracted_windows: ResMut, screenshot_manager: Extract>, - mut closed: Extract>, + mut closing: Extract>, windows: Extract)>>, mut removed: Extract>, mut window_surfaces: ResMut, @@ -177,9 +177,9 @@ fn extract_windows( } } - for closed_window in closed.read() { - extracted_windows.remove(&closed_window.window); - window_surfaces.remove(&closed_window.window); + for closing_window in closing.read() { + extracted_windows.remove(&closing_window.window); + window_surfaces.remove(&closing_window.window); } for removed_window in removed.read() { extracted_windows.remove(&removed_window); diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 2805840a67694..d58083763aeaa 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -389,7 +389,7 @@ pub fn queue_material2d_meshes( } for (view, visible_entities, tonemapping, dither, mut transparent_phase) in &mut views { - let draw_transparent_pbr = transparent_draw_functions.read().id::>(); + let draw_transparent_2d = transparent_draw_functions.read().id::>(); let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) | Mesh2dPipelineKey::from_hdr(view.hdr); @@ -410,7 +410,7 @@ pub fn queue_material2d_meshes( let Some(mesh_instance) = render_mesh_instances.get_mut(visible_entity) else { continue; }; - let Some(material2d) = render_materials.get(*material_asset_id) else { + let Some(material_2d) = render_materials.get(*material_asset_id) else { continue; }; let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { @@ -424,7 +424,7 @@ pub fn queue_material2d_meshes( &material2d_pipeline, Material2dKey { mesh_key, - bind_group_data: material2d.key.clone(), + bind_group_data: material_2d.key.clone(), }, &mesh.layout, ); @@ -437,18 +437,18 @@ pub fn queue_material2d_meshes( } }; - mesh_instance.material_bind_group_id = material2d.get_bind_group_id(); + mesh_instance.material_bind_group_id = material_2d.get_bind_group_id(); let mesh_z = mesh_instance.transforms.transform.translation.z; transparent_phase.add(Transparent2d { entity: *visible_entity, - draw_function: draw_transparent_pbr, + draw_function: draw_transparent_2d, pipeline: pipeline_id, // NOTE: Back-to-front ordering for transparent with ascending sort means far should have the // lowest sort key and getting closer should increase. As we have // -z in front of the camera, the largest distance is -far with values increasing toward the // camera. As such we can just use mesh_z as the distance - sort_key: FloatOrd(mesh_z + material2d.depth_bias), + sort_key: FloatOrd(mesh_z + material_2d.depth_bias), // Batching is done in batch_and_prepare_render_phase batch_range: 0..1, extra_index: PhaseItemExtraIndex::NONE, diff --git a/crates/bevy_state/Cargo.toml b/crates/bevy_state/Cargo.toml new file mode 100644 index 0000000000000..009d283eaea8a --- /dev/null +++ b/crates/bevy_state/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "bevy_state" +version = "0.14.0-dev" +edition = "2021" +description = "Bevy Engine's entity component system" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["ecs", "game", "bevy"] +categories = ["game-engines", "data-structures"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["bevy_reflect"] + + +[dependencies] +bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" } +bevy_state_macros = { path = "macros", version = "0.14.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", optional = true } + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_state/macros/Cargo.toml b/crates/bevy_state/macros/Cargo.toml new file mode 100644 index 0000000000000..70ff618e4749a --- /dev/null +++ b/crates/bevy_state/macros/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bevy_state_macros" +version = "0.14.0-dev" +description = "Bevy ECS Macros" +edition = "2021" +license = "MIT OR Apache-2.0" + +[lib] +proc-macro = true + +[dependencies] +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" } + +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_state/macros/src/lib.rs b/crates/bevy_state/macros/src/lib.rs new file mode 100644 index 0000000000000..7d401a4793612 --- /dev/null +++ b/crates/bevy_state/macros/src/lib.rs @@ -0,0 +1,24 @@ +// FIXME(3492): remove once docs are ready +#![allow(missing_docs)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +extern crate proc_macro; + +mod states; + +use bevy_macro_utils::BevyManifest; +use proc_macro::TokenStream; + +#[proc_macro_derive(States)] +pub fn derive_states(input: TokenStream) -> TokenStream { + states::derive_states(input) +} + +#[proc_macro_derive(SubStates, attributes(source))] +pub fn derive_substates(input: TokenStream) -> TokenStream { + states::derive_substates(input) +} + +pub(crate) fn bevy_state_path() -> syn::Path { + BevyManifest::default().get_path("bevy_state") +} diff --git a/crates/bevy_state/macros/src/states.rs b/crates/bevy_state/macros/src/states.rs new file mode 100644 index 0000000000000..76a6cbcddf1e4 --- /dev/null +++ b/crates/bevy_state/macros/src/states.rs @@ -0,0 +1,140 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Pat, Path, Result}; + +use crate::bevy_state_path; + +pub fn derive_states(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + + let generics = ast.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let mut base_trait_path = bevy_state_path(); + base_trait_path.segments.push(format_ident!("state").into()); + + let mut trait_path = base_trait_path.clone(); + trait_path.segments.push(format_ident!("States").into()); + + let mut state_mutation_trait_path = base_trait_path.clone(); + state_mutation_trait_path + .segments + .push(format_ident!("FreelyMutableState").into()); + + let struct_name = &ast.ident; + + quote! { + impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause {} + + impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { + } + } + .into() +} + +struct Source { + source_type: Path, + source_value: Pat, +} + +fn parse_sources_attr(ast: &DeriveInput) -> Result { + let mut result = ast + .attrs + .iter() + .filter(|a| a.path().is_ident("source")) + .map(|meta| { + let mut source = None; + let value = meta.parse_nested_meta(|nested| { + let source_type = nested.path.clone(); + let source_value = Pat::parse_multi(nested.value()?)?; + source = Some(Source { + source_type, + source_value, + }); + Ok(()) + }); + match source { + Some(value) => Ok(value), + None => match value { + Ok(_) => Err(syn::Error::new( + ast.span(), + "Couldn't parse SubStates source", + )), + Err(e) => Err(e), + }, + } + }) + .collect::>>()?; + + if result.len() > 1 { + return Err(syn::Error::new( + ast.span(), + "Only one source is allowed for SubStates", + )); + } + + let Some(result) = result.pop() else { + return Err(syn::Error::new(ast.span(), "SubStates require a source")); + }; + + Ok(result) +} + +pub fn derive_substates(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let sources = parse_sources_attr(&ast).expect("Failed to parse substate sources"); + + let generics = ast.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let mut base_trait_path = bevy_state_path(); + base_trait_path.segments.push(format_ident!("state").into()); + + let mut trait_path = base_trait_path.clone(); + trait_path.segments.push(format_ident!("SubStates").into()); + + let mut state_set_trait_path = base_trait_path.clone(); + state_set_trait_path + .segments + .push(format_ident!("StateSet").into()); + + let mut state_trait_path = base_trait_path.clone(); + state_trait_path + .segments + .push(format_ident!("States").into()); + + let mut state_mutation_trait_path = base_trait_path.clone(); + state_mutation_trait_path + .segments + .push(format_ident!("FreelyMutableState").into()); + + let struct_name = &ast.ident; + + let source_state_type = sources.source_type; + let source_state_value = sources.source_value; + + let result = quote! { + impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause { + type SourceStates = #source_state_type; + + fn should_exist(sources: #source_state_type) -> Option { + if matches!(sources, #source_state_value) { + Some(Self::default()) + } else { + None + } + } + } + + impl #impl_generics #state_trait_path for #struct_name #ty_generics #where_clause { + const DEPENDENCY_DEPTH : usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; + } + + impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { + } + }; + + // panic!("Got Result\n{}", result.to_string()); + + result.into() +} diff --git a/crates/bevy_state/src/condition.rs b/crates/bevy_state/src/condition.rs new file mode 100644 index 0000000000000..c0ff5abe49dd1 --- /dev/null +++ b/crates/bevy_state/src/condition.rs @@ -0,0 +1,204 @@ +use bevy_ecs::{change_detection::DetectChanges, system::Res}; +use bevy_utils::warn_once; + +use crate::state::{State, States}; + +/// A [`Condition`](bevy_ecs::prelude::Condition)-satisfying system that returns `true` +/// if the state machine exists. +/// +/// # Example +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// # #[derive(Resource, Default)] +/// # struct Counter(u8); +/// # let mut app = Schedule::default(); +/// # let mut world = World::new(); +/// # world.init_resource::(); +/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] +/// enum GameState { +/// #[default] +/// Playing, +/// Paused, +/// } +/// +/// app.add_systems( +/// // `state_exists` will only return true if the +/// // given state exists +/// my_system.run_if(state_exists::), +/// ); +/// +/// fn my_system(mut counter: ResMut) { +/// counter.0 += 1; +/// } +/// +/// // `GameState` does not yet exist `my_system` won't run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 0); +/// +/// world.init_resource::>(); +/// +/// // `GameState` now exists so `my_system` will run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 1); +/// ``` +pub fn state_exists(current_state: Option>>) -> bool { + current_state.is_some() +} + +/// Generates a [`Condition`](bevy_ecs::prelude::Condition)-satisfying closure that returns `true` +/// if the state machine is currently in `state`. +/// +/// Will return `false` if the state does not exist or if not in `state`. +/// +/// # Example +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// # #[derive(Resource, Default)] +/// # struct Counter(u8); +/// # let mut app = Schedule::default(); +/// # let mut world = World::new(); +/// # world.init_resource::(); +/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] +/// enum GameState { +/// #[default] +/// Playing, +/// Paused, +/// } +/// +/// world.init_resource::>(); +/// +/// app.add_systems(( +/// // `in_state` will only return true if the +/// // given state equals the given value +/// play_system.run_if(in_state(GameState::Playing)), +/// pause_system.run_if(in_state(GameState::Paused)), +/// )); +/// +/// fn play_system(mut counter: ResMut) { +/// counter.0 += 1; +/// } +/// +/// fn pause_system(mut counter: ResMut) { +/// counter.0 -= 1; +/// } +/// +/// // We default to `GameState::Playing` so `play_system` runs +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 1); +/// +/// *world.resource_mut::>() = State::new(GameState::Paused); +/// +/// // Now that we are in `GameState::Pause`, `pause_system` will run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 0); +/// ``` +pub fn in_state(state: S) -> impl FnMut(Option>>) -> bool + Clone { + move |current_state: Option>>| match current_state { + Some(current_state) => *current_state == state, + None => { + warn_once!("No state matching the type for {} exists - did you forget to `init_state` when initializing the app?", { + let debug_state = format!("{state:?}"); + let result = debug_state + .split("::") + .next() + .unwrap_or("Unknown State Type"); + result.to_string() + }); + + false + } + } +} + +/// A [`Condition`](bevy_ecs::prelude::Condition)-satisfying system that returns `true` +/// if the state machine changed state. +/// +/// To do things on transitions to/from specific states, use their respective OnEnter/OnExit +/// schedules. Use this run condition if you want to detect any change, regardless of the value. +/// +/// Returns false if the state does not exist or the state has not changed. +/// +/// # Example +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// # #[derive(Resource, Default)] +/// # struct Counter(u8); +/// # let mut app = Schedule::default(); +/// # let mut world = World::new(); +/// # world.init_resource::(); +/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] +/// enum GameState { +/// #[default] +/// Playing, +/// Paused, +/// } +/// +/// world.init_resource::>(); +/// +/// app.add_systems( +/// // `state_changed` will only return true if the +/// // given states value has just been updated or +/// // the state has just been added +/// my_system.run_if(state_changed::), +/// ); +/// +/// fn my_system(mut counter: ResMut) { +/// counter.0 += 1; +/// } +/// +/// // `GameState` has just been added so `my_system` will run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 1); +/// +/// // `GameState` has not been updated so `my_system` will not run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 1); +/// +/// *world.resource_mut::>() = State::new(GameState::Paused); +/// +/// // Now that `GameState` has been updated `my_system` will run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 2); +/// ``` +pub fn state_changed(current_state: Option>>) -> bool { + let Some(current_state) = current_state else { + return false; + }; + current_state.is_changed() +} + +#[cfg(test)] +mod tests { + use crate as bevy_state; + + use bevy_ecs::schedule::{Condition, IntoSystemConfigs, Schedule}; + + use crate::prelude::*; + use bevy_state_macros::States; + + #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] + enum TestState { + #[default] + A, + B, + } + + fn test_system() {} + + // Ensure distributive_run_if compiles with the common conditions. + #[test] + fn distributive_run_if_compiles() { + Schedule::default().add_systems( + (test_system, test_system) + .distributive_run_if(state_exists::) + .distributive_run_if(in_state(TestState::A).or_else(in_state(TestState::B))) + .distributive_run_if(state_changed::), + ); + } +} diff --git a/crates/bevy_state/src/lib.rs b/crates/bevy_state/src/lib.rs new file mode 100644 index 0000000000000..2104dcedcd897 --- /dev/null +++ b/crates/bevy_state/src/lib.rs @@ -0,0 +1,44 @@ +//! In Bevy, states are app-wide interdependent, finite state machines that are generally used to model the large scale structure of your program: whether a game is paused, if the player is in combat, if assets are loaded and so on. +//! +//! This module provides 3 distinct types of state, all of which implement the [`States`](state::States) trait: +//! +//! - Standard [`States`](state::States) can only be changed by manually setting the [`NextState`](state::NextState) resource. +//! These states are the baseline on which the other state types are built, and can be used on +//! their own for many simple patterns. See the [state example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/state.rs) +//! for a simple use case. +//! - [`SubStates`](state::SubStates) are children of other states - they can be changed manually using [`NextState`](state::NextState), +//! but are removed from the [`World`](bevy_ecs::prelude::World) if the source states aren't in the right state. See the [sub_states example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/sub_states.rs) +//! for a simple use case based on the derive macro, or read the trait docs for more complex scenarios. +//! - [`ComputedStates`](state::ComputedStates) are fully derived from other states - they provide a [`compute`](state::ComputedStates::compute) method +//! that takes in the source states and returns their derived value. They are particularly useful for situations +//! where a simplified view of the source states is necessary - such as having an `InAMenu` computed state, derived +//! from a source state that defines multiple distinct menus. See the [computed state example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/computed_states.rs) +//! to see usage samples for these states. +//! +//! Most of the utilities around state involve running systems during transitions between states, or +//! determining whether to run certain systems, though they can be used more directly as well. This +//! makes it easier to transition between menus, add loading screens, pause games, and the more. +//! +//! Specifically, Bevy provides the following utilities: +//! +//! - 3 Transition Schedules - [`OnEnter`](crate::state::OnEnter), [`OnExit`](crate::state::OnExit) and [`OnTransition`](crate::state::OnTransition) - which are used +//! to trigger systems specifically during matching transitions. +//! - A [`StateTransitionEvent`](crate::state::StateTransitionEvent) that gets fired when a given state changes. +//! - The [`in_state`](crate::condition::in_state) and [`state_changed`](crate::condition::state_changed) run conditions - which are used +//! to determine whether a system should run based on the current state. + +/// Provides definitions for the runtime conditions that interact with the state system +pub mod condition; +/// Provides definitions for the basic traits required by the state system +pub mod state; + +/// Most commonly used re-exported types. +pub mod prelude { + #[doc(hidden)] + pub use crate::condition::*; + #[doc(hidden)] + pub use crate::state::{ + apply_state_transition, ComputedStates, NextState, OnEnter, OnExit, OnTransition, State, + StateSet, StateTransition, StateTransitionEvent, States, SubStates, + }; +} diff --git a/crates/bevy_state/src/state/computed_states.rs b/crates/bevy_state/src/state/computed_states.rs new file mode 100644 index 0000000000000..fda0f99d8c821 --- /dev/null +++ b/crates/bevy_state/src/state/computed_states.rs @@ -0,0 +1,97 @@ +use std::fmt::Debug; +use std::hash::Hash; + +use bevy_ecs::schedule::Schedule; + +use super::state_set::StateSet; +use super::states::States; + +/// A state whose value is automatically computed based on the values of other [`States`]. +/// +/// A **computed state** is a state that is deterministically derived from a set of `SourceStates`. +/// The [`StateSet`] is passed into the `compute` method whenever one of them changes, and the +/// result becomes the state's value. +/// +/// ``` +/// # use bevy_state::prelude::*; +/// # use bevy_ecs::prelude::*; +/// +/// /// Computed States require some state to derive from +/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// enum AppState { +/// #[default] +/// Menu, +/// InGame { paused: bool } +/// } +/// +/// +/// #[derive(Clone, PartialEq, Eq, Hash, Debug)] +/// struct InGame; +/// +/// impl ComputedStates for InGame { +/// /// We set the source state to be the state, or a tuple of states, +/// /// we want to depend on. You can also wrap each state in an Option, +/// /// if you want the computed state to execute even if the state doesn't +/// /// currently exist in the world. +/// type SourceStates = AppState; +/// +/// /// We then define the compute function, which takes in +/// /// your SourceStates +/// fn compute(sources: AppState) -> Option { +/// match sources { +/// /// When we are in game, we want to return the InGame state +/// AppState::InGame { .. } => Some(InGame), +/// /// Otherwise, we don't want the `State` resource to exist, +/// /// so we return None. +/// _ => None +/// } +/// } +/// } +/// ``` +/// +/// you can then add it to an App, and from there you use the state as normal +/// +/// ``` +/// # use bevy_state::prelude::*; +/// # use bevy_ecs::prelude::*; +/// +/// # struct App; +/// # impl App { +/// # fn new() -> Self { App } +/// # fn init_state(&mut self) -> &mut Self {self} +/// # fn add_computed_state(&mut self) -> &mut Self {self} +/// # } +/// # struct AppState; +/// # struct InGame; +/// +/// App::new() +/// .init_state::() +/// .add_computed_state::(); +/// ``` +pub trait ComputedStates: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug { + /// The set of states from which the [`Self`] is derived. + /// + /// This can either be a single type that implements [`States`], an Option of a type + /// that implements [`States`], or a tuple + /// containing multiple types that implement [`States`] or Optional versions of them. + /// + /// For example, `(MapState, EnemyState)` is valid, as is `(MapState, Option)` + type SourceStates: StateSet; + + /// Computes the next value of [`State`](crate::state::State). + /// This function gets called whenever one of the [`SourceStates`](Self::SourceStates) changes. + /// + /// If the result is [`None`], the [`State`](crate::state::State) resource will be removed from the world. + fn compute(sources: Self::SourceStates) -> Option; + + /// This function sets up systems that compute the state whenever one of the [`SourceStates`](Self::SourceStates) + /// change. It is called by `App::add_computed_state`, but can be called manually if `App` is not + /// used. + fn register_computed_state_systems(schedule: &mut Schedule) { + Self::SourceStates::register_computed_state_systems_in_schedule::(schedule); + } +} + +impl States for S { + const DEPENDENCY_DEPTH: usize = S::SourceStates::SET_DEPENDENCY_DEPTH + 1; +} diff --git a/crates/bevy_state/src/state/freely_mutable_state.rs b/crates/bevy_state/src/state/freely_mutable_state.rs new file mode 100644 index 0000000000000..1fc809f9e5d1f --- /dev/null +++ b/crates/bevy_state/src/state/freely_mutable_state.rs @@ -0,0 +1,39 @@ +use bevy_ecs::prelude::Schedule; +use bevy_ecs::schedule::{IntoSystemConfigs, IntoSystemSetConfigs}; +use bevy_ecs::system::IntoSystem; + +use super::states::States; +use super::transitions::*; + +/// This trait allows a state to be mutated directly using the [`NextState`](crate::state::NextState) resource. +/// +/// While ordinary states are freely mutable (and implement this trait as part of their derive macro), +/// computed states are not: instead, they can *only* change when the states that drive them do. +pub trait FreelyMutableState: States { + /// This function registers all the necessary systems to apply state changes and run transition schedules + fn register_state(schedule: &mut Schedule) { + schedule + .add_systems( + apply_state_transition::.in_set(ApplyStateTransition::::apply()), + ) + .add_systems( + should_run_transition::> + .pipe(run_enter::) + .in_set(StateTransitionSteps::EnterSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_exit::) + .in_set(StateTransitionSteps::ExitSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_transition::) + .in_set(StateTransitionSteps::TransitionSchedules), + ) + .configure_sets( + ApplyStateTransition::::apply() + .in_set(StateTransitionSteps::ManualTransitions), + ); + } +} diff --git a/crates/bevy_state/src/state/mod.rs b/crates/bevy_state/src/state/mod.rs new file mode 100644 index 0000000000000..5ea5f8903837c --- /dev/null +++ b/crates/bevy_state/src/state/mod.rs @@ -0,0 +1,500 @@ +mod computed_states; +mod freely_mutable_state; +mod resources; +mod state_set; +mod states; +mod sub_states; +mod transitions; + +pub use bevy_state_macros::*; +pub use computed_states::*; +pub use freely_mutable_state::*; +pub use resources::*; +pub use state_set::*; +pub use states::*; +pub use sub_states::*; +pub use transitions::*; + +#[cfg(test)] +mod tests { + use bevy_ecs::event::EventRegistry; + use bevy_ecs::prelude::*; + use bevy_ecs::schedule::ScheduleLabel; + use bevy_state_macros::States; + use bevy_state_macros::SubStates; + + use super::*; + use crate as bevy_state; + + #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] + enum SimpleState { + #[default] + A, + B(bool), + } + + #[derive(PartialEq, Eq, Debug, Hash, Clone)] + enum TestComputedState { + BisTrue, + BisFalse, + } + + impl ComputedStates for TestComputedState { + type SourceStates = Option; + + fn compute(sources: Option) -> Option { + sources.and_then(|source| match source { + SimpleState::A => None, + SimpleState::B(value) => Some(if value { Self::BisTrue } else { Self::BisFalse }), + }) + } + } + + #[test] + fn computed_state_with_a_single_source_is_correctly_derived() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + world.init_resource::>(); + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + TestComputedState::register_computed_state_systems(&mut apply_changes); + SimpleState::register_state(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + + setup_state_transitions_in_world(&mut world, None); + + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!( + world.resource::>().0, + TestComputedState::BisTrue + ); + + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(false) + ); + assert_eq!( + world.resource::>().0, + TestComputedState::BisFalse + ); + + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + } + + #[derive(SubStates, PartialEq, Eq, Debug, Default, Hash, Clone)] + #[source(SimpleState = SimpleState::B(true))] + enum SubState { + #[default] + One, + Two, + } + + #[test] + fn sub_state_exists_only_when_allowed_but_can_be_modified_freely() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + world.init_resource::>(); + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + SubState::register_sub_state_systems(&mut apply_changes); + SimpleState::register_state(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + + setup_state_transitions_in_world(&mut world, None); + + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SubState::Two)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!(world.resource::>().0, SubState::One); + + world.insert_resource(NextState::Pending(SubState::Two)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!(world.resource::>().0, SubState::Two); + + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(false) + ); + assert!(!world.contains_resource::>()); + } + + #[derive(SubStates, PartialEq, Eq, Debug, Default, Hash, Clone)] + #[source(TestComputedState = TestComputedState::BisTrue)] + enum SubStateOfComputed { + #[default] + One, + Two, + } + + #[test] + fn substate_of_computed_states_works_appropriately() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + world.init_resource::>(); + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + TestComputedState::register_computed_state_systems(&mut apply_changes); + SubStateOfComputed::register_sub_state_systems(&mut apply_changes); + SimpleState::register_state(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + + setup_state_transitions_in_world(&mut world, None); + + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SubStateOfComputed::Two)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!( + world.resource::>().0, + SubStateOfComputed::One + ); + + world.insert_resource(NextState::Pending(SubStateOfComputed::Two)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!( + world.resource::>().0, + SubStateOfComputed::Two + ); + + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(false) + ); + assert!(!world.contains_resource::>()); + } + + #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] + struct OtherState { + a_flexible_value: &'static str, + another_value: u8, + } + + #[derive(PartialEq, Eq, Debug, Hash, Clone)] + enum ComplexComputedState { + InAAndStrIsBobOrJane, + InTrueBAndUsizeAbove8, + } + + impl ComputedStates for ComplexComputedState { + type SourceStates = (Option, Option); + + fn compute(sources: (Option, Option)) -> Option { + match sources { + (Some(simple), Some(complex)) => { + if simple == SimpleState::A + && (complex.a_flexible_value == "bob" || complex.a_flexible_value == "jane") + { + Some(ComplexComputedState::InAAndStrIsBobOrJane) + } else if simple == SimpleState::B(true) && complex.another_value > 8 { + Some(ComplexComputedState::InTrueBAndUsizeAbove8) + } else { + None + } + } + _ => None, + } + } + } + + #[test] + fn complex_computed_state_gets_derived_correctly() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + world.init_resource::>(); + world.init_resource::>(); + + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + + ComplexComputedState::register_computed_state_systems(&mut apply_changes); + + SimpleState::register_state(&mut apply_changes); + OtherState::register_state(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + + setup_state_transitions_in_world(&mut world, None); + + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!( + world.resource::>().0, + OtherState::default() + ); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(OtherState { + a_flexible_value: "felix", + another_value: 13, + })); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + ComplexComputedState::InTrueBAndUsizeAbove8 + ); + + world.insert_resource(NextState::Pending(SimpleState::A)); + world.insert_resource(NextState::Pending(OtherState { + a_flexible_value: "jane", + another_value: 13, + })); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + ComplexComputedState::InAAndStrIsBobOrJane + ); + + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.insert_resource(NextState::Pending(OtherState { + a_flexible_value: "jane", + another_value: 13, + })); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + } + + #[derive(Resource, Default)] + struct ComputedStateTransitionCounter { + enter: usize, + exit: usize, + } + + #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] + enum SimpleState2 { + #[default] + A1, + B2, + } + + #[derive(PartialEq, Eq, Debug, Hash, Clone)] + enum TestNewcomputedState { + A1, + B2, + B1, + } + + impl ComputedStates for TestNewcomputedState { + type SourceStates = (Option, Option); + + fn compute((s1, s2): (Option, Option)) -> Option { + match (s1, s2) { + (Some(SimpleState::A), Some(SimpleState2::A1)) => Some(TestNewcomputedState::A1), + (Some(SimpleState::B(true)), Some(SimpleState2::B2)) => { + Some(TestNewcomputedState::B2) + } + (Some(SimpleState::B(true)), _) => Some(TestNewcomputedState::B1), + _ => None, + } + } + } + + #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] + struct Startup; + + #[test] + fn computed_state_transitions_are_produced_correctly() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + world.init_resource::>(); + world.init_resource::>(); + world.init_resource::(); + + setup_state_transitions_in_world(&mut world, Some(Startup.intern())); + + let mut schedules = world + .get_resource_mut::() + .expect("Schedules don't exist in world"); + let apply_changes = schedules + .get_mut(StateTransition) + .expect("State Transition Schedule Doesn't Exist"); + + TestNewcomputedState::register_computed_state_systems(apply_changes); + + SimpleState::register_state(apply_changes); + SimpleState2::register_state(apply_changes); + + schedules.insert({ + let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::A1)); + schedule.add_systems(|mut count: ResMut| { + count.enter += 1; + }); + schedule + }); + + schedules.insert({ + let mut schedule = Schedule::new(OnExit(TestNewcomputedState::A1)); + schedule.add_systems(|mut count: ResMut| { + count.exit += 1; + }); + schedule + }); + + schedules.insert({ + let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::B1)); + schedule.add_systems(|mut count: ResMut| { + count.enter += 1; + }); + schedule + }); + + schedules.insert({ + let mut schedule = Schedule::new(OnExit(TestNewcomputedState::B1)); + schedule.add_systems(|mut count: ResMut| { + count.exit += 1; + }); + schedule + }); + + schedules.insert({ + let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::B2)); + schedule.add_systems(|mut count: ResMut| { + count.enter += 1; + }); + schedule + }); + + schedules.insert({ + let mut schedule = Schedule::new(OnExit(TestNewcomputedState::B2)); + schedule.add_systems(|mut count: ResMut| { + count.exit += 1; + }); + schedule + }); + + world.init_resource::(); + + setup_state_transitions_in_world(&mut world, None); + + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::A1); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + TestNewcomputedState::B2 + ); + assert_eq!(world.resource::().enter, 1); + assert_eq!(world.resource::().exit, 0); + + world.insert_resource(NextState::Pending(SimpleState2::A1)); + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + TestNewcomputedState::A1 + ); + assert_eq!( + world.resource::().enter, + 2, + "Should Only Enter Twice" + ); + assert_eq!( + world.resource::().exit, + 1, + "Should Only Exit Once" + ); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + TestNewcomputedState::B2 + ); + assert_eq!( + world.resource::().enter, + 3, + "Should Only Enter Three Times" + ); + assert_eq!( + world.resource::().exit, + 2, + "Should Only Exit Twice" + ); + + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + assert_eq!( + world.resource::().enter, + 3, + "Should Only Enter Three Times" + ); + assert_eq!( + world.resource::().exit, + 3, + "Should Only Exit Twice" + ); + } +} diff --git a/crates/bevy_state/src/state/resources.rs b/crates/bevy_state/src/state/resources.rs new file mode 100644 index 0000000000000..9ef49788b1bd3 --- /dev/null +++ b/crates/bevy_state/src/state/resources.rs @@ -0,0 +1,133 @@ +use std::ops::Deref; + +use bevy_ecs::{ + system::Resource, + world::{FromWorld, World}, +}; + +use super::{freely_mutable_state::FreelyMutableState, states::States}; + +#[cfg(feature = "bevy_reflect")] +use bevy_ecs::prelude::ReflectResource; + +/// A finite-state machine whose transitions have associated schedules +/// ([`OnEnter(state)`](crate::state::OnEnter) and [`OnExit(state)`](crate::state::OnExit)). +/// +/// The current state value can be accessed through this resource. To *change* the state, +/// queue a transition in the [`NextState`] resource, and it will be applied by the next +/// [`apply_state_transition::`](crate::state::apply_state_transition) system. +/// +/// The starting state is defined via the [`Default`] implementation for `S`. +/// +/// ``` +/// use bevy_state::prelude::*; +/// use bevy_ecs::prelude::*; +/// use bevy_state_macros::States; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// SettingsMenu, +/// InGame, +/// } +/// +/// fn game_logic(game_state: Res>) { +/// match game_state.get() { +/// GameState::InGame => { +/// // Run game logic here... +/// }, +/// _ => {}, +/// } +/// } +/// ``` +#[derive(Resource, Debug)] +#[cfg_attr( + feature = "bevy_reflect", + derive(bevy_reflect::Reflect), + reflect(Resource) +)] +pub struct State(pub(crate) S); + +impl State { + /// Creates a new state with a specific value. + /// + /// To change the state use [`NextState`] rather than using this to modify the `State`. + pub fn new(state: S) -> Self { + Self(state) + } + + /// Get the current state. + pub fn get(&self) -> &S { + &self.0 + } +} + +impl FromWorld for State { + fn from_world(world: &mut World) -> Self { + Self(S::from_world(world)) + } +} + +impl PartialEq for State { + fn eq(&self, other: &S) -> bool { + self.get() == other + } +} + +impl Deref for State { + type Target = S; + + fn deref(&self) -> &Self::Target { + self.get() + } +} + +/// The next state of [`State`]. +/// +/// To queue a transition, just set the contained value to `Some(next_state)`. +/// +/// Note that these transitions can be overridden by other systems: +/// only the actual value of this resource at the time of [`apply_state_transition`](crate::state::apply_state_transition) matters. +/// +/// ``` +/// use bevy_state::prelude::*; +/// use bevy_ecs::prelude::*; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// SettingsMenu, +/// InGame, +/// } +/// +/// fn start_game(mut next_game_state: ResMut>) { +/// next_game_state.set(GameState::InGame); +/// } +/// ``` +#[derive(Resource, Debug, Default)] +#[cfg_attr( + feature = "bevy_reflect", + derive(bevy_reflect::Reflect), + reflect(Resource) +)] +pub enum NextState { + /// No state transition is pending + #[default] + Unchanged, + /// There is a pending transition for state `S` + Pending(S), +} + +impl NextState { + /// Tentatively set a pending state transition to `Some(state)`. + pub fn set(&mut self, state: S) { + *self = Self::Pending(state); + } + + /// Remove any pending changes to [`State`] + pub fn reset(&mut self) { + *self = Self::Unchanged; + } +} diff --git a/crates/bevy_state/src/state/state_set.rs b/crates/bevy_state/src/state/state_set.rs new file mode 100644 index 0000000000000..067711829dbf0 --- /dev/null +++ b/crates/bevy_state/src/state/state_set.rs @@ -0,0 +1,287 @@ +use bevy_ecs::{ + event::{EventReader, EventWriter}, + schedule::{IntoSystemConfigs, IntoSystemSetConfigs, Schedule}, + system::{Commands, IntoSystem, Res, ResMut}, +}; +use bevy_utils::all_tuples; + +use self::sealed::StateSetSealed; + +use super::{ + apply_state_transition, computed_states::ComputedStates, internal_apply_state_transition, + run_enter, run_exit, run_transition, should_run_transition, sub_states::SubStates, + ApplyStateTransition, OnEnter, OnExit, OnTransition, State, StateTransitionEvent, + StateTransitionSteps, States, +}; + +mod sealed { + /// Sealed trait used to prevent external implementations of [`StateSet`](super::StateSet). + pub trait StateSetSealed {} +} + +/// A [`States`] type or tuple of types which implement [`States`]. +/// +/// This trait is used allow implementors of [`States`], as well +/// as tuples containing exclusively implementors of [`States`], to +/// be used as [`ComputedStates::SourceStates`]. +/// +/// It is sealed, and auto implemented for all [`States`] types and +/// tuples containing them. +pub trait StateSet: sealed::StateSetSealed { + /// The total [`DEPENDENCY_DEPTH`](`States::DEPENDENCY_DEPTH`) of all + /// the states that are part of this [`StateSet`], added together. + /// + /// Used to de-duplicate computed state executions and prevent cyclic + /// computed states. + const SET_DEPENDENCY_DEPTH: usize; + + /// Sets up the systems needed to compute `T` whenever any `State` in this + /// `StateSet` is changed. + fn register_computed_state_systems_in_schedule>( + schedule: &mut Schedule, + ); + + /// Sets up the systems needed to compute whether `T` exists whenever any `State` in this + /// `StateSet` is changed. + fn register_sub_state_systems_in_schedule>( + schedule: &mut Schedule, + ); +} + +/// The `InnerStateSet` trait is used to isolate [`ComputedStates`] & [`SubStates`] from +/// needing to wrap all state dependencies in an [`Option`]. +/// +/// Some [`ComputedStates`]'s might need to exist in different states based on the existence +/// of other states. So we needed the ability to use[`Option`] when appropriate. +/// +/// The isolation works because it is implemented for both S & [`Option`], and has the `RawState` associated type +/// that allows it to know what the resource in the world should be. We can then essentially "unwrap" it in our +/// `StateSet` implementation - and the behaviour of that unwrapping will depend on the arguments expected by the +/// the [`ComputedStates`] & [`SubStates]`. +trait InnerStateSet: Sized { + type RawState: States; + + const DEPENDENCY_DEPTH: usize; + + fn convert_to_usable_state(wrapped: Option<&State>) -> Option; +} + +impl InnerStateSet for S { + type RawState = Self; + + const DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH; + + fn convert_to_usable_state(wrapped: Option<&State>) -> Option { + wrapped.map(|v| v.0.clone()) + } +} + +impl InnerStateSet for Option { + type RawState = S; + + const DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH; + + fn convert_to_usable_state(wrapped: Option<&State>) -> Option { + Some(wrapped.map(|v| v.0.clone())) + } +} + +impl StateSetSealed for S {} + +impl StateSet for S { + const SET_DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH; + + fn register_computed_state_systems_in_schedule>( + schedule: &mut Schedule, + ) { + let system = |mut parent_changed: EventReader>, + event: EventWriter>, + commands: Commands, + current_state: Option>>, + state_set: Option>>| { + if parent_changed.is_empty() { + return; + } + parent_changed.clear(); + + let new_state = + if let Some(state_set) = S::convert_to_usable_state(state_set.as_deref()) { + T::compute(state_set) + } else { + None + }; + + internal_apply_state_transition(event, commands, current_state, new_state); + }; + + schedule + .add_systems(system.in_set(ApplyStateTransition::::apply())) + .add_systems( + should_run_transition::> + .pipe(run_enter::) + .in_set(StateTransitionSteps::EnterSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_exit::) + .in_set(StateTransitionSteps::ExitSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_transition::) + .in_set(StateTransitionSteps::TransitionSchedules), + ) + .configure_sets( + ApplyStateTransition::::apply() + .in_set(StateTransitionSteps::DependentTransitions) + .after(ApplyStateTransition::::apply()), + ); + } + + fn register_sub_state_systems_in_schedule>( + schedule: &mut Schedule, + ) { + let system = |mut parent_changed: EventReader>, + event: EventWriter>, + commands: Commands, + current_state: Option>>, + state_set: Option>>| { + if parent_changed.is_empty() { + return; + } + parent_changed.clear(); + + let new_state = + if let Some(state_set) = S::convert_to_usable_state(state_set.as_deref()) { + T::should_exist(state_set) + } else { + None + }; + + match new_state { + Some(value) => { + if current_state.is_none() { + internal_apply_state_transition( + event, + commands, + current_state, + Some(value), + ); + } + } + None => { + internal_apply_state_transition(event, commands, current_state, None); + } + }; + }; + + schedule + .add_systems(system.in_set(ApplyStateTransition::::apply())) + .add_systems( + apply_state_transition::.in_set(StateTransitionSteps::ManualTransitions), + ) + .add_systems( + should_run_transition::> + .pipe(run_enter::) + .in_set(StateTransitionSteps::EnterSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_exit::) + .in_set(StateTransitionSteps::ExitSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_transition::) + .in_set(StateTransitionSteps::TransitionSchedules), + ) + .configure_sets( + ApplyStateTransition::::apply() + .in_set(StateTransitionSteps::DependentTransitions) + .after(ApplyStateTransition::::apply()), + ); + } +} + +macro_rules! impl_state_set_sealed_tuples { + ($(($param: ident, $val: ident, $evt: ident)), *) => { + impl<$($param: InnerStateSet),*> StateSetSealed for ($($param,)*) {} + + impl<$($param: InnerStateSet),*> StateSet for ($($param,)*) { + + const SET_DEPENDENCY_DEPTH : usize = $($param::DEPENDENCY_DEPTH +)* 0; + + + fn register_computed_state_systems_in_schedule>( + schedule: &mut Schedule, + ) { + let system = |($(mut $evt),*,): ($(EventReader>),*,), event: EventWriter>, commands: Commands, current_state: Option>>, ($($val),*,): ($(Option>>),*,)| { + if ($($evt.is_empty())&&*) { + return; + } + $($evt.clear();)* + + let new_state = if let ($(Some($val)),*,) = ($($param::convert_to_usable_state($val.as_deref())),*,) { + T::compute(($($val),*, )) + } else { + None + }; + + internal_apply_state_transition(event, commands, current_state, new_state); + }; + + schedule + .add_systems(system.in_set(ApplyStateTransition::::apply())) + .add_systems(should_run_transition::>.pipe(run_enter::).in_set(StateTransitionSteps::EnterSchedules)) + .add_systems(should_run_transition::>.pipe(run_exit::).in_set(StateTransitionSteps::ExitSchedules)) + .add_systems(should_run_transition::>.pipe(run_transition::).in_set(StateTransitionSteps::TransitionSchedules)) + .configure_sets( + ApplyStateTransition::::apply() + .in_set(StateTransitionSteps::DependentTransitions) + $(.after(ApplyStateTransition::<$param::RawState>::apply()))* + ); + } + + fn register_sub_state_systems_in_schedule>( + schedule: &mut Schedule, + ) { + let system = |($(mut $evt),*,): ($(EventReader>),*,), event: EventWriter>, commands: Commands, current_state: Option>>, ($($val),*,): ($(Option>>),*,)| { + if ($($evt.is_empty())&&*) { + return; + } + $($evt.clear();)* + + let new_state = if let ($(Some($val)),*,) = ($($param::convert_to_usable_state($val.as_deref())),*,) { + T::should_exist(($($val),*, )) + } else { + None + }; + match new_state { + Some(value) => { + if current_state.is_none() { + internal_apply_state_transition(event, commands, current_state, Some(value)); + } + } + None => { + internal_apply_state_transition(event, commands, current_state, None); + }, + }; + }; + + schedule + .add_systems(system.in_set(ApplyStateTransition::::apply())) + .add_systems(apply_state_transition::.in_set(StateTransitionSteps::ManualTransitions)) + .add_systems(should_run_transition::>.pipe(run_enter::).in_set(StateTransitionSteps::EnterSchedules)) + .add_systems(should_run_transition::>.pipe(run_exit::).in_set(StateTransitionSteps::ExitSchedules)) + .add_systems(should_run_transition::>.pipe(run_transition::).in_set(StateTransitionSteps::TransitionSchedules)) + .configure_sets( + ApplyStateTransition::::apply() + .in_set(StateTransitionSteps::DependentTransitions) + $(.after(ApplyStateTransition::<$param::RawState>::apply()))* + ); + } + } + }; +} + +all_tuples!(impl_state_set_sealed_tuples, 1, 15, S, s, ereader); diff --git a/crates/bevy_state/src/state/states.rs b/crates/bevy_state/src/state/states.rs new file mode 100644 index 0000000000000..6f2be17cd5759 --- /dev/null +++ b/crates/bevy_state/src/state/states.rs @@ -0,0 +1,62 @@ +use std::fmt::Debug; + +use std::hash::Hash; + +/// Types that can define world-wide states in a finite-state machine. +/// +/// The [`Default`] trait defines the starting state. +/// Multiple states can be defined for the same world, +/// allowing you to classify the state of the world across orthogonal dimensions. +/// You can access the current state of type `T` with the [`State`](crate::state::State) resource, +/// and the queued state with the [`NextState`](crate::state::NextState) resource. +/// +/// State transitions typically occur in the [`OnEnter`](crate::state::OnEnter) and [`OnExit`](crate::state::OnExit) schedules, +/// which can be run by triggering the [`StateTransition`](crate::state::StateTransition) schedule. +/// +/// Types used as [`ComputedStates`](crate::state::ComputedStates) do not need to and should not derive [`States`]. +/// [`ComputedStates`](crate::state::ComputedStates) should not be manually mutated: functionality provided +/// by the [`States`] derive and the associated [`FreelyMutableState`](crate::state::FreelyMutableState) trait. +/// +/// # Example +/// +/// ``` +/// use bevy_state::prelude::*; +/// use bevy_ecs::prelude::IntoSystemConfigs; +/// use bevy_ecs::system::ResMut; +/// +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// SettingsMenu, +/// InGame, +/// } +/// +/// fn handle_escape_pressed(mut next_state: ResMut>) { +/// # let escape_pressed = true; +/// if escape_pressed { +/// next_state.set(GameState::SettingsMenu); +/// } +/// } +/// +/// fn open_settings_menu() { +/// // Show the settings menu... +/// } +/// +/// # struct AppMock; +/// # impl AppMock { +/// # fn add_systems(&mut self, schedule: S, systems: impl IntoSystemConfigs) {} +/// # } +/// # struct Update; +/// # let mut app = AppMock; +/// +/// app.add_systems(Update, handle_escape_pressed.run_if(in_state(GameState::MainMenu))); +/// app.add_systems(OnEnter(GameState::SettingsMenu), open_settings_menu); +/// ``` +pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug { + /// How many other states this state depends on. + /// Used to help order transitions and de-duplicate [`ComputedStates`](crate::state::ComputedStates), as well as prevent cyclical + /// `ComputedState` dependencies. + const DEPENDENCY_DEPTH: usize = 1; +} diff --git a/crates/bevy_state/src/state/sub_states.rs b/crates/bevy_state/src/state/sub_states.rs new file mode 100644 index 0000000000000..8046a059b9652 --- /dev/null +++ b/crates/bevy_state/src/state/sub_states.rs @@ -0,0 +1,167 @@ +use bevy_ecs::schedule::Schedule; + +use super::{freely_mutable_state::FreelyMutableState, state_set::StateSet, states::States}; +pub use bevy_state_macros::SubStates; + +/// A sub-state is a state that exists only when the source state meet certain conditions, +/// but unlike [`ComputedStates`](crate::state::ComputedStates) - while they exist they can be manually modified. +/// +/// The default approach to creating [`SubStates`] is using the derive macro, and defining a single source state +/// and value to determine it's existence. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// +/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// enum AppState { +/// #[default] +/// Menu, +/// InGame +/// } +/// +/// +/// #[derive(SubStates, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// #[source(AppState = AppState::InGame)] +/// enum GamePhase { +/// #[default] +/// Setup, +/// Battle, +/// Conclusion +/// } +/// ``` +/// +/// you can then add it to an App, and from there you use the state as normal: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// +/// # struct App; +/// # impl App { +/// # fn new() -> Self { App } +/// # fn init_state(&mut self) -> &mut Self {self} +/// # fn add_sub_state(&mut self) -> &mut Self {self} +/// # } +/// # struct AppState; +/// # struct GamePhase; +/// +/// App::new() +/// .init_state::() +/// .add_sub_state::(); +/// ``` +/// +/// In more complex situations, the recommendation is to use an intermediary computed state, like so: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// +/// /// Computed States require some state to derive from +/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// enum AppState { +/// #[default] +/// Menu, +/// InGame { paused: bool } +/// } +/// +/// #[derive(Clone, PartialEq, Eq, Hash, Debug)] +/// struct InGame; +/// +/// impl ComputedStates for InGame { +/// /// We set the source state to be the state, or set of states, +/// /// we want to depend on. Any of the states can be wrapped in an Option. +/// type SourceStates = Option; +/// +/// /// We then define the compute function, which takes in the AppState +/// fn compute(sources: Option) -> Option { +/// match sources { +/// /// When we are in game, we want to return the InGame state +/// Some(AppState::InGame { .. }) => Some(InGame), +/// /// Otherwise, we don't want the `State` resource to exist, +/// /// so we return None. +/// _ => None +/// } +/// } +/// } +/// +/// #[derive(SubStates, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// #[source(InGame = InGame)] +/// enum GamePhase { +/// #[default] +/// Setup, +/// Battle, +/// Conclusion +/// } +/// ``` +/// +/// However, you can also manually implement them. If you do so, you'll also need to manually implement the `States` & `FreelyMutableState` traits. +/// Unlike the derive, this does not require an implementation of [`Default`], since you are providing the `exists` function +/// directly. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// # use bevy_state::state::FreelyMutableState; +/// +/// /// Computed States require some state to derive from +/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// enum AppState { +/// #[default] +/// Menu, +/// InGame { paused: bool } +/// } +/// +/// #[derive(Clone, PartialEq, Eq, Hash, Debug)] +/// enum GamePhase { +/// Setup, +/// Battle, +/// Conclusion +/// } +/// +/// impl SubStates for GamePhase { +/// /// We set the source state to be the state, or set of states, +/// /// we want to depend on. Any of the states can be wrapped in an Option. +/// type SourceStates = Option; +/// +/// /// We then define the compute function, which takes in the [`Self::SourceStates`] +/// fn should_exist(sources: Option) -> Option { +/// match sources { +/// /// When we are in game, so we want a GamePhase state to exist, and the default is +/// /// GamePhase::Setup +/// Some(AppState::InGame { .. }) => Some(GamePhase::Setup), +/// /// Otherwise, we don't want the `State` resource to exist, +/// /// so we return None. +/// _ => None +/// } +/// } +/// } +/// +/// impl States for GamePhase { +/// const DEPENDENCY_DEPTH : usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; +/// } +/// +/// impl FreelyMutableState for GamePhase {} +/// ``` +pub trait SubStates: States + FreelyMutableState { + /// The set of states from which the [`Self`] is derived. + /// + /// This can either be a single type that implements [`States`], or a tuple + /// containing multiple types that implement [`States`], or any combination of + /// types implementing [`States`] and Options of types implementing [`States`] + type SourceStates: StateSet; + + /// This function gets called whenever one of the [`SourceStates`](Self::SourceStates) changes. + /// The result is used to determine the existence of [`State`](crate::state::State). + /// + /// If the result is [`None`], the [`State`](crate::state::State) resource will be removed from the world, otherwise + /// if the [`State`](crate::state::State) resource doesn't exist - it will be created with the [`Some`] value. + fn should_exist(sources: Self::SourceStates) -> Option; + + /// This function sets up systems that compute the state whenever one of the [`SourceStates`](Self::SourceStates) + /// change. It is called by `App::add_computed_state`, but can be called manually if `App` is not + /// used. + fn register_sub_state_systems(schedule: &mut Schedule) { + Self::SourceStates::register_sub_state_systems_in_schedule::(schedule); + } +} diff --git a/crates/bevy_state/src/state/transitions.rs b/crates/bevy_state/src/state/transitions.rs new file mode 100644 index 0000000000000..5e15ae6293f69 --- /dev/null +++ b/crates/bevy_state/src/state/transitions.rs @@ -0,0 +1,276 @@ +use std::{marker::PhantomData, mem, ops::DerefMut}; + +use bevy_ecs::{ + event::{Event, EventReader, EventWriter}, + schedule::{ + InternedScheduleLabel, IntoSystemSetConfigs, Schedule, ScheduleLabel, Schedules, SystemSet, + }, + system::{Commands, In, Local, Res, ResMut}, + world::World, +}; + +use super::{ + freely_mutable_state::FreelyMutableState, + resources::{NextState, State}, + states::States, +}; + +/// The label of a [`Schedule`] that runs whenever [`State`] +/// enters this state. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct OnEnter(pub S); + +/// The label of a [`Schedule`] that runs whenever [`State`] +/// exits this state. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct OnExit(pub S); + +/// The label of a [`Schedule`] that **only** runs whenever [`State`] +/// exits the `from` state, AND enters the `to` state. +/// +/// Systems added to this schedule are always ran *after* [`OnExit`], and *before* [`OnEnter`]. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct OnTransition { + /// The state being exited. + pub from: S, + /// The state being entered. + pub to: S, +} + +/// Runs [state transitions](States). +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct StateTransition; + +/// Event sent when any state transition of `S` happens. +/// +/// If you know exactly what state you want to respond to ahead of time, consider [`OnEnter`], [`OnTransition`], or [`OnExit`] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Event)] +pub struct StateTransitionEvent { + /// the state we were in before + pub before: Option, + /// the state we're in now + pub after: Option, +} + +/// Applies manual state transitions using [`NextState`]. +/// +/// These system sets are run sequentially, in the order of the enum variants. +#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) enum StateTransitionSteps { + ManualTransitions, + DependentTransitions, + ExitSchedules, + TransitionSchedules, + EnterSchedules, +} + +/// Defines a system set to aid with dependent state ordering +#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] +pub struct ApplyStateTransition(PhantomData); + +impl ApplyStateTransition { + pub(crate) fn apply() -> Self { + Self(PhantomData) + } +} + +/// This function actually applies a state change, and registers the required +/// schedules for downstream computed states and transition schedules. +/// +/// The `new_state` is an option to allow for removal - `None` will trigger the +/// removal of the `State` resource from the [`World`]. +pub(crate) fn internal_apply_state_transition( + mut event: EventWriter>, + mut commands: Commands, + current_state: Option>>, + new_state: Option, +) { + match new_state { + Some(entered) => { + match current_state { + // If the [`State`] resource exists, and the state is not the one we are + // entering - we need to set the new value, compute dependant states, send transition events + // and register transition schedules. + Some(mut state_resource) => { + if *state_resource != entered { + let exited = mem::replace(&mut state_resource.0, entered.clone()); + + event.send(StateTransitionEvent { + before: Some(exited.clone()), + after: Some(entered.clone()), + }); + } + } + None => { + // If the [`State`] resource does not exist, we create it, compute dependant states, send a transition event and register the `OnEnter` schedule. + commands.insert_resource(State(entered.clone())); + + event.send(StateTransitionEvent { + before: None, + after: Some(entered.clone()), + }); + } + }; + } + None => { + // We first remove the [`State`] resource, and if one existed we compute dependant states, send a transition event and run the `OnExit` schedule. + if let Some(resource) = current_state { + commands.remove_resource::>(); + + event.send(StateTransitionEvent { + before: Some(resource.get().clone()), + after: None, + }); + } + } + } +} + +/// Sets up the schedules and systems for handling state transitions +/// within a [`World`]. +/// +/// Runs automatically when using `App` to insert states, but needs to +/// be added manually in other situations. +pub fn setup_state_transitions_in_world( + world: &mut World, + startup_label: Option, +) { + let mut schedules = world.get_resource_or_insert_with(Schedules::default); + if schedules.contains(StateTransition) { + return; + } + let mut schedule = Schedule::new(StateTransition); + schedule.configure_sets( + ( + StateTransitionSteps::ManualTransitions, + StateTransitionSteps::DependentTransitions, + StateTransitionSteps::ExitSchedules, + StateTransitionSteps::TransitionSchedules, + StateTransitionSteps::EnterSchedules, + ) + .chain(), + ); + schedules.insert(schedule); + + if let Some(startup) = startup_label { + schedules.add_systems(startup, |world: &mut World| { + let _ = world.try_run_schedule(StateTransition); + }); + } +} + +/// If a new state is queued in [`NextState`], this system +/// takes the new state value from [`NextState`] and updates [`State`], as well as +/// sending the relevant [`StateTransitionEvent`]. +/// +/// If the [`State`] resource does not exist, it does nothing. Removing or adding states +/// should be done at App creation or at your own risk. +/// +/// For [`SubStates`](crate::state::SubStates) - it only applies the state if the `SubState` currently exists. Otherwise, it is wiped. +/// When a `SubState` is re-created, it will use the result of it's `should_exist` method. +pub fn apply_state_transition( + event: EventWriter>, + commands: Commands, + current_state: Option>>, + next_state: Option>>, +) { + // We want to check if the State and NextState resources exist + let Some(mut next_state_resource) = next_state else { + return; + }; + + match next_state_resource.as_ref() { + NextState::Pending(new_state) => { + if let Some(current_state) = current_state { + if new_state != current_state.get() { + let new_state = new_state.clone(); + internal_apply_state_transition( + event, + commands, + Some(current_state), + Some(new_state), + ); + } + } + } + NextState::Unchanged => { + // This is the default value, so we don't need to re-insert the resource + return; + } + } + + *next_state_resource.as_mut() = NextState::::Unchanged; +} + +pub(crate) fn should_run_transition( + mut first: Local, + res: Option>>, + mut event: EventReader>, +) -> (Option>, PhantomData) { + let first_mut = first.deref_mut(); + if !*first_mut { + *first_mut = true; + if let Some(res) = res { + event.clear(); + + return ( + Some(StateTransitionEvent { + before: None, + after: Some(res.get().clone()), + }), + PhantomData, + ); + } + } + (event.read().last().cloned(), PhantomData) +} + +pub(crate) fn run_enter( + In((transition, _)): In<(Option>, PhantomData>)>, + world: &mut World, +) { + let Some(transition) = transition else { + return; + }; + + let Some(after) = transition.after else { + return; + }; + + let _ = world.try_run_schedule(OnEnter(after)); +} + +pub(crate) fn run_exit( + In((transition, _)): In<(Option>, PhantomData>)>, + world: &mut World, +) { + let Some(transition) = transition else { + return; + }; + + let Some(before) = transition.before else { + return; + }; + + let _ = world.try_run_schedule(OnExit(before)); +} + +pub(crate) fn run_transition( + In((transition, _)): In<( + Option>, + PhantomData>, + )>, + world: &mut World, +) { + let Some(transition) = transition else { + return; + }; + let Some(from) = transition.before else { + return; + }; + let Some(to) = transition.after else { + return; + }; + + let _ = world.try_run_schedule(OnTransition { from, to }); +} diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index eb9cc232e5bf1..c7db7eeae7aec 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -multi-threaded = ["dep:async-channel", "dep:concurrent-queue"] +multi_threaded = ["dep:async-channel", "dep:concurrent-queue"] [dependencies] futures-lite = "2.0.1" diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs index 34011532d6b96..17cfb348ef2c5 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -11,14 +11,14 @@ pub use slice::{ParallelSlice, ParallelSliceMut}; mod task; pub use task::Task; -#[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] mod task_pool; -#[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] pub use task_pool::{Scope, TaskPool, TaskPoolBuilder}; -#[cfg(any(target_arch = "wasm32", not(feature = "multi-threaded")))] +#[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] mod single_threaded_task_pool; -#[cfg(any(target_arch = "wasm32", not(feature = "multi-threaded")))] +#[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] pub use single_threaded_task_pool::{FakeTask, Scope, TaskPool, TaskPoolBuilder, ThreadExecutor}; mod usages; @@ -26,9 +26,9 @@ mod usages; pub use usages::tick_global_task_pools_on_main_thread; pub use usages::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool}; -#[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] mod thread_executor; -#[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] pub use thread_executor::{ThreadExecutor, ThreadExecutorTicker}; #[cfg(feature = "async-io")] diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index cf99ec72e459d..0b3ae44dec6f6 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -61,8 +61,15 @@ impl Text2dBounds { #[derive(Bundle, Clone, Debug, Default)] pub struct Text2dBundle { /// Contains the text. + /// + /// With `Text2dBundle` the alignment field of `Text` only affects the internal alignment of a block of text and not its + /// relative position which is controlled by the `Anchor` component. + /// This means that for a block of text consisting of only one line that doesn't wrap, the `alignment` field will have no effect. pub text: Text, /// How the text is positioned relative to its transform. + /// + /// `text_anchor` does not affect the internal alignment of the block of text, only + /// its position. pub text_anchor: Anchor, /// The maximum width and height of the text. pub text_2d_bounds: Text2dBounds, diff --git a/crates/bevy_time/Cargo.toml b/crates/bevy_time/Cargo.toml index a2ea432d86e1f..9fcfaf93df285 100644 --- a/crates/bevy_time/Cargo.toml +++ b/crates/bevy_time/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = [] +default = ["bevy_reflect"] serialize = ["serde"] [dependencies] @@ -20,7 +20,7 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev", features = [ ] } bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [ "bevy", -] } +], optional = true } bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } # other diff --git a/crates/bevy_time/src/fixed.rs b/crates/bevy_time/src/fixed.rs index 9e5314d4d880a..a49763905577a 100644 --- a/crates/bevy_time/src/fixed.rs +++ b/crates/bevy_time/src/fixed.rs @@ -1,5 +1,6 @@ use bevy_app::FixedMain; use bevy_ecs::world::World; +#[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; use bevy_utils::Duration; @@ -63,7 +64,8 @@ use crate::{time::Time, virt::Virtual}; /// [`FixedUpdate`](bevy_app::FixedUpdate), even if it is still during the same /// frame. Any [`overstep()`](Time::overstep) present in the accumulator will be /// processed according to the new [`timestep()`](Time::timestep) value. -#[derive(Debug, Copy, Clone, Reflect)] +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct Fixed { timestep: Duration, overstep: Duration, diff --git a/crates/bevy_time/src/lib.rs b/crates/bevy_time/src/lib.rs index 912a600bb237e..9778e1517efc7 100644 --- a/crates/bevy_time/src/lib.rs +++ b/crates/bevy_time/src/lib.rs @@ -51,13 +51,18 @@ impl Plugin for TimePlugin { .init_resource::>() .init_resource::>() .init_resource::>() - .init_resource::() - .register_type::