From bfbc6c3d110ac649345bf8b1ea5f3d95a3bd0b97 Mon Sep 17 00:00:00 2001 From: atlv Date: Sat, 5 Jul 2025 09:24:20 -0400 Subject: [PATCH 01/14] move some Visibility stuff to bevy_camera::visibility (#19954) # Objective - Make bevy_light possible ## Solution - Move some stuff it needs out of somewhere it cant depend on. Plus it makes sense, visibility stuff goes in visibility. ## Testing - 3d_scene runs Note: no breaking changes thanks to re-exports --- crates/bevy_camera/src/visibility/mod.rs | 49 +++++++++++++++++++++++- crates/bevy_pbr/Cargo.toml | 1 + crates/bevy_pbr/src/components.rs | 46 ++-------------------- crates/bevy_pbr/src/lib.rs | 3 -- 4 files changed, 52 insertions(+), 47 deletions(-) diff --git a/crates/bevy_camera/src/visibility/mod.rs b/crates/bevy_camera/src/visibility/mod.rs index 684ac403c7..478db336e3 100644 --- a/crates/bevy_camera/src/visibility/mod.rs +++ b/crates/bevy_camera/src/visibility/mod.rs @@ -3,7 +3,7 @@ mod render_layers; use core::any::TypeId; -use bevy_ecs::entity::EntityHashSet; +use bevy_ecs::entity::{EntityHashMap, EntityHashSet}; use bevy_ecs::lifecycle::HookContext; use bevy_ecs::world::DeferredWorld; use derive_more::derive::{Deref, DerefMut}; @@ -267,6 +267,50 @@ impl VisibleEntities { } } +/// Collection of mesh entities visible for 3D lighting. +/// +/// This component contains all mesh entities visible from the current light view. +/// The collection is updated automatically by `bevy_pbr::SimulationLightSystems`. +#[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)] +#[reflect(Component, Debug, Default, Clone)] +pub struct VisibleMeshEntities { + #[reflect(ignore, clone)] + pub entities: Vec, +} + +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Debug, Default, Clone)] +pub struct CubemapVisibleEntities { + #[reflect(ignore, clone)] + data: [VisibleMeshEntities; 6], +} + +impl CubemapVisibleEntities { + pub fn get(&self, i: usize) -> &VisibleMeshEntities { + &self.data[i] + } + + pub fn get_mut(&mut self, i: usize) -> &mut VisibleMeshEntities { + &mut self.data[i] + } + + pub fn iter(&self) -> impl DoubleEndedIterator { + self.data.iter() + } + + pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { + self.data.iter_mut() + } +} + +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Default, Clone)] +pub struct CascadesVisibleEntities { + /// Map of view entity to the visible entities for each cascade frustum. + #[reflect(ignore, clone)] + pub entities: EntityHashMap>, +} + #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum VisibilitySystems { /// Label for the [`calculate_bounds`], `calculate_bounds_2d` and `calculate_bounds_text2d` systems, @@ -303,6 +347,9 @@ impl Plugin for VisibilityPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() + .register_type::() + .register_type::() .register_required_components::() .register_required_components::() .register_required_components::() diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 56dfbf77b3..754627558c 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -44,6 +44,7 @@ bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_camera = { path = "../bevy_camera", version = "0.17.0-dev" } bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", optional = true } bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } diff --git a/crates/bevy_pbr/src/components.rs b/crates/bevy_pbr/src/components.rs index fca31b3b03..4c451e53f5 100644 --- a/crates/bevy_pbr/src/components.rs +++ b/crates/bevy_pbr/src/components.rs @@ -1,19 +1,12 @@ +pub use bevy_camera::visibility::{ + CascadesVisibleEntities, CubemapVisibleEntities, VisibleMeshEntities, +}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::component::Component; use bevy_ecs::entity::{Entity, EntityHashMap}; use bevy_ecs::reflect::ReflectComponent; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::sync_world::MainEntity; -/// Collection of mesh entities visible for 3D lighting. -/// -/// This component contains all mesh entities visible from the current light view. -/// The collection is updated automatically by [`crate::SimulationLightSystems`]. -#[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)] -#[reflect(Component, Debug, Default, Clone)] -pub struct VisibleMeshEntities { - #[reflect(ignore, clone)] - pub entities: Vec, -} #[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)] #[reflect(Component, Debug, Default, Clone)] @@ -22,31 +15,6 @@ pub struct RenderVisibleMeshEntities { pub entities: Vec<(Entity, MainEntity)>, } -#[derive(Component, Clone, Debug, Default, Reflect)] -#[reflect(Component, Debug, Default, Clone)] -pub struct CubemapVisibleEntities { - #[reflect(ignore, clone)] - data: [VisibleMeshEntities; 6], -} - -impl CubemapVisibleEntities { - pub fn get(&self, i: usize) -> &VisibleMeshEntities { - &self.data[i] - } - - pub fn get_mut(&mut self, i: usize) -> &mut VisibleMeshEntities { - &mut self.data[i] - } - - pub fn iter(&self) -> impl DoubleEndedIterator { - self.data.iter() - } - - pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { - self.data.iter_mut() - } -} - #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component, Debug, Default, Clone)] pub struct RenderCubemapVisibleEntities { @@ -72,14 +40,6 @@ impl RenderCubemapVisibleEntities { } } -#[derive(Component, Clone, Debug, Default, Reflect)] -#[reflect(Component, Default, Clone)] -pub struct CascadesVisibleEntities { - /// Map of view entity to the visible entities for each cascade frustum. - #[reflect(ignore, clone)] - pub entities: EntityHashMap>, -} - #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component, Default, Clone)] pub struct RenderCascadesVisibleEntities { diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index f0e6fa90d7..86d2bf8ede 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -208,10 +208,7 @@ impl Plugin for PbrPlugin { .register_type::() .register_type::() .register_type::() - .register_type::() - .register_type::() .register_type::() - .register_type::() .register_type::() .register_type::() .register_type::() From 1b09c4051e0417b85d53a11cc23cea872c66caec Mon Sep 17 00:00:00 2001 From: atlv Date: Sat, 5 Jul 2025 09:24:28 -0400 Subject: [PATCH 02/14] Move Camera3d/2d to bevy_camera (#19953) # Objective - define scenes without bevy_render ## Solution - Move Camera2d/3d components out of bevy_core_pipeline ## Testing - 3d_scene runs fine Note: no breaking changes thanks to re-exports --- .../src/components.rs} | 38 ++++++++----------- crates/bevy_camera/src/lib.rs | 2 + crates/bevy_core_pipeline/Cargo.toml | 1 + .../src/core_2d/camera_2d.rs | 26 ------------- crates/bevy_core_pipeline/src/core_2d/mod.rs | 14 +++++-- crates/bevy_core_pipeline/src/core_3d/mod.rs | 13 +++++-- crates/bevy_render/src/camera.rs | 28 +++++++++++--- 7 files changed, 63 insertions(+), 59 deletions(-) rename crates/{bevy_core_pipeline/src/core_3d/camera_3d.rs => bevy_camera/src/components.rs} (87%) delete mode 100644 crates/bevy_core_pipeline/src/core_2d/camera_2d.rs diff --git a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs b/crates/bevy_camera/src/components.rs similarity index 87% rename from crates/bevy_core_pipeline/src/core_3d/camera_3d.rs rename to crates/bevy_camera/src/components.rs index f5314c736d..867936727c 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_camera/src/components.rs @@ -1,39 +1,33 @@ -use crate::{ - core_3d::graph::Core3d, - tonemapping::{DebandDither, Tonemapping}, -}; +use crate::{primitives::Frustum, Camera, CameraProjection, OrthographicProjection, Projection}; use bevy_ecs::prelude::*; use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize}; -use bevy_render::{ - camera::{Camera, CameraRenderGraph, Exposure, Projection}, - extract_component::ExtractComponent, - render_resource::{LoadOp, TextureUsages}, - view::ColorGrading, -}; +use bevy_transform::prelude::{GlobalTransform, Transform}; use serde::{Deserialize, Serialize}; +use wgpu_types::{LoadOp, TextureUsages}; + +/// A 2D camera component. Enables the 2D render graph for a [`Camera`]. +#[derive(Component, Default, Reflect, Clone)] +#[reflect(Component, Default, Clone)] +#[require( + Camera, + Projection::Orthographic(OrthographicProjection::default_2d()), + Frustum = OrthographicProjection::default_2d().compute_frustum(&GlobalTransform::from(Transform::default())), +)] +pub struct Camera2d; /// A 3D camera component. Enables the main 3D render graph for a [`Camera`]. /// /// The camera coordinate space is right-handed X-right, Y-up, Z-back. /// This means "forward" is -Z. -#[derive(Component, Reflect, Clone, ExtractComponent)] -#[extract_component_filter(With)] +#[derive(Component, Reflect, Clone)] #[reflect(Component, Default, Clone)] -#[require( - Camera, - DebandDither::Enabled, - CameraRenderGraph::new(Core3d), - Projection, - Tonemapping, - ColorGrading, - Exposure -)] +#[require(Camera, Projection)] pub struct Camera3d { /// The depth clear operation to perform for the main 3d pass. pub depth_load_op: Camera3dDepthLoadOp, /// The texture usages for the depth texture created for the main 3d pass. pub depth_texture_usages: Camera3dDepthTextureUsage, - /// How many individual steps should be performed in the [`Transmissive3d`](crate::core_3d::Transmissive3d) pass. + /// How many individual steps should be performed in the `Transmissive3d` pass. /// /// Roughly corresponds to how many “layers of transparency” are rendered for screen space /// specular transmissive objects. Each step requires making one additional diff --git a/crates/bevy_camera/src/lib.rs b/crates/bevy_camera/src/lib.rs index 6fd284d49d..bf0ededae8 100644 --- a/crates/bevy_camera/src/lib.rs +++ b/crates/bevy_camera/src/lib.rs @@ -1,12 +1,14 @@ #![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] mod camera; mod clear_color; +mod components; pub mod primitives; mod projection; pub mod visibility; pub use camera::*; pub use clear_color::*; +pub use components::*; pub use projection::*; use bevy_app::{App, Plugin}; diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index 9b3f158af2..680d57efa1 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -27,6 +27,7 @@ bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_camera = { path = "../bevy_camera", version = "0.17.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } diff --git a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs deleted file mode 100644 index d46174192b..0000000000 --- a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::{ - core_2d::graph::Core2d, - tonemapping::{DebandDither, Tonemapping}, -}; -use bevy_ecs::prelude::*; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::{ - camera::{Camera, CameraProjection, CameraRenderGraph, OrthographicProjection, Projection}, - extract_component::ExtractComponent, - primitives::Frustum, -}; -use bevy_transform::prelude::{GlobalTransform, Transform}; - -/// A 2D camera component. Enables the 2D render graph for a [`Camera`]. -#[derive(Component, Default, Reflect, Clone, ExtractComponent)] -#[extract_component_filter(With)] -#[reflect(Component, Default, Clone)] -#[require( - Camera, - DebandDither, - CameraRenderGraph::new(Core2d), - Projection::Orthographic(OrthographicProjection::default_2d()), - Frustum = OrthographicProjection::default_2d().compute_frustum(&GlobalTransform::from(Transform::default())), - Tonemapping::None, -)] -pub struct Camera2d; diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index f50d3e5984..f051c1164c 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -1,4 +1,3 @@ -mod camera_2d; mod main_opaque_pass_2d_node; mod main_transparent_pass_2d_node; @@ -34,18 +33,22 @@ pub mod graph { use core::ops::Range; use bevy_asset::UntypedAssetId; +pub use bevy_camera::Camera2d; use bevy_image::ToExtents; use bevy_platform::collections::{HashMap, HashSet}; use bevy_render::{ batching::gpu_preprocessing::GpuPreprocessingMode, + camera::CameraRenderGraph, render_phase::PhaseItemBatchSetKey, view::{ExtractedView, RetainedViewEntity}, }; -pub use camera_2d::*; pub use main_opaque_pass_2d_node::*; pub use main_transparent_pass_2d_node::*; -use crate::{tonemapping::TonemappingNode, upscaling::UpscalingNode}; +use crate::{ + tonemapping::{DebandDither, Tonemapping, TonemappingNode}, + upscaling::UpscalingNode, +}; use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; use bevy_math::FloatOrd; @@ -78,6 +81,11 @@ pub struct Core2dPlugin; impl Plugin for Core2dPlugin { fn build(&self, app: &mut App) { app.register_type::() + .register_required_components::() + .register_required_components_with::(|| { + CameraRenderGraph::new(Core2d) + }) + .register_required_components_with::(|| Tonemapping::None) .add_plugins(ExtractComponentPlugin::::default()); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 3a127631cc..9fd7880869 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -1,4 +1,3 @@ -mod camera_3d; mod main_opaque_pass_3d_node; mod main_transmissive_pass_3d_node; mod main_transparent_pass_3d_node; @@ -70,14 +69,17 @@ pub const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = true; use core::ops::Range; +pub use bevy_camera::{ + Camera3d, Camera3dDepthLoadOp, Camera3dDepthTextureUsage, ScreenSpaceTransmissionQuality, +}; use bevy_render::{ batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, + camera::CameraRenderGraph, experimental::occlusion_culling::OcclusionCulling, mesh::allocator::SlabId, render_phase::PhaseItemBatchSetKey, view::{prepare_view_targets, NoIndirectDrawing, RetainedViewEntity}, }; -pub use camera_3d::*; pub use main_opaque_pass_3d_node::*; pub use main_transparent_pass_3d_node::*; @@ -127,7 +129,7 @@ use crate::{ ViewPrepassTextures, MOTION_VECTOR_PREPASS_FORMAT, NORMAL_PREPASS_FORMAT, }, skybox::SkyboxPlugin, - tonemapping::TonemappingNode, + tonemapping::{DebandDither, Tonemapping, TonemappingNode}, upscaling::UpscalingNode, }; @@ -139,6 +141,11 @@ impl Plugin for Core3dPlugin { fn build(&self, app: &mut App) { app.register_type::() .register_type::() + .register_required_components_with::(|| DebandDither::Enabled) + .register_required_components_with::(|| { + CameraRenderGraph::new(Core3d) + }) + .register_required_components::() .add_plugins((SkyboxPlugin, ExtractComponentPlugin::::default())) .add_systems(PostUpdate, check_msaa); diff --git a/crates/bevy_render/src/camera.rs b/crates/bevy_render/src/camera.rs index b5dcc6baa5..346762aecc 100644 --- a/crates/bevy_render/src/camera.rs +++ b/crates/bevy_render/src/camera.rs @@ -29,7 +29,7 @@ use bevy_ecs::{ event::EventReader, lifecycle::HookContext, prelude::With, - query::Has, + query::{Has, QueryItem}, reflect::ReflectComponent, resource::Resource, schedule::IntoScheduleConfigs, @@ -59,6 +59,8 @@ impl Plugin for CameraPlugin { .register_type::() .register_required_components::() .register_required_components::() + .register_required_components::() + .register_required_components::() .add_plugins(( ExtractResourcePlugin::::default(), ExtractComponentPlugin::::default(), @@ -95,7 +97,7 @@ fn warn_on_no_render_graph(world: DeferredWorld, HookContext { entity, caller, . } impl ExtractResource for ClearColor { - type Source = ClearColor; + type Source = Self; fn extract_resource(source: &Self::Source) -> Self { source.clone() @@ -106,12 +108,28 @@ impl ExtractComponent for CameraMainTextureUsages { type QueryFilter = (); type Out = Self; - fn extract_component( - item: bevy_ecs::query::QueryItem<'_, '_, Self::QueryData>, - ) -> Option { + fn extract_component(item: QueryItem) -> Option { Some(*item) } } +impl ExtractComponent for Camera2d { + type QueryData = &'static Self; + type QueryFilter = With; + type Out = Self; + + fn extract_component(item: QueryItem) -> Option { + Some(item.clone()) + } +} +impl ExtractComponent for Camera3d { + type QueryData = &'static Self; + type QueryFilter = With; + type Out = Self; + + fn extract_component(item: QueryItem) -> Option { + Some(item.clone()) + } +} /// Configures the [`RenderGraph`] name assigned to be run for a given [`Camera`] entity. #[derive(Component, Debug, Deref, DerefMut, Reflect, Clone)] From d0896bf10abfd6823cbe57a64f7a908dc0fda4b3 Mon Sep 17 00:00:00 2001 From: atlv Date: Sat, 5 Jul 2025 10:39:41 -0400 Subject: [PATCH 03/14] make cluster assign not depend on RenderAdapter/RenderDevice (#19957) # Objective - Make bevy_light possible by making it possible to split out clusterable into bevy_camera ## Solution - Use a resource to store cluster settings instead of recalculating it every time from the render adapter/device ## Testing - 3d_scene runs --- crates/bevy_pbr/src/cluster/assign.rs | 27 ++++++++------------------- crates/bevy_pbr/src/cluster/mod.rs | 23 ++++++++++++++++++++++- crates/bevy_pbr/src/lib.rs | 3 +++ 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/crates/bevy_pbr/src/cluster/assign.rs b/crates/bevy_pbr/src/cluster/assign.rs index b0c0fb6347..c2526bc2ff 100644 --- a/crates/bevy_pbr/src/cluster/assign.rs +++ b/crates/bevy_pbr/src/cluster/assign.rs @@ -12,8 +12,6 @@ use bevy_math::{ use bevy_render::{ camera::Camera, primitives::{Aabb, Frustum, HalfSpace, Sphere}, - render_resource::BufferBindingType, - renderer::{RenderAdapter, RenderDevice}, view::{RenderLayers, ViewVisibility}, }; use bevy_transform::components::GlobalTransform; @@ -21,12 +19,10 @@ use bevy_utils::prelude::default; use tracing::warn; use crate::{ - decal::{self, clustered::ClusteredDecal}, - prelude::EnvironmentMapLight, - ClusterConfig, ClusterFarZMode, Clusters, ExtractedPointLight, GlobalVisibleClusterableObjects, + decal::clustered::ClusteredDecal, prelude::EnvironmentMapLight, ClusterConfig, ClusterFarZMode, + Clusters, ExtractedPointLight, GlobalClusterSettings, GlobalVisibleClusterableObjects, LightProbe, PointLight, SpotLight, ViewClusterBindings, VisibleClusterableObjects, - VolumetricLight, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, - MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS, + VolumetricLight, MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS, }; const NDC_MIN: Vec2 = Vec2::NEG_ONE; @@ -180,9 +176,9 @@ pub(crate) fn assign_objects_to_clusters( mut clusterable_objects: Local>, mut cluster_aabb_spheres: Local>>, mut max_clusterable_objects_warning_emitted: Local, - (render_device, render_adapter): (Option>, Option>), + global_cluster_settings: Option>, ) { - let (Some(render_device), Some(render_adapter)) = (render_device, render_adapter) else { + let Some(global_cluster_settings) = global_cluster_settings else { return; }; @@ -229,20 +225,13 @@ pub(crate) fn assign_objects_to_clusters( ), ); - let clustered_forward_buffer_binding_type = - render_device.get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT); - let supports_storage_buffers = matches!( - clustered_forward_buffer_binding_type, - BufferBindingType::Storage { .. } - ); - // Gather up light probes, but only if we're clustering them. // // UBOs aren't large enough to hold indices for light probes, so we can't // cluster light probes on such platforms (mainly WebGL 2). Besides, those // platforms typically lack bindless textures, so multiple light probes // wouldn't be supported anyhow. - if supports_storage_buffers { + if global_cluster_settings.supports_storage_buffers { clusterable_objects.extend(light_probes_query.iter().map( |(entity, transform, is_reflection_probe)| ClusterableObjectAssignmentData { entity, @@ -259,7 +248,7 @@ pub(crate) fn assign_objects_to_clusters( } // Add decals if the current platform supports them. - if decal::clustered::clustered_decals_are_usable(&render_device, &render_adapter) { + if global_cluster_settings.clustered_decals_are_usable { clusterable_objects.extend(decals_query.iter().map(|(entity, transform)| { ClusterableObjectAssignmentData { entity, @@ -272,7 +261,7 @@ pub(crate) fn assign_objects_to_clusters( } if clusterable_objects.len() > MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS - && !supports_storage_buffers + && !global_cluster_settings.supports_storage_buffers { clusterable_objects.sort_by_cached_key(|clusterable_object| { ( diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index 501f4091fc..7f90a4becb 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -21,7 +21,7 @@ use bevy_render::{ BindingResource, BufferBindingType, ShaderSize as _, ShaderType, StorageBuffer, UniformBuffer, }, - renderer::{RenderDevice, RenderQueue}, + renderer::{RenderAdapter, RenderDevice, RenderQueue}, sync_world::RenderEntity, Extract, }; @@ -63,6 +63,27 @@ const CLUSTER_COUNT_MASK: u32 = (1 << CLUSTER_COUNT_SIZE) - 1; // The z-slicing method mentioned in the aortiz article is originally from Tiago Sousa's Siggraph 2016 talk about Doom 2016: // http://advances.realtimerendering.com/s2016/Siggraph2016_idTech6.pdf +#[derive(Resource)] +pub struct GlobalClusterSettings { + pub supports_storage_buffers: bool, + pub clustered_decals_are_usable: bool, +} + +pub(crate) fn make_global_cluster_settings(world: &World) -> GlobalClusterSettings { + let device = world.resource::(); + let adapter = world.resource::(); + let clustered_decals_are_usable = + crate::decal::clustered::clustered_decals_are_usable(device, adapter); + let supports_storage_buffers = matches!( + device.get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT), + BufferBindingType::Storage { .. } + ); + GlobalClusterSettings { + supports_storage_buffers, + clustered_decals_are_usable, + } +} + /// Configure the far z-plane mode used for the furthest depth slice for clustered forward /// rendering #[derive(Debug, Copy, Clone, Reflect)] diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 86d2bf8ede..0604c06328 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -396,6 +396,9 @@ impl Plugin for PbrPlugin { .init_resource::() .init_resource::() .init_resource::(); + + let global_cluster_settings = make_global_cluster_settings(render_app.world()); + app.insert_resource(global_cluster_settings); } } From bdb39cf723a5076cac017c6cdc009aaa1b4ccac1 Mon Sep 17 00:00:00 2001 From: atlv Date: Sat, 5 Jul 2025 10:40:06 -0400 Subject: [PATCH 04/14] move spot light function into spot light file (#19956) # Objective - Make bevy_light possible ## Solution - Move some stuff it needs out of somewhere it cant depend on. Plus it makes sense, spotlight stuff goes in spotlight file. ## Testing - 3d_scene runs Note: no breaking changes thanks to re-exports --- crates/bevy_pbr/src/light/mod.rs | 1 + crates/bevy_pbr/src/light/spot_light.rs | 32 +++++++++++++++++++++++++ crates/bevy_pbr/src/render/light.rs | 32 ------------------------- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index a93e3e4c58..004085fda6 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -20,6 +20,7 @@ use bevy_utils::Parallel; use core::{marker::PhantomData, ops::DerefMut}; use crate::*; +pub use light::spot_light::{spot_light_clip_from_view, spot_light_world_from_view}; mod ambient_light; pub use ambient_light::AmbientLight; diff --git a/crates/bevy_pbr/src/light/spot_light.rs b/crates/bevy_pbr/src/light/spot_light.rs index a7cfe1b817..7e0bd43f15 100644 --- a/crates/bevy_pbr/src/light/spot_light.rs +++ b/crates/bevy_pbr/src/light/spot_light.rs @@ -140,3 +140,35 @@ impl Default for SpotLight { } } } + +// this method of constructing a basis from a vec3 is used by glam::Vec3::any_orthonormal_pair +// we will also construct it in the fragment shader and need our implementations to match, +// so we reproduce it here to avoid a mismatch if glam changes. we also switch the handedness +// could move this onto transform but it's pretty niche +pub fn spot_light_world_from_view(transform: &GlobalTransform) -> Mat4 { + // the matrix z_local (opposite of transform.forward()) + let fwd_dir = transform.back().extend(0.0); + + let sign = 1f32.copysign(fwd_dir.z); + let a = -1.0 / (fwd_dir.z + sign); + let b = fwd_dir.x * fwd_dir.y * a; + let up_dir = Vec4::new( + 1.0 + sign * fwd_dir.x * fwd_dir.x * a, + sign * b, + -sign * fwd_dir.x, + 0.0, + ); + let right_dir = Vec4::new(-b, -sign - fwd_dir.y * fwd_dir.y * a, fwd_dir.y, 0.0); + + Mat4::from_cols( + right_dir, + up_dir, + fwd_dir, + transform.translation().extend(1.0), + ) +} + +pub fn spot_light_clip_from_view(angle: f32, near_z: f32) -> Mat4 { + // spot light projection FOV is 2x the angle from spot light center to outer edge + Mat4::perspective_infinite_reverse_rh(angle * 2.0, 1.0, near_z) +} diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 1a55eb0926..cee3d76e14 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -711,38 +711,6 @@ pub fn calculate_cluster_factors( } } -// this method of constructing a basis from a vec3 is used by glam::Vec3::any_orthonormal_pair -// we will also construct it in the fragment shader and need our implementations to match, -// so we reproduce it here to avoid a mismatch if glam changes. we also switch the handedness -// could move this onto transform but it's pretty niche -pub(crate) fn spot_light_world_from_view(transform: &GlobalTransform) -> Mat4 { - // the matrix z_local (opposite of transform.forward()) - let fwd_dir = transform.back().extend(0.0); - - let sign = 1f32.copysign(fwd_dir.z); - let a = -1.0 / (fwd_dir.z + sign); - let b = fwd_dir.x * fwd_dir.y * a; - let up_dir = Vec4::new( - 1.0 + sign * fwd_dir.x * fwd_dir.x * a, - sign * b, - -sign * fwd_dir.x, - 0.0, - ); - let right_dir = Vec4::new(-b, -sign - fwd_dir.y * fwd_dir.y * a, fwd_dir.y, 0.0); - - Mat4::from_cols( - right_dir, - up_dir, - fwd_dir, - transform.translation().extend(1.0), - ) -} - -pub(crate) fn spot_light_clip_from_view(angle: f32, near_z: f32) -> Mat4 { - // spot light projection FOV is 2x the angle from spot light center to outer edge - Mat4::perspective_infinite_reverse_rh(angle * 2.0, 1.0, near_z) -} - pub fn prepare_lights( mut commands: Commands, mut texture_cache: ResMut, From a869383cdaf1a50e5cda89a1f441cdb3f248bb4c Mon Sep 17 00:00:00 2001 From: theotherphil Date: Sat, 5 Jul 2025 15:40:23 +0100 Subject: [PATCH 05/14] Minor code readability improvement in enum_utility.access_field (#19961) Small cleanup copied from https://github.com/bevyengine/bevy/pull/16250 --- .../bevy_reflect/derive/src/enum_utility.rs | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/crates/bevy_reflect/derive/src/enum_utility.rs b/crates/bevy_reflect/derive/src/enum_utility.rs index 5571b861a6..ad41a1a8df 100644 --- a/crates/bevy_reflect/derive/src/enum_utility.rs +++ b/crates/bevy_reflect/derive/src/enum_utility.rs @@ -48,20 +48,15 @@ pub(crate) trait VariantBuilder: Sized { /// * `this`: The identifier of the enum /// * `field`: The field to access fn access_field(&self, this: &Ident, field: VariantField) -> TokenStream { - match &field.field.data.ident { - Some(field_ident) => { - let name = field_ident.to_string(); - quote!(#this.field(#name)) - } - None => { - if let Some(field_index) = field.field.reflection_index { - quote!(#this.field_at(#field_index)) - } else { - quote!(::core::compile_error!( - "internal bevy_reflect error: field should be active" - )) - } - } + if let Some(field_ident) = &field.field.data.ident { + let name = field_ident.to_string(); + quote!(#this.field(#name)) + } else if let Some(field_index) = field.field.reflection_index { + quote!(#this.field_at(#field_index)) + } else { + quote!(::core::compile_error!( + "internal bevy_reflect error: field should be active" + )) } } From f987920bbd073af6ceeffa2c48459b7df0c0442b Mon Sep 17 00:00:00 2001 From: atlv Date: Sat, 5 Jul 2025 10:40:28 -0400 Subject: [PATCH 06/14] Move CubemapLayout out of decal code (#19960) # Objective - Make bevy_light possible by making it possible to split out clusterable into bevy_camera ## Solution - Move cubemap stuff next to cubemap stuff. ## Testing - 3d_scene runs Note: no breaking changes thanks to re-exports --- crates/bevy_camera/src/primitives.rs | 36 +++++++++++++++++++++++++ crates/bevy_pbr/src/decal/clustered.rs | 37 +------------------------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/crates/bevy_camera/src/primitives.rs b/crates/bevy_camera/src/primitives.rs index ddde695423..e08422b052 100644 --- a/crates/bevy_camera/src/primitives.rs +++ b/crates/bevy_camera/src/primitives.rs @@ -363,6 +363,42 @@ impl CubemapFrusta { } } +/// Cubemap layout defines the order of images in a packed cubemap image. +#[derive(Default, Reflect, Debug, Clone, Copy)] +pub enum CubemapLayout { + /// layout in a vertical cross format + /// ```text + /// +y + /// -x -z +x + /// -y + /// +z + /// ``` + #[default] + CrossVertical = 0, + /// layout in a horizontal cross format + /// ```text + /// +y + /// -x -z +x +z + /// -y + /// ``` + CrossHorizontal = 1, + /// layout in a vertical sequence + /// ```text + /// +x + /// -y + /// +y + /// -y + /// -z + /// +z + /// ``` + SequenceVertical = 2, + /// layout in a horizontal sequence + /// ```text + /// +x -y +y -y -z +z + /// ``` + SequenceHorizontal = 3, +} + #[derive(Component, Debug, Default, Reflect, Clone)] #[reflect(Component, Default, Debug, Clone)] pub struct CascadesFrusta { diff --git a/crates/bevy_pbr/src/decal/clustered.rs b/crates/bevy_pbr/src/decal/clustered.rs index ec386670ec..d89c91c7ab 100644 --- a/crates/bevy_pbr/src/decal/clustered.rs +++ b/crates/bevy_pbr/src/decal/clustered.rs @@ -32,6 +32,7 @@ use bevy_image::Image; use bevy_math::Mat4; use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; +pub use bevy_render::primitives::CubemapLayout; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, load_shader_library, @@ -95,42 +96,6 @@ pub struct ClusteredDecal { pub tag: u32, } -/// Cubemap layout defines the order of images in a packed cubemap image. -#[derive(Default, Reflect, Debug, Clone, Copy)] -pub enum CubemapLayout { - /// layout in a vertical cross format - /// ```text - /// +y - /// -x -z +x - /// -y - /// +z - /// ``` - #[default] - CrossVertical = 0, - /// layout in a horizontal cross format - /// ```text - /// +y - /// -x -z +x +z - /// -y - /// ``` - CrossHorizontal = 1, - /// layout in a vertical sequence - /// ```text - /// +x - /// -y - /// +y - /// -y - /// -z - /// +z - /// ``` - SequenceVertical = 2, - /// layout in a horizontal sequence - /// ```text - /// +x -y +y -y -z +z - /// ``` - SequenceHorizontal = 3, -} - /// Add to a [`PointLight`] to add a light texture effect. /// A texture mask is applied to the light source to modulate its intensity, /// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. From 59e8702a6554660db2677e5013b7afd8c2086a07 Mon Sep 17 00:00:00 2001 From: atlv Date: Sat, 5 Jul 2025 10:40:33 -0400 Subject: [PATCH 07/14] move calculate_cluster_factors to cluster assign (#19958) # Objective - Make bevy_light possible by making it possible to split out clusterable into bevy_camera ## Solution - Move some stuff so i can split it out cleanly. ## Testing - 3d_scene runs --- crates/bevy_pbr/src/cluster/assign.rs | 19 ++++++++++++++++++- crates/bevy_pbr/src/render/light.rs | 19 ++----------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/crates/bevy_pbr/src/cluster/assign.rs b/crates/bevy_pbr/src/cluster/assign.rs index c2526bc2ff..84b2eb702e 100644 --- a/crates/bevy_pbr/src/cluster/assign.rs +++ b/crates/bevy_pbr/src/cluster/assign.rs @@ -381,7 +381,7 @@ pub(crate) fn assign_objects_to_clusters( // NOTE: Ensure the far_z is at least as far as the first_depth_slice to avoid clustering problems. let far_z = far_z.max(first_slice_depth); - let cluster_factors = crate::calculate_cluster_factors( + let cluster_factors = calculate_cluster_factors( first_slice_depth, far_z, requested_cluster_dimensions.z as f32, @@ -871,6 +871,23 @@ pub(crate) fn assign_objects_to_clusters( } } +pub fn calculate_cluster_factors( + near: f32, + far: f32, + z_slices: f32, + is_orthographic: bool, +) -> Vec2 { + if is_orthographic { + Vec2::new(-near, z_slices / (-far - -near)) + } else { + let z_slices_of_ln_zfar_over_znear = (z_slices - 1.0) / ops::ln(far / near); + Vec2::new( + z_slices_of_ln_zfar_over_znear, + ops::ln(near) * z_slices_of_ln_zfar_over_znear, + ) + } +} + fn compute_aabb_for_cluster( z_near: f32, z_far: f32, diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index cee3d76e14..c7d9efbab7 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,4 +1,5 @@ use self::assign::ClusterableObjectType; +use crate::assign::calculate_cluster_factors; use crate::*; use bevy_asset::UntypedAssetId; use bevy_color::ColorToComponents; @@ -11,7 +12,7 @@ use bevy_ecs::{ prelude::*, system::lifetimeless::Read, }; -use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +use bevy_math::{ops, Mat4, UVec4, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_platform::collections::{HashMap, HashSet}; use bevy_platform::hash::FixedHasher; use bevy_render::erased_render_asset::ErasedRenderAssets; @@ -694,22 +695,6 @@ pub enum LightEntity { light_entity: Entity, }, } -pub fn calculate_cluster_factors( - near: f32, - far: f32, - z_slices: f32, - is_orthographic: bool, -) -> Vec2 { - if is_orthographic { - Vec2::new(-near, z_slices / (-far - -near)) - } else { - let z_slices_of_ln_zfar_over_znear = (z_slices - 1.0) / ops::ln(far / near); - Vec2::new( - z_slices_of_ln_zfar_over_znear, - ops::ln(near) * z_slices_of_ln_zfar_over_znear, - ) - } -} pub fn prepare_lights( mut commands: Commands, From 47e99c828530db18b85e4f0648170096cfed1f37 Mon Sep 17 00:00:00 2001 From: atlv Date: Sat, 5 Jul 2025 10:40:59 -0400 Subject: [PATCH 08/14] move Cubemap stuff alongside CubemapFrusta in bevy_camera::primitives (#19955) # Objective - Make bevy_light possible ## Solution - Move some stuff it needs out of somewhere it cant depend on. Plus it makes sense, cubemap stuff goes next to cubemap stuff. ## Testing - 3d_scene runs Note: no breaking changes thanks to re-exports --- crates/bevy_camera/src/primitives.rs | 57 +++++++++++++++++++++++++++ crates/bevy_pbr/src/render/light.rs | 58 +--------------------------- 2 files changed, 58 insertions(+), 57 deletions(-) diff --git a/crates/bevy_camera/src/primitives.rs b/crates/bevy_camera/src/primitives.rs index e08422b052..127a2d9c29 100644 --- a/crates/bevy_camera/src/primitives.rs +++ b/crates/bevy_camera/src/primitives.rs @@ -347,6 +347,63 @@ impl Frustum { } } +pub struct CubeMapFace { + pub target: Vec3, + pub up: Vec3, +} + +// Cubemap faces are [+X, -X, +Y, -Y, +Z, -Z], per https://www.w3.org/TR/webgpu/#texture-view-creation +// Note: Cubemap coordinates are left-handed y-up, unlike the rest of Bevy. +// See https://registry.khronos.org/vulkan/specs/1.2/html/chap16.html#_cube_map_face_selection +// +// For each cubemap face, we take care to specify the appropriate target/up axis such that the rendered +// texture using Bevy's right-handed y-up coordinate space matches the expected cubemap face in +// left-handed y-up cubemap coordinates. +pub const CUBE_MAP_FACES: [CubeMapFace; 6] = [ + // +X + CubeMapFace { + target: Vec3::X, + up: Vec3::Y, + }, + // -X + CubeMapFace { + target: Vec3::NEG_X, + up: Vec3::Y, + }, + // +Y + CubeMapFace { + target: Vec3::Y, + up: Vec3::Z, + }, + // -Y + CubeMapFace { + target: Vec3::NEG_Y, + up: Vec3::NEG_Z, + }, + // +Z (with left-handed conventions, pointing forwards) + CubeMapFace { + target: Vec3::NEG_Z, + up: Vec3::Y, + }, + // -Z (with left-handed conventions, pointing backwards) + CubeMapFace { + target: Vec3::Z, + up: Vec3::Y, + }, +]; + +pub fn face_index_to_name(face_index: usize) -> &'static str { + match face_index { + 0 => "+x", + 1 => "-x", + 2 => "+y", + 3 => "-y", + 4 => "+z", + 5 => "-z", + _ => "invalid", + } +} + #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component, Default, Debug, Clone)] pub struct CubemapFrusta { diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index c7d9efbab7..1f36d45fd6 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -2,6 +2,7 @@ use self::assign::ClusterableObjectType; use crate::assign::calculate_cluster_factors; use crate::*; use bevy_asset::UntypedAssetId; +pub use bevy_camera::primitives::{face_index_to_name, CubeMapFace, CUBE_MAP_FACES}; use bevy_color::ColorToComponents; use bevy_core_pipeline::core_3d::{Camera3d, CORE_3D_DEPTH_FORMAT}; use bevy_derive::{Deref, DerefMut}; @@ -585,63 +586,6 @@ pub(crate) fn remove_light_view_entities( } } -pub(crate) struct CubeMapFace { - pub(crate) target: Vec3, - pub(crate) up: Vec3, -} - -// Cubemap faces are [+X, -X, +Y, -Y, +Z, -Z], per https://www.w3.org/TR/webgpu/#texture-view-creation -// Note: Cubemap coordinates are left-handed y-up, unlike the rest of Bevy. -// See https://registry.khronos.org/vulkan/specs/1.2/html/chap16.html#_cube_map_face_selection -// -// For each cubemap face, we take care to specify the appropriate target/up axis such that the rendered -// texture using Bevy's right-handed y-up coordinate space matches the expected cubemap face in -// left-handed y-up cubemap coordinates. -pub(crate) const CUBE_MAP_FACES: [CubeMapFace; 6] = [ - // +X - CubeMapFace { - target: Vec3::X, - up: Vec3::Y, - }, - // -X - CubeMapFace { - target: Vec3::NEG_X, - up: Vec3::Y, - }, - // +Y - CubeMapFace { - target: Vec3::Y, - up: Vec3::Z, - }, - // -Y - CubeMapFace { - target: Vec3::NEG_Y, - up: Vec3::NEG_Z, - }, - // +Z (with left-handed conventions, pointing forwards) - CubeMapFace { - target: Vec3::NEG_Z, - up: Vec3::Y, - }, - // -Z (with left-handed conventions, pointing backwards) - CubeMapFace { - target: Vec3::Z, - up: Vec3::Y, - }, -]; - -fn face_index_to_name(face_index: usize) -> &'static str { - match face_index { - 0 => "+x", - 1 => "-x", - 2 => "+y", - 3 => "-y", - 4 => "+z", - 5 => "-z", - _ => "invalid", - } -} - #[derive(Component)] pub struct ShadowView { pub depth_attachment: DepthAttachment, From ced36021d01219dc64069233fa1b36ea6b00ef32 Mon Sep 17 00:00:00 2001 From: atlv Date: Sat, 5 Jul 2025 13:04:21 -0400 Subject: [PATCH 09/14] move light stuff out of decal cluster (#19962) # Objective - Make bevy_light possible ## Solution - Move light stuff into light module ## Testing - 3d_scene runs Note: no breaking changes thanks to re-exports --- crates/bevy_pbr/src/decal/clustered.rs | 42 +------------------ .../bevy_pbr/src/light/directional_light.rs | 13 ++++++ crates/bevy_pbr/src/light/mod.rs | 6 +-- crates/bevy_pbr/src/light/point_light.rs | 15 +++++++ crates/bevy_pbr/src/light/spot_light.rs | 12 ++++++ 5 files changed, 45 insertions(+), 43 deletions(-) diff --git a/crates/bevy_pbr/src/decal/clustered.rs b/crates/bevy_pbr/src/decal/clustered.rs index d89c91c7ab..98f18ce0e4 100644 --- a/crates/bevy_pbr/src/decal/clustered.rs +++ b/crates/bevy_pbr/src/decal/clustered.rs @@ -51,9 +51,9 @@ use bevy_transform::{components::GlobalTransform, prelude::Transform}; use bytemuck::{Pod, Zeroable}; use crate::{ - binding_arrays_are_usable, prepare_lights, DirectionalLight, GlobalClusterableObjectMeta, - LightVisibilityClass, PointLight, SpotLight, + binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta, LightVisibilityClass, }; +pub use crate::{DirectionalLightTexture, PointLightTexture, SpotLightTexture}; /// The maximum number of decals that can be present in a view. /// @@ -96,44 +96,6 @@ pub struct ClusteredDecal { pub tag: u32, } -/// Add to a [`PointLight`] to add a light texture effect. -/// A texture mask is applied to the light source to modulate its intensity, -/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. -#[derive(Clone, Component, Debug, Reflect)] -#[reflect(Component, Debug)] -#[require(PointLight)] -pub struct PointLightTexture { - /// The texture image. Only the R channel is read. - pub image: Handle, - /// The cubemap layout. The image should be a packed cubemap in one of the formats described by the [`CubemapLayout`] enum. - pub cubemap_layout: CubemapLayout, -} - -/// Add to a [`SpotLight`] to add a light texture effect. -/// A texture mask is applied to the light source to modulate its intensity, -/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. -#[derive(Clone, Component, Debug, Reflect)] -#[reflect(Component, Debug)] -#[require(SpotLight)] -pub struct SpotLightTexture { - /// The texture image. Only the R channel is read. - /// Note the border of the image should be entirely black to avoid leaking light. - pub image: Handle, -} - -/// Add to a [`DirectionalLight`] to add a light texture effect. -/// A texture mask is applied to the light source to modulate its intensity, -/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. -#[derive(Clone, Component, Debug, Reflect)] -#[reflect(Component, Debug)] -#[require(DirectionalLight)] -pub struct DirectionalLightTexture { - /// The texture image. Only the R channel is read. - pub image: Handle, - /// Whether to tile the image infinitely, or use only a single tile centered at the light's translation - pub tiled: bool, -} - /// Stores information about all the clustered decals in the scene. #[derive(Resource, Default)] pub struct RenderClusteredDecals { diff --git a/crates/bevy_pbr/src/light/directional_light.rs b/crates/bevy_pbr/src/light/directional_light.rs index a5798fdde7..2d182c1c83 100644 --- a/crates/bevy_pbr/src/light/directional_light.rs +++ b/crates/bevy_pbr/src/light/directional_light.rs @@ -141,3 +141,16 @@ impl DirectionalLight { pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02; pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8; } + +/// Add to a [`DirectionalLight`] to add a light texture effect. +/// A texture mask is applied to the light source to modulate its intensity, +/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. +#[derive(Clone, Component, Debug, Reflect)] +#[reflect(Component, Debug)] +#[require(DirectionalLight)] +pub struct DirectionalLightTexture { + /// The texture image. Only the R channel is read. + pub image: Handle, + /// Whether to tile the image infinitely, or use only a single tile centered at the light's translation + pub tiled: bool, +} diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 004085fda6..663b9f52c3 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -26,11 +26,11 @@ mod ambient_light; pub use ambient_light::AmbientLight; mod point_light; -pub use point_light::PointLight; +pub use point_light::{PointLight, PointLightTexture}; mod spot_light; -pub use spot_light::SpotLight; +pub use spot_light::{SpotLight, SpotLightTexture}; mod directional_light; -pub use directional_light::DirectionalLight; +pub use directional_light::{DirectionalLight, DirectionalLightTexture}; /// Constants for operating with the light units: lumens, and lux. pub mod light_consts { diff --git a/crates/bevy_pbr/src/light/point_light.rs b/crates/bevy_pbr/src/light/point_light.rs index f2e4224d28..c977d0be33 100644 --- a/crates/bevy_pbr/src/light/point_light.rs +++ b/crates/bevy_pbr/src/light/point_light.rs @@ -1,5 +1,7 @@ use bevy_render::view::{self, Visibility}; +use crate::decal::clustered::CubemapLayout; + use super::*; /// A light that emits light in all directions from a central point. @@ -136,3 +138,16 @@ impl PointLight { pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6; pub const DEFAULT_SHADOW_MAP_NEAR_Z: f32 = 0.1; } + +/// Add to a [`PointLight`] to add a light texture effect. +/// A texture mask is applied to the light source to modulate its intensity, +/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. +#[derive(Clone, Component, Debug, Reflect)] +#[reflect(Component, Debug)] +#[require(PointLight)] +pub struct PointLightTexture { + /// The texture image. Only the R channel is read. + pub image: Handle, + /// The cubemap layout. The image should be a packed cubemap in one of the formats described by the [`CubemapLayout`] enum. + pub cubemap_layout: CubemapLayout, +} diff --git a/crates/bevy_pbr/src/light/spot_light.rs b/crates/bevy_pbr/src/light/spot_light.rs index 7e0bd43f15..393e9efc0c 100644 --- a/crates/bevy_pbr/src/light/spot_light.rs +++ b/crates/bevy_pbr/src/light/spot_light.rs @@ -172,3 +172,15 @@ pub fn spot_light_clip_from_view(angle: f32, near_z: f32) -> Mat4 { // spot light projection FOV is 2x the angle from spot light center to outer edge Mat4::perspective_infinite_reverse_rh(angle * 2.0, 1.0, near_z) } + +/// Add to a [`SpotLight`] to add a light texture effect. +/// A texture mask is applied to the light source to modulate its intensity, +/// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. +#[derive(Clone, Component, Debug, Reflect)] +#[reflect(Component, Debug)] +#[require(SpotLight)] +pub struct SpotLightTexture { + /// The texture image. Only the R channel is read. + /// Note the border of the image should be entirely black to avoid leaking light. + pub image: Handle, +} From 6ab8e0d9c745aa87379cee418d0a684895a4f304 Mon Sep 17 00:00:00 2001 From: atlv Date: Sat, 5 Jul 2025 13:05:14 -0400 Subject: [PATCH 10/14] move ShadowsEnabled to material (#19963) # Objective - Make bevy_light possible ## Solution - Move non-light stuff out of light module (its a marker for whether a material should cast shadows: thats a material property not a light property) ## Testing - 3d_scene runs --- crates/bevy_pbr/src/light/mod.rs | 12 +----------- crates/bevy_pbr/src/material.rs | 10 ++++++++++ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 663b9f52c3..f73cebdba3 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -17,7 +17,7 @@ use bevy_render::{ }; use bevy_transform::components::{GlobalTransform, Transform}; use bevy_utils::Parallel; -use core::{marker::PhantomData, ops::DerefMut}; +use core::ops::DerefMut; use crate::*; pub use light::spot_light::{spot_light_clip_from_view, spot_light_world_from_view}; @@ -91,16 +91,6 @@ pub mod light_consts { } } -/// Marker resource for whether shadows are enabled for this material type -#[derive(Resource, Debug)] -pub struct ShadowsEnabled(PhantomData); - -impl Default for ShadowsEnabled { - fn default() -> Self { - Self(PhantomData) - } -} - /// Controls the resolution of [`PointLight`] shadow maps. /// /// ``` diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index bb1f9afde3..cc3a69a0ad 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -1717,3 +1717,13 @@ pub fn write_material_bind_group_buffers( allocator.write_buffers(&render_device, &render_queue); } } + +/// Marker resource for whether shadows are enabled for this material type +#[derive(Resource, Debug)] +pub struct ShadowsEnabled(PhantomData); + +impl Default for ShadowsEnabled { + fn default() -> Self { + Self(PhantomData) + } +} From 7aaf4bbd94e74e88a7f7976f86373722d77af627 Mon Sep 17 00:00:00 2001 From: atlv Date: Sat, 5 Jul 2025 13:05:56 -0400 Subject: [PATCH 11/14] fix a couple typos in CubemapLayout (#19964) # Objective - Rob pointed out a couple typos in #19960 (i just did a copy paste, but the original had an issue) ## Solution - Fix --- crates/bevy_camera/src/primitives.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_camera/src/primitives.rs b/crates/bevy_camera/src/primitives.rs index 127a2d9c29..32bb557b93 100644 --- a/crates/bevy_camera/src/primitives.rs +++ b/crates/bevy_camera/src/primitives.rs @@ -442,7 +442,7 @@ pub enum CubemapLayout { /// layout in a vertical sequence /// ```text /// +x - /// -y + /// -x /// +y /// -y /// -z @@ -451,7 +451,7 @@ pub enum CubemapLayout { SequenceVertical = 2, /// layout in a horizontal sequence /// ```text - /// +x -y +y -y -z +z + /// +x -x +y -y -z +z /// ``` SequenceHorizontal = 3, } From 0b771d9f59f4518926ed76cbf94830366dfad69a Mon Sep 17 00:00:00 2001 From: atlv Date: Sat, 5 Jul 2025 15:31:59 -0400 Subject: [PATCH 12/14] move ClusteredDecal to cluster module (#19959) # Objective - Make bevy_light possible by making it possible to split out clusterable into bevy_camera ## Solution - move ClusteredDecal to cluster module - Depends on #19957 (because of the imports shuffling around) (draft until thats merged) ## Testing - 3d_scene runs Note: no breaking changes thanks to re-exports --- crates/bevy_pbr/src/cluster/assign.rs | 21 ++++++------ crates/bevy_pbr/src/cluster/mod.rs | 36 ++++++++++++++++++++- crates/bevy_pbr/src/decal/clustered.rs | 44 ++++---------------------- 3 files changed, 53 insertions(+), 48 deletions(-) diff --git a/crates/bevy_pbr/src/cluster/assign.rs b/crates/bevy_pbr/src/cluster/assign.rs index 84b2eb702e..9dc9a56b52 100644 --- a/crates/bevy_pbr/src/cluster/assign.rs +++ b/crates/bevy_pbr/src/cluster/assign.rs @@ -1,5 +1,10 @@ //! Assigning objects to clusters. +use bevy_camera::{ + primitives::{Aabb, Frustum, HalfSpace, Sphere}, + visibility::{RenderLayers, ViewVisibility}, + Camera, +}; use bevy_ecs::{ entity::Entity, query::{Has, With}, @@ -9,20 +14,18 @@ use bevy_math::{ ops::{self, sin_cos}, Mat4, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles as _, Vec4, Vec4Swizzles as _, }; -use bevy_render::{ - camera::Camera, - primitives::{Aabb, Frustum, HalfSpace, Sphere}, - view::{RenderLayers, ViewVisibility}, -}; use bevy_transform::components::GlobalTransform; use bevy_utils::prelude::default; use tracing::warn; +use super::{ + ClusterConfig, ClusterFarZMode, ClusteredDecal, Clusters, GlobalClusterSettings, + GlobalVisibleClusterableObjects, ViewClusterBindings, VisibleClusterableObjects, + MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS, +}; use crate::{ - decal::clustered::ClusteredDecal, prelude::EnvironmentMapLight, ClusterConfig, ClusterFarZMode, - Clusters, ExtractedPointLight, GlobalClusterSettings, GlobalVisibleClusterableObjects, - LightProbe, PointLight, SpotLight, ViewClusterBindings, VisibleClusterableObjects, - VolumetricLight, MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS, + prelude::EnvironmentMapLight, ExtractedPointLight, LightProbe, PointLight, SpotLight, + VolumetricLight, }; const NDC_MIN: Vec2 = Vec2::NEG_ONE; diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index 7f90a4becb..e7f03cc55c 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -2,6 +2,8 @@ use core::num::NonZero; +use bevy_asset::Handle; +use bevy_camera::visibility; use bevy_core_pipeline::core_3d::Camera3d; use bevy_ecs::{ component::Component, @@ -12,23 +14,27 @@ use bevy_ecs::{ system::{Commands, Query, Res}, world::{FromWorld, World}, }; +use bevy_image::Image; use bevy_math::{uvec4, AspectRatio, UVec2, UVec3, UVec4, Vec3Swizzles as _, Vec4}; use bevy_platform::collections::HashSet; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ camera::Camera, + extract_component::ExtractComponent, render_resource::{ BindingResource, BufferBindingType, ShaderSize as _, ShaderType, StorageBuffer, UniformBuffer, }, renderer::{RenderAdapter, RenderDevice, RenderQueue}, sync_world::RenderEntity, + view::{Visibility, VisibilityClass}, Extract, }; +use bevy_transform::components::Transform; use tracing::warn; pub(crate) use crate::cluster::assign::assign_objects_to_clusters; -use crate::MeshPipeline; +use crate::{LightVisibilityClass, MeshPipeline}; pub(crate) mod assign; @@ -230,6 +236,34 @@ struct ClusterableObjectCounts { decals: u32, } +/// An object that projects a decal onto surfaces within its bounds. +/// +/// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It +/// projects the given [`Self::image`] onto surfaces in the -Z direction (thus +/// you may find [`Transform::looking_at`] useful). +/// +/// Clustered decals are the highest-quality types of decals that Bevy supports, +/// but they require bindless textures. This means that they presently can't be +/// used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used +/// with forward or deferred rendering and don't require a prepass. +#[derive(Component, Debug, Clone, Reflect, ExtractComponent)] +#[reflect(Component, Debug, Clone)] +#[require(Transform, Visibility, VisibilityClass)] +#[component(on_add = visibility::add_visibility_class::)] +pub struct ClusteredDecal { + /// The image that the clustered decal projects. + /// + /// This must be a 2D image. If it has an alpha channel, it'll be alpha + /// blended with the underlying surface and/or other decals. All decal + /// images in the scene must use the same sampler. + pub image: Handle, + + /// An application-specific tag you can use for any purpose you want. + /// + /// See the `clustered_decals` example for an example of use. + pub tag: u32, +} + enum ExtractedClusterableObjectElement { ClusterHeader(ClusterableObjectCounts), ClusterableObjectEntity(Entity), diff --git a/crates/bevy_pbr/src/decal/clustered.rs b/crates/bevy_pbr/src/decal/clustered.rs index 98f18ce0e4..d4ac27a1f5 100644 --- a/crates/bevy_pbr/src/decal/clustered.rs +++ b/crates/bevy_pbr/src/decal/clustered.rs @@ -17,12 +17,10 @@ use core::{num::NonZero, ops::Deref}; use bevy_app::{App, Plugin}; -use bevy_asset::{AssetId, Handle}; +use bevy_asset::AssetId; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - component::Component, entity::{Entity, EntityHashMap}, - prelude::ReflectComponent, query::With, resource::Resource, schedule::IntoScheduleConfigs as _, @@ -31,10 +29,9 @@ use bevy_ecs::{ use bevy_image::Image; use bevy_math::Mat4; use bevy_platform::collections::HashMap; -use bevy_reflect::Reflect; pub use bevy_render::primitives::CubemapLayout; use bevy_render::{ - extract_component::{ExtractComponent, ExtractComponentPlugin}, + extract_component::ExtractComponentPlugin, load_shader_library, render_asset::RenderAssets, render_resource::{ @@ -44,15 +41,14 @@ use bevy_render::{ renderer::{RenderAdapter, RenderDevice, RenderQueue}, sync_world::RenderEntity, texture::{FallbackImage, GpuImage}, - view::{self, ViewVisibility, Visibility, VisibilityClass}, + view::ViewVisibility, Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; -use bevy_transform::{components::GlobalTransform, prelude::Transform}; +use bevy_transform::components::GlobalTransform; use bytemuck::{Pod, Zeroable}; -use crate::{ - binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta, LightVisibilityClass, -}; +pub use crate::ClusteredDecal; +use crate::{binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta}; pub use crate::{DirectionalLightTexture, PointLightTexture, SpotLightTexture}; /// The maximum number of decals that can be present in a view. @@ -68,34 +64,6 @@ pub(crate) const MAX_VIEW_DECALS: usize = 8; /// can still be added to a scene, but they won't project any decals. pub struct ClusteredDecalPlugin; -/// An object that projects a decal onto surfaces within its bounds. -/// -/// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It -/// projects the given [`Self::image`] onto surfaces in the -Z direction (thus -/// you may find [`Transform::looking_at`] useful). -/// -/// Clustered decals are the highest-quality types of decals that Bevy supports, -/// but they require bindless textures. This means that they presently can't be -/// used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used -/// with forward or deferred rendering and don't require a prepass. -#[derive(Component, Debug, Clone, Reflect, ExtractComponent)] -#[reflect(Component, Debug, Clone)] -#[require(Transform, Visibility, VisibilityClass)] -#[component(on_add = view::add_visibility_class::)] -pub struct ClusteredDecal { - /// The image that the clustered decal projects. - /// - /// This must be a 2D image. If it has an alpha channel, it'll be alpha - /// blended with the underlying surface and/or other decals. All decal - /// images in the scene must use the same sampler. - pub image: Handle, - - /// An application-specific tag you can use for any purpose you want. - /// - /// See the `clustered_decals` example for an example of use. - pub tag: u32, -} - /// Stores information about all the clustered decals in the scene. #[derive(Resource, Default)] pub struct RenderClusteredDecals { From dd57db44d98abcc75b1591af53479c18e95d70a0 Mon Sep 17 00:00:00 2001 From: atlv Date: Sun, 6 Jul 2025 00:11:46 -0400 Subject: [PATCH 13/14] prepare bevy_light for split (#19965) # Objective - prepare bevy_light for split ## Solution - extract cascade module (this is not strictly necessary for bevy_light) - clean up imports to be less globby and tangled - move light specific stuff into light modules - move light system and type init from pbr into new LightPlugin ## Testing - 3d_scene, lighting NOTE TO REVIEWERS: it may help to review commits independently. --- crates/bevy_pbr/src/lib.rs | 94 +-- crates/bevy_pbr/src/light/ambient_light.rs | 10 +- crates/bevy_pbr/src/light/cascade.rs | 333 ++++++++++ .../bevy_pbr/src/light/directional_light.rs | 79 ++- crates/bevy_pbr/src/light/mod.rs | 625 +++--------------- crates/bevy_pbr/src/light/point_light.rs | 102 ++- crates/bevy_pbr/src/light/spot_light.rs | 54 +- crates/bevy_pbr/src/render/light.rs | 1 + 8 files changed, 676 insertions(+), 622 deletions(-) create mode 100644 crates/bevy_pbr/src/light/cascade.rs diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 0604c06328..cad7b8979c 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -122,6 +122,7 @@ pub mod graph { } } +pub use crate::cascade::{CascadeShadowConfig, CascadeShadowConfigBuilder, Cascades}; use crate::{deferred::DeferredPbrLightingPlugin, graph::NodePbr}; use bevy_app::prelude::*; use bevy_asset::{AssetApp, AssetPath, Assets, Handle}; @@ -130,19 +131,16 @@ use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_render::{ alpha::AlphaMode, - camera::{sort_cameras, CameraUpdateSystems, Projection}, + camera::{sort_cameras, Projection}, extract_component::ExtractComponentPlugin, extract_resource::ExtractResourcePlugin, load_shader_library, render_graph::RenderGraph, render_resource::ShaderRef, sync_component::SyncComponentPlugin, - view::VisibilitySystems, ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSystems, }; -use bevy_transform::TransformSystems; - use std::path::PathBuf; fn shader_ref(path: PathBuf) -> ShaderRef { @@ -205,22 +203,8 @@ impl Plugin for PbrPlugin { load_shader_library!(app, "meshlet/dummy_visibility_buffer_resolve.wgsl"); app.register_asset_reflect::() - .register_type::() - .register_type::() - .register_type::() .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .init_resource::() .init_resource::() - .init_resource::() - .init_resource::() .register_type::() .init_resource::() .add_plugins(( @@ -243,7 +227,7 @@ impl Plugin for PbrPlugin { ExtractComponentPlugin::::default(), LightmapPlugin, LightProbePlugin, - PbrProjectionPlugin, + LightPlugin, GpuMeshPreprocessPlugin { use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder, }, @@ -266,64 +250,6 @@ impl Plugin for PbrPlugin { SimulationLightSystems::AssignLightsToClusters, ) .chain(), - ) - .configure_sets( - PostUpdate, - SimulationLightSystems::UpdateDirectionalLightCascades - .ambiguous_with(SimulationLightSystems::UpdateDirectionalLightCascades), - ) - .configure_sets( - PostUpdate, - SimulationLightSystems::CheckLightVisibility - .ambiguous_with(SimulationLightSystems::CheckLightVisibility), - ) - .add_systems( - PostUpdate, - ( - add_clusters - .in_set(SimulationLightSystems::AddClusters) - .after(CameraUpdateSystems), - assign_objects_to_clusters - .in_set(SimulationLightSystems::AssignLightsToClusters) - .after(TransformSystems::Propagate) - .after(VisibilitySystems::CheckVisibility) - .after(CameraUpdateSystems), - clear_directional_light_cascades - .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) - .after(TransformSystems::Propagate) - .after(CameraUpdateSystems), - update_directional_light_frusta - .in_set(SimulationLightSystems::UpdateLightFrusta) - // This must run after CheckVisibility because it relies on `ViewVisibility` - .after(VisibilitySystems::CheckVisibility) - .after(TransformSystems::Propagate) - .after(SimulationLightSystems::UpdateDirectionalLightCascades) - // We assume that no entity will be both a directional light and a spot light, - // so these systems will run independently of one another. - // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. - .ambiguous_with(update_spot_light_frusta), - update_point_light_frusta - .in_set(SimulationLightSystems::UpdateLightFrusta) - .after(TransformSystems::Propagate) - .after(SimulationLightSystems::AssignLightsToClusters), - update_spot_light_frusta - .in_set(SimulationLightSystems::UpdateLightFrusta) - .after(TransformSystems::Propagate) - .after(SimulationLightSystems::AssignLightsToClusters), - ( - check_dir_light_mesh_visibility, - check_point_light_mesh_visibility, - ) - .in_set(SimulationLightSystems::CheckLightVisibility) - .after(VisibilitySystems::CalculateBounds) - .after(TransformSystems::Propagate) - .after(SimulationLightSystems::UpdateLightFrusta) - // NOTE: This MUST be scheduled AFTER the core renderer visibility check - // because that resets entity `ViewVisibility` for the first view - // which would override any results from this otherwise - .after(VisibilitySystems::CheckVisibility) - .before(VisibilitySystems::MarkNewlyHiddenEntitiesInvisible), - ), ); if self.add_default_deferred_lighting_plugin { @@ -401,17 +327,3 @@ impl Plugin for PbrPlugin { app.insert_resource(global_cluster_settings); } } - -/// Camera projection PBR functionality. -#[derive(Default)] -pub struct PbrProjectionPlugin; -impl Plugin for PbrProjectionPlugin { - fn build(&self, app: &mut App) { - app.add_systems( - PostUpdate, - build_directional_light_cascades - .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) - .after(clear_directional_light_cascades), - ); - } -} diff --git a/crates/bevy_pbr/src/light/ambient_light.rs b/crates/bevy_pbr/src/light/ambient_light.rs index cfbe99963b..dfb9cdfecc 100644 --- a/crates/bevy_pbr/src/light/ambient_light.rs +++ b/crates/bevy_pbr/src/light/ambient_light.rs @@ -1,8 +1,12 @@ -use super::*; +use bevy_camera::Camera; +use bevy_color::Color; +use bevy_ecs::prelude::*; +use bevy_reflect::prelude::*; +use bevy_render::{extract_component::ExtractComponent, extract_resource::ExtractResource}; /// An ambient light, which lights the entire scene equally. /// -/// This resource is inserted by the [`PbrPlugin`] and by default it is set to a low ambient light. +/// This resource is inserted by the [`LightPlugin`] and by default it is set to a low ambient light. /// /// It can also be added to a camera to override the resource (or default) ambient for that camera only. /// @@ -17,6 +21,8 @@ use super::*; /// ambient_light.brightness = 100.0; /// } /// ``` +/// +/// [`LightPlugin`]: crate::LightPlugin #[derive(Resource, Component, Clone, Debug, ExtractResource, ExtractComponent, Reflect)] #[reflect(Resource, Component, Debug, Default, Clone)] #[require(Camera)] diff --git a/crates/bevy_pbr/src/light/cascade.rs b/crates/bevy_pbr/src/light/cascade.rs new file mode 100644 index 0000000000..a6ebe5a89b --- /dev/null +++ b/crates/bevy_pbr/src/light/cascade.rs @@ -0,0 +1,333 @@ +pub use bevy_camera::primitives::{face_index_to_name, CubeMapFace, CUBE_MAP_FACES}; +use bevy_camera::{Camera, Projection}; +use bevy_ecs::{entity::EntityHashMap, prelude::*}; +use bevy_math::{ops, Mat4, Vec3A, Vec4}; +use bevy_reflect::prelude::*; +use bevy_transform::components::GlobalTransform; + +use crate::{DirectionalLight, DirectionalLightShadowMap}; + +/// Controls how cascaded shadow mapping works. +/// Prefer using [`CascadeShadowConfigBuilder`] to construct an instance. +/// +/// ``` +/// # use bevy_pbr::CascadeShadowConfig; +/// # use bevy_pbr::CascadeShadowConfigBuilder; +/// # use bevy_utils::default; +/// # +/// let config: CascadeShadowConfig = CascadeShadowConfigBuilder { +/// maximum_distance: 100.0, +/// ..default() +/// }.into(); +/// ``` +#[derive(Component, Clone, Debug, Reflect)] +#[reflect(Component, Default, Debug, Clone)] +pub struct CascadeShadowConfig { + /// The (positive) distance to the far boundary of each cascade. + pub bounds: Vec, + /// The proportion of overlap each cascade has with the previous cascade. + pub overlap_proportion: f32, + /// The (positive) distance to the near boundary of the first cascade. + pub minimum_distance: f32, +} + +impl Default for CascadeShadowConfig { + fn default() -> Self { + CascadeShadowConfigBuilder::default().into() + } +} + +fn calculate_cascade_bounds( + num_cascades: usize, + nearest_bound: f32, + shadow_maximum_distance: f32, +) -> Vec { + if num_cascades == 1 { + return vec![shadow_maximum_distance]; + } + let base = ops::powf( + shadow_maximum_distance / nearest_bound, + 1.0 / (num_cascades - 1) as f32, + ); + (0..num_cascades) + .map(|i| nearest_bound * ops::powf(base, i as f32)) + .collect() +} + +/// Builder for [`CascadeShadowConfig`]. +pub struct CascadeShadowConfigBuilder { + /// The number of shadow cascades. + /// More cascades increases shadow quality by mitigating perspective aliasing - a phenomenon where areas + /// nearer the camera are covered by fewer shadow map texels than areas further from the camera, causing + /// blocky looking shadows. + /// + /// This does come at the cost increased rendering overhead, however this overhead is still less + /// than if you were to use fewer cascades and much larger shadow map textures to achieve the + /// same quality level. + /// + /// In case rendered geometry covers a relatively narrow and static depth relative to camera, it may + /// make more sense to use fewer cascades and a higher resolution shadow map texture as perspective aliasing + /// is not as much an issue. Be sure to adjust `minimum_distance` and `maximum_distance` appropriately. + pub num_cascades: usize, + /// The minimum shadow distance, which can help improve the texel resolution of the first cascade. + /// Areas nearer to the camera than this will likely receive no shadows. + /// + /// NOTE: Due to implementation details, this usually does not impact shadow quality as much as + /// `first_cascade_far_bound` and `maximum_distance`. At many view frustum field-of-views, the + /// texel resolution of the first cascade is dominated by the width / height of the view frustum plane + /// at `first_cascade_far_bound` rather than the depth of the frustum from `minimum_distance` to + /// `first_cascade_far_bound`. + pub minimum_distance: f32, + /// The maximum shadow distance. + /// Areas further from the camera than this will likely receive no shadows. + pub maximum_distance: f32, + /// Sets the far bound of the first cascade, relative to the view origin. + /// In-between cascades will be exponentially spaced relative to the maximum shadow distance. + /// NOTE: This is ignored if there is only one cascade, the maximum distance takes precedence. + pub first_cascade_far_bound: f32, + /// Sets the overlap proportion between cascades. + /// The overlap is used to make the transition from one cascade's shadow map to the next + /// less abrupt by blending between both shadow maps. + pub overlap_proportion: f32, +} + +impl CascadeShadowConfigBuilder { + /// Returns the cascade config as specified by this builder. + pub fn build(&self) -> CascadeShadowConfig { + assert!( + self.num_cascades > 0, + "num_cascades must be positive, but was {}", + self.num_cascades + ); + assert!( + self.minimum_distance >= 0.0, + "maximum_distance must be non-negative, but was {}", + self.minimum_distance + ); + assert!( + self.num_cascades == 1 || self.minimum_distance < self.first_cascade_far_bound, + "minimum_distance must be less than first_cascade_far_bound, but was {}", + self.minimum_distance + ); + assert!( + self.maximum_distance > self.minimum_distance, + "maximum_distance must be greater than minimum_distance, but was {}", + self.maximum_distance + ); + assert!( + (0.0..1.0).contains(&self.overlap_proportion), + "overlap_proportion must be in [0.0, 1.0) but was {}", + self.overlap_proportion + ); + CascadeShadowConfig { + bounds: calculate_cascade_bounds( + self.num_cascades, + self.first_cascade_far_bound, + self.maximum_distance, + ), + overlap_proportion: self.overlap_proportion, + minimum_distance: self.minimum_distance, + } + } +} + +impl Default for CascadeShadowConfigBuilder { + fn default() -> Self { + // The defaults are chosen to be similar to be Unity, Unreal, and Godot. + // Unity: first cascade far bound = 10.05, maximum distance = 150.0 + // Unreal Engine 5: maximum distance = 200.0 + // Godot: first cascade far bound = 10.0, maximum distance = 100.0 + Self { + // Currently only support one cascade in WebGL 2. + num_cascades: if cfg!(all( + feature = "webgl", + target_arch = "wasm32", + not(feature = "webgpu") + )) { + 1 + } else { + 4 + }, + minimum_distance: 0.1, + maximum_distance: 150.0, + first_cascade_far_bound: 10.0, + overlap_proportion: 0.2, + } + } +} + +impl From for CascadeShadowConfig { + fn from(builder: CascadeShadowConfigBuilder) -> Self { + builder.build() + } +} + +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Debug, Default, Clone)] +pub struct Cascades { + /// Map from a view to the configuration of each of its [`Cascade`]s. + pub cascades: EntityHashMap>, +} + +#[derive(Clone, Debug, Default, Reflect)] +#[reflect(Clone, Default)] +pub struct Cascade { + /// The transform of the light, i.e. the view to world matrix. + pub world_from_cascade: Mat4, + /// The orthographic projection for this cascade. + pub clip_from_cascade: Mat4, + /// The view-projection matrix for this cascade, converting world space into light clip space. + /// Importantly, this is derived and stored separately from `view_transform` and `projection` to + /// ensure shadow stability. + pub clip_from_world: Mat4, + /// Size of each shadow map texel in world units. + pub texel_size: f32, +} + +pub fn clear_directional_light_cascades(mut lights: Query<(&DirectionalLight, &mut Cascades)>) { + for (directional_light, mut cascades) in lights.iter_mut() { + if !directional_light.shadows_enabled { + continue; + } + cascades.cascades.clear(); + } +} + +pub fn build_directional_light_cascades( + directional_light_shadow_map: Res, + views: Query<(Entity, &GlobalTransform, &Projection, &Camera)>, + mut lights: Query<( + &GlobalTransform, + &DirectionalLight, + &CascadeShadowConfig, + &mut Cascades, + )>, +) { + let views = views + .iter() + .filter_map(|(entity, transform, projection, camera)| { + if camera.is_active { + Some((entity, projection, transform.to_matrix())) + } else { + None + } + }) + .collect::>(); + + for (transform, directional_light, cascades_config, mut cascades) in &mut lights { + if !directional_light.shadows_enabled { + continue; + } + + // It is very important to the numerical and thus visual stability of shadows that + // light_to_world has orthogonal upper-left 3x3 and zero translation. + // Even though only the direction (i.e. rotation) of the light matters, we don't constrain + // users to not change any other aspects of the transform - there's no guarantee + // `transform.to_matrix()` will give us a matrix with our desired properties. + // Instead, we directly create a good matrix from just the rotation. + let world_from_light = Mat4::from_quat(transform.compute_transform().rotation); + let light_to_world_inverse = world_from_light.inverse(); + + for (view_entity, projection, view_to_world) in views.iter().copied() { + let camera_to_light_view = light_to_world_inverse * view_to_world; + let view_cascades = cascades_config + .bounds + .iter() + .enumerate() + .map(|(idx, far_bound)| { + // Negate bounds as -z is camera forward direction. + let z_near = if idx > 0 { + (1.0 - cascades_config.overlap_proportion) + * -cascades_config.bounds[idx - 1] + } else { + -cascades_config.minimum_distance + }; + let z_far = -far_bound; + + let corners = projection.get_frustum_corners(z_near, z_far); + + calculate_cascade( + corners, + directional_light_shadow_map.size as f32, + world_from_light, + camera_to_light_view, + ) + }) + .collect(); + cascades.cascades.insert(view_entity, view_cascades); + } + } +} + +/// Returns a [`Cascade`] for the frustum defined by `frustum_corners`. +/// +/// The corner vertices should be specified in the following order: +/// first the bottom right, top right, top left, bottom left for the near plane, then similar for the far plane. +fn calculate_cascade( + frustum_corners: [Vec3A; 8], + cascade_texture_size: f32, + world_from_light: Mat4, + light_from_camera: Mat4, +) -> Cascade { + let mut min = Vec3A::splat(f32::MAX); + let mut max = Vec3A::splat(f32::MIN); + for corner_camera_view in frustum_corners { + let corner_light_view = light_from_camera.transform_point3a(corner_camera_view); + min = min.min(corner_light_view); + max = max.max(corner_light_view); + } + + // NOTE: Use the larger of the frustum slice far plane diagonal and body diagonal lengths as this + // will be the maximum possible projection size. Use the ceiling to get an integer which is + // very important for floating point stability later. It is also important that these are + // calculated using the original camera space corner positions for floating point precision + // as even though the lengths using corner_light_view above should be the same, precision can + // introduce small but significant differences. + // NOTE: The size remains the same unless the view frustum or cascade configuration is modified. + let cascade_diameter = (frustum_corners[0] - frustum_corners[6]) + .length() + .max((frustum_corners[4] - frustum_corners[6]).length()) + .ceil(); + + // NOTE: If we ensure that cascade_texture_size is a power of 2, then as we made cascade_diameter an + // integer, cascade_texel_size is then an integer multiple of a power of 2 and can be + // exactly represented in a floating point value. + let cascade_texel_size = cascade_diameter / cascade_texture_size; + // NOTE: For shadow stability it is very important that the near_plane_center is at integer + // multiples of the texel size to be exactly representable in a floating point value. + let near_plane_center = Vec3A::new( + (0.5 * (min.x + max.x) / cascade_texel_size).floor() * cascade_texel_size, + (0.5 * (min.y + max.y) / cascade_texel_size).floor() * cascade_texel_size, + // NOTE: max.z is the near plane for right-handed y-up + max.z, + ); + + // It is critical for `world_to_cascade` to be stable. So rather than forming `cascade_to_world` + // and inverting it, which risks instability due to numerical precision, we directly form + // `world_to_cascade` as the reference material suggests. + let light_to_world_transpose = world_from_light.transpose(); + let cascade_from_world = Mat4::from_cols( + light_to_world_transpose.x_axis, + light_to_world_transpose.y_axis, + light_to_world_transpose.z_axis, + (-near_plane_center).extend(1.0), + ); + + // Right-handed orthographic projection, centered at `near_plane_center`. + // NOTE: This is different from the reference material, as we use reverse Z. + let r = (max.z - min.z).recip(); + let clip_from_cascade = Mat4::from_cols( + Vec4::new(2.0 / cascade_diameter, 0.0, 0.0, 0.0), + Vec4::new(0.0, 2.0 / cascade_diameter, 0.0, 0.0), + Vec4::new(0.0, 0.0, r, 0.0), + Vec4::new(0.0, 0.0, 1.0, 1.0), + ); + + let clip_from_world = clip_from_cascade * cascade_from_world; + Cascade { + world_from_cascade: cascade_from_world.inverse(), + clip_from_cascade, + clip_from_world, + texel_size: cascade_texel_size, + } +} diff --git a/crates/bevy_pbr/src/light/directional_light.rs b/crates/bevy_pbr/src/light/directional_light.rs index 2d182c1c83..dd2da1d975 100644 --- a/crates/bevy_pbr/src/light/directional_light.rs +++ b/crates/bevy_pbr/src/light/directional_light.rs @@ -1,6 +1,16 @@ -use bevy_render::view::{self, Visibility}; +use bevy_asset::Handle; +use bevy_camera::{ + primitives::{CascadesFrusta, Frustum}, + visibility::{self, CascadesVisibleEntities, ViewVisibility, Visibility, VisibilityClass}, + Camera, +}; +use bevy_color::Color; +use bevy_ecs::prelude::*; +use bevy_image::Image; +use bevy_reflect::prelude::*; +use bevy_transform::components::Transform; -use super::*; +use crate::{cascade::CascadeShadowConfig, light_consts, Cascades, LightVisibilityClass}; /// A Directional light. /// @@ -53,7 +63,7 @@ use super::*; Visibility, VisibilityClass )] -#[component(on_add = view::add_visibility_class::)] +#[component(on_add = visibility::add_visibility_class::)] pub struct DirectionalLight { /// The color of the light. /// @@ -90,6 +100,8 @@ pub struct DirectionalLight { /// /// Note that soft shadows are significantly more expensive to render than /// hard shadows. + /// + /// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal #[cfg(feature = "experimental_pbr_pcss")] pub soft_shadow_size: Option, @@ -154,3 +166,64 @@ pub struct DirectionalLightTexture { /// Whether to tile the image infinitely, or use only a single tile centered at the light's translation pub tiled: bool, } + +/// Controls the resolution of [`DirectionalLight`] shadow maps. +/// +/// ``` +/// # use bevy_app::prelude::*; +/// # use bevy_pbr::DirectionalLightShadowMap; +/// App::new() +/// .insert_resource(DirectionalLightShadowMap { size: 4096 }); +/// ``` +#[derive(Resource, Clone, Debug, Reflect)] +#[reflect(Resource, Debug, Default, Clone)] +pub struct DirectionalLightShadowMap { + // The width and height of each cascade. + /// + /// Defaults to `2048`. + pub size: usize, +} + +impl Default for DirectionalLightShadowMap { + fn default() -> Self { + Self { size: 2048 } + } +} + +pub fn update_directional_light_frusta( + mut views: Query< + ( + &Cascades, + &DirectionalLight, + &ViewVisibility, + &mut CascadesFrusta, + ), + ( + // Prevents this query from conflicting with camera queries. + Without, + ), + >, +) { + for (cascades, directional_light, visibility, mut frusta) in &mut views { + // The frustum is used for culling meshes to the light for shadow mapping + // so if shadow mapping is disabled for this light, then the frustum is + // not needed. + if !directional_light.shadows_enabled || !visibility.get() { + continue; + } + + frusta.frusta = cascades + .cascades + .iter() + .map(|(view, cascades)| { + ( + *view, + cascades + .iter() + .map(|c| Frustum::from_clip_from_world(&c.clip_from_world)) + .collect::>(), + ) + }) + .collect(); + } +} diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index f73cebdba3..53199d39f1 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -1,36 +1,43 @@ -use bevy_ecs::{ - entity::{EntityHashMap, EntityHashSet}, - prelude::*, -}; -use bevy_math::{ops, Mat4, Vec3A, Vec4}; -use bevy_reflect::prelude::*; -use bevy_render::{ - camera::{Camera, Projection}, - extract_component::ExtractComponent, - extract_resource::ExtractResource, - mesh::Mesh3d, +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_camera::{ primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere}, - view::{ - InheritedVisibility, NoFrustumCulling, PreviousVisibleEntities, RenderLayers, - ViewVisibility, VisibilityClass, VisibilityRange, VisibleEntityRanges, + visibility::{ + CascadesVisibleEntities, CubemapVisibleEntities, InheritedVisibility, NoFrustumCulling, + PreviousVisibleEntities, RenderLayers, ViewVisibility, VisibilityRange, VisibilitySystems, + VisibleEntityRanges, VisibleMeshEntities, }, + CameraUpdateSystems, }; -use bevy_transform::components::{GlobalTransform, Transform}; +use bevy_ecs::{entity::EntityHashSet, prelude::*}; +use bevy_math::Vec3A; +use bevy_reflect::prelude::*; +use bevy_render::{extract_component::ExtractComponent, mesh::Mesh3d}; +use bevy_transform::{components::GlobalTransform, TransformSystems}; use bevy_utils::Parallel; use core::ops::DerefMut; -use crate::*; -pub use light::spot_light::{spot_light_clip_from_view, spot_light_world_from_view}; +pub use crate::light::spot_light::{spot_light_clip_from_view, spot_light_world_from_view}; +use crate::{ + add_clusters, assign_objects_to_clusters, + cascade::{build_directional_light_cascades, clear_directional_light_cascades}, + CascadeShadowConfig, Cascades, VisibleClusterableObjects, +}; mod ambient_light; pub use ambient_light::AmbientLight; +pub mod cascade; mod point_light; -pub use point_light::{PointLight, PointLightTexture}; +pub use point_light::{ + update_point_light_frusta, PointLight, PointLightShadowMap, PointLightTexture, +}; mod spot_light; -pub use spot_light::{SpotLight, SpotLightTexture}; +pub use spot_light::{update_spot_light_frusta, SpotLight, SpotLightTexture}; mod directional_light; -pub use directional_light::{DirectionalLight, DirectionalLightTexture}; +pub use directional_light::{ + update_directional_light_frusta, DirectionalLight, DirectionalLightShadowMap, + DirectionalLightTexture, +}; /// Constants for operating with the light units: lumens, and lux. pub mod light_consts { @@ -91,26 +98,85 @@ pub mod light_consts { } } -/// Controls the resolution of [`PointLight`] shadow maps. -/// -/// ``` -/// # use bevy_app::prelude::*; -/// # use bevy_pbr::PointLightShadowMap; -/// App::new() -/// .insert_resource(PointLightShadowMap { size: 2048 }); -/// ``` -#[derive(Resource, Clone, Debug, Reflect)] -#[reflect(Resource, Debug, Default, Clone)] -pub struct PointLightShadowMap { - /// The width and height of each of the 6 faces of the cubemap. - /// - /// Defaults to `1024`. - pub size: usize, -} +pub struct LightPlugin; -impl Default for PointLightShadowMap { - fn default() -> Self { - Self { size: 1024 } +impl Plugin for LightPlugin { + fn build(&self, app: &mut App) { + app.register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .init_resource::() + .init_resource::() + .init_resource::() + .configure_sets( + PostUpdate, + SimulationLightSystems::UpdateDirectionalLightCascades + .ambiguous_with(SimulationLightSystems::UpdateDirectionalLightCascades), + ) + .configure_sets( + PostUpdate, + SimulationLightSystems::CheckLightVisibility + .ambiguous_with(SimulationLightSystems::CheckLightVisibility), + ) + .add_systems( + PostUpdate, + ( + add_clusters + .in_set(SimulationLightSystems::AddClusters) + .after(CameraUpdateSystems), + assign_objects_to_clusters + .in_set(SimulationLightSystems::AssignLightsToClusters) + .after(TransformSystems::Propagate) + .after(VisibilitySystems::CheckVisibility) + .after(CameraUpdateSystems), + clear_directional_light_cascades + .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) + .after(TransformSystems::Propagate) + .after(CameraUpdateSystems), + update_directional_light_frusta + .in_set(SimulationLightSystems::UpdateLightFrusta) + // This must run after CheckVisibility because it relies on `ViewVisibility` + .after(VisibilitySystems::CheckVisibility) + .after(TransformSystems::Propagate) + .after(SimulationLightSystems::UpdateDirectionalLightCascades) + // We assume that no entity will be both a directional light and a spot light, + // so these systems will run independently of one another. + // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. + .ambiguous_with(update_spot_light_frusta), + update_point_light_frusta + .in_set(SimulationLightSystems::UpdateLightFrusta) + .after(TransformSystems::Propagate) + .after(SimulationLightSystems::AssignLightsToClusters), + update_spot_light_frusta + .in_set(SimulationLightSystems::UpdateLightFrusta) + .after(TransformSystems::Propagate) + .after(SimulationLightSystems::AssignLightsToClusters), + ( + check_dir_light_mesh_visibility, + check_point_light_mesh_visibility, + ) + .in_set(SimulationLightSystems::CheckLightVisibility) + .after(VisibilitySystems::CalculateBounds) + .after(TransformSystems::Propagate) + .after(SimulationLightSystems::UpdateLightFrusta) + // NOTE: This MUST be scheduled AFTER the core renderer visibility check + // because that resets entity `ViewVisibility` for the first view + // which would override any results from this otherwise + .after(VisibilitySystems::CheckVisibility) + .before(VisibilitySystems::MarkNewlyHiddenEntitiesInvisible), + build_directional_light_cascades + .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) + .after(clear_directional_light_cascades), + ), + ); } } @@ -118,353 +184,6 @@ impl Default for PointLightShadowMap { /// With)>`, for use with [`bevy_render::view::VisibleEntities`]. pub type WithLight = Or<(With, With, With)>; -/// Controls the resolution of [`DirectionalLight`] shadow maps. -/// -/// ``` -/// # use bevy_app::prelude::*; -/// # use bevy_pbr::DirectionalLightShadowMap; -/// App::new() -/// .insert_resource(DirectionalLightShadowMap { size: 4096 }); -/// ``` -#[derive(Resource, Clone, Debug, Reflect)] -#[reflect(Resource, Debug, Default, Clone)] -pub struct DirectionalLightShadowMap { - // The width and height of each cascade. - /// - /// Defaults to `2048`. - pub size: usize, -} - -impl Default for DirectionalLightShadowMap { - fn default() -> Self { - Self { size: 2048 } - } -} - -/// Controls how cascaded shadow mapping works. -/// Prefer using [`CascadeShadowConfigBuilder`] to construct an instance. -/// -/// ``` -/// # use bevy_pbr::CascadeShadowConfig; -/// # use bevy_pbr::CascadeShadowConfigBuilder; -/// # use bevy_utils::default; -/// # -/// let config: CascadeShadowConfig = CascadeShadowConfigBuilder { -/// maximum_distance: 100.0, -/// ..default() -/// }.into(); -/// ``` -#[derive(Component, Clone, Debug, Reflect)] -#[reflect(Component, Default, Debug, Clone)] -pub struct CascadeShadowConfig { - /// The (positive) distance to the far boundary of each cascade. - pub bounds: Vec, - /// The proportion of overlap each cascade has with the previous cascade. - pub overlap_proportion: f32, - /// The (positive) distance to the near boundary of the first cascade. - pub minimum_distance: f32, -} - -impl Default for CascadeShadowConfig { - fn default() -> Self { - CascadeShadowConfigBuilder::default().into() - } -} - -fn calculate_cascade_bounds( - num_cascades: usize, - nearest_bound: f32, - shadow_maximum_distance: f32, -) -> Vec { - if num_cascades == 1 { - return vec![shadow_maximum_distance]; - } - let base = ops::powf( - shadow_maximum_distance / nearest_bound, - 1.0 / (num_cascades - 1) as f32, - ); - (0..num_cascades) - .map(|i| nearest_bound * ops::powf(base, i as f32)) - .collect() -} - -/// Builder for [`CascadeShadowConfig`]. -pub struct CascadeShadowConfigBuilder { - /// The number of shadow cascades. - /// More cascades increases shadow quality by mitigating perspective aliasing - a phenomenon where areas - /// nearer the camera are covered by fewer shadow map texels than areas further from the camera, causing - /// blocky looking shadows. - /// - /// This does come at the cost increased rendering overhead, however this overhead is still less - /// than if you were to use fewer cascades and much larger shadow map textures to achieve the - /// same quality level. - /// - /// In case rendered geometry covers a relatively narrow and static depth relative to camera, it may - /// make more sense to use fewer cascades and a higher resolution shadow map texture as perspective aliasing - /// is not as much an issue. Be sure to adjust `minimum_distance` and `maximum_distance` appropriately. - pub num_cascades: usize, - /// The minimum shadow distance, which can help improve the texel resolution of the first cascade. - /// Areas nearer to the camera than this will likely receive no shadows. - /// - /// NOTE: Due to implementation details, this usually does not impact shadow quality as much as - /// `first_cascade_far_bound` and `maximum_distance`. At many view frustum field-of-views, the - /// texel resolution of the first cascade is dominated by the width / height of the view frustum plane - /// at `first_cascade_far_bound` rather than the depth of the frustum from `minimum_distance` to - /// `first_cascade_far_bound`. - pub minimum_distance: f32, - /// The maximum shadow distance. - /// Areas further from the camera than this will likely receive no shadows. - pub maximum_distance: f32, - /// Sets the far bound of the first cascade, relative to the view origin. - /// In-between cascades will be exponentially spaced relative to the maximum shadow distance. - /// NOTE: This is ignored if there is only one cascade, the maximum distance takes precedence. - pub first_cascade_far_bound: f32, - /// Sets the overlap proportion between cascades. - /// The overlap is used to make the transition from one cascade's shadow map to the next - /// less abrupt by blending between both shadow maps. - pub overlap_proportion: f32, -} - -impl CascadeShadowConfigBuilder { - /// Returns the cascade config as specified by this builder. - pub fn build(&self) -> CascadeShadowConfig { - assert!( - self.num_cascades > 0, - "num_cascades must be positive, but was {}", - self.num_cascades - ); - assert!( - self.minimum_distance >= 0.0, - "maximum_distance must be non-negative, but was {}", - self.minimum_distance - ); - assert!( - self.num_cascades == 1 || self.minimum_distance < self.first_cascade_far_bound, - "minimum_distance must be less than first_cascade_far_bound, but was {}", - self.minimum_distance - ); - assert!( - self.maximum_distance > self.minimum_distance, - "maximum_distance must be greater than minimum_distance, but was {}", - self.maximum_distance - ); - assert!( - (0.0..1.0).contains(&self.overlap_proportion), - "overlap_proportion must be in [0.0, 1.0) but was {}", - self.overlap_proportion - ); - CascadeShadowConfig { - bounds: calculate_cascade_bounds( - self.num_cascades, - self.first_cascade_far_bound, - self.maximum_distance, - ), - overlap_proportion: self.overlap_proportion, - minimum_distance: self.minimum_distance, - } - } -} - -impl Default for CascadeShadowConfigBuilder { - fn default() -> Self { - // The defaults are chosen to be similar to be Unity, Unreal, and Godot. - // Unity: first cascade far bound = 10.05, maximum distance = 150.0 - // Unreal Engine 5: maximum distance = 200.0 - // Godot: first cascade far bound = 10.0, maximum distance = 100.0 - Self { - // Currently only support one cascade in WebGL 2. - num_cascades: if cfg!(all( - feature = "webgl", - target_arch = "wasm32", - not(feature = "webgpu") - )) { - 1 - } else { - 4 - }, - minimum_distance: 0.1, - maximum_distance: 150.0, - first_cascade_far_bound: 10.0, - overlap_proportion: 0.2, - } - } -} - -impl From for CascadeShadowConfig { - fn from(builder: CascadeShadowConfigBuilder) -> Self { - builder.build() - } -} - -#[derive(Component, Clone, Debug, Default, Reflect)] -#[reflect(Component, Debug, Default, Clone)] -pub struct Cascades { - /// Map from a view to the configuration of each of its [`Cascade`]s. - pub cascades: EntityHashMap>, -} - -#[derive(Clone, Debug, Default, Reflect)] -#[reflect(Clone, Default)] -pub struct Cascade { - /// The transform of the light, i.e. the view to world matrix. - pub world_from_cascade: Mat4, - /// The orthographic projection for this cascade. - pub clip_from_cascade: Mat4, - /// The view-projection matrix for this cascade, converting world space into light clip space. - /// Importantly, this is derived and stored separately from `view_transform` and `projection` to - /// ensure shadow stability. - pub clip_from_world: Mat4, - /// Size of each shadow map texel in world units. - pub texel_size: f32, -} - -pub fn clear_directional_light_cascades(mut lights: Query<(&DirectionalLight, &mut Cascades)>) { - for (directional_light, mut cascades) in lights.iter_mut() { - if !directional_light.shadows_enabled { - continue; - } - cascades.cascades.clear(); - } -} - -pub fn build_directional_light_cascades( - directional_light_shadow_map: Res, - views: Query<(Entity, &GlobalTransform, &Projection, &Camera)>, - mut lights: Query<( - &GlobalTransform, - &DirectionalLight, - &CascadeShadowConfig, - &mut Cascades, - )>, -) { - let views = views - .iter() - .filter_map(|(entity, transform, projection, camera)| { - if camera.is_active { - Some((entity, projection, transform.to_matrix())) - } else { - None - } - }) - .collect::>(); - - for (transform, directional_light, cascades_config, mut cascades) in &mut lights { - if !directional_light.shadows_enabled { - continue; - } - - // It is very important to the numerical and thus visual stability of shadows that - // light_to_world has orthogonal upper-left 3x3 and zero translation. - // Even though only the direction (i.e. rotation) of the light matters, we don't constrain - // users to not change any other aspects of the transform - there's no guarantee - // `transform.to_matrix()` will give us a matrix with our desired properties. - // Instead, we directly create a good matrix from just the rotation. - let world_from_light = Mat4::from_quat(transform.compute_transform().rotation); - let light_to_world_inverse = world_from_light.inverse(); - - for (view_entity, projection, view_to_world) in views.iter().copied() { - let camera_to_light_view = light_to_world_inverse * view_to_world; - let view_cascades = cascades_config - .bounds - .iter() - .enumerate() - .map(|(idx, far_bound)| { - // Negate bounds as -z is camera forward direction. - let z_near = if idx > 0 { - (1.0 - cascades_config.overlap_proportion) - * -cascades_config.bounds[idx - 1] - } else { - -cascades_config.minimum_distance - }; - let z_far = -far_bound; - - let corners = projection.get_frustum_corners(z_near, z_far); - - calculate_cascade( - corners, - directional_light_shadow_map.size as f32, - world_from_light, - camera_to_light_view, - ) - }) - .collect(); - cascades.cascades.insert(view_entity, view_cascades); - } - } -} - -/// Returns a [`Cascade`] for the frustum defined by `frustum_corners`. -/// -/// The corner vertices should be specified in the following order: -/// first the bottom right, top right, top left, bottom left for the near plane, then similar for the far plane. -fn calculate_cascade( - frustum_corners: [Vec3A; 8], - cascade_texture_size: f32, - world_from_light: Mat4, - light_from_camera: Mat4, -) -> Cascade { - let mut min = Vec3A::splat(f32::MAX); - let mut max = Vec3A::splat(f32::MIN); - for corner_camera_view in frustum_corners { - let corner_light_view = light_from_camera.transform_point3a(corner_camera_view); - min = min.min(corner_light_view); - max = max.max(corner_light_view); - } - - // NOTE: Use the larger of the frustum slice far plane diagonal and body diagonal lengths as this - // will be the maximum possible projection size. Use the ceiling to get an integer which is - // very important for floating point stability later. It is also important that these are - // calculated using the original camera space corner positions for floating point precision - // as even though the lengths using corner_light_view above should be the same, precision can - // introduce small but significant differences. - // NOTE: The size remains the same unless the view frustum or cascade configuration is modified. - let cascade_diameter = (frustum_corners[0] - frustum_corners[6]) - .length() - .max((frustum_corners[4] - frustum_corners[6]).length()) - .ceil(); - - // NOTE: If we ensure that cascade_texture_size is a power of 2, then as we made cascade_diameter an - // integer, cascade_texel_size is then an integer multiple of a power of 2 and can be - // exactly represented in a floating point value. - let cascade_texel_size = cascade_diameter / cascade_texture_size; - // NOTE: For shadow stability it is very important that the near_plane_center is at integer - // multiples of the texel size to be exactly representable in a floating point value. - let near_plane_center = Vec3A::new( - (0.5 * (min.x + max.x) / cascade_texel_size).floor() * cascade_texel_size, - (0.5 * (min.y + max.y) / cascade_texel_size).floor() * cascade_texel_size, - // NOTE: max.z is the near plane for right-handed y-up - max.z, - ); - - // It is critical for `world_to_cascade` to be stable. So rather than forming `cascade_to_world` - // and inverting it, which risks instability due to numerical precision, we directly form - // `world_to_cascade` as the reference material suggests. - let light_to_world_transpose = world_from_light.transpose(); - let cascade_from_world = Mat4::from_cols( - light_to_world_transpose.x_axis, - light_to_world_transpose.y_axis, - light_to_world_transpose.z_axis, - (-near_plane_center).extend(1.0), - ); - - // Right-handed orthographic projection, centered at `near_plane_center`. - // NOTE: This is different from the reference material, as we use reverse Z. - let r = (max.z - min.z).recip(); - let clip_from_cascade = Mat4::from_cols( - Vec4::new(2.0 / cascade_diameter, 0.0, 0.0, 0.0), - Vec4::new(0.0, 2.0 / cascade_diameter, 0.0, 0.0), - Vec4::new(0.0, 0.0, r, 0.0), - Vec4::new(0.0, 0.0, 1.0, 1.0), - ); - - let clip_from_world = clip_from_cascade * cascade_from_world; - Cascade { - world_from_cascade: cascade_from_world.inverse(), - clip_from_cascade, - clip_from_world, - texel_size: cascade_texel_size, - } -} /// Add this component to make a [`Mesh3d`] not cast shadows. #[derive(Debug, Component, Reflect, Default)] #[reflect(Component, Default, Debug)] @@ -525,6 +244,8 @@ pub enum ShadowFilteringMethod { } /// The [`VisibilityClass`] used for all lights (point, directional, and spot). +/// +/// [`VisibilityClass`]: bevy_camera::visibility::VisibilityClass pub struct LightVisibilityClass; /// System sets used to run light-related systems. @@ -543,138 +264,6 @@ pub enum SimulationLightSystems { CheckLightVisibility, } -pub fn update_directional_light_frusta( - mut views: Query< - ( - &Cascades, - &DirectionalLight, - &ViewVisibility, - &mut CascadesFrusta, - ), - ( - // Prevents this query from conflicting with camera queries. - Without, - ), - >, -) { - for (cascades, directional_light, visibility, mut frusta) in &mut views { - // The frustum is used for culling meshes to the light for shadow mapping - // so if shadow mapping is disabled for this light, then the frustum is - // not needed. - if !directional_light.shadows_enabled || !visibility.get() { - continue; - } - - frusta.frusta = cascades - .cascades - .iter() - .map(|(view, cascades)| { - ( - *view, - cascades - .iter() - .map(|c| Frustum::from_clip_from_world(&c.clip_from_world)) - .collect::>(), - ) - }) - .collect(); - } -} - -// NOTE: Run this after assign_lights_to_clusters! -pub fn update_point_light_frusta( - global_lights: Res, - mut views: Query<(Entity, &GlobalTransform, &PointLight, &mut CubemapFrusta)>, - changed_lights: Query< - Entity, - ( - With, - Or<(Changed, Changed)>, - ), - >, -) { - let view_rotations = CUBE_MAP_FACES - .iter() - .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up)) - .collect::>(); - - for (entity, transform, point_light, mut cubemap_frusta) in &mut views { - // If this light hasn't changed, and neither has the set of global_lights, - // then we can skip this calculation. - if !global_lights.is_changed() && !changed_lights.contains(entity) { - continue; - } - - // The frusta are used for culling meshes to the light for shadow mapping - // so if shadow mapping is disabled for this light, then the frusta are - // not needed. - // Also, if the light is not relevant for any cluster, it will not be in the - // global lights set and so there is no need to update its frusta. - if !point_light.shadows_enabled || !global_lights.entities.contains(&entity) { - continue; - } - - let clip_from_view = Mat4::perspective_infinite_reverse_rh( - core::f32::consts::FRAC_PI_2, - 1.0, - point_light.shadow_map_near_z, - ); - - // ignore scale because we don't want to effectively scale light radius and range - // by applying those as a view transform to shadow map rendering of objects - // and ignore rotation because we want the shadow map projections to align with the axes - let view_translation = Transform::from_translation(transform.translation()); - let view_backward = transform.back(); - - for (view_rotation, frustum) in view_rotations.iter().zip(cubemap_frusta.iter_mut()) { - let world_from_view = view_translation * *view_rotation; - let clip_from_world = clip_from_view * world_from_view.to_matrix().inverse(); - - *frustum = Frustum::from_clip_from_world_custom_far( - &clip_from_world, - &transform.translation(), - &view_backward, - point_light.range, - ); - } - } -} - -pub fn update_spot_light_frusta( - global_lights: Res, - mut views: Query< - (Entity, &GlobalTransform, &SpotLight, &mut Frustum), - Or<(Changed, Changed)>, - >, -) { - for (entity, transform, spot_light, mut frustum) in &mut views { - // The frusta are used for culling meshes to the light for shadow mapping - // so if shadow mapping is disabled for this light, then the frusta are - // not needed. - // Also, if the light is not relevant for any cluster, it will not be in the - // global lights set and so there is no need to update its frusta. - if !spot_light.shadows_enabled || !global_lights.entities.contains(&entity) { - continue; - } - - // ignore scale because we don't want to effectively scale light radius and range - // by applying those as a view transform to shadow map rendering of objects - let view_backward = transform.back(); - - let spot_world_from_view = spot_light_world_from_view(transform); - let spot_clip_from_view = - spot_light_clip_from_view(spot_light.outer_angle, spot_light.shadow_map_near_z); - let clip_from_world = spot_clip_from_view * spot_world_from_view.inverse(); - - *frustum = Frustum::from_clip_from_world_custom_far( - &clip_from_world, - &transform.translation(), - &view_backward, - spot_light.range, - ); - } -} - fn shrink_entities(visible_entities: &mut Vec) { // Check that visible entities capacity() is no more than two times greater than len() let capacity = visible_entities.capacity(); diff --git a/crates/bevy_pbr/src/light/point_light.rs b/crates/bevy_pbr/src/light/point_light.rs index c977d0be33..8ba108adcc 100644 --- a/crates/bevy_pbr/src/light/point_light.rs +++ b/crates/bevy_pbr/src/light/point_light.rs @@ -1,8 +1,16 @@ -use bevy_render::view::{self, Visibility}; +use bevy_asset::Handle; +use bevy_camera::{ + primitives::{CubeMapFace, CubemapFrusta, CubemapLayout, Frustum, CUBE_MAP_FACES}, + visibility::{self, CubemapVisibleEntities, Visibility, VisibilityClass}, +}; +use bevy_color::Color; +use bevy_ecs::prelude::*; +use bevy_image::Image; +use bevy_math::Mat4; +use bevy_reflect::prelude::*; +use bevy_transform::components::{GlobalTransform, Transform}; -use crate::decal::clustered::CubemapLayout; - -use super::*; +use crate::{GlobalVisibleClusterableObjects, LightVisibilityClass}; /// A light that emits light in all directions from a central point. /// @@ -36,7 +44,7 @@ use super::*; Visibility, VisibilityClass )] -#[component(on_add = view::add_visibility_class::)] +#[component(on_add = visibility::add_visibility_class::)] pub struct PointLight { /// The color of this light source. pub color: Color, @@ -76,6 +84,8 @@ pub struct PointLight { /// /// Note that soft shadows are significantly more expensive to render than /// hard shadows. + /// + /// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal #[cfg(feature = "experimental_pbr_pcss")] pub soft_shadows_enabled: bool, @@ -151,3 +161,85 @@ pub struct PointLightTexture { /// The cubemap layout. The image should be a packed cubemap in one of the formats described by the [`CubemapLayout`] enum. pub cubemap_layout: CubemapLayout, } + +/// Controls the resolution of [`PointLight`] shadow maps. +/// +/// ``` +/// # use bevy_app::prelude::*; +/// # use bevy_pbr::PointLightShadowMap; +/// App::new() +/// .insert_resource(PointLightShadowMap { size: 2048 }); +/// ``` +#[derive(Resource, Clone, Debug, Reflect)] +#[reflect(Resource, Debug, Default, Clone)] +pub struct PointLightShadowMap { + /// The width and height of each of the 6 faces of the cubemap. + /// + /// Defaults to `1024`. + pub size: usize, +} + +impl Default for PointLightShadowMap { + fn default() -> Self { + Self { size: 1024 } + } +} + +// NOTE: Run this after assign_lights_to_clusters! +pub fn update_point_light_frusta( + global_lights: Res, + mut views: Query<(Entity, &GlobalTransform, &PointLight, &mut CubemapFrusta)>, + changed_lights: Query< + Entity, + ( + With, + Or<(Changed, Changed)>, + ), + >, +) { + let view_rotations = CUBE_MAP_FACES + .iter() + .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up)) + .collect::>(); + + for (entity, transform, point_light, mut cubemap_frusta) in &mut views { + // If this light hasn't changed, and neither has the set of global_lights, + // then we can skip this calculation. + if !global_lights.is_changed() && !changed_lights.contains(entity) { + continue; + } + + // The frusta are used for culling meshes to the light for shadow mapping + // so if shadow mapping is disabled for this light, then the frusta are + // not needed. + // Also, if the light is not relevant for any cluster, it will not be in the + // global lights set and so there is no need to update its frusta. + if !point_light.shadows_enabled || !global_lights.entities.contains(&entity) { + continue; + } + + let clip_from_view = Mat4::perspective_infinite_reverse_rh( + core::f32::consts::FRAC_PI_2, + 1.0, + point_light.shadow_map_near_z, + ); + + // ignore scale because we don't want to effectively scale light radius and range + // by applying those as a view transform to shadow map rendering of objects + // and ignore rotation because we want the shadow map projections to align with the axes + let view_translation = Transform::from_translation(transform.translation()); + let view_backward = transform.back(); + + for (view_rotation, frustum) in view_rotations.iter().zip(cubemap_frusta.iter_mut()) { + let world_from_view = view_translation * *view_rotation; + let clip_from_world = clip_from_view * world_from_view.to_matrix().inverse(); + + *frustum = Frustum::from_clip_from_world_custom_far( + &clip_from_world, + &transform.translation(), + &view_backward, + point_light.range, + ); + } + } +} diff --git a/crates/bevy_pbr/src/light/spot_light.rs b/crates/bevy_pbr/src/light/spot_light.rs index 393e9efc0c..0e09b1c509 100644 --- a/crates/bevy_pbr/src/light/spot_light.rs +++ b/crates/bevy_pbr/src/light/spot_light.rs @@ -1,6 +1,17 @@ -use bevy_render::view::{self, Visibility}; +use bevy_asset::Handle; +use bevy_camera::{ + primitives::Frustum, + visibility::{self, Visibility, VisibilityClass}, +}; +use bevy_color::Color; +use bevy_ecs::prelude::*; +use bevy_image::Image; +use bevy_math::{Mat4, Vec4}; +use bevy_reflect::prelude::*; +use bevy_render::view::VisibleMeshEntities; +use bevy_transform::components::{GlobalTransform, Transform}; -use super::*; +use crate::{GlobalVisibleClusterableObjects, LightVisibilityClass}; /// A light that emits light in a given direction from a central point. /// @@ -10,7 +21,7 @@ use super::*; #[derive(Component, Debug, Clone, Copy, Reflect)] #[reflect(Component, Default, Debug, Clone)] #[require(Frustum, VisibleMeshEntities, Transform, Visibility, VisibilityClass)] -#[component(on_add = view::add_visibility_class::)] +#[component(on_add = visibility::add_visibility_class::)] pub struct SpotLight { /// The color of the light. /// @@ -58,6 +69,8 @@ pub struct SpotLight { /// /// Note that soft shadows are significantly more expensive to render than /// hard shadows. + /// + /// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal #[cfg(feature = "experimental_pbr_pcss")] pub soft_shadows_enabled: bool, @@ -184,3 +197,38 @@ pub struct SpotLightTexture { /// Note the border of the image should be entirely black to avoid leaking light. pub image: Handle, } + +pub fn update_spot_light_frusta( + global_lights: Res, + mut views: Query< + (Entity, &GlobalTransform, &SpotLight, &mut Frustum), + Or<(Changed, Changed)>, + >, +) { + for (entity, transform, spot_light, mut frustum) in &mut views { + // The frusta are used for culling meshes to the light for shadow mapping + // so if shadow mapping is disabled for this light, then the frusta are + // not needed. + // Also, if the light is not relevant for any cluster, it will not be in the + // global lights set and so there is no need to update its frusta. + if !spot_light.shadows_enabled || !global_lights.entities.contains(&entity) { + continue; + } + + // ignore scale because we don't want to effectively scale light radius and range + // by applying those as a view transform to shadow map rendering of objects + let view_backward = transform.back(); + + let spot_world_from_view = spot_light_world_from_view(transform); + let spot_clip_from_view = + spot_light_clip_from_view(spot_light.outer_angle, spot_light.shadow_map_near_z); + let clip_from_world = spot_clip_from_view * spot_world_from_view.inverse(); + + *frustum = Frustum::from_clip_from_world_custom_far( + &clip_from_world, + &transform.translation(), + &view_backward, + spot_light.range, + ); + } +} diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 1f36d45fd6..883147acc3 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,5 +1,6 @@ use self::assign::ClusterableObjectType; use crate::assign::calculate_cluster_factors; +use crate::cascade::{Cascade, CascadeShadowConfig, Cascades}; use crate::*; use bevy_asset::UntypedAssetId; pub use bevy_camera::primitives::{face_index_to_name, CubeMapFace, CUBE_MAP_FACES}; From c5fe7f0975175043bad05c87270baf769e735ed1 Mon Sep 17 00:00:00 2001 From: atlv Date: Sun, 6 Jul 2025 00:25:26 -0400 Subject: [PATCH 14/14] consistently dont use smallvec default features (#19972) # Objective - for smallvec some crates specify default features false, other dont. turns out we dont need them ## Solution - remove ## Testing - 3d_scene --- crates/bevy_animation/Cargo.toml | 2 +- crates/bevy_camera/Cargo.toml | 2 +- crates/bevy_core_pipeline/Cargo.toml | 2 +- crates/bevy_ecs/Cargo.toml | 5 ++++- crates/bevy_gltf/Cargo.toml | 2 +- crates/bevy_math/Cargo.toml | 2 +- crates/bevy_pbr/Cargo.toml | 2 +- crates/bevy_reflect/Cargo.toml | 2 +- crates/bevy_render/Cargo.toml | 2 +- crates/bevy_text/Cargo.toml | 2 +- crates/bevy_ui/Cargo.toml | 2 +- crates/bevy_ui_render/Cargo.toml | 2 +- 12 files changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 380c276539..a5de22f1a4 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -41,7 +41,7 @@ derive_more = { version = "2", default-features = false, features = ["from"] } either = "1.13" thread_local = "1" uuid = { version = "1.13.1", features = ["v4"] } -smallvec = "1" +smallvec = { version = "1", default-features = false } tracing = { version = "0.1", default-features = false, features = ["std"] } [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/bevy_camera/Cargo.toml b/crates/bevy_camera/Cargo.toml index 65aadce0eb..6ed3998a82 100644 --- a/crates/bevy_camera/Cargo.toml +++ b/crates/bevy_camera/Cargo.toml @@ -31,7 +31,7 @@ serde = { version = "1", default-features = false, features = ["derive"] } thiserror = { version = "2", default-features = false } downcast-rs = { version = "2", default-features = false, features = ["std"] } derive_more = { version = "2", default-features = false, features = ["from"] } -smallvec = { version = "1.11", features = ["const_new"] } +smallvec = { version = "1", default-features = false, features = ["const_new"] } [features] default = [] diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index 680d57efa1..837869bc07 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -43,7 +43,7 @@ serde = { version = "1", features = ["derive"] } bitflags = "2.3" radsort = "0.1" nonmax = "0.5" -smallvec = "1" +smallvec = { version = "1", default-features = false } thiserror = { version = "2", default-features = false } tracing = { version = "0.1", default-features = false, features = ["std"] } bytemuck = { version = "1" } diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 8fd5f6eb9c..f0f9b782af 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -112,7 +112,10 @@ derive_more = { version = "2", default-features = false, features = [ ] } nonmax = { version = "0.5", default-features = false } arrayvec = { version = "0.7.4", default-features = false, optional = true } -smallvec = { version = "1", features = ["union", "const_generics"] } +smallvec = { version = "1", default-features = false, features = [ + "union", + "const_generics", +] } indexmap = { version = "2.5.0", default-features = false } variadics_please = { version = "1.1", default-features = false } tracing = { version = "0.1", default-features = false, optional = true } diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index 36e9508f4c..e35d7771f5 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -63,7 +63,7 @@ itertools = "0.14" percent-encoding = "2.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.140" -smallvec = "1.11" +smallvec = { version = "1", default-features = false } tracing = { version = "0.1", default-features = false, features = ["std"] } bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 3fad8e6209..459ff6e90a 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -24,7 +24,7 @@ libm = { version = "0.2", optional = true } approx = { version = "0.5", default-features = false, optional = true } rand = { version = "0.8", default-features = false, optional = true } rand_distr = { version = "0.4.3", optional = true } -smallvec = { version = "1.11" } +smallvec = { version = "1", default-features = false } bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "glam", ], optional = true } diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 754627558c..f183e4e044 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -71,7 +71,7 @@ bitvec = { version = "1", optional = true } # direct dependency required for derive macro bytemuck = { version = "1", features = ["derive", "must_cast"] } radsort = "0.1" -smallvec = "1.6" +smallvec = { version = "1", default-features = false } nonmax = "0.5" static_assertions = "1" tracing = { version = "0.1", default-features = false, features = ["std"] } diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index aba26258b6..8e2d4d0f38 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -96,7 +96,7 @@ thiserror = { version = "2", default-features = false } derive_more = { version = "2", default-features = false, features = ["from"] } serde = { version = "1", default-features = false, features = ["alloc"] } assert_type_match = "0.1.1" -smallvec = { version = "1.11", default-features = false, optional = true } +smallvec = { version = "1", default-features = false, optional = true } glam = { version = "0.29.3", default-features = false, features = [ "serde", ], optional = true } diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index e8f076f014..27463b9272 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -114,7 +114,7 @@ profiling = { version = "1", features = [ ], optional = true } async-channel = "2.3.0" nonmax = "0.5" -smallvec = { version = "1.11", features = ["const_new"] } +smallvec = { version = "1", default-features = false, features = ["const_new"] } offset-allocator = "0.2" variadics_please = "1.1" tracing = { version = "0.1", default-features = false, features = ["std"] } diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index c71fb5ce0c..58bcfb1c5b 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -36,7 +36,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea cosmic-text = { version = "0.14", features = ["shape-run-cache"] } thiserror = { version = "2", default-features = false } serde = { version = "1", features = ["derive"] } -smallvec = "1.13" +smallvec = { version = "1", default-features = false } unicode-bidi = "0.3.13" sys-locale = "0.3.0" tracing = { version = "0.1", default-features = false, features = ["std"] } diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index d26226eb2e..86297f6df6 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -40,7 +40,7 @@ bytemuck = { version = "1.5", features = ["derive"] } thiserror = { version = "2", default-features = false } derive_more = { version = "2", default-features = false, features = ["from"] } nonmax = "0.5" -smallvec = "1.11" +smallvec = { version = "1", default-features = false } accesskit = "0.19" tracing = { version = "0.1", default-features = false, features = ["std"] } diff --git a/crates/bevy_ui_render/Cargo.toml b/crates/bevy_ui_render/Cargo.toml index 4f01fdf570..249372c7f0 100644 --- a/crates/bevy_ui_render/Cargo.toml +++ b/crates/bevy_ui_render/Cargo.toml @@ -37,7 +37,7 @@ bytemuck = { version = "1.5", features = ["derive"] } thiserror = { version = "2", default-features = false } derive_more = { version = "1", default-features = false, features = ["from"] } nonmax = "0.5" -smallvec = "1.11" +smallvec = { version = "1", default-features = false } accesskit = "0.18" tracing = { version = "0.1", default-features = false, features = ["std"] }