Merge branch 'main' into proper-json-schema
This commit is contained in:
commit
02ed81c75f
@ -41,7 +41,7 @@ derive_more = { version = "2", default-features = false, features = ["from"] }
|
|||||||
either = "1.13"
|
either = "1.13"
|
||||||
thread_local = "1"
|
thread_local = "1"
|
||||||
uuid = { version = "1.13.1", features = ["v4"] }
|
uuid = { version = "1.13.1", features = ["v4"] }
|
||||||
smallvec = "1"
|
smallvec = { version = "1", default-features = false }
|
||||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
|||||||
@ -31,7 +31,7 @@ serde = { version = "1", default-features = false, features = ["derive"] }
|
|||||||
thiserror = { version = "2", default-features = false }
|
thiserror = { version = "2", default-features = false }
|
||||||
downcast-rs = { version = "2", default-features = false, features = ["std"] }
|
downcast-rs = { version = "2", default-features = false, features = ["std"] }
|
||||||
derive_more = { version = "2", default-features = false, features = ["from"] }
|
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]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|||||||
@ -1,39 +1,33 @@
|
|||||||
use crate::{
|
use crate::{primitives::Frustum, Camera, CameraProjection, OrthographicProjection, Projection};
|
||||||
core_3d::graph::Core3d,
|
|
||||||
tonemapping::{DebandDither, Tonemapping},
|
|
||||||
};
|
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize};
|
use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize};
|
||||||
use bevy_render::{
|
use bevy_transform::prelude::{GlobalTransform, Transform};
|
||||||
camera::{Camera, CameraRenderGraph, Exposure, Projection},
|
|
||||||
extract_component::ExtractComponent,
|
|
||||||
render_resource::{LoadOp, TextureUsages},
|
|
||||||
view::ColorGrading,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
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`].
|
/// 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.
|
/// The camera coordinate space is right-handed X-right, Y-up, Z-back.
|
||||||
/// This means "forward" is -Z.
|
/// This means "forward" is -Z.
|
||||||
#[derive(Component, Reflect, Clone, ExtractComponent)]
|
#[derive(Component, Reflect, Clone)]
|
||||||
#[extract_component_filter(With<Camera>)]
|
|
||||||
#[reflect(Component, Default, Clone)]
|
#[reflect(Component, Default, Clone)]
|
||||||
#[require(
|
#[require(Camera, Projection)]
|
||||||
Camera,
|
|
||||||
DebandDither::Enabled,
|
|
||||||
CameraRenderGraph::new(Core3d),
|
|
||||||
Projection,
|
|
||||||
Tonemapping,
|
|
||||||
ColorGrading,
|
|
||||||
Exposure
|
|
||||||
)]
|
|
||||||
pub struct Camera3d {
|
pub struct Camera3d {
|
||||||
/// The depth clear operation to perform for the main 3d pass.
|
/// The depth clear operation to perform for the main 3d pass.
|
||||||
pub depth_load_op: Camera3dDepthLoadOp,
|
pub depth_load_op: Camera3dDepthLoadOp,
|
||||||
/// The texture usages for the depth texture created for the main 3d pass.
|
/// The texture usages for the depth texture created for the main 3d pass.
|
||||||
pub depth_texture_usages: Camera3dDepthTextureUsage,
|
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
|
/// Roughly corresponds to how many “layers of transparency” are rendered for screen space
|
||||||
/// specular transmissive objects. Each step requires making one additional
|
/// specular transmissive objects. Each step requires making one additional
|
||||||
@ -1,12 +1,14 @@
|
|||||||
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
|
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
|
||||||
mod camera;
|
mod camera;
|
||||||
mod clear_color;
|
mod clear_color;
|
||||||
|
mod components;
|
||||||
pub mod primitives;
|
pub mod primitives;
|
||||||
mod projection;
|
mod projection;
|
||||||
pub mod visibility;
|
pub mod visibility;
|
||||||
|
|
||||||
pub use camera::*;
|
pub use camera::*;
|
||||||
pub use clear_color::*;
|
pub use clear_color::*;
|
||||||
|
pub use components::*;
|
||||||
pub use projection::*;
|
pub use projection::*;
|
||||||
|
|
||||||
use bevy_app::{App, Plugin};
|
use bevy_app::{App, Plugin};
|
||||||
|
|||||||
@ -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)]
|
#[derive(Component, Clone, Debug, Default, Reflect)]
|
||||||
#[reflect(Component, Default, Debug, Clone)]
|
#[reflect(Component, Default, Debug, Clone)]
|
||||||
pub struct CubemapFrusta {
|
pub struct CubemapFrusta {
|
||||||
@ -363,6 +420,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
|
||||||
|
/// -x
|
||||||
|
/// +y
|
||||||
|
/// -y
|
||||||
|
/// -z
|
||||||
|
/// +z
|
||||||
|
/// ```
|
||||||
|
SequenceVertical = 2,
|
||||||
|
/// layout in a horizontal sequence
|
||||||
|
/// ```text
|
||||||
|
/// +x -x +y -y -z +z
|
||||||
|
/// ```
|
||||||
|
SequenceHorizontal = 3,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Component, Debug, Default, Reflect, Clone)]
|
#[derive(Component, Debug, Default, Reflect, Clone)]
|
||||||
#[reflect(Component, Default, Debug, Clone)]
|
#[reflect(Component, Default, Debug, Clone)]
|
||||||
pub struct CascadesFrusta {
|
pub struct CascadesFrusta {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ mod render_layers;
|
|||||||
|
|
||||||
use core::any::TypeId;
|
use core::any::TypeId;
|
||||||
|
|
||||||
use bevy_ecs::entity::EntityHashSet;
|
use bevy_ecs::entity::{EntityHashMap, EntityHashSet};
|
||||||
use bevy_ecs::lifecycle::HookContext;
|
use bevy_ecs::lifecycle::HookContext;
|
||||||
use bevy_ecs::world::DeferredWorld;
|
use bevy_ecs::world::DeferredWorld;
|
||||||
use derive_more::derive::{Deref, DerefMut};
|
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<Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Item = &VisibleMeshEntities> {
|
||||||
|
self.data.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut VisibleMeshEntities> {
|
||||||
|
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<Vec<VisibleMeshEntities>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
||||||
pub enum VisibilitySystems {
|
pub enum VisibilitySystems {
|
||||||
/// Label for the [`calculate_bounds`], `calculate_bounds_2d` and `calculate_bounds_text2d` systems,
|
/// Label for the [`calculate_bounds`], `calculate_bounds_2d` and `calculate_bounds_text2d` systems,
|
||||||
@ -303,6 +347,9 @@ impl Plugin for VisibilityPlugin {
|
|||||||
.register_type::<RenderLayers>()
|
.register_type::<RenderLayers>()
|
||||||
.register_type::<Visibility>()
|
.register_type::<Visibility>()
|
||||||
.register_type::<VisibleEntities>()
|
.register_type::<VisibleEntities>()
|
||||||
|
.register_type::<CascadesVisibleEntities>()
|
||||||
|
.register_type::<VisibleMeshEntities>()
|
||||||
|
.register_type::<CubemapVisibleEntities>()
|
||||||
.register_required_components::<Mesh3d, Visibility>()
|
.register_required_components::<Mesh3d, Visibility>()
|
||||||
.register_required_components::<Mesh3d, VisibilityClass>()
|
.register_required_components::<Mesh3d, VisibilityClass>()
|
||||||
.register_required_components::<Mesh2d, Visibility>()
|
.register_required_components::<Mesh2d, Visibility>()
|
||||||
|
|||||||
@ -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_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" }
|
||||||
bevy_ecs = { path = "../bevy_ecs", 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_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_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" }
|
||||||
bevy_render = { path = "../bevy_render", 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" }
|
bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
|
||||||
@ -42,7 +43,7 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
bitflags = "2.3"
|
bitflags = "2.3"
|
||||||
radsort = "0.1"
|
radsort = "0.1"
|
||||||
nonmax = "0.5"
|
nonmax = "0.5"
|
||||||
smallvec = "1"
|
smallvec = { version = "1", default-features = false }
|
||||||
thiserror = { version = "2", default-features = false }
|
thiserror = { version = "2", default-features = false }
|
||||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
bytemuck = { version = "1" }
|
bytemuck = { version = "1" }
|
||||||
|
|||||||
@ -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<Camera>)]
|
|
||||||
#[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;
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
mod camera_2d;
|
|
||||||
mod main_opaque_pass_2d_node;
|
mod main_opaque_pass_2d_node;
|
||||||
mod main_transparent_pass_2d_node;
|
mod main_transparent_pass_2d_node;
|
||||||
|
|
||||||
@ -34,18 +33,22 @@ pub mod graph {
|
|||||||
use core::ops::Range;
|
use core::ops::Range;
|
||||||
|
|
||||||
use bevy_asset::UntypedAssetId;
|
use bevy_asset::UntypedAssetId;
|
||||||
|
pub use bevy_camera::Camera2d;
|
||||||
use bevy_image::ToExtents;
|
use bevy_image::ToExtents;
|
||||||
use bevy_platform::collections::{HashMap, HashSet};
|
use bevy_platform::collections::{HashMap, HashSet};
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
batching::gpu_preprocessing::GpuPreprocessingMode,
|
batching::gpu_preprocessing::GpuPreprocessingMode,
|
||||||
|
camera::CameraRenderGraph,
|
||||||
render_phase::PhaseItemBatchSetKey,
|
render_phase::PhaseItemBatchSetKey,
|
||||||
view::{ExtractedView, RetainedViewEntity},
|
view::{ExtractedView, RetainedViewEntity},
|
||||||
};
|
};
|
||||||
pub use camera_2d::*;
|
|
||||||
pub use main_opaque_pass_2d_node::*;
|
pub use main_opaque_pass_2d_node::*;
|
||||||
pub use main_transparent_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_app::{App, Plugin};
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
use bevy_math::FloatOrd;
|
use bevy_math::FloatOrd;
|
||||||
@ -78,6 +81,11 @@ pub struct Core2dPlugin;
|
|||||||
impl Plugin for Core2dPlugin {
|
impl Plugin for Core2dPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.register_type::<Camera2d>()
|
app.register_type::<Camera2d>()
|
||||||
|
.register_required_components::<Camera2d, DebandDither>()
|
||||||
|
.register_required_components_with::<Camera2d, CameraRenderGraph>(|| {
|
||||||
|
CameraRenderGraph::new(Core2d)
|
||||||
|
})
|
||||||
|
.register_required_components_with::<Camera2d, Tonemapping>(|| Tonemapping::None)
|
||||||
.add_plugins(ExtractComponentPlugin::<Camera2d>::default());
|
.add_plugins(ExtractComponentPlugin::<Camera2d>::default());
|
||||||
|
|
||||||
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
mod camera_3d;
|
|
||||||
mod main_opaque_pass_3d_node;
|
mod main_opaque_pass_3d_node;
|
||||||
mod main_transmissive_pass_3d_node;
|
mod main_transmissive_pass_3d_node;
|
||||||
mod main_transparent_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;
|
use core::ops::Range;
|
||||||
|
|
||||||
|
pub use bevy_camera::{
|
||||||
|
Camera3d, Camera3dDepthLoadOp, Camera3dDepthTextureUsage, ScreenSpaceTransmissionQuality,
|
||||||
|
};
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport},
|
batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport},
|
||||||
|
camera::CameraRenderGraph,
|
||||||
experimental::occlusion_culling::OcclusionCulling,
|
experimental::occlusion_culling::OcclusionCulling,
|
||||||
mesh::allocator::SlabId,
|
mesh::allocator::SlabId,
|
||||||
render_phase::PhaseItemBatchSetKey,
|
render_phase::PhaseItemBatchSetKey,
|
||||||
view::{prepare_view_targets, NoIndirectDrawing, RetainedViewEntity},
|
view::{prepare_view_targets, NoIndirectDrawing, RetainedViewEntity},
|
||||||
};
|
};
|
||||||
pub use camera_3d::*;
|
|
||||||
pub use main_opaque_pass_3d_node::*;
|
pub use main_opaque_pass_3d_node::*;
|
||||||
pub use main_transparent_pass_3d_node::*;
|
pub use main_transparent_pass_3d_node::*;
|
||||||
|
|
||||||
@ -127,7 +129,7 @@ use crate::{
|
|||||||
ViewPrepassTextures, MOTION_VECTOR_PREPASS_FORMAT, NORMAL_PREPASS_FORMAT,
|
ViewPrepassTextures, MOTION_VECTOR_PREPASS_FORMAT, NORMAL_PREPASS_FORMAT,
|
||||||
},
|
},
|
||||||
skybox::SkyboxPlugin,
|
skybox::SkyboxPlugin,
|
||||||
tonemapping::TonemappingNode,
|
tonemapping::{DebandDither, Tonemapping, TonemappingNode},
|
||||||
upscaling::UpscalingNode,
|
upscaling::UpscalingNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -139,6 +141,11 @@ impl Plugin for Core3dPlugin {
|
|||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.register_type::<Camera3d>()
|
app.register_type::<Camera3d>()
|
||||||
.register_type::<ScreenSpaceTransmissionQuality>()
|
.register_type::<ScreenSpaceTransmissionQuality>()
|
||||||
|
.register_required_components_with::<Camera3d, DebandDither>(|| DebandDither::Enabled)
|
||||||
|
.register_required_components_with::<Camera3d, CameraRenderGraph>(|| {
|
||||||
|
CameraRenderGraph::new(Core3d)
|
||||||
|
})
|
||||||
|
.register_required_components::<Camera3d, Tonemapping>()
|
||||||
.add_plugins((SkyboxPlugin, ExtractComponentPlugin::<Camera3d>::default()))
|
.add_plugins((SkyboxPlugin, ExtractComponentPlugin::<Camera3d>::default()))
|
||||||
.add_systems(PostUpdate, check_msaa);
|
.add_systems(PostUpdate, check_msaa);
|
||||||
|
|
||||||
|
|||||||
@ -112,7 +112,10 @@ derive_more = { version = "2", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
nonmax = { version = "0.5", default-features = false }
|
nonmax = { version = "0.5", default-features = false }
|
||||||
arrayvec = { version = "0.7.4", default-features = false, optional = true }
|
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 }
|
indexmap = { version = "2.5.0", default-features = false }
|
||||||
variadics_please = { version = "1.1", default-features = false }
|
variadics_please = { version = "1.1", default-features = false }
|
||||||
tracing = { version = "0.1", default-features = false, optional = true }
|
tracing = { version = "0.1", default-features = false, optional = true }
|
||||||
|
|||||||
@ -63,7 +63,7 @@ itertools = "0.14"
|
|||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
smallvec = "1.11"
|
smallvec = { version = "1", default-features = false }
|
||||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
bevy_log = { path = "../bevy_log", version = "0.17.0-dev" }
|
bevy_log = { path = "../bevy_log", version = "0.17.0-dev" }
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,7 @@ libm = { version = "0.2", optional = true }
|
|||||||
approx = { version = "0.5", default-features = false, optional = true }
|
approx = { version = "0.5", default-features = false, optional = true }
|
||||||
rand = { version = "0.8", default-features = false, optional = true }
|
rand = { version = "0.8", default-features = false, optional = true }
|
||||||
rand_distr = { version = "0.4.3", 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 = [
|
bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [
|
||||||
"glam",
|
"glam",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
|||||||
@ -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_math = { path = "../bevy_math", version = "0.17.0-dev" }
|
||||||
bevy_reflect = { path = "../bevy_reflect", 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_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_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", optional = true }
|
||||||
bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
|
bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
|
||||||
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" }
|
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" }
|
||||||
@ -70,7 +71,7 @@ bitvec = { version = "1", optional = true }
|
|||||||
# direct dependency required for derive macro
|
# direct dependency required for derive macro
|
||||||
bytemuck = { version = "1", features = ["derive", "must_cast"] }
|
bytemuck = { version = "1", features = ["derive", "must_cast"] }
|
||||||
radsort = "0.1"
|
radsort = "0.1"
|
||||||
smallvec = "1.6"
|
smallvec = { version = "1", default-features = false }
|
||||||
nonmax = "0.5"
|
nonmax = "0.5"
|
||||||
static_assertions = "1"
|
static_assertions = "1"
|
||||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
//! Assigning objects to clusters.
|
//! Assigning objects to clusters.
|
||||||
|
|
||||||
|
use bevy_camera::{
|
||||||
|
primitives::{Aabb, Frustum, HalfSpace, Sphere},
|
||||||
|
visibility::{RenderLayers, ViewVisibility},
|
||||||
|
Camera,
|
||||||
|
};
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
entity::Entity,
|
entity::Entity,
|
||||||
query::{Has, With},
|
query::{Has, With},
|
||||||
@ -9,25 +14,19 @@ use bevy_math::{
|
|||||||
ops::{self, sin_cos},
|
ops::{self, sin_cos},
|
||||||
Mat4, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles as _, Vec4, Vec4Swizzles as _,
|
Mat4, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles as _, Vec4, Vec4Swizzles as _,
|
||||||
};
|
};
|
||||||
use bevy_render::{
|
|
||||||
camera::Camera,
|
|
||||||
primitives::{Aabb, Frustum, HalfSpace, Sphere},
|
|
||||||
render_resource::BufferBindingType,
|
|
||||||
renderer::{RenderAdapter, RenderDevice},
|
|
||||||
view::{RenderLayers, ViewVisibility},
|
|
||||||
};
|
|
||||||
use bevy_transform::components::GlobalTransform;
|
use bevy_transform::components::GlobalTransform;
|
||||||
use bevy_utils::prelude::default;
|
use bevy_utils::prelude::default;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::{
|
use super::{
|
||||||
decal::{self, clustered::ClusteredDecal},
|
ClusterConfig, ClusterFarZMode, ClusteredDecal, Clusters, GlobalClusterSettings,
|
||||||
prelude::EnvironmentMapLight,
|
GlobalVisibleClusterableObjects, ViewClusterBindings, VisibleClusterableObjects,
|
||||||
ClusterConfig, ClusterFarZMode, Clusters, ExtractedPointLight, GlobalVisibleClusterableObjects,
|
|
||||||
LightProbe, PointLight, SpotLight, ViewClusterBindings, VisibleClusterableObjects,
|
|
||||||
VolumetricLight, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT,
|
|
||||||
MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS,
|
MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS,
|
||||||
};
|
};
|
||||||
|
use crate::{
|
||||||
|
prelude::EnvironmentMapLight, ExtractedPointLight, LightProbe, PointLight, SpotLight,
|
||||||
|
VolumetricLight,
|
||||||
|
};
|
||||||
|
|
||||||
const NDC_MIN: Vec2 = Vec2::NEG_ONE;
|
const NDC_MIN: Vec2 = Vec2::NEG_ONE;
|
||||||
const NDC_MAX: Vec2 = Vec2::ONE;
|
const NDC_MAX: Vec2 = Vec2::ONE;
|
||||||
@ -180,9 +179,9 @@ pub(crate) fn assign_objects_to_clusters(
|
|||||||
mut clusterable_objects: Local<Vec<ClusterableObjectAssignmentData>>,
|
mut clusterable_objects: Local<Vec<ClusterableObjectAssignmentData>>,
|
||||||
mut cluster_aabb_spheres: Local<Vec<Option<Sphere>>>,
|
mut cluster_aabb_spheres: Local<Vec<Option<Sphere>>>,
|
||||||
mut max_clusterable_objects_warning_emitted: Local<bool>,
|
mut max_clusterable_objects_warning_emitted: Local<bool>,
|
||||||
(render_device, render_adapter): (Option<Res<RenderDevice>>, Option<Res<RenderAdapter>>),
|
global_cluster_settings: Option<Res<GlobalClusterSettings>>,
|
||||||
) {
|
) {
|
||||||
let (Some(render_device), Some(render_adapter)) = (render_device, render_adapter) else {
|
let Some(global_cluster_settings) = global_cluster_settings else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -229,20 +228,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.
|
// 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
|
// 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
|
// cluster light probes on such platforms (mainly WebGL 2). Besides, those
|
||||||
// platforms typically lack bindless textures, so multiple light probes
|
// platforms typically lack bindless textures, so multiple light probes
|
||||||
// wouldn't be supported anyhow.
|
// wouldn't be supported anyhow.
|
||||||
if supports_storage_buffers {
|
if global_cluster_settings.supports_storage_buffers {
|
||||||
clusterable_objects.extend(light_probes_query.iter().map(
|
clusterable_objects.extend(light_probes_query.iter().map(
|
||||||
|(entity, transform, is_reflection_probe)| ClusterableObjectAssignmentData {
|
|(entity, transform, is_reflection_probe)| ClusterableObjectAssignmentData {
|
||||||
entity,
|
entity,
|
||||||
@ -259,7 +251,7 @@ pub(crate) fn assign_objects_to_clusters(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add decals if the current platform supports them.
|
// 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)| {
|
clusterable_objects.extend(decals_query.iter().map(|(entity, transform)| {
|
||||||
ClusterableObjectAssignmentData {
|
ClusterableObjectAssignmentData {
|
||||||
entity,
|
entity,
|
||||||
@ -272,7 +264,7 @@ pub(crate) fn assign_objects_to_clusters(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if clusterable_objects.len() > MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS
|
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| {
|
clusterable_objects.sort_by_cached_key(|clusterable_object| {
|
||||||
(
|
(
|
||||||
@ -392,7 +384,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.
|
// 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 far_z = far_z.max(first_slice_depth);
|
||||||
let cluster_factors = crate::calculate_cluster_factors(
|
let cluster_factors = calculate_cluster_factors(
|
||||||
first_slice_depth,
|
first_slice_depth,
|
||||||
far_z,
|
far_z,
|
||||||
requested_cluster_dimensions.z as f32,
|
requested_cluster_dimensions.z as f32,
|
||||||
@ -882,6 +874,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(
|
fn compute_aabb_for_cluster(
|
||||||
z_near: f32,
|
z_near: f32,
|
||||||
z_far: f32,
|
z_far: f32,
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
use core::num::NonZero;
|
use core::num::NonZero;
|
||||||
|
|
||||||
|
use bevy_asset::Handle;
|
||||||
|
use bevy_camera::visibility;
|
||||||
use bevy_core_pipeline::core_3d::Camera3d;
|
use bevy_core_pipeline::core_3d::Camera3d;
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
component::Component,
|
component::Component,
|
||||||
@ -12,23 +14,27 @@ use bevy_ecs::{
|
|||||||
system::{Commands, Query, Res},
|
system::{Commands, Query, Res},
|
||||||
world::{FromWorld, World},
|
world::{FromWorld, World},
|
||||||
};
|
};
|
||||||
|
use bevy_image::Image;
|
||||||
use bevy_math::{uvec4, AspectRatio, UVec2, UVec3, UVec4, Vec3Swizzles as _, Vec4};
|
use bevy_math::{uvec4, AspectRatio, UVec2, UVec3, UVec4, Vec3Swizzles as _, Vec4};
|
||||||
use bevy_platform::collections::HashSet;
|
use bevy_platform::collections::HashSet;
|
||||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
camera::Camera,
|
camera::Camera,
|
||||||
|
extract_component::ExtractComponent,
|
||||||
render_resource::{
|
render_resource::{
|
||||||
BindingResource, BufferBindingType, ShaderSize as _, ShaderType, StorageBuffer,
|
BindingResource, BufferBindingType, ShaderSize as _, ShaderType, StorageBuffer,
|
||||||
UniformBuffer,
|
UniformBuffer,
|
||||||
},
|
},
|
||||||
renderer::{RenderDevice, RenderQueue},
|
renderer::{RenderAdapter, RenderDevice, RenderQueue},
|
||||||
sync_world::RenderEntity,
|
sync_world::RenderEntity,
|
||||||
|
view::{Visibility, VisibilityClass},
|
||||||
Extract,
|
Extract,
|
||||||
};
|
};
|
||||||
|
use bevy_transform::components::Transform;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
pub(crate) use crate::cluster::assign::assign_objects_to_clusters;
|
pub(crate) use crate::cluster::assign::assign_objects_to_clusters;
|
||||||
use crate::MeshPipeline;
|
use crate::{LightVisibilityClass, MeshPipeline};
|
||||||
|
|
||||||
pub(crate) mod assign;
|
pub(crate) mod assign;
|
||||||
|
|
||||||
@ -63,6 +69,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:
|
// 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
|
// 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::<RenderDevice>();
|
||||||
|
let adapter = world.resource::<RenderAdapter>();
|
||||||
|
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
|
/// Configure the far z-plane mode used for the furthest depth slice for clustered forward
|
||||||
/// rendering
|
/// rendering
|
||||||
#[derive(Debug, Copy, Clone, Reflect)]
|
#[derive(Debug, Copy, Clone, Reflect)]
|
||||||
@ -209,6 +236,34 @@ struct ClusterableObjectCounts {
|
|||||||
decals: u32,
|
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::<LightVisibilityClass>)]
|
||||||
|
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<Image>,
|
||||||
|
|
||||||
|
/// 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 {
|
enum ExtractedClusterableObjectElement {
|
||||||
ClusterHeader(ClusterableObjectCounts),
|
ClusterHeader(ClusterableObjectCounts),
|
||||||
ClusterableObjectEntity(Entity),
|
ClusterableObjectEntity(Entity),
|
||||||
|
|||||||
@ -1,19 +1,12 @@
|
|||||||
|
pub use bevy_camera::visibility::{
|
||||||
|
CascadesVisibleEntities, CubemapVisibleEntities, VisibleMeshEntities,
|
||||||
|
};
|
||||||
use bevy_derive::{Deref, DerefMut};
|
use bevy_derive::{Deref, DerefMut};
|
||||||
use bevy_ecs::component::Component;
|
use bevy_ecs::component::Component;
|
||||||
use bevy_ecs::entity::{Entity, EntityHashMap};
|
use bevy_ecs::entity::{Entity, EntityHashMap};
|
||||||
use bevy_ecs::reflect::ReflectComponent;
|
use bevy_ecs::reflect::ReflectComponent;
|
||||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
||||||
use bevy_render::sync_world::MainEntity;
|
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<Entity>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)]
|
#[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)]
|
||||||
#[reflect(Component, Debug, Default, Clone)]
|
#[reflect(Component, Debug, Default, Clone)]
|
||||||
@ -22,31 +15,6 @@ pub struct RenderVisibleMeshEntities {
|
|||||||
pub entities: Vec<(Entity, MainEntity)>,
|
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<Item = &VisibleMeshEntities> {
|
|
||||||
self.data.iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut VisibleMeshEntities> {
|
|
||||||
self.data.iter_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Component, Clone, Debug, Default, Reflect)]
|
#[derive(Component, Clone, Debug, Default, Reflect)]
|
||||||
#[reflect(Component, Debug, Default, Clone)]
|
#[reflect(Component, Debug, Default, Clone)]
|
||||||
pub struct RenderCubemapVisibleEntities {
|
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<Vec<VisibleMeshEntities>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Component, Clone, Debug, Default, Reflect)]
|
#[derive(Component, Clone, Debug, Default, Reflect)]
|
||||||
#[reflect(Component, Default, Clone)]
|
#[reflect(Component, Default, Clone)]
|
||||||
pub struct RenderCascadesVisibleEntities {
|
pub struct RenderCascadesVisibleEntities {
|
||||||
|
|||||||
@ -17,12 +17,10 @@
|
|||||||
use core::{num::NonZero, ops::Deref};
|
use core::{num::NonZero, ops::Deref};
|
||||||
|
|
||||||
use bevy_app::{App, Plugin};
|
use bevy_app::{App, Plugin};
|
||||||
use bevy_asset::{AssetId, Handle};
|
use bevy_asset::AssetId;
|
||||||
use bevy_derive::{Deref, DerefMut};
|
use bevy_derive::{Deref, DerefMut};
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
component::Component,
|
|
||||||
entity::{Entity, EntityHashMap},
|
entity::{Entity, EntityHashMap},
|
||||||
prelude::ReflectComponent,
|
|
||||||
query::With,
|
query::With,
|
||||||
resource::Resource,
|
resource::Resource,
|
||||||
schedule::IntoScheduleConfigs as _,
|
schedule::IntoScheduleConfigs as _,
|
||||||
@ -31,9 +29,9 @@ use bevy_ecs::{
|
|||||||
use bevy_image::Image;
|
use bevy_image::Image;
|
||||||
use bevy_math::Mat4;
|
use bevy_math::Mat4;
|
||||||
use bevy_platform::collections::HashMap;
|
use bevy_platform::collections::HashMap;
|
||||||
use bevy_reflect::Reflect;
|
pub use bevy_render::primitives::CubemapLayout;
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
extract_component::ExtractComponentPlugin,
|
||||||
load_shader_library,
|
load_shader_library,
|
||||||
render_asset::RenderAssets,
|
render_asset::RenderAssets,
|
||||||
render_resource::{
|
render_resource::{
|
||||||
@ -43,16 +41,15 @@ use bevy_render::{
|
|||||||
renderer::{RenderAdapter, RenderDevice, RenderQueue},
|
renderer::{RenderAdapter, RenderDevice, RenderQueue},
|
||||||
sync_world::RenderEntity,
|
sync_world::RenderEntity,
|
||||||
texture::{FallbackImage, GpuImage},
|
texture::{FallbackImage, GpuImage},
|
||||||
view::{self, ViewVisibility, Visibility, VisibilityClass},
|
view::ViewVisibility,
|
||||||
Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
|
Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
|
||||||
};
|
};
|
||||||
use bevy_transform::{components::GlobalTransform, prelude::Transform};
|
use bevy_transform::components::GlobalTransform;
|
||||||
use bytemuck::{Pod, Zeroable};
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
|
||||||
use crate::{
|
pub use crate::ClusteredDecal;
|
||||||
binding_arrays_are_usable, prepare_lights, DirectionalLight, GlobalClusterableObjectMeta,
|
use crate::{binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta};
|
||||||
LightVisibilityClass, PointLight, SpotLight,
|
pub use crate::{DirectionalLightTexture, PointLightTexture, SpotLightTexture};
|
||||||
};
|
|
||||||
|
|
||||||
/// The maximum number of decals that can be present in a view.
|
/// The maximum number of decals that can be present in a view.
|
||||||
///
|
///
|
||||||
@ -67,108 +64,6 @@ pub(crate) const MAX_VIEW_DECALS: usize = 8;
|
|||||||
/// can still be added to a scene, but they won't project any decals.
|
/// can still be added to a scene, but they won't project any decals.
|
||||||
pub struct ClusteredDecalPlugin;
|
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::<LightVisibilityClass>)]
|
|
||||||
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<Image>,
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
#[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<Image>,
|
|
||||||
/// 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<Image>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<Image>,
|
|
||||||
/// 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.
|
/// Stores information about all the clustered decals in the scene.
|
||||||
#[derive(Resource, Default)]
|
#[derive(Resource, Default)]
|
||||||
pub struct RenderClusteredDecals {
|
pub struct RenderClusteredDecals {
|
||||||
|
|||||||
@ -122,6 +122,7 @@ pub mod graph {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub use crate::cascade::{CascadeShadowConfig, CascadeShadowConfigBuilder, Cascades};
|
||||||
use crate::{deferred::DeferredPbrLightingPlugin, graph::NodePbr};
|
use crate::{deferred::DeferredPbrLightingPlugin, graph::NodePbr};
|
||||||
use bevy_app::prelude::*;
|
use bevy_app::prelude::*;
|
||||||
use bevy_asset::{AssetApp, AssetPath, Assets, Handle};
|
use bevy_asset::{AssetApp, AssetPath, Assets, Handle};
|
||||||
@ -130,19 +131,16 @@ use bevy_ecs::prelude::*;
|
|||||||
use bevy_image::Image;
|
use bevy_image::Image;
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
alpha::AlphaMode,
|
alpha::AlphaMode,
|
||||||
camera::{sort_cameras, CameraUpdateSystems, Projection},
|
camera::{sort_cameras, Projection},
|
||||||
extract_component::ExtractComponentPlugin,
|
extract_component::ExtractComponentPlugin,
|
||||||
extract_resource::ExtractResourcePlugin,
|
extract_resource::ExtractResourcePlugin,
|
||||||
load_shader_library,
|
load_shader_library,
|
||||||
render_graph::RenderGraph,
|
render_graph::RenderGraph,
|
||||||
render_resource::ShaderRef,
|
render_resource::ShaderRef,
|
||||||
sync_component::SyncComponentPlugin,
|
sync_component::SyncComponentPlugin,
|
||||||
view::VisibilitySystems,
|
|
||||||
ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSystems,
|
ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSystems,
|
||||||
};
|
};
|
||||||
|
|
||||||
use bevy_transform::TransformSystems;
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
fn shader_ref(path: PathBuf) -> ShaderRef {
|
fn shader_ref(path: PathBuf) -> ShaderRef {
|
||||||
@ -205,25 +203,8 @@ impl Plugin for PbrPlugin {
|
|||||||
load_shader_library!(app, "meshlet/dummy_visibility_buffer_resolve.wgsl");
|
load_shader_library!(app, "meshlet/dummy_visibility_buffer_resolve.wgsl");
|
||||||
|
|
||||||
app.register_asset_reflect::<StandardMaterial>()
|
app.register_asset_reflect::<StandardMaterial>()
|
||||||
.register_type::<AmbientLight>()
|
|
||||||
.register_type::<CascadeShadowConfig>()
|
|
||||||
.register_type::<Cascades>()
|
|
||||||
.register_type::<CascadesVisibleEntities>()
|
|
||||||
.register_type::<VisibleMeshEntities>()
|
|
||||||
.register_type::<ClusterConfig>()
|
.register_type::<ClusterConfig>()
|
||||||
.register_type::<CubemapVisibleEntities>()
|
|
||||||
.register_type::<DirectionalLight>()
|
|
||||||
.register_type::<DirectionalLightShadowMap>()
|
|
||||||
.register_type::<NotShadowCaster>()
|
|
||||||
.register_type::<NotShadowReceiver>()
|
|
||||||
.register_type::<PointLight>()
|
|
||||||
.register_type::<PointLightShadowMap>()
|
|
||||||
.register_type::<SpotLight>()
|
|
||||||
.register_type::<ShadowFilteringMethod>()
|
|
||||||
.init_resource::<AmbientLight>()
|
|
||||||
.init_resource::<GlobalVisibleClusterableObjects>()
|
.init_resource::<GlobalVisibleClusterableObjects>()
|
||||||
.init_resource::<DirectionalLightShadowMap>()
|
|
||||||
.init_resource::<PointLightShadowMap>()
|
|
||||||
.register_type::<DefaultOpaqueRendererMethod>()
|
.register_type::<DefaultOpaqueRendererMethod>()
|
||||||
.init_resource::<DefaultOpaqueRendererMethod>()
|
.init_resource::<DefaultOpaqueRendererMethod>()
|
||||||
.add_plugins((
|
.add_plugins((
|
||||||
@ -246,7 +227,7 @@ impl Plugin for PbrPlugin {
|
|||||||
ExtractComponentPlugin::<ShadowFilteringMethod>::default(),
|
ExtractComponentPlugin::<ShadowFilteringMethod>::default(),
|
||||||
LightmapPlugin,
|
LightmapPlugin,
|
||||||
LightProbePlugin,
|
LightProbePlugin,
|
||||||
PbrProjectionPlugin,
|
LightPlugin,
|
||||||
GpuMeshPreprocessPlugin {
|
GpuMeshPreprocessPlugin {
|
||||||
use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder,
|
use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder,
|
||||||
},
|
},
|
||||||
@ -269,64 +250,6 @@ impl Plugin for PbrPlugin {
|
|||||||
SimulationLightSystems::AssignLightsToClusters,
|
SimulationLightSystems::AssignLightsToClusters,
|
||||||
)
|
)
|
||||||
.chain(),
|
.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 {
|
if self.add_default_deferred_lighting_plugin {
|
||||||
@ -399,19 +322,8 @@ impl Plugin for PbrPlugin {
|
|||||||
.init_resource::<ShadowSamplers>()
|
.init_resource::<ShadowSamplers>()
|
||||||
.init_resource::<GlobalClusterableObjectMeta>()
|
.init_resource::<GlobalClusterableObjectMeta>()
|
||||||
.init_resource::<FallbackBindlessResources>();
|
.init_resource::<FallbackBindlessResources>();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Camera projection PBR functionality.
|
let global_cluster_settings = make_global_cluster_settings(render_app.world());
|
||||||
#[derive(Default)]
|
app.insert_resource(global_cluster_settings);
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
/// 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.
|
/// 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;
|
/// ambient_light.brightness = 100.0;
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`LightPlugin`]: crate::LightPlugin
|
||||||
#[derive(Resource, Component, Clone, Debug, ExtractResource, ExtractComponent, Reflect)]
|
#[derive(Resource, Component, Clone, Debug, ExtractResource, ExtractComponent, Reflect)]
|
||||||
#[reflect(Resource, Component, Debug, Default, Clone)]
|
#[reflect(Resource, Component, Debug, Default, Clone)]
|
||||||
#[require(Camera)]
|
#[require(Camera)]
|
||||||
|
|||||||
333
crates/bevy_pbr/src/light/cascade.rs
Normal file
333
crates/bevy_pbr/src/light/cascade.rs
Normal file
@ -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<f32>,
|
||||||
|
/// 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<f32> {
|
||||||
|
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<CascadeShadowConfigBuilder> 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<Vec<Cascade>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<DirectionalLightShadowMap>,
|
||||||
|
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::<Vec<_>>();
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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.
|
/// A Directional light.
|
||||||
///
|
///
|
||||||
@ -53,7 +63,7 @@ use super::*;
|
|||||||
Visibility,
|
Visibility,
|
||||||
VisibilityClass
|
VisibilityClass
|
||||||
)]
|
)]
|
||||||
#[component(on_add = view::add_visibility_class::<LightVisibilityClass>)]
|
#[component(on_add = visibility::add_visibility_class::<LightVisibilityClass>)]
|
||||||
pub struct DirectionalLight {
|
pub struct DirectionalLight {
|
||||||
/// The color of the light.
|
/// The color of the light.
|
||||||
///
|
///
|
||||||
@ -90,6 +100,8 @@ pub struct DirectionalLight {
|
|||||||
///
|
///
|
||||||
/// Note that soft shadows are significantly more expensive to render than
|
/// Note that soft shadows are significantly more expensive to render than
|
||||||
/// hard shadows.
|
/// hard shadows.
|
||||||
|
///
|
||||||
|
/// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal
|
||||||
#[cfg(feature = "experimental_pbr_pcss")]
|
#[cfg(feature = "experimental_pbr_pcss")]
|
||||||
pub soft_shadow_size: Option<f32>,
|
pub soft_shadow_size: Option<f32>,
|
||||||
|
|
||||||
@ -141,3 +153,77 @@ impl DirectionalLight {
|
|||||||
pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02;
|
pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02;
|
||||||
pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8;
|
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<Image>,
|
||||||
|
/// 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<Camera>,
|
||||||
|
),
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
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::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,35 +1,43 @@
|
|||||||
use bevy_ecs::{
|
use bevy_app::{App, Plugin, PostUpdate};
|
||||||
entity::{EntityHashMap, EntityHashSet},
|
use bevy_camera::{
|
||||||
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,
|
|
||||||
primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere},
|
primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere},
|
||||||
view::{
|
visibility::{
|
||||||
InheritedVisibility, NoFrustumCulling, PreviousVisibleEntities, RenderLayers,
|
CascadesVisibleEntities, CubemapVisibleEntities, InheritedVisibility, NoFrustumCulling,
|
||||||
ViewVisibility, VisibilityClass, VisibilityRange, VisibleEntityRanges,
|
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 bevy_utils::Parallel;
|
||||||
use core::{marker::PhantomData, ops::DerefMut};
|
use core::ops::DerefMut;
|
||||||
|
|
||||||
use crate::*;
|
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;
|
mod ambient_light;
|
||||||
pub use ambient_light::AmbientLight;
|
pub use ambient_light::AmbientLight;
|
||||||
|
|
||||||
|
pub mod cascade;
|
||||||
mod point_light;
|
mod point_light;
|
||||||
pub use point_light::PointLight;
|
pub use point_light::{
|
||||||
|
update_point_light_frusta, PointLight, PointLightShadowMap, PointLightTexture,
|
||||||
|
};
|
||||||
mod spot_light;
|
mod spot_light;
|
||||||
pub use spot_light::SpotLight;
|
pub use spot_light::{update_spot_light_frusta, SpotLight, SpotLightTexture};
|
||||||
mod directional_light;
|
mod directional_light;
|
||||||
pub use directional_light::DirectionalLight;
|
pub use directional_light::{
|
||||||
|
update_directional_light_frusta, DirectionalLight, DirectionalLightShadowMap,
|
||||||
|
DirectionalLightTexture,
|
||||||
|
};
|
||||||
|
|
||||||
/// Constants for operating with the light units: lumens, and lux.
|
/// Constants for operating with the light units: lumens, and lux.
|
||||||
pub mod light_consts {
|
pub mod light_consts {
|
||||||
@ -90,36 +98,85 @@ pub mod light_consts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marker resource for whether shadows are enabled for this material type
|
pub struct LightPlugin;
|
||||||
#[derive(Resource, Debug)]
|
|
||||||
pub struct ShadowsEnabled<M: Material>(PhantomData<M>);
|
|
||||||
|
|
||||||
impl<M: Material> Default for ShadowsEnabled<M> {
|
impl Plugin for LightPlugin {
|
||||||
fn default() -> Self {
|
fn build(&self, app: &mut App) {
|
||||||
Self(PhantomData)
|
app.register_type::<AmbientLight>()
|
||||||
}
|
.register_type::<CascadeShadowConfig>()
|
||||||
}
|
.register_type::<Cascades>()
|
||||||
|
.register_type::<DirectionalLight>()
|
||||||
/// Controls the resolution of [`PointLight`] shadow maps.
|
.register_type::<DirectionalLightShadowMap>()
|
||||||
///
|
.register_type::<NotShadowCaster>()
|
||||||
/// ```
|
.register_type::<NotShadowReceiver>()
|
||||||
/// # use bevy_app::prelude::*;
|
.register_type::<PointLight>()
|
||||||
/// # use bevy_pbr::PointLightShadowMap;
|
.register_type::<PointLightShadowMap>()
|
||||||
/// App::new()
|
.register_type::<SpotLight>()
|
||||||
/// .insert_resource(PointLightShadowMap { size: 2048 });
|
.register_type::<ShadowFilteringMethod>()
|
||||||
/// ```
|
.init_resource::<AmbientLight>()
|
||||||
#[derive(Resource, Clone, Debug, Reflect)]
|
.init_resource::<DirectionalLightShadowMap>()
|
||||||
#[reflect(Resource, Debug, Default, Clone)]
|
.init_resource::<PointLightShadowMap>()
|
||||||
pub struct PointLightShadowMap {
|
.configure_sets(
|
||||||
/// The width and height of each of the 6 faces of the cubemap.
|
PostUpdate,
|
||||||
///
|
SimulationLightSystems::UpdateDirectionalLightCascades
|
||||||
/// Defaults to `1024`.
|
.ambiguous_with(SimulationLightSystems::UpdateDirectionalLightCascades),
|
||||||
pub size: usize,
|
)
|
||||||
}
|
.configure_sets(
|
||||||
|
PostUpdate,
|
||||||
impl Default for PointLightShadowMap {
|
SimulationLightSystems::CheckLightVisibility
|
||||||
fn default() -> Self {
|
.ambiguous_with(SimulationLightSystems::CheckLightVisibility),
|
||||||
Self { size: 1024 }
|
)
|
||||||
|
.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),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,353 +184,6 @@ impl Default for PointLightShadowMap {
|
|||||||
/// With<DirectionalLight>)>`, for use with [`bevy_render::view::VisibleEntities`].
|
/// With<DirectionalLight>)>`, for use with [`bevy_render::view::VisibleEntities`].
|
||||||
pub type WithLight = Or<(With<PointLight>, With<SpotLight>, With<DirectionalLight>)>;
|
pub type WithLight = Or<(With<PointLight>, With<SpotLight>, With<DirectionalLight>)>;
|
||||||
|
|
||||||
/// 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<f32>,
|
|
||||||
/// 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<f32> {
|
|
||||||
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<CascadeShadowConfigBuilder> 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<Vec<Cascade>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<DirectionalLightShadowMap>,
|
|
||||||
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::<Vec<_>>();
|
|
||||||
|
|
||||||
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.
|
/// Add this component to make a [`Mesh3d`] not cast shadows.
|
||||||
#[derive(Debug, Component, Reflect, Default)]
|
#[derive(Debug, Component, Reflect, Default)]
|
||||||
#[reflect(Component, Default, Debug)]
|
#[reflect(Component, Default, Debug)]
|
||||||
@ -534,6 +244,8 @@ pub enum ShadowFilteringMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The [`VisibilityClass`] used for all lights (point, directional, and spot).
|
/// The [`VisibilityClass`] used for all lights (point, directional, and spot).
|
||||||
|
///
|
||||||
|
/// [`VisibilityClass`]: bevy_camera::visibility::VisibilityClass
|
||||||
pub struct LightVisibilityClass;
|
pub struct LightVisibilityClass;
|
||||||
|
|
||||||
/// System sets used to run light-related systems.
|
/// System sets used to run light-related systems.
|
||||||
@ -552,138 +264,6 @@ pub enum SimulationLightSystems {
|
|||||||
CheckLightVisibility,
|
CheckLightVisibility,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_directional_light_frusta(
|
|
||||||
mut views: Query<
|
|
||||||
(
|
|
||||||
&Cascades,
|
|
||||||
&DirectionalLight,
|
|
||||||
&ViewVisibility,
|
|
||||||
&mut CascadesFrusta,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
// Prevents this query from conflicting with camera queries.
|
|
||||||
Without<Camera>,
|
|
||||||
),
|
|
||||||
>,
|
|
||||||
) {
|
|
||||||
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::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: Run this after assign_lights_to_clusters!
|
|
||||||
pub fn update_point_light_frusta(
|
|
||||||
global_lights: Res<GlobalVisibleClusterableObjects>,
|
|
||||||
mut views: Query<(Entity, &GlobalTransform, &PointLight, &mut CubemapFrusta)>,
|
|
||||||
changed_lights: Query<
|
|
||||||
Entity,
|
|
||||||
(
|
|
||||||
With<PointLight>,
|
|
||||||
Or<(Changed<GlobalTransform>, Changed<PointLight>)>,
|
|
||||||
),
|
|
||||||
>,
|
|
||||||
) {
|
|
||||||
let view_rotations = CUBE_MAP_FACES
|
|
||||||
.iter()
|
|
||||||
.map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
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<GlobalVisibleClusterableObjects>,
|
|
||||||
mut views: Query<
|
|
||||||
(Entity, &GlobalTransform, &SpotLight, &mut Frustum),
|
|
||||||
Or<(Changed<GlobalTransform>, Changed<SpotLight>)>,
|
|
||||||
>,
|
|
||||||
) {
|
|
||||||
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<Entity>) {
|
fn shrink_entities(visible_entities: &mut Vec<Entity>) {
|
||||||
// Check that visible entities capacity() is no more than two times greater than len()
|
// Check that visible entities capacity() is no more than two times greater than len()
|
||||||
let capacity = visible_entities.capacity();
|
let capacity = visible_entities.capacity();
|
||||||
|
|||||||
@ -1,6 +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 super::*;
|
use crate::{GlobalVisibleClusterableObjects, LightVisibilityClass};
|
||||||
|
|
||||||
/// A light that emits light in all directions from a central point.
|
/// A light that emits light in all directions from a central point.
|
||||||
///
|
///
|
||||||
@ -34,7 +44,7 @@ use super::*;
|
|||||||
Visibility,
|
Visibility,
|
||||||
VisibilityClass
|
VisibilityClass
|
||||||
)]
|
)]
|
||||||
#[component(on_add = view::add_visibility_class::<LightVisibilityClass>)]
|
#[component(on_add = visibility::add_visibility_class::<LightVisibilityClass>)]
|
||||||
pub struct PointLight {
|
pub struct PointLight {
|
||||||
/// The color of this light source.
|
/// The color of this light source.
|
||||||
pub color: Color,
|
pub color: Color,
|
||||||
@ -74,6 +84,8 @@ pub struct PointLight {
|
|||||||
///
|
///
|
||||||
/// Note that soft shadows are significantly more expensive to render than
|
/// Note that soft shadows are significantly more expensive to render than
|
||||||
/// hard shadows.
|
/// hard shadows.
|
||||||
|
///
|
||||||
|
/// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal
|
||||||
#[cfg(feature = "experimental_pbr_pcss")]
|
#[cfg(feature = "experimental_pbr_pcss")]
|
||||||
pub soft_shadows_enabled: bool,
|
pub soft_shadows_enabled: bool,
|
||||||
|
|
||||||
@ -136,3 +148,98 @@ impl PointLight {
|
|||||||
pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6;
|
pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6;
|
||||||
pub const DEFAULT_SHADOW_MAP_NEAR_Z: f32 = 0.1;
|
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<Image>,
|
||||||
|
/// 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<GlobalVisibleClusterableObjects>,
|
||||||
|
mut views: Query<(Entity, &GlobalTransform, &PointLight, &mut CubemapFrusta)>,
|
||||||
|
changed_lights: Query<
|
||||||
|
Entity,
|
||||||
|
(
|
||||||
|
With<PointLight>,
|
||||||
|
Or<(Changed<GlobalTransform>, Changed<PointLight>)>,
|
||||||
|
),
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
let view_rotations = CUBE_MAP_FACES
|
||||||
|
.iter()
|
||||||
|
.map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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.
|
/// 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)]
|
#[derive(Component, Debug, Clone, Copy, Reflect)]
|
||||||
#[reflect(Component, Default, Debug, Clone)]
|
#[reflect(Component, Default, Debug, Clone)]
|
||||||
#[require(Frustum, VisibleMeshEntities, Transform, Visibility, VisibilityClass)]
|
#[require(Frustum, VisibleMeshEntities, Transform, Visibility, VisibilityClass)]
|
||||||
#[component(on_add = view::add_visibility_class::<LightVisibilityClass>)]
|
#[component(on_add = visibility::add_visibility_class::<LightVisibilityClass>)]
|
||||||
pub struct SpotLight {
|
pub struct SpotLight {
|
||||||
/// The color of the light.
|
/// The color of the light.
|
||||||
///
|
///
|
||||||
@ -58,6 +69,8 @@ pub struct SpotLight {
|
|||||||
///
|
///
|
||||||
/// Note that soft shadows are significantly more expensive to render than
|
/// Note that soft shadows are significantly more expensive to render than
|
||||||
/// hard shadows.
|
/// hard shadows.
|
||||||
|
///
|
||||||
|
/// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal
|
||||||
#[cfg(feature = "experimental_pbr_pcss")]
|
#[cfg(feature = "experimental_pbr_pcss")]
|
||||||
pub soft_shadows_enabled: bool,
|
pub soft_shadows_enabled: bool,
|
||||||
|
|
||||||
@ -140,3 +153,82 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Image>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_spot_light_frusta(
|
||||||
|
global_lights: Res<GlobalVisibleClusterableObjects>,
|
||||||
|
mut views: Query<
|
||||||
|
(Entity, &GlobalTransform, &SpotLight, &mut Frustum),
|
||||||
|
Or<(Changed<GlobalTransform>, Changed<SpotLight>)>,
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1717,3 +1717,13 @@ pub fn write_material_bind_group_buffers(
|
|||||||
allocator.write_buffers(&render_device, &render_queue);
|
allocator.write_buffers(&render_device, &render_queue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Marker resource for whether shadows are enabled for this material type
|
||||||
|
#[derive(Resource, Debug)]
|
||||||
|
pub struct ShadowsEnabled<M: Material>(PhantomData<M>);
|
||||||
|
|
||||||
|
impl<M: Material> Default for ShadowsEnabled<M> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(PhantomData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
use self::assign::ClusterableObjectType;
|
use self::assign::ClusterableObjectType;
|
||||||
|
use crate::assign::calculate_cluster_factors;
|
||||||
|
use crate::cascade::{Cascade, CascadeShadowConfig, Cascades};
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use bevy_asset::UntypedAssetId;
|
use bevy_asset::UntypedAssetId;
|
||||||
|
pub use bevy_camera::primitives::{face_index_to_name, CubeMapFace, CUBE_MAP_FACES};
|
||||||
use bevy_color::ColorToComponents;
|
use bevy_color::ColorToComponents;
|
||||||
use bevy_core_pipeline::core_3d::{Camera3d, CORE_3D_DEPTH_FORMAT};
|
use bevy_core_pipeline::core_3d::{Camera3d, CORE_3D_DEPTH_FORMAT};
|
||||||
use bevy_derive::{Deref, DerefMut};
|
use bevy_derive::{Deref, DerefMut};
|
||||||
@ -11,7 +14,7 @@ use bevy_ecs::{
|
|||||||
prelude::*,
|
prelude::*,
|
||||||
system::lifetimeless::Read,
|
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::collections::{HashMap, HashSet};
|
||||||
use bevy_platform::hash::FixedHasher;
|
use bevy_platform::hash::FixedHasher;
|
||||||
use bevy_render::erased_render_asset::ErasedRenderAssets;
|
use bevy_render::erased_render_asset::ErasedRenderAssets;
|
||||||
@ -584,63 +587,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)]
|
#[derive(Component)]
|
||||||
pub struct ShadowView {
|
pub struct ShadowView {
|
||||||
pub depth_attachment: DepthAttachment,
|
pub depth_attachment: DepthAttachment,
|
||||||
@ -694,54 +640,6 @@ pub enum LightEntity {
|
|||||||
light_entity: Entity,
|
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(
|
pub fn prepare_lights(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
|
|||||||
@ -96,7 +96,7 @@ thiserror = { version = "2", default-features = false }
|
|||||||
derive_more = { version = "2", default-features = false, features = ["from"] }
|
derive_more = { version = "2", default-features = false, features = ["from"] }
|
||||||
serde = { version = "1", default-features = false, features = ["alloc"] }
|
serde = { version = "1", default-features = false, features = ["alloc"] }
|
||||||
assert_type_match = "0.1.1"
|
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 = [
|
glam = { version = "0.29.3", default-features = false, features = [
|
||||||
"serde",
|
"serde",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
|||||||
@ -48,20 +48,15 @@ pub(crate) trait VariantBuilder: Sized {
|
|||||||
/// * `this`: The identifier of the enum
|
/// * `this`: The identifier of the enum
|
||||||
/// * `field`: The field to access
|
/// * `field`: The field to access
|
||||||
fn access_field(&self, this: &Ident, field: VariantField) -> TokenStream {
|
fn access_field(&self, this: &Ident, field: VariantField) -> TokenStream {
|
||||||
match &field.field.data.ident {
|
if let Some(field_ident) = &field.field.data.ident {
|
||||||
Some(field_ident) => {
|
let name = field_ident.to_string();
|
||||||
let name = field_ident.to_string();
|
quote!(#this.field(#name))
|
||||||
quote!(#this.field(#name))
|
} else if let Some(field_index) = field.field.reflection_index {
|
||||||
}
|
quote!(#this.field_at(#field_index))
|
||||||
None => {
|
} else {
|
||||||
if let Some(field_index) = field.field.reflection_index {
|
quote!(::core::compile_error!(
|
||||||
quote!(#this.field_at(#field_index))
|
"internal bevy_reflect error: field should be active"
|
||||||
} else {
|
))
|
||||||
quote!(::core::compile_error!(
|
|
||||||
"internal bevy_reflect error: field should be active"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -114,7 +114,7 @@ profiling = { version = "1", features = [
|
|||||||
], optional = true }
|
], optional = true }
|
||||||
async-channel = "2.3.0"
|
async-channel = "2.3.0"
|
||||||
nonmax = "0.5"
|
nonmax = "0.5"
|
||||||
smallvec = { version = "1.11", features = ["const_new"] }
|
smallvec = { version = "1", default-features = false, features = ["const_new"] }
|
||||||
offset-allocator = "0.2"
|
offset-allocator = "0.2"
|
||||||
variadics_please = "1.1"
|
variadics_please = "1.1"
|
||||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
|
|||||||
@ -29,7 +29,7 @@ use bevy_ecs::{
|
|||||||
event::EventReader,
|
event::EventReader,
|
||||||
lifecycle::HookContext,
|
lifecycle::HookContext,
|
||||||
prelude::With,
|
prelude::With,
|
||||||
query::Has,
|
query::{Has, QueryItem},
|
||||||
reflect::ReflectComponent,
|
reflect::ReflectComponent,
|
||||||
resource::Resource,
|
resource::Resource,
|
||||||
schedule::IntoScheduleConfigs,
|
schedule::IntoScheduleConfigs,
|
||||||
@ -59,6 +59,8 @@ impl Plugin for CameraPlugin {
|
|||||||
.register_type::<MipBias>()
|
.register_type::<MipBias>()
|
||||||
.register_required_components::<Camera, Msaa>()
|
.register_required_components::<Camera, Msaa>()
|
||||||
.register_required_components::<Camera, SyncToRenderWorld>()
|
.register_required_components::<Camera, SyncToRenderWorld>()
|
||||||
|
.register_required_components::<Camera3d, ColorGrading>()
|
||||||
|
.register_required_components::<Camera3d, Exposure>()
|
||||||
.add_plugins((
|
.add_plugins((
|
||||||
ExtractResourcePlugin::<ClearColor>::default(),
|
ExtractResourcePlugin::<ClearColor>::default(),
|
||||||
ExtractComponentPlugin::<CameraMainTextureUsages>::default(),
|
ExtractComponentPlugin::<CameraMainTextureUsages>::default(),
|
||||||
@ -95,7 +97,7 @@ fn warn_on_no_render_graph(world: DeferredWorld, HookContext { entity, caller, .
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ExtractResource for ClearColor {
|
impl ExtractResource for ClearColor {
|
||||||
type Source = ClearColor;
|
type Source = Self;
|
||||||
|
|
||||||
fn extract_resource(source: &Self::Source) -> Self {
|
fn extract_resource(source: &Self::Source) -> Self {
|
||||||
source.clone()
|
source.clone()
|
||||||
@ -106,12 +108,28 @@ impl ExtractComponent for CameraMainTextureUsages {
|
|||||||
type QueryFilter = ();
|
type QueryFilter = ();
|
||||||
type Out = Self;
|
type Out = Self;
|
||||||
|
|
||||||
fn extract_component(
|
fn extract_component(item: QueryItem<Self::QueryData>) -> Option<Self::Out> {
|
||||||
item: bevy_ecs::query::QueryItem<'_, '_, Self::QueryData>,
|
|
||||||
) -> Option<Self::Out> {
|
|
||||||
Some(*item)
|
Some(*item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl ExtractComponent for Camera2d {
|
||||||
|
type QueryData = &'static Self;
|
||||||
|
type QueryFilter = With<Camera>;
|
||||||
|
type Out = Self;
|
||||||
|
|
||||||
|
fn extract_component(item: QueryItem<Self::QueryData>) -> Option<Self::Out> {
|
||||||
|
Some(item.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ExtractComponent for Camera3d {
|
||||||
|
type QueryData = &'static Self;
|
||||||
|
type QueryFilter = With<Camera>;
|
||||||
|
type Out = Self;
|
||||||
|
|
||||||
|
fn extract_component(item: QueryItem<Self::QueryData>) -> Option<Self::Out> {
|
||||||
|
Some(item.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Configures the [`RenderGraph`] name assigned to be run for a given [`Camera`] entity.
|
/// Configures the [`RenderGraph`] name assigned to be run for a given [`Camera`] entity.
|
||||||
#[derive(Component, Debug, Deref, DerefMut, Reflect, Clone)]
|
#[derive(Component, Debug, Deref, DerefMut, Reflect, Clone)]
|
||||||
|
|||||||
@ -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"] }
|
cosmic-text = { version = "0.14", features = ["shape-run-cache"] }
|
||||||
thiserror = { version = "2", default-features = false }
|
thiserror = { version = "2", default-features = false }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
smallvec = "1.13"
|
smallvec = { version = "1", default-features = false }
|
||||||
unicode-bidi = "0.3.13"
|
unicode-bidi = "0.3.13"
|
||||||
sys-locale = "0.3.0"
|
sys-locale = "0.3.0"
|
||||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
|
|||||||
@ -40,7 +40,7 @@ bytemuck = { version = "1.5", features = ["derive"] }
|
|||||||
thiserror = { version = "2", default-features = false }
|
thiserror = { version = "2", default-features = false }
|
||||||
derive_more = { version = "2", default-features = false, features = ["from"] }
|
derive_more = { version = "2", default-features = false, features = ["from"] }
|
||||||
nonmax = "0.5"
|
nonmax = "0.5"
|
||||||
smallvec = "1.11"
|
smallvec = { version = "1", default-features = false }
|
||||||
accesskit = "0.19"
|
accesskit = "0.19"
|
||||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,7 @@ bytemuck = { version = "1.5", features = ["derive"] }
|
|||||||
thiserror = { version = "2", default-features = false }
|
thiserror = { version = "2", default-features = false }
|
||||||
derive_more = { version = "1", default-features = false, features = ["from"] }
|
derive_more = { version = "1", default-features = false, features = ["from"] }
|
||||||
nonmax = "0.5"
|
nonmax = "0.5"
|
||||||
smallvec = "1.11"
|
smallvec = { version = "1", default-features = false }
|
||||||
accesskit = "0.18"
|
accesskit = "0.18"
|
||||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user