# Objective Closes #19564. The current `Event` trait looks like this: ```rust pub trait Event: Send + Sync + 'static { type Traversal: Traversal<Self>; const AUTO_PROPAGATE: bool = false; fn register_component_id(world: &mut World) -> ComponentId { ... } fn component_id(world: &World) -> Option<ComponentId> { ... } } ``` The `Event` trait is used by both buffered events (`EventReader`/`EventWriter`) and observer events. If they are observer events, they can optionally be targeted at specific `Entity`s or `ComponentId`s, and can even be propagated to other entities. However, there has long been a desire to split the trait semantically for a variety of reasons, see #14843, #14272, and #16031 for discussion. Some reasons include: - It's very uncommon to use a single event type as both a buffered event and targeted observer event. They are used differently and tend to have distinct semantics. - A common footgun is using buffered events with observers or event readers with observer events, as there is no type-level error that prevents this kind of misuse. - #19440 made `Trigger::target` return an `Option<Entity>`. This *seriously* hurts ergonomics for the general case of entity observers, as you need to `.unwrap()` each time. If we could statically determine whether the event is expected to have an entity target, this would be unnecessary. There's really two main ways that we can categorize events: push vs. pull (i.e. "observer event" vs. "buffered event") and global vs. targeted: | | Push | Pull | | ------------ | --------------- | --------------------------- | | **Global** | Global observer | `EventReader`/`EventWriter` | | **Targeted** | Entity observer | - | There are many ways to approach this, each with their tradeoffs. Ultimately, we kind of want to split events both ways: - A type-level distinction between observer events and buffered events, to prevent people from using the wrong kind of event in APIs - A statically designated entity target for observer events to avoid accidentally using untargeted events for targeted APIs This PR achieves these goals by splitting event traits into `Event`, `EntityEvent`, and `BufferedEvent`, with `Event` being the shared trait implemented by all events. ## `Event`, `EntityEvent`, and `BufferedEvent` `Event` is now a very simple trait shared by all events. ```rust pub trait Event: Send + Sync + 'static { // Required for observer APIs fn register_component_id(world: &mut World) -> ComponentId { ... } fn component_id(world: &World) -> Option<ComponentId> { ... } } ``` You can call `trigger` for *any* event, and use a global observer for listening to the event. ```rust #[derive(Event)] struct Speak { message: String, } // ... app.add_observer(|trigger: On<Speak>| { println!("{}", trigger.message); }); // ... commands.trigger(Speak { message: "Y'all like these reworked events?".to_string(), }); ``` To allow an event to be targeted at entities and even propagated further, you can additionally implement the `EntityEvent` trait: ```rust pub trait EntityEvent: Event { type Traversal: Traversal<Self>; const AUTO_PROPAGATE: bool = false; } ``` This lets you call `trigger_targets`, and to use targeted observer APIs like `EntityCommands::observe`: ```rust #[derive(Event, EntityEvent)] #[entity_event(traversal = &'static ChildOf, auto_propagate)] struct Damage { amount: f32, } // ... let enemy = commands.spawn((Enemy, Health(100.0))).id(); // Spawn some armor as a child of the enemy entity. // When the armor takes damage, it will bubble the event up to the enemy. let armor_piece = commands .spawn((ArmorPiece, Health(25.0), ChildOf(enemy))) .observe(|trigger: On<Damage>, mut query: Query<&mut Health>| { // Note: `On::target` only exists because this is an `EntityEvent`. let mut health = query.get(trigger.target()).unwrap(); health.0 -= trigger.amount(); }); commands.trigger_targets(Damage { amount: 10.0 }, armor_piece); ``` > [!NOTE] > You *can* still also trigger an `EntityEvent` without targets using `trigger`. We probably *could* make this an either-or thing, but I'm not sure that's actually desirable. To allow an event to be used with the buffered API, you can implement `BufferedEvent`: ```rust pub trait BufferedEvent: Event {} ``` The event can then be used with `EventReader`/`EventWriter`: ```rust #[derive(Event, BufferedEvent)] struct Message(String); fn write_hello(mut writer: EventWriter<Message>) { writer.write(Message("I hope these examples are alright".to_string())); } fn read_messages(mut reader: EventReader<Message>) { // Process all buffered events of type `Message`. for Message(message) in reader.read() { println!("{message}"); } } ``` In summary: - Need a basic event you can trigger and observe? Derive `Event`! - Need the event to be targeted at an entity? Derive `EntityEvent`! - Need the event to be buffered and support the `EventReader`/`EventWriter` API? Derive `BufferedEvent`! ## Alternatives I'll now cover some of the alternative approaches I have considered and briefly explored. I made this section collapsible since it ended up being quite long :P <details> <summary>Expand this to see alternatives</summary> ### 1. Unified `Event` Trait One option is not to have *three* separate traits (`Event`, `EntityEvent`, `BufferedEvent`), and to instead just use associated constants on `Event` to determine whether an event supports targeting and buffering or not: ```rust pub trait Event: Send + Sync + 'static { type Traversal: Traversal<Self>; const AUTO_PROPAGATE: bool = false; const TARGETED: bool = false; const BUFFERED: bool = false; fn register_component_id(world: &mut World) -> ComponentId { ... } fn component_id(world: &World) -> Option<ComponentId> { ... } } ``` Methods can then use bounds like `where E: Event<TARGETED = true>` or `where E: Event<BUFFERED = true>` to limit APIs to specific kinds of events. This would keep everything under one `Event` trait, but I don't think it's necessarily a good idea. It makes APIs harder to read, and docs can't easily refer to specific types of events. You can also create weird invariants: what if you specify `TARGETED = false`, but have `Traversal` and/or `AUTO_PROPAGATE` enabled? ### 2. `Event` and `Trigger` Another option is to only split the traits between buffered events and observer events, since that is the main thing people have been asking for, and they have the largest API difference. If we did this, I think we would need to make the terms *clearly* separate. We can't really use `Event` and `BufferedEvent` as the names, since it would be strange that `BufferedEvent` doesn't implement `Event`. Something like `ObserverEvent` and `BufferedEvent` could work, but it'd be more verbose. For this approach, I would instead keep `Event` for the current `EventReader`/`EventWriter` API, and call the observer event a `Trigger`, since the "trigger" terminology is already used in the observer context within Bevy (both as a noun and a verb). This is also what a long [bikeshed on Discord](https://discord.com/channels/691052431525675048/749335865876021248/1298057661878898791) seemed to land on at the end of last year. ```rust // For `EventReader`/`EventWriter` pub trait Event: Send + Sync + 'static {} // For observers pub trait Trigger: Send + Sync + 'static { type Traversal: Traversal<Self>; const AUTO_PROPAGATE: bool = false; const TARGETED: bool = false; fn register_component_id(world: &mut World) -> ComponentId { ... } fn component_id(world: &World) -> Option<ComponentId> { ... } } ``` The problem is that "event" is just a really good term for something that "happens". Observers are rapidly becoming the more prominent API, so it'd be weird to give them the `Trigger` name and leave the good `Event` name for the less common API. So, even though a split like this seems neat on the surface, I think it ultimately wouldn't really work. We want to keep the `Event` name for observer events, and there is no good alternative for the buffered variant. (`Message` was suggested, but saying stuff like "sends a collision message" is weird.) ### 3. `GlobalEvent` + `TargetedEvent` What if instead of focusing on the buffered vs. observed split, we *only* make a distinction between global and targeted events? ```rust // A shared event trait to allow global observers to work pub trait Event: Send + Sync + 'static { fn register_component_id(world: &mut World) -> ComponentId { ... } fn component_id(world: &World) -> Option<ComponentId> { ... } } // For buffered events and non-targeted observer events pub trait GlobalEvent: Event {} // For targeted observer events pub trait TargetedEvent: Event { type Traversal: Traversal<Self>; const AUTO_PROPAGATE: bool = false; } ``` This is actually the first approach I implemented, and it has the neat characteristic that you can only use non-targeted APIs like `trigger` with a `GlobalEvent` and targeted APIs like `trigger_targets` with a `TargetedEvent`. You have full control over whether the entity should or should not have a target, as they are fully distinct at the type-level. However, there's a few problems: - There is no type-level indication of whether a `GlobalEvent` supports buffered events or just non-targeted observer events - An `Event` on its own does literally nothing, it's just a shared trait required to make global observers accept both non-targeted and targeted events - If an event is both a `GlobalEvent` and `TargetedEvent`, global observers again have ambiguity on whether an event has a target or not, undermining some of the benefits - The names are not ideal ### 4. `Event` and `EntityEvent` We can fix some of the problems of Alternative 3 by accepting that targeted events can also be used in non-targeted contexts, and simply having the `Event` and `EntityEvent` traits: ```rust // For buffered events and non-targeted observer events pub trait Event: Send + Sync + 'static { fn register_component_id(world: &mut World) -> ComponentId { ... } fn component_id(world: &World) -> Option<ComponentId> { ... } } // For targeted observer events pub trait EntityEvent: Event { type Traversal: Traversal<Self>; const AUTO_PROPAGATE: bool = false; } ``` This is essentially identical to this PR, just without a dedicated `BufferedEvent`. The remaining major "problem" is that there is still zero type-level indication of whether an `Event` event *actually* supports the buffered API. This leads us to the solution proposed in this PR, using `Event`, `EntityEvent`, and `BufferedEvent`. </details> ## Conclusion The `Event` + `EntityEvent` + `BufferedEvent` split proposed in this PR aims to solve all the common problems with Bevy's current event model while keeping the "weirdness" factor minimal. It splits in terms of both the push vs. pull *and* global vs. targeted aspects, while maintaining a shared concept for an "event". ### Why I Like This - The term "event" remains as a single concept for all the different kinds of events in Bevy. - Despite all event types being "events", they use fundamentally different APIs. Instead of assuming that you can use an event type with any pattern (when only one is typically supported), you explicitly opt in to each one with dedicated traits. - Using separate traits for each type of event helps with documentation and clearer function signatures. - I can safely make assumptions on expected usage. - If I see that an event is an `EntityEvent`, I can assume that I can use `observe` on it and get targeted events. - If I see that an event is a `BufferedEvent`, I can assume that I can use `EventReader` to read events. - If I see both `EntityEvent` and `BufferedEvent`, I can assume that both APIs are supported. In summary: This allows for a unified concept for events, while limiting the different ways to use them with opt-in traits. No more guess-work involved when using APIs. ### Problems? - Because `BufferedEvent` implements `Event` (for more consistent semantics etc.), you can still use all buffered events for non-targeted observers. I think this is fine/good. The important part is that if you see that an event implements `BufferedEvent`, you know that the `EventReader`/`EventWriter` API should be supported. Whether it *also* supports other APIs is secondary. - I currently only support `trigger_targets` for an `EntityEvent`. However, you can technically target components too, without targeting any entities. I consider that such a niche and advanced use case that it's not a huge problem to only support it for `EntityEvent`s, but we could also split `trigger_targets` into `trigger_entities` and `trigger_components` if we wanted to (or implement components as entities :P). - You can still trigger an `EntityEvent` *without* targets. I consider this correct, since `Event` implements the non-targeted behavior, and it'd be weird if implementing another trait *removed* behavior. However, it does mean that global observers for entity events can technically return `Entity::PLACEHOLDER` again (since I got rid of the `Option<Entity>` added in #19440 for ergonomics). I think that's enough of an edge case that it's not a huge problem, but it is worth keeping in mind. - ~~Deriving both `EntityEvent` and `BufferedEvent` for the same type currently duplicates the `Event` implementation, so you instead need to manually implement one of them.~~ Changed to always requiring `Event` to be derived. ## Related Work There are plans to implement multi-event support for observers, especially for UI contexts. [Cart's example](https://github.com/bevyengine/bevy/issues/14649#issuecomment-2960402508) API looked like this: ```rust // Truncated for brevity trigger: Trigger<( OnAdd<Pressed>, OnRemove<Pressed>, OnAdd<InteractionDisabled>, OnRemove<InteractionDisabled>, OnInsert<Hovered>, )>, ``` I believe this shouldn't be in conflict with this PR. If anything, this PR might *help* achieve the multi-event pattern for entity observers with fewer footguns: by statically enforcing that all of these events are `EntityEvent`s in the context of `EntityCommands::observe`, we can avoid misuse or weird cases where *some* events inside the trigger are targeted while others are not.
2387 lines
90 KiB
Rust
2387 lines
90 KiB
Rust
use self::assign::ClusterableObjectType;
|
|
use crate::material_bind_groups::MaterialBindGroupAllocator;
|
|
use crate::*;
|
|
use bevy_asset::UntypedAssetId;
|
|
use bevy_color::ColorToComponents;
|
|
use bevy_core_pipeline::core_3d::{Camera3d, CORE_3D_DEPTH_FORMAT};
|
|
use bevy_derive::{Deref, DerefMut};
|
|
use bevy_ecs::component::Tick;
|
|
use bevy_ecs::system::SystemChangeTick;
|
|
use bevy_ecs::{
|
|
entity::{EntityHashMap, EntityHashSet},
|
|
prelude::*,
|
|
system::lifetimeless::Read,
|
|
};
|
|
use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};
|
|
use bevy_platform::collections::{HashMap, HashSet};
|
|
use bevy_platform::hash::FixedHasher;
|
|
use bevy_render::experimental::occlusion_culling::{
|
|
OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities,
|
|
};
|
|
use bevy_render::sync_world::MainEntityHashMap;
|
|
use bevy_render::{
|
|
batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport},
|
|
camera::SortedCameras,
|
|
mesh::allocator::MeshAllocator,
|
|
view::{NoIndirectDrawing, RetainedViewEntity},
|
|
};
|
|
use bevy_render::{
|
|
diagnostic::RecordDiagnostics,
|
|
mesh::RenderMesh,
|
|
primitives::{CascadesFrusta, CubemapFrusta, Frustum, HalfSpace},
|
|
render_asset::RenderAssets,
|
|
render_graph::{Node, NodeRunError, RenderGraphContext},
|
|
render_phase::*,
|
|
render_resource::*,
|
|
renderer::{RenderContext, RenderDevice, RenderQueue},
|
|
texture::*,
|
|
view::{ExtractedView, RenderLayers, ViewVisibility},
|
|
Extract,
|
|
};
|
|
use bevy_render::{
|
|
mesh::allocator::SlabId,
|
|
sync_world::{MainEntity, RenderEntity},
|
|
};
|
|
use bevy_transform::{components::GlobalTransform, prelude::Transform};
|
|
use bevy_utils::default;
|
|
use core::{hash::Hash, marker::PhantomData, ops::Range};
|
|
#[cfg(feature = "trace")]
|
|
use tracing::info_span;
|
|
use tracing::{error, warn};
|
|
|
|
#[derive(Component)]
|
|
pub struct ExtractedPointLight {
|
|
pub color: LinearRgba,
|
|
/// luminous intensity in lumens per steradian
|
|
pub intensity: f32,
|
|
pub range: f32,
|
|
pub radius: f32,
|
|
pub transform: GlobalTransform,
|
|
pub shadows_enabled: bool,
|
|
pub shadow_depth_bias: f32,
|
|
pub shadow_normal_bias: f32,
|
|
pub shadow_map_near_z: f32,
|
|
pub spot_light_angles: Option<(f32, f32)>,
|
|
pub volumetric: bool,
|
|
pub soft_shadows_enabled: bool,
|
|
/// whether this point light contributes diffuse light to lightmapped meshes
|
|
pub affects_lightmapped_mesh_diffuse: bool,
|
|
}
|
|
|
|
#[derive(Component, Debug)]
|
|
pub struct ExtractedDirectionalLight {
|
|
pub color: LinearRgba,
|
|
pub illuminance: f32,
|
|
pub transform: GlobalTransform,
|
|
pub shadows_enabled: bool,
|
|
pub volumetric: bool,
|
|
/// whether this directional light contributes diffuse light to lightmapped
|
|
/// meshes
|
|
pub affects_lightmapped_mesh_diffuse: bool,
|
|
pub shadow_depth_bias: f32,
|
|
pub shadow_normal_bias: f32,
|
|
pub cascade_shadow_config: CascadeShadowConfig,
|
|
pub cascades: EntityHashMap<Vec<Cascade>>,
|
|
pub frusta: EntityHashMap<Vec<Frustum>>,
|
|
pub render_layers: RenderLayers,
|
|
pub soft_shadow_size: Option<f32>,
|
|
/// True if this light is using two-phase occlusion culling.
|
|
pub occlusion_culling: bool,
|
|
}
|
|
|
|
// NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl!
|
|
bitflags::bitflags! {
|
|
#[repr(transparent)]
|
|
struct PointLightFlags: u32 {
|
|
const SHADOWS_ENABLED = 1 << 0;
|
|
const SPOT_LIGHT_Y_NEGATIVE = 1 << 1;
|
|
const VOLUMETRIC = 1 << 2;
|
|
const AFFECTS_LIGHTMAPPED_MESH_DIFFUSE = 1 << 3;
|
|
const NONE = 0;
|
|
const UNINITIALIZED = 0xFFFF;
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, ShaderType, Default, Debug)]
|
|
pub struct GpuDirectionalCascade {
|
|
clip_from_world: Mat4,
|
|
texel_size: f32,
|
|
far_bound: f32,
|
|
}
|
|
|
|
#[derive(Copy, Clone, ShaderType, Default, Debug)]
|
|
pub struct GpuDirectionalLight {
|
|
cascades: [GpuDirectionalCascade; MAX_CASCADES_PER_LIGHT],
|
|
color: Vec4,
|
|
dir_to_light: Vec3,
|
|
flags: u32,
|
|
soft_shadow_size: f32,
|
|
shadow_depth_bias: f32,
|
|
shadow_normal_bias: f32,
|
|
num_cascades: u32,
|
|
cascades_overlap_proportion: f32,
|
|
depth_texture_base_index: u32,
|
|
}
|
|
|
|
// NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl!
|
|
bitflags::bitflags! {
|
|
#[repr(transparent)]
|
|
struct DirectionalLightFlags: u32 {
|
|
const SHADOWS_ENABLED = 1 << 0;
|
|
const VOLUMETRIC = 1 << 1;
|
|
const AFFECTS_LIGHTMAPPED_MESH_DIFFUSE = 1 << 2;
|
|
const NONE = 0;
|
|
const UNINITIALIZED = 0xFFFF;
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, ShaderType)]
|
|
pub struct GpuLights {
|
|
directional_lights: [GpuDirectionalLight; MAX_DIRECTIONAL_LIGHTS],
|
|
ambient_color: Vec4,
|
|
// xyz are x/y/z cluster dimensions and w is the number of clusters
|
|
cluster_dimensions: UVec4,
|
|
// xy are vec2<f32>(cluster_dimensions.xy) / vec2<f32>(view.width, view.height)
|
|
// z is cluster_dimensions.z / log(far / near)
|
|
// w is cluster_dimensions.z * log(near) / log(far / near)
|
|
cluster_factors: Vec4,
|
|
n_directional_lights: u32,
|
|
// offset from spot light's light index to spot light's shadow map index
|
|
spot_light_shadowmap_offset: i32,
|
|
ambient_light_affects_lightmapped_meshes: u32,
|
|
}
|
|
|
|
// NOTE: When running bevy on Adreno GPU chipsets in WebGL, any value above 1 will result in a crash
|
|
// when loading the wgsl "pbr_functions.wgsl" in the function apply_fog.
|
|
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
|
|
pub const MAX_DIRECTIONAL_LIGHTS: usize = 1;
|
|
#[cfg(any(
|
|
not(feature = "webgl"),
|
|
not(target_arch = "wasm32"),
|
|
feature = "webgpu"
|
|
))]
|
|
pub const MAX_DIRECTIONAL_LIGHTS: usize = 10;
|
|
#[cfg(any(
|
|
not(feature = "webgl"),
|
|
not(target_arch = "wasm32"),
|
|
feature = "webgpu"
|
|
))]
|
|
pub const MAX_CASCADES_PER_LIGHT: usize = 4;
|
|
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
|
|
pub const MAX_CASCADES_PER_LIGHT: usize = 1;
|
|
|
|
#[derive(Resource, Clone)]
|
|
pub struct ShadowSamplers {
|
|
pub point_light_comparison_sampler: Sampler,
|
|
#[cfg(feature = "experimental_pbr_pcss")]
|
|
pub point_light_linear_sampler: Sampler,
|
|
pub directional_light_comparison_sampler: Sampler,
|
|
#[cfg(feature = "experimental_pbr_pcss")]
|
|
pub directional_light_linear_sampler: Sampler,
|
|
}
|
|
|
|
// TODO: this pattern for initializing the shaders / pipeline isn't ideal. this should be handled by the asset system
|
|
impl FromWorld for ShadowSamplers {
|
|
fn from_world(world: &mut World) -> Self {
|
|
let render_device = world.resource::<RenderDevice>();
|
|
|
|
let base_sampler_descriptor = SamplerDescriptor {
|
|
address_mode_u: AddressMode::ClampToEdge,
|
|
address_mode_v: AddressMode::ClampToEdge,
|
|
address_mode_w: AddressMode::ClampToEdge,
|
|
mag_filter: FilterMode::Linear,
|
|
min_filter: FilterMode::Linear,
|
|
mipmap_filter: FilterMode::Nearest,
|
|
..default()
|
|
};
|
|
|
|
ShadowSamplers {
|
|
point_light_comparison_sampler: render_device.create_sampler(&SamplerDescriptor {
|
|
compare: Some(CompareFunction::GreaterEqual),
|
|
..base_sampler_descriptor
|
|
}),
|
|
#[cfg(feature = "experimental_pbr_pcss")]
|
|
point_light_linear_sampler: render_device.create_sampler(&base_sampler_descriptor),
|
|
directional_light_comparison_sampler: render_device.create_sampler(
|
|
&SamplerDescriptor {
|
|
compare: Some(CompareFunction::GreaterEqual),
|
|
..base_sampler_descriptor
|
|
},
|
|
),
|
|
#[cfg(feature = "experimental_pbr_pcss")]
|
|
directional_light_linear_sampler: render_device
|
|
.create_sampler(&base_sampler_descriptor),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn extract_lights(
|
|
mut commands: Commands,
|
|
point_light_shadow_map: Extract<Res<PointLightShadowMap>>,
|
|
directional_light_shadow_map: Extract<Res<DirectionalLightShadowMap>>,
|
|
global_visible_clusterable: Extract<Res<GlobalVisibleClusterableObjects>>,
|
|
previous_point_lights: Query<
|
|
Entity,
|
|
(
|
|
With<RenderCubemapVisibleEntities>,
|
|
With<ExtractedPointLight>,
|
|
),
|
|
>,
|
|
previous_spot_lights: Query<
|
|
Entity,
|
|
(With<RenderVisibleMeshEntities>, With<ExtractedPointLight>),
|
|
>,
|
|
point_lights: Extract<
|
|
Query<(
|
|
Entity,
|
|
RenderEntity,
|
|
&PointLight,
|
|
&CubemapVisibleEntities,
|
|
&GlobalTransform,
|
|
&ViewVisibility,
|
|
&CubemapFrusta,
|
|
Option<&VolumetricLight>,
|
|
)>,
|
|
>,
|
|
spot_lights: Extract<
|
|
Query<(
|
|
Entity,
|
|
RenderEntity,
|
|
&SpotLight,
|
|
&VisibleMeshEntities,
|
|
&GlobalTransform,
|
|
&ViewVisibility,
|
|
&Frustum,
|
|
Option<&VolumetricLight>,
|
|
)>,
|
|
>,
|
|
directional_lights: Extract<
|
|
Query<
|
|
(
|
|
Entity,
|
|
RenderEntity,
|
|
&DirectionalLight,
|
|
&CascadesVisibleEntities,
|
|
&Cascades,
|
|
&CascadeShadowConfig,
|
|
&CascadesFrusta,
|
|
&GlobalTransform,
|
|
&ViewVisibility,
|
|
Option<&RenderLayers>,
|
|
Option<&VolumetricLight>,
|
|
Has<OcclusionCulling>,
|
|
),
|
|
Without<SpotLight>,
|
|
>,
|
|
>,
|
|
mapper: Extract<Query<RenderEntity>>,
|
|
mut previous_point_lights_len: Local<usize>,
|
|
mut previous_spot_lights_len: Local<usize>,
|
|
) {
|
|
// NOTE: These shadow map resources are extracted here as they are used here too so this avoids
|
|
// races between scheduling of ExtractResourceSystems and this system.
|
|
if point_light_shadow_map.is_changed() {
|
|
commands.insert_resource(point_light_shadow_map.clone());
|
|
}
|
|
if directional_light_shadow_map.is_changed() {
|
|
commands.insert_resource(directional_light_shadow_map.clone());
|
|
}
|
|
|
|
// Clear previous visible entities for all point/spot lights as they might not be in the
|
|
// `global_visible_clusterable` list anymore.
|
|
commands.try_insert_batch(
|
|
previous_point_lights
|
|
.iter()
|
|
.map(|render_entity| (render_entity, RenderCubemapVisibleEntities::default()))
|
|
.collect::<Vec<_>>(),
|
|
);
|
|
commands.try_insert_batch(
|
|
previous_spot_lights
|
|
.iter()
|
|
.map(|render_entity| (render_entity, RenderVisibleMeshEntities::default()))
|
|
.collect::<Vec<_>>(),
|
|
);
|
|
|
|
// This is the point light shadow map texel size for one face of the cube as a distance of 1.0
|
|
// world unit from the light.
|
|
// point_light_texel_size = 2.0 * 1.0 * tan(PI / 4.0) / cube face width in texels
|
|
// PI / 4.0 is half the cube face fov, tan(PI / 4.0) = 1.0, so this simplifies to:
|
|
// point_light_texel_size = 2.0 / cube face width in texels
|
|
// NOTE: When using various PCF kernel sizes, this will need to be adjusted, according to:
|
|
// https://catlikecoding.com/unity/tutorials/custom-srp/point-and-spot-shadows/
|
|
let point_light_texel_size = 2.0 / point_light_shadow_map.size as f32;
|
|
|
|
let mut point_lights_values = Vec::with_capacity(*previous_point_lights_len);
|
|
for entity in global_visible_clusterable.iter().copied() {
|
|
let Ok((
|
|
main_entity,
|
|
render_entity,
|
|
point_light,
|
|
cubemap_visible_entities,
|
|
transform,
|
|
view_visibility,
|
|
frusta,
|
|
volumetric_light,
|
|
)) = point_lights.get(entity)
|
|
else {
|
|
continue;
|
|
};
|
|
if !view_visibility.get() {
|
|
continue;
|
|
}
|
|
let render_cubemap_visible_entities = RenderCubemapVisibleEntities {
|
|
data: cubemap_visible_entities
|
|
.iter()
|
|
.map(|v| create_render_visible_mesh_entities(&mapper, v))
|
|
.collect::<Vec<_>>()
|
|
.try_into()
|
|
.unwrap(),
|
|
};
|
|
|
|
let extracted_point_light = ExtractedPointLight {
|
|
color: point_light.color.into(),
|
|
// NOTE: Map from luminous power in lumens to luminous intensity in lumens per steradian
|
|
// for a point light. See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower
|
|
// for details.
|
|
intensity: point_light.intensity / (4.0 * core::f32::consts::PI),
|
|
range: point_light.range,
|
|
radius: point_light.radius,
|
|
transform: *transform,
|
|
shadows_enabled: point_light.shadows_enabled,
|
|
shadow_depth_bias: point_light.shadow_depth_bias,
|
|
// The factor of SQRT_2 is for the worst-case diagonal offset
|
|
shadow_normal_bias: point_light.shadow_normal_bias
|
|
* point_light_texel_size
|
|
* core::f32::consts::SQRT_2,
|
|
shadow_map_near_z: point_light.shadow_map_near_z,
|
|
spot_light_angles: None,
|
|
volumetric: volumetric_light.is_some(),
|
|
affects_lightmapped_mesh_diffuse: point_light.affects_lightmapped_mesh_diffuse,
|
|
#[cfg(feature = "experimental_pbr_pcss")]
|
|
soft_shadows_enabled: point_light.soft_shadows_enabled,
|
|
#[cfg(not(feature = "experimental_pbr_pcss"))]
|
|
soft_shadows_enabled: false,
|
|
};
|
|
point_lights_values.push((
|
|
render_entity,
|
|
(
|
|
extracted_point_light,
|
|
render_cubemap_visible_entities,
|
|
(*frusta).clone(),
|
|
MainEntity::from(main_entity),
|
|
),
|
|
));
|
|
}
|
|
*previous_point_lights_len = point_lights_values.len();
|
|
commands.try_insert_batch(point_lights_values);
|
|
|
|
let mut spot_lights_values = Vec::with_capacity(*previous_spot_lights_len);
|
|
for entity in global_visible_clusterable.iter().copied() {
|
|
if let Ok((
|
|
main_entity,
|
|
render_entity,
|
|
spot_light,
|
|
visible_entities,
|
|
transform,
|
|
view_visibility,
|
|
frustum,
|
|
volumetric_light,
|
|
)) = spot_lights.get(entity)
|
|
{
|
|
if !view_visibility.get() {
|
|
continue;
|
|
}
|
|
let render_visible_entities =
|
|
create_render_visible_mesh_entities(&mapper, visible_entities);
|
|
|
|
let texel_size =
|
|
2.0 * ops::tan(spot_light.outer_angle) / directional_light_shadow_map.size as f32;
|
|
|
|
spot_lights_values.push((
|
|
render_entity,
|
|
(
|
|
ExtractedPointLight {
|
|
color: spot_light.color.into(),
|
|
// NOTE: Map from luminous power in lumens to luminous intensity in lumens per steradian
|
|
// for a point light. See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower
|
|
// for details.
|
|
// Note: Filament uses a divisor of PI for spot lights. We choose to use the same 4*PI divisor
|
|
// in both cases so that toggling between point light and spot light keeps lit areas lit equally,
|
|
// which seems least surprising for users
|
|
intensity: spot_light.intensity / (4.0 * core::f32::consts::PI),
|
|
range: spot_light.range,
|
|
radius: spot_light.radius,
|
|
transform: *transform,
|
|
shadows_enabled: spot_light.shadows_enabled,
|
|
shadow_depth_bias: spot_light.shadow_depth_bias,
|
|
// The factor of SQRT_2 is for the worst-case diagonal offset
|
|
shadow_normal_bias: spot_light.shadow_normal_bias
|
|
* texel_size
|
|
* core::f32::consts::SQRT_2,
|
|
shadow_map_near_z: spot_light.shadow_map_near_z,
|
|
spot_light_angles: Some((spot_light.inner_angle, spot_light.outer_angle)),
|
|
volumetric: volumetric_light.is_some(),
|
|
affects_lightmapped_mesh_diffuse: spot_light
|
|
.affects_lightmapped_mesh_diffuse,
|
|
#[cfg(feature = "experimental_pbr_pcss")]
|
|
soft_shadows_enabled: spot_light.soft_shadows_enabled,
|
|
#[cfg(not(feature = "experimental_pbr_pcss"))]
|
|
soft_shadows_enabled: false,
|
|
},
|
|
render_visible_entities,
|
|
*frustum,
|
|
MainEntity::from(main_entity),
|
|
),
|
|
));
|
|
}
|
|
}
|
|
*previous_spot_lights_len = spot_lights_values.len();
|
|
commands.try_insert_batch(spot_lights_values);
|
|
|
|
for (
|
|
main_entity,
|
|
entity,
|
|
directional_light,
|
|
visible_entities,
|
|
cascades,
|
|
cascade_config,
|
|
frusta,
|
|
transform,
|
|
view_visibility,
|
|
maybe_layers,
|
|
volumetric_light,
|
|
occlusion_culling,
|
|
) in &directional_lights
|
|
{
|
|
if !view_visibility.get() {
|
|
commands
|
|
.get_entity(entity)
|
|
.expect("Light entity wasn't synced.")
|
|
.remove::<(ExtractedDirectionalLight, RenderCascadesVisibleEntities)>();
|
|
continue;
|
|
}
|
|
|
|
// TODO: update in place instead of reinserting.
|
|
let mut extracted_cascades = EntityHashMap::default();
|
|
let mut extracted_frusta = EntityHashMap::default();
|
|
let mut cascade_visible_entities = EntityHashMap::default();
|
|
for (e, v) in cascades.cascades.iter() {
|
|
if let Ok(entity) = mapper.get(*e) {
|
|
extracted_cascades.insert(entity, v.clone());
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
for (e, v) in frusta.frusta.iter() {
|
|
if let Ok(entity) = mapper.get(*e) {
|
|
extracted_frusta.insert(entity, v.clone());
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
for (e, v) in visible_entities.entities.iter() {
|
|
if let Ok(entity) = mapper.get(*e) {
|
|
cascade_visible_entities.insert(
|
|
entity,
|
|
v.iter()
|
|
.map(|v| create_render_visible_mesh_entities(&mapper, v))
|
|
.collect(),
|
|
);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
commands
|
|
.get_entity(entity)
|
|
.expect("Light entity wasn't synced.")
|
|
.insert((
|
|
ExtractedDirectionalLight {
|
|
color: directional_light.color.into(),
|
|
illuminance: directional_light.illuminance,
|
|
transform: *transform,
|
|
volumetric: volumetric_light.is_some(),
|
|
affects_lightmapped_mesh_diffuse: directional_light
|
|
.affects_lightmapped_mesh_diffuse,
|
|
#[cfg(feature = "experimental_pbr_pcss")]
|
|
soft_shadow_size: directional_light.soft_shadow_size,
|
|
#[cfg(not(feature = "experimental_pbr_pcss"))]
|
|
soft_shadow_size: None,
|
|
shadows_enabled: directional_light.shadows_enabled,
|
|
shadow_depth_bias: directional_light.shadow_depth_bias,
|
|
// The factor of SQRT_2 is for the worst-case diagonal offset
|
|
shadow_normal_bias: directional_light.shadow_normal_bias
|
|
* core::f32::consts::SQRT_2,
|
|
cascade_shadow_config: cascade_config.clone(),
|
|
cascades: extracted_cascades,
|
|
frusta: extracted_frusta,
|
|
render_layers: maybe_layers.unwrap_or_default().clone(),
|
|
occlusion_culling,
|
|
},
|
|
RenderCascadesVisibleEntities {
|
|
entities: cascade_visible_entities,
|
|
},
|
|
MainEntity::from(main_entity),
|
|
));
|
|
}
|
|
}
|
|
|
|
fn create_render_visible_mesh_entities(
|
|
mapper: &Extract<Query<RenderEntity>>,
|
|
visible_entities: &VisibleMeshEntities,
|
|
) -> RenderVisibleMeshEntities {
|
|
RenderVisibleMeshEntities {
|
|
entities: visible_entities
|
|
.iter()
|
|
.map(|e| {
|
|
let render_entity = mapper.get(*e).unwrap_or(Entity::PLACEHOLDER);
|
|
(render_entity, MainEntity::from(*e))
|
|
})
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
#[derive(Component, Default, Deref, DerefMut)]
|
|
/// Component automatically attached to a light entity to track light-view entities
|
|
/// for each view.
|
|
pub struct LightViewEntities(EntityHashMap<Vec<Entity>>);
|
|
|
|
// TODO: using required component
|
|
pub(crate) fn add_light_view_entities(
|
|
trigger: On<Add, (ExtractedDirectionalLight, ExtractedPointLight)>,
|
|
mut commands: Commands,
|
|
) {
|
|
if let Ok(mut v) = commands.get_entity(trigger.target()) {
|
|
v.insert(LightViewEntities::default());
|
|
}
|
|
}
|
|
|
|
/// Removes [`LightViewEntities`] when light is removed. See [`add_light_view_entities`].
|
|
pub(crate) fn extracted_light_removed(
|
|
trigger: On<Remove, (ExtractedDirectionalLight, ExtractedPointLight)>,
|
|
mut commands: Commands,
|
|
) {
|
|
if let Ok(mut v) = commands.get_entity(trigger.target()) {
|
|
v.try_remove::<LightViewEntities>();
|
|
}
|
|
}
|
|
|
|
pub(crate) fn remove_light_view_entities(
|
|
trigger: On<Remove, LightViewEntities>,
|
|
query: Query<&LightViewEntities>,
|
|
mut commands: Commands,
|
|
) {
|
|
if let Ok(entities) = query.get(trigger.target()) {
|
|
for v in entities.0.values() {
|
|
for e in v.iter().copied() {
|
|
if let Ok(mut v) = commands.get_entity(e) {
|
|
v.despawn();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) struct CubeMapFace {
|
|
pub(crate) target: Vec3,
|
|
pub(crate) up: Vec3,
|
|
}
|
|
|
|
// Cubemap faces are [+X, -X, +Y, -Y, +Z, -Z], per https://www.w3.org/TR/webgpu/#texture-view-creation
|
|
// Note: Cubemap coordinates are left-handed y-up, unlike the rest of Bevy.
|
|
// See https://registry.khronos.org/vulkan/specs/1.2/html/chap16.html#_cube_map_face_selection
|
|
//
|
|
// For each cubemap face, we take care to specify the appropriate target/up axis such that the rendered
|
|
// texture using Bevy's right-handed y-up coordinate space matches the expected cubemap face in
|
|
// left-handed y-up cubemap coordinates.
|
|
pub(crate) const CUBE_MAP_FACES: [CubeMapFace; 6] = [
|
|
// +X
|
|
CubeMapFace {
|
|
target: Vec3::X,
|
|
up: Vec3::Y,
|
|
},
|
|
// -X
|
|
CubeMapFace {
|
|
target: Vec3::NEG_X,
|
|
up: Vec3::Y,
|
|
},
|
|
// +Y
|
|
CubeMapFace {
|
|
target: Vec3::Y,
|
|
up: Vec3::Z,
|
|
},
|
|
// -Y
|
|
CubeMapFace {
|
|
target: Vec3::NEG_Y,
|
|
up: Vec3::NEG_Z,
|
|
},
|
|
// +Z (with left-handed conventions, pointing forwards)
|
|
CubeMapFace {
|
|
target: Vec3::NEG_Z,
|
|
up: Vec3::Y,
|
|
},
|
|
// -Z (with left-handed conventions, pointing backwards)
|
|
CubeMapFace {
|
|
target: Vec3::Z,
|
|
up: Vec3::Y,
|
|
},
|
|
];
|
|
|
|
fn face_index_to_name(face_index: usize) -> &'static str {
|
|
match face_index {
|
|
0 => "+x",
|
|
1 => "-x",
|
|
2 => "+y",
|
|
3 => "-y",
|
|
4 => "+z",
|
|
5 => "-z",
|
|
_ => "invalid",
|
|
}
|
|
}
|
|
|
|
#[derive(Component)]
|
|
pub struct ShadowView {
|
|
pub depth_attachment: DepthAttachment,
|
|
pub pass_name: String,
|
|
}
|
|
|
|
#[derive(Component)]
|
|
pub struct ViewShadowBindings {
|
|
pub point_light_depth_texture: Texture,
|
|
pub point_light_depth_texture_view: TextureView,
|
|
pub directional_light_depth_texture: Texture,
|
|
pub directional_light_depth_texture_view: TextureView,
|
|
}
|
|
|
|
/// A component that holds the shadow cascade views for all shadow cascades
|
|
/// associated with a camera.
|
|
///
|
|
/// Note: Despite the name, this component actually holds the shadow cascade
|
|
/// views, not the lights themselves.
|
|
#[derive(Component)]
|
|
pub struct ViewLightEntities {
|
|
/// The shadow cascade views for all shadow cascades associated with a
|
|
/// camera.
|
|
///
|
|
/// Note: Despite the name, this component actually holds the shadow cascade
|
|
/// views, not the lights themselves.
|
|
pub lights: Vec<Entity>,
|
|
}
|
|
|
|
#[derive(Component)]
|
|
pub struct ViewLightsUniformOffset {
|
|
pub offset: u32,
|
|
}
|
|
|
|
#[derive(Resource, Default)]
|
|
pub struct LightMeta {
|
|
pub view_gpu_lights: DynamicUniformBuffer<GpuLights>,
|
|
}
|
|
|
|
#[derive(Component)]
|
|
pub enum LightEntity {
|
|
Directional {
|
|
light_entity: Entity,
|
|
cascade_index: usize,
|
|
},
|
|
Point {
|
|
light_entity: Entity,
|
|
face_index: usize,
|
|
},
|
|
Spot {
|
|
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(
|
|
mut commands: Commands,
|
|
mut texture_cache: ResMut<TextureCache>,
|
|
(render_device, render_queue): (Res<RenderDevice>, Res<RenderQueue>),
|
|
mut global_light_meta: ResMut<GlobalClusterableObjectMeta>,
|
|
mut light_meta: ResMut<LightMeta>,
|
|
views: Query<
|
|
(
|
|
Entity,
|
|
MainEntity,
|
|
&ExtractedView,
|
|
&ExtractedClusterConfig,
|
|
Option<&RenderLayers>,
|
|
Has<NoIndirectDrawing>,
|
|
Option<&AmbientLight>,
|
|
),
|
|
With<Camera3d>,
|
|
>,
|
|
ambient_light: Res<AmbientLight>,
|
|
point_light_shadow_map: Res<PointLightShadowMap>,
|
|
directional_light_shadow_map: Res<DirectionalLightShadowMap>,
|
|
mut shadow_render_phases: ResMut<ViewBinnedRenderPhases<Shadow>>,
|
|
(
|
|
mut max_directional_lights_warning_emitted,
|
|
mut max_cascades_per_light_warning_emitted,
|
|
mut live_shadow_mapping_lights,
|
|
): (Local<bool>, Local<bool>, Local<HashSet<RetainedViewEntity>>),
|
|
point_lights: Query<(
|
|
Entity,
|
|
&MainEntity,
|
|
&ExtractedPointLight,
|
|
AnyOf<(&CubemapFrusta, &Frustum)>,
|
|
)>,
|
|
directional_lights: Query<(Entity, &MainEntity, &ExtractedDirectionalLight)>,
|
|
mut light_view_entities: Query<&mut LightViewEntities>,
|
|
sorted_cameras: Res<SortedCameras>,
|
|
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
|
|
) {
|
|
let views_iter = views.iter();
|
|
let views_count = views_iter.len();
|
|
let Some(mut view_gpu_lights_writer) =
|
|
light_meta
|
|
.view_gpu_lights
|
|
.get_writer(views_count, &render_device, &render_queue)
|
|
else {
|
|
return;
|
|
};
|
|
|
|
// Pre-calculate for PointLights
|
|
let cube_face_rotations = CUBE_MAP_FACES
|
|
.iter()
|
|
.map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up))
|
|
.collect::<Vec<_>>();
|
|
|
|
global_light_meta.entity_to_index.clear();
|
|
|
|
let mut point_lights: Vec<_> = point_lights.iter().collect::<Vec<_>>();
|
|
let mut directional_lights: Vec<_> = directional_lights.iter().collect::<Vec<_>>();
|
|
|
|
#[cfg(any(
|
|
not(feature = "webgl"),
|
|
not(target_arch = "wasm32"),
|
|
feature = "webgpu"
|
|
))]
|
|
let max_texture_array_layers = render_device.limits().max_texture_array_layers as usize;
|
|
#[cfg(any(
|
|
not(feature = "webgl"),
|
|
not(target_arch = "wasm32"),
|
|
feature = "webgpu"
|
|
))]
|
|
let max_texture_cubes = max_texture_array_layers / 6;
|
|
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
|
|
let max_texture_array_layers = 1;
|
|
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
|
|
let max_texture_cubes = 1;
|
|
|
|
if !*max_directional_lights_warning_emitted && directional_lights.len() > MAX_DIRECTIONAL_LIGHTS
|
|
{
|
|
warn!(
|
|
"The amount of directional lights of {} is exceeding the supported limit of {}.",
|
|
directional_lights.len(),
|
|
MAX_DIRECTIONAL_LIGHTS
|
|
);
|
|
*max_directional_lights_warning_emitted = true;
|
|
}
|
|
|
|
if !*max_cascades_per_light_warning_emitted
|
|
&& directional_lights
|
|
.iter()
|
|
.any(|(_, _, light)| light.cascade_shadow_config.bounds.len() > MAX_CASCADES_PER_LIGHT)
|
|
{
|
|
warn!(
|
|
"The number of cascades configured for a directional light exceeds the supported limit of {}.",
|
|
MAX_CASCADES_PER_LIGHT
|
|
);
|
|
*max_cascades_per_light_warning_emitted = true;
|
|
}
|
|
|
|
let point_light_count = point_lights
|
|
.iter()
|
|
.filter(|light| light.2.spot_light_angles.is_none())
|
|
.count();
|
|
|
|
let point_light_volumetric_enabled_count = point_lights
|
|
.iter()
|
|
.filter(|(_, _, light, _)| light.volumetric && light.spot_light_angles.is_none())
|
|
.count()
|
|
.min(max_texture_cubes);
|
|
|
|
let point_light_shadow_maps_count = point_lights
|
|
.iter()
|
|
.filter(|light| light.2.shadows_enabled && light.2.spot_light_angles.is_none())
|
|
.count()
|
|
.min(max_texture_cubes);
|
|
|
|
let directional_volumetric_enabled_count = directional_lights
|
|
.iter()
|
|
.take(MAX_DIRECTIONAL_LIGHTS)
|
|
.filter(|(_, _, light)| light.volumetric)
|
|
.count()
|
|
.min(max_texture_array_layers / MAX_CASCADES_PER_LIGHT);
|
|
|
|
let directional_shadow_enabled_count = directional_lights
|
|
.iter()
|
|
.take(MAX_DIRECTIONAL_LIGHTS)
|
|
.filter(|(_, _, light)| light.shadows_enabled)
|
|
.count()
|
|
.min(max_texture_array_layers / MAX_CASCADES_PER_LIGHT);
|
|
|
|
let spot_light_count = point_lights
|
|
.iter()
|
|
.filter(|(_, _, light, _)| light.spot_light_angles.is_some())
|
|
.count()
|
|
.min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT);
|
|
|
|
let spot_light_volumetric_enabled_count = point_lights
|
|
.iter()
|
|
.filter(|(_, _, light, _)| light.volumetric && light.spot_light_angles.is_some())
|
|
.count()
|
|
.min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT);
|
|
|
|
let spot_light_shadow_maps_count = point_lights
|
|
.iter()
|
|
.filter(|(_, _, light, _)| light.shadows_enabled && light.spot_light_angles.is_some())
|
|
.count()
|
|
.min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT);
|
|
|
|
// Sort lights by
|
|
// - point-light vs spot-light, so that we can iterate point lights and spot lights in contiguous blocks in the fragment shader,
|
|
// - then those with shadows enabled first, so that the index can be used to render at most `point_light_shadow_maps_count`
|
|
// point light shadows and `spot_light_shadow_maps_count` spot light shadow maps,
|
|
// - then by entity as a stable key to ensure that a consistent set of lights are chosen if the light count limit is exceeded.
|
|
point_lights.sort_by_cached_key(|(entity, _, light, _)| {
|
|
(
|
|
ClusterableObjectType::from_point_or_spot_light(light).ordering(),
|
|
*entity,
|
|
)
|
|
});
|
|
|
|
// Sort lights by
|
|
// - those with volumetric (and shadows) enabled first, so that the
|
|
// volumetric lighting pass can quickly find the volumetric lights;
|
|
// - then those with shadows enabled second, so that the index can be used
|
|
// to render at most `directional_light_shadow_maps_count` directional light
|
|
// shadows
|
|
// - then by entity as a stable key to ensure that a consistent set of
|
|
// lights are chosen if the light count limit is exceeded.
|
|
// - because entities are unique, we can use `sort_unstable_by_key`
|
|
// and still end up with a stable order.
|
|
directional_lights.sort_unstable_by_key(|(entity, _, light)| {
|
|
(light.volumetric, light.shadows_enabled, *entity)
|
|
});
|
|
|
|
if global_light_meta.entity_to_index.capacity() < point_lights.len() {
|
|
global_light_meta
|
|
.entity_to_index
|
|
.reserve(point_lights.len());
|
|
}
|
|
|
|
let mut gpu_point_lights = Vec::new();
|
|
for (index, &(entity, _, light, _)) in point_lights.iter().enumerate() {
|
|
let mut flags = PointLightFlags::NONE;
|
|
|
|
// Lights are sorted, shadow enabled lights are first
|
|
if light.shadows_enabled
|
|
&& (index < point_light_shadow_maps_count
|
|
|| (light.spot_light_angles.is_some()
|
|
&& index - point_light_count < spot_light_shadow_maps_count))
|
|
{
|
|
flags |= PointLightFlags::SHADOWS_ENABLED;
|
|
}
|
|
|
|
let cube_face_projection = Mat4::perspective_infinite_reverse_rh(
|
|
core::f32::consts::FRAC_PI_2,
|
|
1.0,
|
|
light.shadow_map_near_z,
|
|
);
|
|
if light.shadows_enabled
|
|
&& light.volumetric
|
|
&& (index < point_light_volumetric_enabled_count
|
|
|| (light.spot_light_angles.is_some()
|
|
&& index - point_light_count < spot_light_volumetric_enabled_count))
|
|
{
|
|
flags |= PointLightFlags::VOLUMETRIC;
|
|
}
|
|
|
|
if light.affects_lightmapped_mesh_diffuse {
|
|
flags |= PointLightFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE;
|
|
}
|
|
|
|
let (light_custom_data, spot_light_tan_angle) = match light.spot_light_angles {
|
|
Some((inner, outer)) => {
|
|
let light_direction = light.transform.forward();
|
|
if light_direction.y.is_sign_negative() {
|
|
flags |= PointLightFlags::SPOT_LIGHT_Y_NEGATIVE;
|
|
}
|
|
|
|
let cos_outer = ops::cos(outer);
|
|
let spot_scale = 1.0 / f32::max(ops::cos(inner) - cos_outer, 1e-4);
|
|
let spot_offset = -cos_outer * spot_scale;
|
|
|
|
(
|
|
// For spot lights: the direction (x,z), spot_scale and spot_offset
|
|
light_direction.xz().extend(spot_scale).extend(spot_offset),
|
|
ops::tan(outer),
|
|
)
|
|
}
|
|
None => {
|
|
(
|
|
// For point lights: the lower-right 2x2 values of the projection matrix [2][2] [2][3] [3][2] [3][3]
|
|
Vec4::new(
|
|
cube_face_projection.z_axis.z,
|
|
cube_face_projection.z_axis.w,
|
|
cube_face_projection.w_axis.z,
|
|
cube_face_projection.w_axis.w,
|
|
),
|
|
// unused
|
|
0.0,
|
|
)
|
|
}
|
|
};
|
|
|
|
gpu_point_lights.push(GpuClusterableObject {
|
|
light_custom_data,
|
|
// premultiply color by intensity
|
|
// we don't use the alpha at all, so no reason to multiply only [0..3]
|
|
color_inverse_square_range: (Vec4::from_slice(&light.color.to_f32_array())
|
|
* light.intensity)
|
|
.xyz()
|
|
.extend(1.0 / (light.range * light.range)),
|
|
position_radius: light.transform.translation().extend(light.radius),
|
|
flags: flags.bits(),
|
|
shadow_depth_bias: light.shadow_depth_bias,
|
|
shadow_normal_bias: light.shadow_normal_bias,
|
|
shadow_map_near_z: light.shadow_map_near_z,
|
|
spot_light_tan_angle,
|
|
pad_a: 0.0,
|
|
pad_b: 0.0,
|
|
soft_shadow_size: if light.soft_shadows_enabled {
|
|
light.radius
|
|
} else {
|
|
0.0
|
|
},
|
|
});
|
|
global_light_meta.entity_to_index.insert(entity, index);
|
|
}
|
|
|
|
// iterate the views once to find the maximum number of cascade shadowmaps we will need
|
|
let mut num_directional_cascades_enabled = 0usize;
|
|
for (
|
|
_entity,
|
|
_camera_main_entity,
|
|
_extracted_view,
|
|
_clusters,
|
|
maybe_layers,
|
|
_no_indirect_drawing,
|
|
_maybe_ambient_override,
|
|
) in sorted_cameras
|
|
.0
|
|
.iter()
|
|
.filter_map(|sorted_camera| views.get(sorted_camera.entity).ok())
|
|
{
|
|
let mut num_directional_cascades_for_this_view = 0usize;
|
|
let render_layers = maybe_layers.unwrap_or_default();
|
|
|
|
for (_light_entity, _, light) in directional_lights.iter() {
|
|
if light.shadows_enabled && light.render_layers.intersects(render_layers) {
|
|
num_directional_cascades_for_this_view += light
|
|
.cascade_shadow_config
|
|
.bounds
|
|
.len()
|
|
.min(MAX_CASCADES_PER_LIGHT);
|
|
}
|
|
}
|
|
|
|
num_directional_cascades_enabled = num_directional_cascades_enabled
|
|
.max(num_directional_cascades_for_this_view)
|
|
.min(max_texture_array_layers);
|
|
}
|
|
|
|
global_light_meta
|
|
.gpu_clusterable_objects
|
|
.set(gpu_point_lights);
|
|
global_light_meta
|
|
.gpu_clusterable_objects
|
|
.write_buffer(&render_device, &render_queue);
|
|
|
|
live_shadow_mapping_lights.clear();
|
|
|
|
let mut point_light_depth_attachments = HashMap::<u32, DepthAttachment>::default();
|
|
let mut directional_light_depth_attachments = HashMap::<u32, DepthAttachment>::default();
|
|
|
|
let point_light_depth_texture = texture_cache.get(
|
|
&render_device,
|
|
TextureDescriptor {
|
|
size: Extent3d {
|
|
width: point_light_shadow_map.size as u32,
|
|
height: point_light_shadow_map.size as u32,
|
|
depth_or_array_layers: point_light_shadow_maps_count.max(1) as u32 * 6,
|
|
},
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: TextureDimension::D2,
|
|
format: CORE_3D_DEPTH_FORMAT,
|
|
label: Some("point_light_shadow_map_texture"),
|
|
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
|
|
view_formats: &[],
|
|
},
|
|
);
|
|
|
|
let point_light_depth_texture_view =
|
|
point_light_depth_texture
|
|
.texture
|
|
.create_view(&TextureViewDescriptor {
|
|
label: Some("point_light_shadow_map_array_texture_view"),
|
|
format: None,
|
|
// NOTE: iOS Simulator is missing CubeArray support so we use Cube instead.
|
|
// See https://github.com/bevyengine/bevy/pull/12052 - remove if support is added.
|
|
#[cfg(all(
|
|
not(target_abi = "sim"),
|
|
any(
|
|
not(feature = "webgl"),
|
|
not(target_arch = "wasm32"),
|
|
feature = "webgpu"
|
|
)
|
|
))]
|
|
dimension: Some(TextureViewDimension::CubeArray),
|
|
#[cfg(any(
|
|
target_abi = "sim",
|
|
all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu"))
|
|
))]
|
|
dimension: Some(TextureViewDimension::Cube),
|
|
usage: None,
|
|
aspect: TextureAspect::DepthOnly,
|
|
base_mip_level: 0,
|
|
mip_level_count: None,
|
|
base_array_layer: 0,
|
|
array_layer_count: None,
|
|
});
|
|
|
|
let directional_light_depth_texture = texture_cache.get(
|
|
&render_device,
|
|
TextureDescriptor {
|
|
size: Extent3d {
|
|
width: (directional_light_shadow_map.size as u32)
|
|
.min(render_device.limits().max_texture_dimension_2d),
|
|
height: (directional_light_shadow_map.size as u32)
|
|
.min(render_device.limits().max_texture_dimension_2d),
|
|
depth_or_array_layers: (num_directional_cascades_enabled
|
|
+ spot_light_shadow_maps_count)
|
|
.max(1) as u32,
|
|
},
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: TextureDimension::D2,
|
|
format: CORE_3D_DEPTH_FORMAT,
|
|
label: Some("directional_light_shadow_map_texture"),
|
|
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
|
|
view_formats: &[],
|
|
},
|
|
);
|
|
|
|
let directional_light_depth_texture_view =
|
|
directional_light_depth_texture
|
|
.texture
|
|
.create_view(&TextureViewDescriptor {
|
|
label: Some("directional_light_shadow_map_array_texture_view"),
|
|
format: None,
|
|
#[cfg(any(
|
|
not(feature = "webgl"),
|
|
not(target_arch = "wasm32"),
|
|
feature = "webgpu"
|
|
))]
|
|
dimension: Some(TextureViewDimension::D2Array),
|
|
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
|
|
dimension: Some(TextureViewDimension::D2),
|
|
usage: None,
|
|
aspect: TextureAspect::DepthOnly,
|
|
base_mip_level: 0,
|
|
mip_level_count: None,
|
|
base_array_layer: 0,
|
|
array_layer_count: None,
|
|
});
|
|
|
|
let mut live_views = EntityHashSet::with_capacity(views_count);
|
|
|
|
// set up light data for each view
|
|
for (
|
|
entity,
|
|
camera_main_entity,
|
|
extracted_view,
|
|
clusters,
|
|
maybe_layers,
|
|
no_indirect_drawing,
|
|
maybe_ambient_override,
|
|
) in sorted_cameras
|
|
.0
|
|
.iter()
|
|
.filter_map(|sorted_camera| views.get(sorted_camera.entity).ok())
|
|
{
|
|
live_views.insert(entity);
|
|
|
|
let view_layers = maybe_layers.unwrap_or_default();
|
|
let mut view_lights = Vec::new();
|
|
let mut view_occlusion_culling_lights = Vec::new();
|
|
|
|
let gpu_preprocessing_mode = gpu_preprocessing_support.min(if !no_indirect_drawing {
|
|
GpuPreprocessingMode::Culling
|
|
} else {
|
|
GpuPreprocessingMode::PreprocessingOnly
|
|
});
|
|
|
|
let is_orthographic = extracted_view.clip_from_view.w_axis.w == 1.0;
|
|
let cluster_factors_zw = calculate_cluster_factors(
|
|
clusters.near,
|
|
clusters.far,
|
|
clusters.dimensions.z as f32,
|
|
is_orthographic,
|
|
);
|
|
|
|
let n_clusters = clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z;
|
|
let ambient_light = maybe_ambient_override.unwrap_or(&ambient_light);
|
|
|
|
let mut gpu_directional_lights = [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS];
|
|
let mut num_directional_cascades_enabled_for_this_view = 0usize;
|
|
let mut num_directional_lights_for_this_view = 0usize;
|
|
for (index, (_light_entity, _, light)) in directional_lights
|
|
.iter()
|
|
.filter(|(_light_entity, _, light)| light.render_layers.intersects(view_layers))
|
|
.enumerate()
|
|
.take(MAX_DIRECTIONAL_LIGHTS)
|
|
{
|
|
num_directional_lights_for_this_view += 1;
|
|
|
|
let mut flags = DirectionalLightFlags::NONE;
|
|
|
|
// Lights are sorted, volumetric and shadow enabled lights are first
|
|
if light.volumetric
|
|
&& light.shadows_enabled
|
|
&& (index < directional_volumetric_enabled_count)
|
|
{
|
|
flags |= DirectionalLightFlags::VOLUMETRIC;
|
|
}
|
|
|
|
// Shadow enabled lights are second
|
|
let mut num_cascades = 0;
|
|
if light.shadows_enabled {
|
|
let cascades = light
|
|
.cascade_shadow_config
|
|
.bounds
|
|
.len()
|
|
.min(MAX_CASCADES_PER_LIGHT);
|
|
|
|
if num_directional_cascades_enabled_for_this_view + cascades
|
|
<= max_texture_array_layers
|
|
{
|
|
flags |= DirectionalLightFlags::SHADOWS_ENABLED;
|
|
num_cascades += cascades;
|
|
}
|
|
}
|
|
|
|
if light.affects_lightmapped_mesh_diffuse {
|
|
flags |= DirectionalLightFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE;
|
|
}
|
|
|
|
gpu_directional_lights[index] = GpuDirectionalLight {
|
|
// Filled in later.
|
|
cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT],
|
|
// premultiply color by illuminance
|
|
// we don't use the alpha at all, so no reason to multiply only [0..3]
|
|
color: Vec4::from_slice(&light.color.to_f32_array()) * light.illuminance,
|
|
// direction is negated to be ready for N.L
|
|
dir_to_light: light.transform.back().into(),
|
|
flags: flags.bits(),
|
|
soft_shadow_size: light.soft_shadow_size.unwrap_or_default(),
|
|
shadow_depth_bias: light.shadow_depth_bias,
|
|
shadow_normal_bias: light.shadow_normal_bias,
|
|
num_cascades: num_cascades as u32,
|
|
cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion,
|
|
depth_texture_base_index: num_directional_cascades_enabled_for_this_view as u32,
|
|
};
|
|
num_directional_cascades_enabled_for_this_view += num_cascades;
|
|
}
|
|
|
|
let mut gpu_lights = GpuLights {
|
|
directional_lights: gpu_directional_lights,
|
|
ambient_color: Vec4::from_slice(&LinearRgba::from(ambient_light.color).to_f32_array())
|
|
* ambient_light.brightness,
|
|
cluster_factors: Vec4::new(
|
|
clusters.dimensions.x as f32 / extracted_view.viewport.z as f32,
|
|
clusters.dimensions.y as f32 / extracted_view.viewport.w as f32,
|
|
cluster_factors_zw.x,
|
|
cluster_factors_zw.y,
|
|
),
|
|
cluster_dimensions: clusters.dimensions.extend(n_clusters),
|
|
n_directional_lights: num_directional_lights_for_this_view as u32,
|
|
// spotlight shadow maps are stored in the directional light array, starting at num_directional_cascades_enabled.
|
|
// the spot lights themselves start in the light array at point_light_count. so to go from light
|
|
// index to shadow map index, we need to subtract point light count and add directional shadowmap count.
|
|
spot_light_shadowmap_offset: num_directional_cascades_enabled as i32
|
|
- point_light_count as i32,
|
|
ambient_light_affects_lightmapped_meshes: ambient_light.affects_lightmapped_meshes
|
|
as u32,
|
|
};
|
|
|
|
// TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query
|
|
for &(light_entity, light_main_entity, light, (point_light_frusta, _)) in point_lights
|
|
.iter()
|
|
// Lights are sorted, shadow enabled lights are first
|
|
.take(point_light_count.min(max_texture_cubes))
|
|
{
|
|
let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else {
|
|
continue;
|
|
};
|
|
|
|
if !light.shadows_enabled {
|
|
if let Some(entities) = light_view_entities.remove(&entity) {
|
|
despawn_entities(&mut commands, entities);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
let light_index = *global_light_meta
|
|
.entity_to_index
|
|
.get(&light_entity)
|
|
.unwrap();
|
|
// 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 = GlobalTransform::from_translation(light.transform.translation());
|
|
|
|
// for each face of a cube and each view we spawn a light entity
|
|
let light_view_entities = light_view_entities
|
|
.entry(entity)
|
|
.or_insert_with(|| (0..6).map(|_| commands.spawn_empty().id()).collect());
|
|
|
|
let cube_face_projection = Mat4::perspective_infinite_reverse_rh(
|
|
core::f32::consts::FRAC_PI_2,
|
|
1.0,
|
|
light.shadow_map_near_z,
|
|
);
|
|
|
|
for (face_index, ((view_rotation, frustum), view_light_entity)) in cube_face_rotations
|
|
.iter()
|
|
.zip(&point_light_frusta.unwrap().frusta)
|
|
.zip(light_view_entities.iter().copied())
|
|
.enumerate()
|
|
{
|
|
let mut first = false;
|
|
let base_array_layer = (light_index * 6 + face_index) as u32;
|
|
|
|
let depth_attachment = point_light_depth_attachments
|
|
.entry(base_array_layer)
|
|
.or_insert_with(|| {
|
|
first = true;
|
|
|
|
let depth_texture_view =
|
|
point_light_depth_texture
|
|
.texture
|
|
.create_view(&TextureViewDescriptor {
|
|
label: Some("point_light_shadow_map_texture_view"),
|
|
format: None,
|
|
dimension: Some(TextureViewDimension::D2),
|
|
usage: None,
|
|
aspect: TextureAspect::All,
|
|
base_mip_level: 0,
|
|
mip_level_count: None,
|
|
base_array_layer,
|
|
array_layer_count: Some(1u32),
|
|
});
|
|
|
|
DepthAttachment::new(depth_texture_view, Some(0.0))
|
|
})
|
|
.clone();
|
|
|
|
let retained_view_entity = RetainedViewEntity::new(
|
|
*light_main_entity,
|
|
Some(camera_main_entity.into()),
|
|
face_index as u32,
|
|
);
|
|
|
|
commands.entity(view_light_entity).insert((
|
|
ShadowView {
|
|
depth_attachment,
|
|
pass_name: format!(
|
|
"shadow pass point light {} {}",
|
|
light_index,
|
|
face_index_to_name(face_index)
|
|
),
|
|
},
|
|
ExtractedView {
|
|
retained_view_entity,
|
|
viewport: UVec4::new(
|
|
0,
|
|
0,
|
|
point_light_shadow_map.size as u32,
|
|
point_light_shadow_map.size as u32,
|
|
),
|
|
world_from_view: view_translation * *view_rotation,
|
|
clip_from_world: None,
|
|
clip_from_view: cube_face_projection,
|
|
hdr: false,
|
|
color_grading: Default::default(),
|
|
},
|
|
*frustum,
|
|
LightEntity::Point {
|
|
light_entity,
|
|
face_index,
|
|
},
|
|
));
|
|
|
|
if !matches!(gpu_preprocessing_mode, GpuPreprocessingMode::Culling) {
|
|
commands.entity(view_light_entity).insert(NoIndirectDrawing);
|
|
}
|
|
|
|
view_lights.push(view_light_entity);
|
|
|
|
if first {
|
|
// Subsequent views with the same light entity will reuse the same shadow map
|
|
shadow_render_phases
|
|
.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode);
|
|
live_shadow_mapping_lights.insert(retained_view_entity);
|
|
}
|
|
}
|
|
}
|
|
|
|
// spot lights
|
|
for (light_index, &(light_entity, light_main_entity, light, (_, spot_light_frustum))) in
|
|
point_lights
|
|
.iter()
|
|
.skip(point_light_count)
|
|
.take(spot_light_count)
|
|
.enumerate()
|
|
{
|
|
let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else {
|
|
continue;
|
|
};
|
|
|
|
if !light.shadows_enabled {
|
|
if let Some(entities) = light_view_entities.remove(&entity) {
|
|
despawn_entities(&mut commands, entities);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
let spot_world_from_view = spot_light_world_from_view(&light.transform);
|
|
let spot_world_from_view = spot_world_from_view.into();
|
|
|
|
let angle = light.spot_light_angles.expect("lights should be sorted so that \
|
|
[point_light_count..point_light_count + spot_light_shadow_maps_count] are spot lights").1;
|
|
let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z);
|
|
|
|
let mut first = false;
|
|
let base_array_layer = (num_directional_cascades_enabled + light_index) as u32;
|
|
|
|
let depth_attachment = directional_light_depth_attachments
|
|
.entry(base_array_layer)
|
|
.or_insert_with(|| {
|
|
first = true;
|
|
|
|
let depth_texture_view = directional_light_depth_texture.texture.create_view(
|
|
&TextureViewDescriptor {
|
|
label: Some("spot_light_shadow_map_texture_view"),
|
|
format: None,
|
|
dimension: Some(TextureViewDimension::D2),
|
|
usage: None,
|
|
aspect: TextureAspect::All,
|
|
base_mip_level: 0,
|
|
mip_level_count: None,
|
|
base_array_layer,
|
|
array_layer_count: Some(1u32),
|
|
},
|
|
);
|
|
|
|
DepthAttachment::new(depth_texture_view, Some(0.0))
|
|
})
|
|
.clone();
|
|
|
|
let light_view_entities = light_view_entities
|
|
.entry(entity)
|
|
.or_insert_with(|| vec![commands.spawn_empty().id()]);
|
|
|
|
let view_light_entity = light_view_entities[0];
|
|
|
|
let retained_view_entity =
|
|
RetainedViewEntity::new(*light_main_entity, Some(camera_main_entity.into()), 0);
|
|
|
|
commands.entity(view_light_entity).insert((
|
|
ShadowView {
|
|
depth_attachment,
|
|
pass_name: format!("shadow pass spot light {light_index}"),
|
|
},
|
|
ExtractedView {
|
|
retained_view_entity,
|
|
viewport: UVec4::new(
|
|
0,
|
|
0,
|
|
directional_light_shadow_map.size as u32,
|
|
directional_light_shadow_map.size as u32,
|
|
),
|
|
world_from_view: spot_world_from_view,
|
|
clip_from_view: spot_projection,
|
|
clip_from_world: None,
|
|
hdr: false,
|
|
color_grading: Default::default(),
|
|
},
|
|
*spot_light_frustum.unwrap(),
|
|
LightEntity::Spot { light_entity },
|
|
));
|
|
|
|
if !matches!(gpu_preprocessing_mode, GpuPreprocessingMode::Culling) {
|
|
commands.entity(view_light_entity).insert(NoIndirectDrawing);
|
|
}
|
|
|
|
view_lights.push(view_light_entity);
|
|
|
|
if first {
|
|
// Subsequent views with the same light entity will reuse the same shadow map
|
|
shadow_render_phases
|
|
.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode);
|
|
live_shadow_mapping_lights.insert(retained_view_entity);
|
|
}
|
|
}
|
|
|
|
// directional lights
|
|
// clear entities for lights that don't intersect the layer
|
|
for &(light_entity, _, _) in directional_lights
|
|
.iter()
|
|
.filter(|(_, _, light)| !light.render_layers.intersects(view_layers))
|
|
{
|
|
let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else {
|
|
continue;
|
|
};
|
|
if let Some(entities) = light_view_entities.remove(&entity) {
|
|
despawn_entities(&mut commands, entities);
|
|
}
|
|
}
|
|
|
|
let mut directional_depth_texture_array_index = 0u32;
|
|
for (light_index, &(light_entity, light_main_entity, light)) in directional_lights
|
|
.iter()
|
|
.filter(|(_, _, light)| light.render_layers.intersects(view_layers))
|
|
.enumerate()
|
|
.take(MAX_DIRECTIONAL_LIGHTS)
|
|
{
|
|
let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else {
|
|
continue;
|
|
};
|
|
|
|
let gpu_light = &mut gpu_lights.directional_lights[light_index];
|
|
|
|
// Only deal with cascades when shadows are enabled.
|
|
if (gpu_light.flags & DirectionalLightFlags::SHADOWS_ENABLED.bits()) == 0u32 {
|
|
if let Some(entities) = light_view_entities.remove(&entity) {
|
|
despawn_entities(&mut commands, entities);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
let cascades = light
|
|
.cascades
|
|
.get(&entity)
|
|
.unwrap()
|
|
.iter()
|
|
.take(MAX_CASCADES_PER_LIGHT);
|
|
let frusta = light
|
|
.frusta
|
|
.get(&entity)
|
|
.unwrap()
|
|
.iter()
|
|
.take(MAX_CASCADES_PER_LIGHT);
|
|
|
|
let iter = cascades
|
|
.zip(frusta)
|
|
.zip(&light.cascade_shadow_config.bounds);
|
|
|
|
let light_view_entities = light_view_entities.entry(entity).or_insert_with(|| {
|
|
(0..iter.len())
|
|
.map(|_| commands.spawn_empty().id())
|
|
.collect()
|
|
});
|
|
if light_view_entities.len() != iter.len() {
|
|
let entities = core::mem::take(light_view_entities);
|
|
despawn_entities(&mut commands, entities);
|
|
light_view_entities.extend((0..iter.len()).map(|_| commands.spawn_empty().id()));
|
|
}
|
|
|
|
for (cascade_index, (((cascade, frustum), bound), view_light_entity)) in
|
|
iter.zip(light_view_entities.iter().copied()).enumerate()
|
|
{
|
|
gpu_lights.directional_lights[light_index].cascades[cascade_index] =
|
|
GpuDirectionalCascade {
|
|
clip_from_world: cascade.clip_from_world,
|
|
texel_size: cascade.texel_size,
|
|
far_bound: *bound,
|
|
};
|
|
|
|
let depth_texture_view =
|
|
directional_light_depth_texture
|
|
.texture
|
|
.create_view(&TextureViewDescriptor {
|
|
label: Some("directional_light_shadow_map_array_texture_view"),
|
|
format: None,
|
|
dimension: Some(TextureViewDimension::D2),
|
|
usage: None,
|
|
aspect: TextureAspect::All,
|
|
base_mip_level: 0,
|
|
mip_level_count: None,
|
|
base_array_layer: directional_depth_texture_array_index,
|
|
array_layer_count: Some(1u32),
|
|
});
|
|
|
|
// NOTE: For point and spotlights, we reuse the same depth attachment for all views.
|
|
// However, for directional lights, we want a new depth attachment for each view,
|
|
// so that the view is cleared for each view.
|
|
let depth_attachment = DepthAttachment::new(depth_texture_view.clone(), Some(0.0));
|
|
|
|
directional_depth_texture_array_index += 1;
|
|
|
|
let mut frustum = *frustum;
|
|
// Push the near clip plane out to infinity for directional lights
|
|
frustum.half_spaces[4] =
|
|
HalfSpace::new(frustum.half_spaces[4].normal().extend(f32::INFINITY));
|
|
|
|
let retained_view_entity = RetainedViewEntity::new(
|
|
*light_main_entity,
|
|
Some(camera_main_entity.into()),
|
|
cascade_index as u32,
|
|
);
|
|
|
|
commands.entity(view_light_entity).insert((
|
|
ShadowView {
|
|
depth_attachment,
|
|
pass_name: format!(
|
|
"shadow pass directional light {light_index} cascade {cascade_index}"
|
|
),
|
|
},
|
|
ExtractedView {
|
|
retained_view_entity,
|
|
viewport: UVec4::new(
|
|
0,
|
|
0,
|
|
directional_light_shadow_map.size as u32,
|
|
directional_light_shadow_map.size as u32,
|
|
),
|
|
world_from_view: GlobalTransform::from(cascade.world_from_cascade),
|
|
clip_from_view: cascade.clip_from_cascade,
|
|
clip_from_world: Some(cascade.clip_from_world),
|
|
hdr: false,
|
|
color_grading: Default::default(),
|
|
},
|
|
frustum,
|
|
LightEntity::Directional {
|
|
light_entity,
|
|
cascade_index,
|
|
},
|
|
));
|
|
|
|
if !matches!(gpu_preprocessing_mode, GpuPreprocessingMode::Culling) {
|
|
commands.entity(view_light_entity).insert(NoIndirectDrawing);
|
|
}
|
|
|
|
view_lights.push(view_light_entity);
|
|
|
|
// If this light is using occlusion culling, add the appropriate components.
|
|
if light.occlusion_culling {
|
|
commands.entity(view_light_entity).insert((
|
|
OcclusionCulling,
|
|
OcclusionCullingSubview {
|
|
depth_texture_view,
|
|
depth_texture_size: directional_light_shadow_map.size as u32,
|
|
},
|
|
));
|
|
view_occlusion_culling_lights.push(view_light_entity);
|
|
}
|
|
|
|
// Subsequent views with the same light entity will **NOT** reuse the same shadow map
|
|
// (Because the cascades are unique to each view)
|
|
// TODO: Implement GPU culling for shadow passes.
|
|
shadow_render_phases
|
|
.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode);
|
|
live_shadow_mapping_lights.insert(retained_view_entity);
|
|
}
|
|
}
|
|
|
|
commands.entity(entity).insert((
|
|
ViewShadowBindings {
|
|
point_light_depth_texture: point_light_depth_texture.texture.clone(),
|
|
point_light_depth_texture_view: point_light_depth_texture_view.clone(),
|
|
directional_light_depth_texture: directional_light_depth_texture.texture.clone(),
|
|
directional_light_depth_texture_view: directional_light_depth_texture_view.clone(),
|
|
},
|
|
ViewLightEntities {
|
|
lights: view_lights,
|
|
},
|
|
ViewLightsUniformOffset {
|
|
offset: view_gpu_lights_writer.write(&gpu_lights),
|
|
},
|
|
));
|
|
|
|
// Make a link from the camera to all shadow cascades with occlusion
|
|
// culling enabled.
|
|
if !view_occlusion_culling_lights.is_empty() {
|
|
commands
|
|
.entity(entity)
|
|
.insert(OcclusionCullingSubviewEntities(
|
|
view_occlusion_culling_lights,
|
|
));
|
|
}
|
|
}
|
|
|
|
// Despawn light-view entities for views that no longer exist
|
|
for mut entities in &mut light_view_entities {
|
|
for (_, light_view_entities) in
|
|
entities.extract_if(|entity, _| !live_views.contains(entity))
|
|
{
|
|
despawn_entities(&mut commands, light_view_entities);
|
|
}
|
|
}
|
|
|
|
shadow_render_phases.retain(|entity, _| live_shadow_mapping_lights.contains(entity));
|
|
}
|
|
|
|
fn despawn_entities(commands: &mut Commands, entities: Vec<Entity>) {
|
|
if entities.is_empty() {
|
|
return;
|
|
}
|
|
commands.queue(move |world: &mut World| {
|
|
for entity in entities {
|
|
world.despawn(entity);
|
|
}
|
|
});
|
|
}
|
|
|
|
// These will be extracted in the material extraction, which will also clear the needs_specialization
|
|
// collection.
|
|
pub fn check_light_entities_needing_specialization<M: Material>(
|
|
needs_specialization: Query<Entity, (With<MeshMaterial3d<M>>, Changed<NotShadowCaster>)>,
|
|
mut entities_needing_specialization: ResMut<EntitiesNeedingSpecialization<M>>,
|
|
mut removed_components: RemovedComponents<NotShadowCaster>,
|
|
) {
|
|
for entity in &needs_specialization {
|
|
entities_needing_specialization.push(entity);
|
|
}
|
|
|
|
for removed in removed_components.read() {
|
|
entities_needing_specialization.entities.push(removed);
|
|
}
|
|
}
|
|
|
|
#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)]
|
|
pub struct LightKeyCache(HashMap<RetainedViewEntity, MeshPipelineKey>);
|
|
|
|
#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)]
|
|
pub struct LightSpecializationTicks(HashMap<RetainedViewEntity, Tick>);
|
|
|
|
#[derive(Resource, Deref, DerefMut)]
|
|
pub struct SpecializedShadowMaterialPipelineCache<M> {
|
|
// view light entity -> view pipeline cache
|
|
#[deref]
|
|
map: HashMap<RetainedViewEntity, SpecializedShadowMaterialViewPipelineCache<M>>,
|
|
marker: PhantomData<M>,
|
|
}
|
|
|
|
#[derive(Deref, DerefMut)]
|
|
pub struct SpecializedShadowMaterialViewPipelineCache<M> {
|
|
#[deref]
|
|
map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>,
|
|
marker: PhantomData<M>,
|
|
}
|
|
|
|
impl<M> Default for SpecializedShadowMaterialPipelineCache<M> {
|
|
fn default() -> Self {
|
|
Self {
|
|
map: HashMap::default(),
|
|
marker: PhantomData,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<M> Default for SpecializedShadowMaterialViewPipelineCache<M> {
|
|
fn default() -> Self {
|
|
Self {
|
|
map: MainEntityHashMap::default(),
|
|
marker: PhantomData,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn check_views_lights_need_specialization(
|
|
view_lights: Query<&ViewLightEntities, With<ExtractedView>>,
|
|
view_light_entities: Query<(&LightEntity, &ExtractedView)>,
|
|
shadow_render_phases: Res<ViewBinnedRenderPhases<Shadow>>,
|
|
mut light_key_cache: ResMut<LightKeyCache>,
|
|
mut light_specialization_ticks: ResMut<LightSpecializationTicks>,
|
|
ticks: SystemChangeTick,
|
|
) {
|
|
for view_lights in &view_lights {
|
|
for view_light_entity in view_lights.lights.iter().copied() {
|
|
let Ok((light_entity, extracted_view_light)) =
|
|
view_light_entities.get(view_light_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
if !shadow_render_phases.contains_key(&extracted_view_light.retained_view_entity) {
|
|
continue;
|
|
}
|
|
|
|
let is_directional_light = matches!(light_entity, LightEntity::Directional { .. });
|
|
let mut light_key = MeshPipelineKey::DEPTH_PREPASS;
|
|
light_key.set(MeshPipelineKey::UNCLIPPED_DEPTH_ORTHO, is_directional_light);
|
|
if let Some(current_key) =
|
|
light_key_cache.get_mut(&extracted_view_light.retained_view_entity)
|
|
{
|
|
if *current_key != light_key {
|
|
light_key_cache.insert(extracted_view_light.retained_view_entity, light_key);
|
|
light_specialization_ticks
|
|
.insert(extracted_view_light.retained_view_entity, ticks.this_run());
|
|
}
|
|
} else {
|
|
light_key_cache.insert(extracted_view_light.retained_view_entity, light_key);
|
|
light_specialization_ticks
|
|
.insert(extracted_view_light.retained_view_entity, ticks.this_run());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn specialize_shadows<M: Material>(
|
|
prepass_pipeline: Res<PrepassPipeline<M>>,
|
|
(
|
|
render_meshes,
|
|
render_mesh_instances,
|
|
render_materials,
|
|
render_material_instances,
|
|
material_bind_group_allocator,
|
|
): (
|
|
Res<RenderAssets<RenderMesh>>,
|
|
Res<RenderMeshInstances>,
|
|
Res<RenderAssets<PreparedMaterial<M>>>,
|
|
Res<RenderMaterialInstances>,
|
|
Res<MaterialBindGroupAllocator<M>>,
|
|
),
|
|
shadow_render_phases: Res<ViewBinnedRenderPhases<Shadow>>,
|
|
mut pipelines: ResMut<SpecializedMeshPipelines<PrepassPipeline<M>>>,
|
|
pipeline_cache: Res<PipelineCache>,
|
|
render_lightmaps: Res<RenderLightmaps>,
|
|
view_lights: Query<(Entity, &ViewLightEntities), With<ExtractedView>>,
|
|
view_light_entities: Query<(&LightEntity, &ExtractedView)>,
|
|
point_light_entities: Query<&RenderCubemapVisibleEntities, With<ExtractedPointLight>>,
|
|
directional_light_entities: Query<
|
|
&RenderCascadesVisibleEntities,
|
|
With<ExtractedDirectionalLight>,
|
|
>,
|
|
spot_light_entities: Query<&RenderVisibleMeshEntities, With<ExtractedPointLight>>,
|
|
light_key_cache: Res<LightKeyCache>,
|
|
mut specialized_material_pipeline_cache: ResMut<SpecializedShadowMaterialPipelineCache<M>>,
|
|
light_specialization_ticks: Res<LightSpecializationTicks>,
|
|
entity_specialization_ticks: Res<EntitySpecializationTicks<M>>,
|
|
ticks: SystemChangeTick,
|
|
) where
|
|
M::Data: PartialEq + Eq + Hash + Clone,
|
|
{
|
|
// Record the retained IDs of all shadow views so that we can expire old
|
|
// pipeline IDs.
|
|
let mut all_shadow_views: HashSet<RetainedViewEntity, FixedHasher> = HashSet::default();
|
|
|
|
for (entity, view_lights) in &view_lights {
|
|
for view_light_entity in view_lights.lights.iter().copied() {
|
|
let Ok((light_entity, extracted_view_light)) =
|
|
view_light_entities.get(view_light_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
all_shadow_views.insert(extracted_view_light.retained_view_entity);
|
|
|
|
if !shadow_render_phases.contains_key(&extracted_view_light.retained_view_entity) {
|
|
continue;
|
|
}
|
|
let Some(light_key) = light_key_cache.get(&extracted_view_light.retained_view_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
let visible_entities = match light_entity {
|
|
LightEntity::Directional {
|
|
light_entity,
|
|
cascade_index,
|
|
} => directional_light_entities
|
|
.get(*light_entity)
|
|
.expect("Failed to get directional light visible entities")
|
|
.entities
|
|
.get(&entity)
|
|
.expect("Failed to get directional light visible entities for view")
|
|
.get(*cascade_index)
|
|
.expect("Failed to get directional light visible entities for cascade"),
|
|
LightEntity::Point {
|
|
light_entity,
|
|
face_index,
|
|
} => point_light_entities
|
|
.get(*light_entity)
|
|
.expect("Failed to get point light visible entities")
|
|
.get(*face_index),
|
|
LightEntity::Spot { light_entity } => spot_light_entities
|
|
.get(*light_entity)
|
|
.expect("Failed to get spot light visible entities"),
|
|
};
|
|
|
|
// NOTE: Lights with shadow mapping disabled will have no visible entities
|
|
// so no meshes will be queued
|
|
|
|
let view_tick = light_specialization_ticks
|
|
.get(&extracted_view_light.retained_view_entity)
|
|
.unwrap();
|
|
let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache
|
|
.entry(extracted_view_light.retained_view_entity)
|
|
.or_default();
|
|
|
|
for (_, visible_entity) in visible_entities.iter().copied() {
|
|
let Some(material_instances) =
|
|
render_material_instances.instances.get(&visible_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
let Ok(material_asset_id) = material_instances.asset_id.try_typed::<M>() else {
|
|
continue;
|
|
};
|
|
let Some(mesh_instance) =
|
|
render_mesh_instances.render_mesh_queue_data(visible_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
let entity_tick = entity_specialization_ticks.get(&visible_entity).unwrap();
|
|
let last_specialized_tick = view_specialized_material_pipeline_cache
|
|
.get(&visible_entity)
|
|
.map(|(tick, _)| *tick);
|
|
let needs_specialization = last_specialized_tick.is_none_or(|tick| {
|
|
view_tick.is_newer_than(tick, ticks.this_run())
|
|
|| entity_tick.is_newer_than(tick, ticks.this_run())
|
|
});
|
|
if !needs_specialization {
|
|
continue;
|
|
}
|
|
let Some(material) = render_materials.get(material_asset_id) else {
|
|
continue;
|
|
};
|
|
if !mesh_instance
|
|
.flags
|
|
.contains(RenderMeshInstanceFlags::SHADOW_CASTER)
|
|
{
|
|
continue;
|
|
}
|
|
let Some(material_bind_group) =
|
|
material_bind_group_allocator.get(material.binding.group)
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {
|
|
continue;
|
|
};
|
|
|
|
let mut mesh_key =
|
|
*light_key | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits());
|
|
|
|
// Even though we don't use the lightmap in the shadow map, the
|
|
// `SetMeshBindGroup` render command will bind the data for it. So
|
|
// we need to include the appropriate flag in the mesh pipeline key
|
|
// to ensure that the necessary bind group layout entries are
|
|
// present.
|
|
if render_lightmaps
|
|
.render_lightmaps
|
|
.contains_key(&visible_entity)
|
|
{
|
|
mesh_key |= MeshPipelineKey::LIGHTMAPPED;
|
|
}
|
|
|
|
mesh_key |= match material.properties.alpha_mode {
|
|
AlphaMode::Mask(_)
|
|
| AlphaMode::Blend
|
|
| AlphaMode::Premultiplied
|
|
| AlphaMode::Add
|
|
| AlphaMode::AlphaToCoverage => MeshPipelineKey::MAY_DISCARD,
|
|
_ => MeshPipelineKey::NONE,
|
|
};
|
|
let pipeline_id = pipelines.specialize(
|
|
&pipeline_cache,
|
|
&prepass_pipeline,
|
|
MaterialPipelineKey {
|
|
mesh_key,
|
|
bind_group_data: material_bind_group
|
|
.get_extra_data(material.binding.slot)
|
|
.clone(),
|
|
},
|
|
&mesh.layout,
|
|
);
|
|
|
|
let pipeline_id = match pipeline_id {
|
|
Ok(id) => id,
|
|
Err(err) => {
|
|
error!("{}", err);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
view_specialized_material_pipeline_cache
|
|
.insert(visible_entity, (ticks.this_run(), pipeline_id));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete specialized pipelines belonging to views that have expired.
|
|
specialized_material_pipeline_cache.retain(|view, _| all_shadow_views.contains(view));
|
|
}
|
|
|
|
/// For each shadow cascade, iterates over all the meshes "visible" from it and
|
|
/// adds them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as
|
|
/// appropriate.
|
|
pub fn queue_shadows<M: Material>(
|
|
shadow_draw_functions: Res<DrawFunctions<Shadow>>,
|
|
render_mesh_instances: Res<RenderMeshInstances>,
|
|
render_materials: Res<RenderAssets<PreparedMaterial<M>>>,
|
|
render_material_instances: Res<RenderMaterialInstances>,
|
|
mut shadow_render_phases: ResMut<ViewBinnedRenderPhases<Shadow>>,
|
|
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
|
|
mesh_allocator: Res<MeshAllocator>,
|
|
view_lights: Query<(Entity, &ViewLightEntities), With<ExtractedView>>,
|
|
view_light_entities: Query<(&LightEntity, &ExtractedView)>,
|
|
point_light_entities: Query<&RenderCubemapVisibleEntities, With<ExtractedPointLight>>,
|
|
directional_light_entities: Query<
|
|
&RenderCascadesVisibleEntities,
|
|
With<ExtractedDirectionalLight>,
|
|
>,
|
|
spot_light_entities: Query<&RenderVisibleMeshEntities, With<ExtractedPointLight>>,
|
|
specialized_material_pipeline_cache: Res<SpecializedShadowMaterialPipelineCache<M>>,
|
|
) where
|
|
M::Data: PartialEq + Eq + Hash + Clone,
|
|
{
|
|
let draw_shadow_mesh = shadow_draw_functions.read().id::<DrawPrepass<M>>();
|
|
for (entity, view_lights) in &view_lights {
|
|
for view_light_entity in view_lights.lights.iter().copied() {
|
|
let Ok((light_entity, extracted_view_light)) =
|
|
view_light_entities.get(view_light_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(shadow_phase) =
|
|
shadow_render_phases.get_mut(&extracted_view_light.retained_view_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
let Some(view_specialized_material_pipeline_cache) =
|
|
specialized_material_pipeline_cache.get(&extracted_view_light.retained_view_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
let visible_entities = match light_entity {
|
|
LightEntity::Directional {
|
|
light_entity,
|
|
cascade_index,
|
|
} => directional_light_entities
|
|
.get(*light_entity)
|
|
.expect("Failed to get directional light visible entities")
|
|
.entities
|
|
.get(&entity)
|
|
.expect("Failed to get directional light visible entities for view")
|
|
.get(*cascade_index)
|
|
.expect("Failed to get directional light visible entities for cascade"),
|
|
LightEntity::Point {
|
|
light_entity,
|
|
face_index,
|
|
} => point_light_entities
|
|
.get(*light_entity)
|
|
.expect("Failed to get point light visible entities")
|
|
.get(*face_index),
|
|
LightEntity::Spot { light_entity } => spot_light_entities
|
|
.get(*light_entity)
|
|
.expect("Failed to get spot light visible entities"),
|
|
};
|
|
|
|
for (entity, main_entity) in visible_entities.iter().copied() {
|
|
let Some((current_change_tick, pipeline_id)) =
|
|
view_specialized_material_pipeline_cache.get(&main_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
// Skip the entity if it's cached in a bin and up to date.
|
|
if shadow_phase.validate_cached_entity(main_entity, *current_change_tick) {
|
|
continue;
|
|
}
|
|
|
|
let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(main_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
if !mesh_instance
|
|
.flags
|
|
.contains(RenderMeshInstanceFlags::SHADOW_CASTER)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
let Some(material_instance) = render_material_instances.instances.get(&main_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
let Ok(material_asset_id) = material_instance.asset_id.try_typed::<M>() else {
|
|
continue;
|
|
};
|
|
let Some(material) = render_materials.get(material_asset_id) else {
|
|
continue;
|
|
};
|
|
|
|
let (vertex_slab, index_slab) =
|
|
mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id);
|
|
|
|
let batch_set_key = ShadowBatchSetKey {
|
|
pipeline: *pipeline_id,
|
|
draw_function: draw_shadow_mesh,
|
|
material_bind_group_index: Some(material.binding.group.0),
|
|
vertex_slab: vertex_slab.unwrap_or_default(),
|
|
index_slab,
|
|
};
|
|
|
|
shadow_phase.add(
|
|
batch_set_key,
|
|
ShadowBinKey {
|
|
asset_id: mesh_instance.mesh_asset_id.into(),
|
|
},
|
|
(entity, main_entity),
|
|
mesh_instance.current_uniform_index,
|
|
BinnedRenderPhaseType::mesh(
|
|
mesh_instance.should_batch(),
|
|
&gpu_preprocessing_support,
|
|
),
|
|
*current_change_tick,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Shadow {
|
|
/// Determines which objects can be placed into a *batch set*.
|
|
///
|
|
/// Objects in a single batch set can potentially be multi-drawn together,
|
|
/// if it's enabled and the current platform supports it.
|
|
pub batch_set_key: ShadowBatchSetKey,
|
|
/// Information that separates items into bins.
|
|
pub bin_key: ShadowBinKey,
|
|
pub representative_entity: (Entity, MainEntity),
|
|
pub batch_range: Range<u32>,
|
|
pub extra_index: PhaseItemExtraIndex,
|
|
}
|
|
|
|
/// Information that must be identical in order to place opaque meshes in the
|
|
/// same *batch set*.
|
|
///
|
|
/// A batch set is a set of batches that can be multi-drawn together, if
|
|
/// multi-draw is in use.
|
|
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
pub struct ShadowBatchSetKey {
|
|
/// The identifier of the render pipeline.
|
|
pub pipeline: CachedRenderPipelineId,
|
|
|
|
/// The function used to draw.
|
|
pub draw_function: DrawFunctionId,
|
|
|
|
/// The ID of a bind group specific to the material.
|
|
///
|
|
/// In the case of PBR, this is the `MaterialBindGroupIndex`.
|
|
pub material_bind_group_index: Option<u32>,
|
|
|
|
/// The ID of the slab of GPU memory that contains vertex data.
|
|
///
|
|
/// For non-mesh items, you can fill this with 0 if your items can be
|
|
/// multi-drawn, or with a unique value if they can't.
|
|
pub vertex_slab: SlabId,
|
|
|
|
/// The ID of the slab of GPU memory that contains index data, if present.
|
|
///
|
|
/// For non-mesh items, you can safely fill this with `None`.
|
|
pub index_slab: Option<SlabId>,
|
|
}
|
|
|
|
impl PhaseItemBatchSetKey for ShadowBatchSetKey {
|
|
fn indexed(&self) -> bool {
|
|
self.index_slab.is_some()
|
|
}
|
|
}
|
|
|
|
/// Data used to bin each object in the shadow map phase.
|
|
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
pub struct ShadowBinKey {
|
|
/// The object.
|
|
pub asset_id: UntypedAssetId,
|
|
}
|
|
|
|
impl PhaseItem for Shadow {
|
|
#[inline]
|
|
fn entity(&self) -> Entity {
|
|
self.representative_entity.0
|
|
}
|
|
|
|
fn main_entity(&self) -> MainEntity {
|
|
self.representative_entity.1
|
|
}
|
|
|
|
#[inline]
|
|
fn draw_function(&self) -> DrawFunctionId {
|
|
self.batch_set_key.draw_function
|
|
}
|
|
|
|
#[inline]
|
|
fn batch_range(&self) -> &Range<u32> {
|
|
&self.batch_range
|
|
}
|
|
|
|
#[inline]
|
|
fn batch_range_mut(&mut self) -> &mut Range<u32> {
|
|
&mut self.batch_range
|
|
}
|
|
|
|
#[inline]
|
|
fn extra_index(&self) -> PhaseItemExtraIndex {
|
|
self.extra_index.clone()
|
|
}
|
|
|
|
#[inline]
|
|
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
|
|
(&mut self.batch_range, &mut self.extra_index)
|
|
}
|
|
}
|
|
|
|
impl BinnedPhaseItem for Shadow {
|
|
type BatchSetKey = ShadowBatchSetKey;
|
|
type BinKey = ShadowBinKey;
|
|
|
|
#[inline]
|
|
fn new(
|
|
batch_set_key: Self::BatchSetKey,
|
|
bin_key: Self::BinKey,
|
|
representative_entity: (Entity, MainEntity),
|
|
batch_range: Range<u32>,
|
|
extra_index: PhaseItemExtraIndex,
|
|
) -> Self {
|
|
Shadow {
|
|
batch_set_key,
|
|
bin_key,
|
|
representative_entity,
|
|
batch_range,
|
|
extra_index,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CachedRenderPipelinePhaseItem for Shadow {
|
|
#[inline]
|
|
fn cached_pipeline(&self) -> CachedRenderPipelineId {
|
|
self.batch_set_key.pipeline
|
|
}
|
|
}
|
|
|
|
/// The rendering node that renders meshes that were "visible" (so to speak)
|
|
/// from a light last frame.
|
|
///
|
|
/// If occlusion culling for a light is disabled, then this node simply renders
|
|
/// all meshes in range of the light.
|
|
#[derive(Deref, DerefMut)]
|
|
pub struct EarlyShadowPassNode(ShadowPassNode);
|
|
|
|
/// The rendering node that renders meshes that became newly "visible" (so to
|
|
/// speak) from a light this frame.
|
|
///
|
|
/// If occlusion culling for a light is disabled, then this node does nothing.
|
|
#[derive(Deref, DerefMut)]
|
|
pub struct LateShadowPassNode(ShadowPassNode);
|
|
|
|
/// Encapsulates rendering logic shared between the early and late shadow pass
|
|
/// nodes.
|
|
pub struct ShadowPassNode {
|
|
/// The query that finds cameras in which shadows are visible.
|
|
main_view_query: QueryState<Read<ViewLightEntities>>,
|
|
/// The query that finds shadow cascades.
|
|
view_light_query: QueryState<(Read<ShadowView>, Read<ExtractedView>, Has<OcclusionCulling>)>,
|
|
}
|
|
|
|
impl FromWorld for EarlyShadowPassNode {
|
|
fn from_world(world: &mut World) -> Self {
|
|
Self(ShadowPassNode::from_world(world))
|
|
}
|
|
}
|
|
|
|
impl FromWorld for LateShadowPassNode {
|
|
fn from_world(world: &mut World) -> Self {
|
|
Self(ShadowPassNode::from_world(world))
|
|
}
|
|
}
|
|
|
|
impl FromWorld for ShadowPassNode {
|
|
fn from_world(world: &mut World) -> Self {
|
|
Self {
|
|
main_view_query: QueryState::new(world),
|
|
view_light_query: QueryState::new(world),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Node for EarlyShadowPassNode {
|
|
fn update(&mut self, world: &mut World) {
|
|
self.0.update(world);
|
|
}
|
|
|
|
fn run<'w>(
|
|
&self,
|
|
graph: &mut RenderGraphContext,
|
|
render_context: &mut RenderContext<'w>,
|
|
world: &'w World,
|
|
) -> Result<(), NodeRunError> {
|
|
self.0.run(graph, render_context, world, false)
|
|
}
|
|
}
|
|
|
|
impl Node for LateShadowPassNode {
|
|
fn update(&mut self, world: &mut World) {
|
|
self.0.update(world);
|
|
}
|
|
|
|
fn run<'w>(
|
|
&self,
|
|
graph: &mut RenderGraphContext,
|
|
render_context: &mut RenderContext<'w>,
|
|
world: &'w World,
|
|
) -> Result<(), NodeRunError> {
|
|
self.0.run(graph, render_context, world, true)
|
|
}
|
|
}
|
|
|
|
impl ShadowPassNode {
|
|
fn update(&mut self, world: &mut World) {
|
|
self.main_view_query.update_archetypes(world);
|
|
self.view_light_query.update_archetypes(world);
|
|
}
|
|
|
|
/// Runs the node logic.
|
|
///
|
|
/// `is_late` is true if this is the late shadow pass or false if this is
|
|
/// the early shadow pass.
|
|
fn run<'w>(
|
|
&self,
|
|
graph: &mut RenderGraphContext,
|
|
render_context: &mut RenderContext<'w>,
|
|
world: &'w World,
|
|
is_late: bool,
|
|
) -> Result<(), NodeRunError> {
|
|
let Some(shadow_render_phases) = world.get_resource::<ViewBinnedRenderPhases<Shadow>>()
|
|
else {
|
|
return Ok(());
|
|
};
|
|
|
|
if let Ok(view_lights) = self.main_view_query.get_manual(world, graph.view_entity()) {
|
|
for view_light_entity in view_lights.lights.iter().copied() {
|
|
let Ok((view_light, extracted_light_view, occlusion_culling)) =
|
|
self.view_light_query.get_manual(world, view_light_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
// There's no need for a late shadow pass if the light isn't
|
|
// using occlusion culling.
|
|
if is_late && !occlusion_culling {
|
|
continue;
|
|
}
|
|
|
|
let Some(shadow_phase) =
|
|
shadow_render_phases.get(&extracted_light_view.retained_view_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
let depth_stencil_attachment =
|
|
Some(view_light.depth_attachment.get_attachment(StoreOp::Store));
|
|
|
|
let diagnostics = render_context.diagnostic_recorder();
|
|
render_context.add_command_buffer_generation_task(move |render_device| {
|
|
#[cfg(feature = "trace")]
|
|
let _shadow_pass_span = info_span!("", "{}", view_light.pass_name).entered();
|
|
let mut command_encoder =
|
|
render_device.create_command_encoder(&CommandEncoderDescriptor {
|
|
label: Some("shadow_pass_command_encoder"),
|
|
});
|
|
|
|
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
|
|
label: Some(&view_light.pass_name),
|
|
color_attachments: &[],
|
|
depth_stencil_attachment,
|
|
timestamp_writes: None,
|
|
occlusion_query_set: None,
|
|
});
|
|
|
|
let mut render_pass = TrackedRenderPass::new(&render_device, render_pass);
|
|
let pass_span =
|
|
diagnostics.pass_span(&mut render_pass, view_light.pass_name.clone());
|
|
|
|
if let Err(err) =
|
|
shadow_phase.render(&mut render_pass, world, view_light_entity)
|
|
{
|
|
error!("Error encountered while rendering the shadow phase {err:?}");
|
|
}
|
|
|
|
pass_span.end(&mut render_pass);
|
|
drop(render_pass);
|
|
command_encoder.finish()
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|