 599e5e4e76
			
		
	
	
		599e5e4e76
		
			
		
	
	
	
	
		
			
			# Objective - As part of the migration process we need to a) see the end effect of the migration on user ergonomics b) check for serious perf regressions c) actually migrate the code - To accomplish this, I'm going to attempt to migrate all of the remaining user-facing usages of `LegacyColor` in one PR, being careful to keep a clean commit history. - Fixes #12056. ## Solution I've chosen to use the polymorphic `Color` type as our standard user-facing API. - [x] Migrate `bevy_gizmos`. - [x] Take `impl Into<Color>` in all `bevy_gizmos` APIs - [x] Migrate sprites - [x] Migrate UI - [x] Migrate `ColorMaterial` - [x] Migrate `MaterialMesh2D` - [x] Migrate fog - [x] Migrate lights - [x] Migrate StandardMaterial - [x] Migrate wireframes - [x] Migrate clear color - [x] Migrate text - [x] Migrate gltf loader - [x] Register color types for reflection - [x] Remove `LegacyColor` - [x] Make sure CI passes Incidental improvements to ease migration: - added `Color::srgba_u8`, `Color::srgba_from_array` and friends - added `set_alpha`, `is_fully_transparent` and `is_fully_opaque` to the `Alpha` trait - add and immediately deprecate (lol) `Color::rgb` and friends in favor of more explicit and consistent `Color::srgb` - standardized on white and black for most example text colors - added vector field traits to `LinearRgba`: ~~`Add`, `Sub`, `AddAssign`, `SubAssign`,~~ `Mul<f32>` and `Div<f32>`. Multiplications and divisions do not scale alpha. `Add` and `Sub` have been cut from this PR. - added `LinearRgba` and `Srgba` `RED/GREEN/BLUE` - added `LinearRgba_to_f32_array` and `LinearRgba::to_u32` ## Migration Guide Bevy's color types have changed! Wherever you used a `bevy::render::Color`, a `bevy::color::Color` is used instead. These are quite similar! Both are enums storing a color in a specific color space (or to be more precise, using a specific color model). However, each of the different color models now has its own type. TODO... - `Color::rgba`, `Color::rgb`, `Color::rbga_u8`, `Color::rgb_u8`, `Color::rgb_from_array` are now `Color::srgba`, `Color::srgb`, `Color::srgba_u8`, `Color::srgb_u8` and `Color::srgb_from_array`. - `Color::set_a` and `Color::a` is now `Color::set_alpha` and `Color::alpha`. These are part of the `Alpha` trait in `bevy_color`. - `Color::is_fully_transparent` is now part of the `Alpha` trait in `bevy_color` - `Color::r`, `Color::set_r`, `Color::with_r` and the equivalents for `g`, `b` `h`, `s` and `l` have been removed due to causing silent relatively expensive conversions. Convert your `Color` into the desired color space, perform your operations there, and then convert it back into a polymorphic `Color` enum. - `Color::hex` is now `Srgba::hex`. Call `.into` or construct a `Color::Srgba` variant manually to convert it. - `WireframeMaterial`, `ExtractedUiNode`, `ExtractedDirectionalLight`, `ExtractedPointLight`, `ExtractedSpotLight` and `ExtractedSprite` now store a `LinearRgba`, rather than a polymorphic `Color` - `Color::rgb_linear` and `Color::rgba_linear` are now `Color::linear_rgb` and `Color::linear_rgba` - The various CSS color constants are no longer stored directly on `Color`. Instead, they're defined in the `Srgba` color space, and accessed via `bevy::color::palettes::css`. Call `.into()` on them to convert them into a `Color` for quick debugging use, and consider using the much prettier `tailwind` palette for prototyping. - The `LIME_GREEN` color has been renamed to `LIMEGREEN` to comply with the standard naming. - Vector field arithmetic operations on `Color` (add, subtract, multiply and divide by a f32) have been removed. Instead, convert your colors into `LinearRgba` space, and perform your operations explicitly there. This is particularly relevant when working with emissive or HDR colors, whose color channel values are routinely outside of the ordinary 0 to 1 range. - `Color::as_linear_rgba_f32` has been removed. Call `LinearRgba::to_f32_array` instead, converting if needed. - `Color::as_linear_rgba_u32` has been removed. Call `LinearRgba::to_u32` instead, converting if needed. - Several other color conversion methods to transform LCH or HSL colors into float arrays or `Vec` types have been removed. Please reimplement these externally or open a PR to re-add them if you found them particularly useful. - Various methods on `Color` such as `rgb` or `hsl` to convert the color into a specific color space have been removed. Convert into `LinearRgba`, then to the color space of your choice. - Various implicitly-converting color value methods on `Color` such as `r`, `g`, `b` or `h` have been removed. Please convert it into the color space of your choice, then check these properties. - `Color` no longer implements `AsBindGroup`. Store a `LinearRgba` internally instead to avoid conversion costs. --------- Co-authored-by: Alice Cecile <alice.i.cecil@gmail.com> Co-authored-by: Afonso Lage <lage.afonso@gmail.com> Co-authored-by: Rob Parrett <robparrett@gmail.com> Co-authored-by: Zachary Harrold <zac@harrold.com.au>
		
			
				
	
	
		
			1849 lines
		
	
	
		
			69 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			1849 lines
		
	
	
		
			69 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use bevy_core_pipeline::core_3d::{Transparent3d, CORE_3D_DEPTH_FORMAT};
 | |
| use bevy_ecs::entity::EntityHashMap;
 | |
| use bevy_ecs::prelude::*;
 | |
| use bevy_math::{Mat4, UVec3, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};
 | |
| use bevy_render::{
 | |
|     camera::Camera,
 | |
|     mesh::Mesh,
 | |
|     primitives::{CascadesFrusta, CubemapFrusta, Frustum},
 | |
|     render_asset::RenderAssets,
 | |
|     render_graph::{Node, NodeRunError, RenderGraphContext},
 | |
|     render_phase::*,
 | |
|     render_resource::*,
 | |
|     renderer::{RenderContext, RenderDevice, RenderQueue},
 | |
|     texture::*,
 | |
|     view::{ExtractedView, RenderLayers, ViewVisibility, VisibleEntities},
 | |
|     Extract,
 | |
| };
 | |
| use bevy_transform::{components::GlobalTransform, prelude::Transform};
 | |
| #[cfg(feature = "trace")]
 | |
| use bevy_utils::tracing::info_span;
 | |
| use bevy_utils::{
 | |
|     nonmax::NonMaxU32,
 | |
|     tracing::{error, warn},
 | |
| };
 | |
| use std::{hash::Hash, num::NonZeroU64, ops::Range};
 | |
| 
 | |
| use crate::*;
 | |
| 
 | |
| #[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 spot_light_angles: Option<(f32, f32)>,
 | |
| }
 | |
| 
 | |
| #[derive(Component, Debug)]
 | |
| pub struct ExtractedDirectionalLight {
 | |
|     pub color: LinearRgba,
 | |
|     pub illuminance: f32,
 | |
|     pub transform: GlobalTransform,
 | |
|     pub shadows_enabled: 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,
 | |
| }
 | |
| 
 | |
| #[derive(Copy, Clone, ShaderType, Default, Debug)]
 | |
| pub struct GpuPointLight {
 | |
|     // For point lights: the lower-right 2x2 values of the projection matrix [2][2] [2][3] [3][2] [3][3]
 | |
|     // For spot lights: 2 components of the direction (x,z), spot_scale and spot_offset
 | |
|     light_custom_data: Vec4,
 | |
|     color_inverse_square_range: Vec4,
 | |
|     position_radius: Vec4,
 | |
|     flags: u32,
 | |
|     shadow_depth_bias: f32,
 | |
|     shadow_normal_bias: f32,
 | |
|     spot_light_tan_angle: f32,
 | |
| }
 | |
| 
 | |
| #[derive(ShaderType)]
 | |
| pub struct GpuPointLightsUniform {
 | |
|     data: Box<[GpuPointLight; MAX_UNIFORM_BUFFER_POINT_LIGHTS]>,
 | |
| }
 | |
| 
 | |
| impl Default for GpuPointLightsUniform {
 | |
|     fn default() -> Self {
 | |
|         Self {
 | |
|             data: Box::new([GpuPointLight::default(); MAX_UNIFORM_BUFFER_POINT_LIGHTS]),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(ShaderType, Default)]
 | |
| pub struct GpuPointLightsStorage {
 | |
|     #[size(runtime)]
 | |
|     data: Vec<GpuPointLight>,
 | |
| }
 | |
| 
 | |
| pub enum GpuPointLights {
 | |
|     Uniform(UniformBuffer<GpuPointLightsUniform>),
 | |
|     Storage(StorageBuffer<GpuPointLightsStorage>),
 | |
| }
 | |
| 
 | |
| impl GpuPointLights {
 | |
|     fn new(buffer_binding_type: BufferBindingType) -> Self {
 | |
|         match buffer_binding_type {
 | |
|             BufferBindingType::Storage { .. } => Self::storage(),
 | |
|             BufferBindingType::Uniform => Self::uniform(),
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     fn uniform() -> Self {
 | |
|         Self::Uniform(UniformBuffer::default())
 | |
|     }
 | |
| 
 | |
|     fn storage() -> Self {
 | |
|         Self::Storage(StorageBuffer::default())
 | |
|     }
 | |
| 
 | |
|     fn set(&mut self, mut lights: Vec<GpuPointLight>) {
 | |
|         match self {
 | |
|             GpuPointLights::Uniform(buffer) => {
 | |
|                 let len = lights.len().min(MAX_UNIFORM_BUFFER_POINT_LIGHTS);
 | |
|                 let src = &lights[..len];
 | |
|                 let dst = &mut buffer.get_mut().data[..len];
 | |
|                 dst.copy_from_slice(src);
 | |
|             }
 | |
|             GpuPointLights::Storage(buffer) => {
 | |
|                 buffer.get_mut().data.clear();
 | |
|                 buffer.get_mut().data.append(&mut lights);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     fn write_buffer(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) {
 | |
|         match self {
 | |
|             GpuPointLights::Uniform(buffer) => buffer.write_buffer(render_device, render_queue),
 | |
|             GpuPointLights::Storage(buffer) => buffer.write_buffer(render_device, render_queue),
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     pub fn binding(&self) -> Option<BindingResource> {
 | |
|         match self {
 | |
|             GpuPointLights::Uniform(buffer) => buffer.binding(),
 | |
|             GpuPointLights::Storage(buffer) => buffer.binding(),
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     pub fn min_size(buffer_binding_type: BufferBindingType) -> NonZeroU64 {
 | |
|         match buffer_binding_type {
 | |
|             BufferBindingType::Storage { .. } => GpuPointLightsStorage::min_size(),
 | |
|             BufferBindingType::Uniform => GpuPointLightsUniform::min_size(),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| // 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 NONE                       = 0;
 | |
|         const UNINITIALIZED              = 0xFFFF;
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(Copy, Clone, ShaderType, Default, Debug)]
 | |
| pub struct GpuDirectionalCascade {
 | |
|     view_projection: 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,
 | |
|     shadow_depth_bias: f32,
 | |
|     shadow_normal_bias: f32,
 | |
|     num_cascades: u32,
 | |
|     cascades_overlap_proportion: f32,
 | |
|     depth_texture_base_index: u32,
 | |
|     render_layers: 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 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,
 | |
| }
 | |
| 
 | |
| // NOTE: this must be kept in sync with the same constants in pbr.frag
 | |
| pub const MAX_UNIFORM_BUFFER_POINT_LIGHTS: usize = 256;
 | |
| 
 | |
| //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_sampler: Sampler,
 | |
|     pub directional_light_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>();
 | |
| 
 | |
|         ShadowSamplers {
 | |
|             point_light_sampler: render_device.create_sampler(&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,
 | |
|                 compare: Some(CompareFunction::GreaterEqual),
 | |
|                 ..Default::default()
 | |
|             }),
 | |
|             directional_light_sampler: render_device.create_sampler(&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,
 | |
|                 compare: Some(CompareFunction::GreaterEqual),
 | |
|                 ..Default::default()
 | |
|             }),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(Component)]
 | |
| pub struct ExtractedClusterConfig {
 | |
|     /// Special near value for cluster calculations
 | |
|     near: f32,
 | |
|     far: f32,
 | |
|     /// Number of clusters in `X` / `Y` / `Z` in the view frustum
 | |
|     dimensions: UVec3,
 | |
| }
 | |
| 
 | |
| enum ExtractedClustersPointLightsElement {
 | |
|     ClusterHeader(u32, u32),
 | |
|     LightEntity(Entity),
 | |
| }
 | |
| 
 | |
| #[derive(Component)]
 | |
| pub struct ExtractedClustersPointLights {
 | |
|     data: Vec<ExtractedClustersPointLightsElement>,
 | |
| }
 | |
| 
 | |
| pub fn extract_clusters(
 | |
|     mut commands: Commands,
 | |
|     views: Extract<Query<(Entity, &Clusters, &Camera)>>,
 | |
| ) {
 | |
|     for (entity, clusters, camera) in &views {
 | |
|         if !camera.is_active {
 | |
|             continue;
 | |
|         }
 | |
| 
 | |
|         let num_entities: usize = clusters.lights.iter().map(|l| l.entities.len()).sum();
 | |
|         let mut data = Vec::with_capacity(clusters.lights.len() + num_entities);
 | |
|         for cluster_lights in &clusters.lights {
 | |
|             data.push(ExtractedClustersPointLightsElement::ClusterHeader(
 | |
|                 cluster_lights.point_light_count as u32,
 | |
|                 cluster_lights.spot_light_count as u32,
 | |
|             ));
 | |
|             for l in &cluster_lights.entities {
 | |
|                 data.push(ExtractedClustersPointLightsElement::LightEntity(*l));
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         commands.get_or_spawn(entity).insert((
 | |
|             ExtractedClustersPointLights { data },
 | |
|             ExtractedClusterConfig {
 | |
|                 near: clusters.near,
 | |
|                 far: clusters.far,
 | |
|                 dimensions: clusters.dimensions,
 | |
|             },
 | |
|         ));
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[allow(clippy::too_many_arguments)]
 | |
| pub fn extract_lights(
 | |
|     mut commands: Commands,
 | |
|     point_light_shadow_map: Extract<Res<PointLightShadowMap>>,
 | |
|     directional_light_shadow_map: Extract<Res<DirectionalLightShadowMap>>,
 | |
|     global_point_lights: Extract<Res<GlobalVisiblePointLights>>,
 | |
|     point_lights: Extract<
 | |
|         Query<(
 | |
|             &PointLight,
 | |
|             &CubemapVisibleEntities,
 | |
|             &GlobalTransform,
 | |
|             &ViewVisibility,
 | |
|             &CubemapFrusta,
 | |
|         )>,
 | |
|     >,
 | |
|     spot_lights: Extract<
 | |
|         Query<(
 | |
|             &SpotLight,
 | |
|             &VisibleEntities,
 | |
|             &GlobalTransform,
 | |
|             &ViewVisibility,
 | |
|             &Frustum,
 | |
|         )>,
 | |
|     >,
 | |
|     directional_lights: Extract<
 | |
|         Query<
 | |
|             (
 | |
|                 Entity,
 | |
|                 &DirectionalLight,
 | |
|                 &CascadesVisibleEntities,
 | |
|                 &Cascades,
 | |
|                 &CascadeShadowConfig,
 | |
|                 &CascadesFrusta,
 | |
|                 &GlobalTransform,
 | |
|                 &ViewVisibility,
 | |
|                 Option<&RenderLayers>,
 | |
|             ),
 | |
|             Without<SpotLight>,
 | |
|         >,
 | |
|     >,
 | |
|     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());
 | |
|     }
 | |
|     // 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_point_lights.iter().copied() {
 | |
|         let Ok((point_light, cubemap_visible_entities, transform, view_visibility, frusta)) =
 | |
|             point_lights.get(entity)
 | |
|         else {
 | |
|             continue;
 | |
|         };
 | |
|         if !view_visibility.get() {
 | |
|             continue;
 | |
|         }
 | |
|         // TODO: This is very much not ideal. We should be able to re-use the vector memory.
 | |
|         // However, since exclusive access to the main world in extract is ill-advised, we just clone here.
 | |
|         let render_cubemap_visible_entities = cubemap_visible_entities.clone();
 | |
|         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 * std::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
 | |
|                 * std::f32::consts::SQRT_2,
 | |
|             spot_light_angles: None,
 | |
|         };
 | |
|         point_lights_values.push((
 | |
|             entity,
 | |
|             (
 | |
|                 extracted_point_light,
 | |
|                 render_cubemap_visible_entities,
 | |
|                 (*frusta).clone(),
 | |
|             ),
 | |
|         ));
 | |
|     }
 | |
|     *previous_point_lights_len = point_lights_values.len();
 | |
|     commands.insert_or_spawn_batch(point_lights_values);
 | |
| 
 | |
|     let mut spot_lights_values = Vec::with_capacity(*previous_spot_lights_len);
 | |
|     for entity in global_point_lights.iter().copied() {
 | |
|         if let Ok((spot_light, visible_entities, transform, view_visibility, frustum)) =
 | |
|             spot_lights.get(entity)
 | |
|         {
 | |
|             if !view_visibility.get() {
 | |
|                 continue;
 | |
|             }
 | |
|             // TODO: This is very much not ideal. We should be able to re-use the vector memory.
 | |
|             // However, since exclusive access to the main world in extract is ill-advised, we just clone here.
 | |
|             let render_visible_entities = visible_entities.clone();
 | |
|             let texel_size =
 | |
|                 2.0 * spot_light.outer_angle.tan() / directional_light_shadow_map.size as f32;
 | |
| 
 | |
|             spot_lights_values.push((
 | |
|                 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 * std::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
 | |
|                             * std::f32::consts::SQRT_2,
 | |
|                         spot_light_angles: Some((spot_light.inner_angle, spot_light.outer_angle)),
 | |
|                     },
 | |
|                     render_visible_entities,
 | |
|                     *frustum,
 | |
|                 ),
 | |
|             ));
 | |
|         }
 | |
|     }
 | |
|     *previous_spot_lights_len = spot_lights_values.len();
 | |
|     commands.insert_or_spawn_batch(spot_lights_values);
 | |
| 
 | |
|     for (
 | |
|         entity,
 | |
|         directional_light,
 | |
|         visible_entities,
 | |
|         cascades,
 | |
|         cascade_config,
 | |
|         frusta,
 | |
|         transform,
 | |
|         view_visibility,
 | |
|         maybe_layers,
 | |
|     ) in &directional_lights
 | |
|     {
 | |
|         if !view_visibility.get() {
 | |
|             continue;
 | |
|         }
 | |
| 
 | |
|         // TODO: As above
 | |
|         let render_visible_entities = visible_entities.clone();
 | |
|         commands.get_or_spawn(entity).insert((
 | |
|             ExtractedDirectionalLight {
 | |
|                 color: directional_light.color.into(),
 | |
|                 illuminance: directional_light.illuminance,
 | |
|                 transform: *transform,
 | |
|                 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 * std::f32::consts::SQRT_2,
 | |
|                 cascade_shadow_config: cascade_config.clone(),
 | |
|                 cascades: cascades.cascades.clone(),
 | |
|                 frusta: frusta.frusta.clone(),
 | |
|                 render_layers: maybe_layers.copied().unwrap_or_default(),
 | |
|             },
 | |
|             render_visible_entities,
 | |
|         ));
 | |
|     }
 | |
| }
 | |
| 
 | |
| pub(crate) const POINT_LIGHT_NEAR_Z: f32 = 0.1f32;
 | |
| 
 | |
| 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,
 | |
| }
 | |
| 
 | |
| #[derive(Component)]
 | |
| pub struct ViewLightEntities {
 | |
|     pub lights: Vec<Entity>,
 | |
| }
 | |
| 
 | |
| #[derive(Component)]
 | |
| pub struct ViewLightsUniformOffset {
 | |
|     pub offset: u32,
 | |
| }
 | |
| 
 | |
| // NOTE: Clustered-forward rendering requires 3 storage buffer bindings so check that
 | |
| // at least that many are supported using this constant and SupportedBindingType::from_device()
 | |
| pub const CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT: u32 = 3;
 | |
| 
 | |
| #[derive(Resource)]
 | |
| pub struct GlobalLightMeta {
 | |
|     pub gpu_point_lights: GpuPointLights,
 | |
|     pub entity_to_index: EntityHashMap<usize>,
 | |
| }
 | |
| 
 | |
| impl FromWorld for GlobalLightMeta {
 | |
|     fn from_world(world: &mut World) -> Self {
 | |
|         Self::new(
 | |
|             world
 | |
|                 .resource::<RenderDevice>()
 | |
|                 .get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT),
 | |
|         )
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl GlobalLightMeta {
 | |
|     pub fn new(buffer_binding_type: BufferBindingType) -> Self {
 | |
|         Self {
 | |
|             gpu_point_lights: GpuPointLights::new(buffer_binding_type),
 | |
|             entity_to_index: EntityHashMap::default(),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[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) / (far / near).ln();
 | |
|         Vec2::new(
 | |
|             z_slices_of_ln_zfar_over_znear,
 | |
|             near.ln() * 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_view_matrix(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_projection_matrix(angle: 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, POINT_LIGHT_NEAR_Z)
 | |
| }
 | |
| 
 | |
| #[allow(clippy::too_many_arguments)]
 | |
| pub fn prepare_lights(
 | |
|     mut commands: Commands,
 | |
|     mut texture_cache: ResMut<TextureCache>,
 | |
|     render_device: Res<RenderDevice>,
 | |
|     render_queue: Res<RenderQueue>,
 | |
|     mut global_light_meta: ResMut<GlobalLightMeta>,
 | |
|     mut light_meta: ResMut<LightMeta>,
 | |
|     views: Query<
 | |
|         (Entity, &ExtractedView, &ExtractedClusterConfig),
 | |
|         With<RenderPhase<Transparent3d>>,
 | |
|     >,
 | |
|     ambient_light: Res<AmbientLight>,
 | |
|     point_light_shadow_map: Res<PointLightShadowMap>,
 | |
|     directional_light_shadow_map: Res<DirectionalLightShadowMap>,
 | |
|     mut max_directional_lights_warning_emitted: Local<bool>,
 | |
|     mut max_cascades_per_light_warning_emitted: Local<bool>,
 | |
|     point_lights: Query<(
 | |
|         Entity,
 | |
|         &ExtractedPointLight,
 | |
|         AnyOf<(&CubemapFrusta, &Frustum)>,
 | |
|     )>,
 | |
|     directional_lights: Query<(Entity, &ExtractedDirectionalLight)>,
 | |
| ) {
 | |
|     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_projection =
 | |
|         Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, POINT_LIGHT_NEAR_Z);
 | |
|     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.1.spot_light_angles.is_none())
 | |
|         .count();
 | |
| 
 | |
|     let point_light_shadow_maps_count = point_lights
 | |
|         .iter()
 | |
|         .filter(|light| light.1.shadows_enabled && light.1.spot_light_angles.is_none())
 | |
|         .count()
 | |
|         .min(max_texture_cubes);
 | |
| 
 | |
|     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_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(|(entity_1, light_1, _), (entity_2, light_2, _)| {
 | |
|         point_light_order(
 | |
|             (
 | |
|                 entity_1,
 | |
|                 &light_1.shadows_enabled,
 | |
|                 &light_1.spot_light_angles.is_some(),
 | |
|             ),
 | |
|             (
 | |
|                 entity_2,
 | |
|                 &light_2.shadows_enabled,
 | |
|                 &light_2.spot_light_angles.is_some(),
 | |
|             ),
 | |
|         )
 | |
|     });
 | |
| 
 | |
|     // Sort lights by
 | |
|     // - those with shadows enabled first, so that the index can be used to render at most `directional_light_shadow_maps_count`
 | |
|     //   directional light shadows
 | |
|     // - then by entity as a stable key to ensure that a consistent set of lights are chosen if the light count limit is exceeded.
 | |
|     directional_lights.sort_by(|(entity_1, light_1), (entity_2, light_2)| {
 | |
|         directional_light_order(
 | |
|             (entity_1, &light_1.shadows_enabled),
 | |
|             (entity_2, &light_2.shadows_enabled),
 | |
|         )
 | |
|     });
 | |
| 
 | |
|     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 (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 = outer.cos();
 | |
|                 let spot_scale = 1.0 / f32::max(inner.cos() - 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),
 | |
|                     outer.tan(),
 | |
|                 )
 | |
|             }
 | |
|             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(GpuPointLight {
 | |
|             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,
 | |
|             spot_light_tan_angle,
 | |
|         });
 | |
|         global_light_meta.entity_to_index.insert(entity, index);
 | |
|     }
 | |
| 
 | |
|     let mut gpu_directional_lights = [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS];
 | |
|     let mut num_directional_cascades_enabled = 0usize;
 | |
|     for (index, (_light_entity, light)) in directional_lights
 | |
|         .iter()
 | |
|         .enumerate()
 | |
|         .take(MAX_DIRECTIONAL_LIGHTS)
 | |
|     {
 | |
|         let mut flags = DirectionalLightFlags::NONE;
 | |
| 
 | |
|         // Lights are sorted, shadow enabled lights are first
 | |
|         if light.shadows_enabled && (index < directional_shadow_enabled_count) {
 | |
|             flags |= DirectionalLightFlags::SHADOWS_ENABLED;
 | |
|         }
 | |
| 
 | |
|         let num_cascades = light
 | |
|             .cascade_shadow_config
 | |
|             .bounds
 | |
|             .len()
 | |
|             .min(MAX_CASCADES_PER_LIGHT);
 | |
|         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(),
 | |
|             flags: flags.bits(),
 | |
|             shadow_depth_bias: light.shadow_depth_bias,
 | |
|             shadow_normal_bias: light.shadow_normal_bias,
 | |
|             num_cascades: num_cascades as u32,
 | |
|             cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion,
 | |
|             depth_texture_base_index: num_directional_cascades_enabled as u32,
 | |
|             render_layers: light.render_layers.bits(),
 | |
|         };
 | |
|         if index < directional_shadow_enabled_count {
 | |
|             num_directional_cascades_enabled += num_cascades;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     global_light_meta.gpu_point_lights.set(gpu_point_lights);
 | |
|     global_light_meta
 | |
|         .gpu_point_lights
 | |
|         .write_buffer(&render_device, &render_queue);
 | |
| 
 | |
|     // set up light data for each view
 | |
|     for (entity, extracted_view, clusters) in &views {
 | |
|         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 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 mut view_lights = Vec::new();
 | |
| 
 | |
|         let is_orthographic = extracted_view.projection.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 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: directional_lights.iter().len() 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,
 | |
|         };
 | |
| 
 | |
|         // 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, (point_light_frusta, _)) in point_lights
 | |
|             .iter()
 | |
|             // Lights are sorted, shadow enabled lights are first
 | |
|             .take(point_light_shadow_maps_count)
 | |
|             .filter(|(_, light, _)| light.shadows_enabled)
 | |
|         {
 | |
|             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 (face_index, (view_rotation, frustum)) in cube_face_rotations
 | |
|                 .iter()
 | |
|                 .zip(&point_light_frusta.unwrap().frusta)
 | |
|                 .enumerate()
 | |
|             {
 | |
|                 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),
 | |
|                             aspect: TextureAspect::All,
 | |
|                             base_mip_level: 0,
 | |
|                             mip_level_count: None,
 | |
|                             base_array_layer: (light_index * 6 + face_index) as u32,
 | |
|                             array_layer_count: Some(1u32),
 | |
|                         });
 | |
| 
 | |
|                 let view_light_entity = commands
 | |
|                     .spawn((
 | |
|                         ShadowView {
 | |
|                             depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)),
 | |
|                             pass_name: format!(
 | |
|                                 "shadow pass point light {} {}",
 | |
|                                 light_index,
 | |
|                                 face_index_to_name(face_index)
 | |
|                             ),
 | |
|                         },
 | |
|                         ExtractedView {
 | |
|                             viewport: UVec4::new(
 | |
|                                 0,
 | |
|                                 0,
 | |
|                                 point_light_shadow_map.size as u32,
 | |
|                                 point_light_shadow_map.size as u32,
 | |
|                             ),
 | |
|                             transform: view_translation * *view_rotation,
 | |
|                             view_projection: None,
 | |
|                             projection: cube_face_projection,
 | |
|                             hdr: false,
 | |
|                             color_grading: Default::default(),
 | |
|                         },
 | |
|                         *frustum,
 | |
|                         RenderPhase::<Shadow>::default(),
 | |
|                         LightEntity::Point {
 | |
|                             light_entity,
 | |
|                             face_index,
 | |
|                         },
 | |
|                     ))
 | |
|                     .id();
 | |
|                 view_lights.push(view_light_entity);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // spot lights
 | |
|         for (light_index, &(light_entity, light, (_, spot_light_frustum))) in point_lights
 | |
|             .iter()
 | |
|             .skip(point_light_count)
 | |
|             .take(spot_light_shadow_maps_count)
 | |
|             .enumerate()
 | |
|         {
 | |
|             let spot_view_matrix = spot_light_view_matrix(&light.transform);
 | |
|             let spot_view_transform = spot_view_matrix.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_projection_matrix(angle);
 | |
| 
 | |
|             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),
 | |
|                         aspect: TextureAspect::All,
 | |
|                         base_mip_level: 0,
 | |
|                         mip_level_count: None,
 | |
|                         base_array_layer: (num_directional_cascades_enabled + light_index) as u32,
 | |
|                         array_layer_count: Some(1u32),
 | |
|                     });
 | |
| 
 | |
|             let view_light_entity = commands
 | |
|                 .spawn((
 | |
|                     ShadowView {
 | |
|                         depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)),
 | |
|                         pass_name: format!("shadow pass spot light {light_index}"),
 | |
|                     },
 | |
|                     ExtractedView {
 | |
|                         viewport: UVec4::new(
 | |
|                             0,
 | |
|                             0,
 | |
|                             directional_light_shadow_map.size as u32,
 | |
|                             directional_light_shadow_map.size as u32,
 | |
|                         ),
 | |
|                         transform: spot_view_transform,
 | |
|                         projection: spot_projection,
 | |
|                         view_projection: None,
 | |
|                         hdr: false,
 | |
|                         color_grading: Default::default(),
 | |
|                     },
 | |
|                     *spot_light_frustum.unwrap(),
 | |
|                     RenderPhase::<Shadow>::default(),
 | |
|                     LightEntity::Spot { light_entity },
 | |
|                 ))
 | |
|                 .id();
 | |
| 
 | |
|             view_lights.push(view_light_entity);
 | |
|         }
 | |
| 
 | |
|         // directional lights
 | |
|         let mut directional_depth_texture_array_index = 0u32;
 | |
|         for (light_index, &(light_entity, light)) in directional_lights
 | |
|             .iter()
 | |
|             .enumerate()
 | |
|             .take(directional_shadow_enabled_count)
 | |
|         {
 | |
|             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);
 | |
|             for (cascade_index, ((cascade, frusta), bound)) in cascades
 | |
|                 .zip(frusta)
 | |
|                 .zip(&light.cascade_shadow_config.bounds)
 | |
|                 .enumerate()
 | |
|             {
 | |
|                 gpu_lights.directional_lights[light_index].cascades[cascade_index] =
 | |
|                     GpuDirectionalCascade {
 | |
|                         view_projection: cascade.view_projection,
 | |
|                         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),
 | |
|                             aspect: TextureAspect::All,
 | |
|                             base_mip_level: 0,
 | |
|                             mip_level_count: None,
 | |
|                             base_array_layer: directional_depth_texture_array_index,
 | |
|                             array_layer_count: Some(1u32),
 | |
|                         });
 | |
|                 directional_depth_texture_array_index += 1;
 | |
| 
 | |
|                 let view_light_entity = commands
 | |
|                     .spawn((
 | |
|                         ShadowView {
 | |
|                             depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)),
 | |
|                             pass_name: format!(
 | |
|                                 "shadow pass directional light {light_index} cascade {cascade_index}"),
 | |
|                         },
 | |
|                         ExtractedView {
 | |
|                             viewport: UVec4::new(
 | |
|                                 0,
 | |
|                                 0,
 | |
|                                 directional_light_shadow_map.size as u32,
 | |
|                                 directional_light_shadow_map.size as u32,
 | |
|                             ),
 | |
|                             transform: GlobalTransform::from(cascade.view_transform),
 | |
|                             projection: cascade.projection,
 | |
|                             view_projection: Some(cascade.view_projection),
 | |
|                             hdr: false,
 | |
|                             color_grading: Default::default(),
 | |
|                         },
 | |
|                        *frusta,
 | |
|                         RenderPhase::<Shadow>::default(),
 | |
|                         LightEntity::Directional {
 | |
|                             light_entity,
 | |
|                             cascade_index,
 | |
|                         },
 | |
|                     ))
 | |
|                     .id();
 | |
|                 view_lights.push(view_light_entity);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         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(feature = "ios_simulator"),
 | |
|                         any(
 | |
|                             not(feature = "webgl"),
 | |
|                             not(target_arch = "wasm32"),
 | |
|                             feature = "webgpu"
 | |
|                         )
 | |
|                     ))]
 | |
|                     dimension: Some(TextureViewDimension::CubeArray),
 | |
|                     #[cfg(any(
 | |
|                         feature = "ios_simulator",
 | |
|                         all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu"))
 | |
|                     ))]
 | |
|                     dimension: Some(TextureViewDimension::Cube),
 | |
|                     aspect: TextureAspect::DepthOnly,
 | |
|                     base_mip_level: 0,
 | |
|                     mip_level_count: None,
 | |
|                     base_array_layer: 0,
 | |
|                     array_layer_count: None,
 | |
|                 });
 | |
|         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),
 | |
|                 aspect: TextureAspect::DepthOnly,
 | |
|                 base_mip_level: 0,
 | |
|                 mip_level_count: None,
 | |
|                 base_array_layer: 0,
 | |
|                 array_layer_count: None,
 | |
|             });
 | |
| 
 | |
|         commands.entity(entity).insert((
 | |
|             ViewShadowBindings {
 | |
|                 point_light_depth_texture: point_light_depth_texture.texture,
 | |
|                 point_light_depth_texture_view,
 | |
|                 directional_light_depth_texture: directional_light_depth_texture.texture,
 | |
|                 directional_light_depth_texture_view,
 | |
|             },
 | |
|             ViewLightEntities {
 | |
|                 lights: view_lights,
 | |
|             },
 | |
|             ViewLightsUniformOffset {
 | |
|                 offset: view_gpu_lights_writer.write(&gpu_lights),
 | |
|             },
 | |
|         ));
 | |
|     }
 | |
| }
 | |
| 
 | |
| // this must match CLUSTER_COUNT_SIZE in pbr.wgsl
 | |
| // and must be large enough to contain MAX_UNIFORM_BUFFER_POINT_LIGHTS
 | |
| const CLUSTER_COUNT_SIZE: u32 = 9;
 | |
| 
 | |
| const CLUSTER_OFFSET_MASK: u32 = (1 << (32 - (CLUSTER_COUNT_SIZE * 2))) - 1;
 | |
| const CLUSTER_COUNT_MASK: u32 = (1 << CLUSTER_COUNT_SIZE) - 1;
 | |
| 
 | |
| // NOTE: With uniform buffer max binding size as 16384 bytes
 | |
| // that means we can fit 256 point lights in one uniform
 | |
| // buffer, which means the count can be at most 256 so it
 | |
| // needs 9 bits.
 | |
| // The array of indices can also use u8 and that means the
 | |
| // offset in to the array of indices needs to be able to address
 | |
| // 16384 values. log2(16384) = 14 bits.
 | |
| // We use 32 bits to store the offset and counts so
 | |
| // we pack the offset into the upper 14 bits of a u32,
 | |
| // the point light count into bits 9-17, and the spot light count into bits 0-8.
 | |
| //  [ 31     ..     18 | 17      ..      9 | 8       ..     0 ]
 | |
| //  [      offset      | point light count | spot light count ]
 | |
| // NOTE: This assumes CPU and GPU endianness are the same which is true
 | |
| // for all common and tested x86/ARM CPUs and AMD/NVIDIA/Intel/Apple/etc GPUs
 | |
| fn pack_offset_and_counts(offset: usize, point_count: usize, spot_count: usize) -> u32 {
 | |
|     ((offset as u32 & CLUSTER_OFFSET_MASK) << (CLUSTER_COUNT_SIZE * 2))
 | |
|         | (point_count as u32 & CLUSTER_COUNT_MASK) << CLUSTER_COUNT_SIZE
 | |
|         | (spot_count as u32 & CLUSTER_COUNT_MASK)
 | |
| }
 | |
| 
 | |
| #[derive(ShaderType)]
 | |
| struct GpuClusterLightIndexListsUniform {
 | |
|     data: Box<[UVec4; ViewClusterBindings::MAX_UNIFORM_ITEMS]>,
 | |
| }
 | |
| 
 | |
| // NOTE: Assert at compile time that GpuClusterLightIndexListsUniform
 | |
| // fits within the maximum uniform buffer binding size
 | |
| const _: () = assert!(GpuClusterLightIndexListsUniform::SHADER_SIZE.get() <= 16384);
 | |
| 
 | |
| impl Default for GpuClusterLightIndexListsUniform {
 | |
|     fn default() -> Self {
 | |
|         Self {
 | |
|             data: Box::new([UVec4::ZERO; ViewClusterBindings::MAX_UNIFORM_ITEMS]),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(ShaderType)]
 | |
| struct GpuClusterOffsetsAndCountsUniform {
 | |
|     data: Box<[UVec4; ViewClusterBindings::MAX_UNIFORM_ITEMS]>,
 | |
| }
 | |
| 
 | |
| impl Default for GpuClusterOffsetsAndCountsUniform {
 | |
|     fn default() -> Self {
 | |
|         Self {
 | |
|             data: Box::new([UVec4::ZERO; ViewClusterBindings::MAX_UNIFORM_ITEMS]),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(ShaderType, Default)]
 | |
| struct GpuClusterLightIndexListsStorage {
 | |
|     #[size(runtime)]
 | |
|     data: Vec<u32>,
 | |
| }
 | |
| 
 | |
| #[derive(ShaderType, Default)]
 | |
| struct GpuClusterOffsetsAndCountsStorage {
 | |
|     #[size(runtime)]
 | |
|     data: Vec<UVec4>,
 | |
| }
 | |
| 
 | |
| enum ViewClusterBuffers {
 | |
|     Uniform {
 | |
|         // NOTE: UVec4 is because all arrays in Std140 layout have 16-byte alignment
 | |
|         cluster_light_index_lists: UniformBuffer<GpuClusterLightIndexListsUniform>,
 | |
|         // NOTE: UVec4 is because all arrays in Std140 layout have 16-byte alignment
 | |
|         cluster_offsets_and_counts: UniformBuffer<GpuClusterOffsetsAndCountsUniform>,
 | |
|     },
 | |
|     Storage {
 | |
|         cluster_light_index_lists: StorageBuffer<GpuClusterLightIndexListsStorage>,
 | |
|         cluster_offsets_and_counts: StorageBuffer<GpuClusterOffsetsAndCountsStorage>,
 | |
|     },
 | |
| }
 | |
| 
 | |
| impl ViewClusterBuffers {
 | |
|     fn new(buffer_binding_type: BufferBindingType) -> Self {
 | |
|         match buffer_binding_type {
 | |
|             BufferBindingType::Storage { .. } => Self::storage(),
 | |
|             BufferBindingType::Uniform => Self::uniform(),
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     fn uniform() -> Self {
 | |
|         ViewClusterBuffers::Uniform {
 | |
|             cluster_light_index_lists: UniformBuffer::default(),
 | |
|             cluster_offsets_and_counts: UniformBuffer::default(),
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     fn storage() -> Self {
 | |
|         ViewClusterBuffers::Storage {
 | |
|             cluster_light_index_lists: StorageBuffer::default(),
 | |
|             cluster_offsets_and_counts: StorageBuffer::default(),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(Component)]
 | |
| pub struct ViewClusterBindings {
 | |
|     n_indices: usize,
 | |
|     n_offsets: usize,
 | |
|     buffers: ViewClusterBuffers,
 | |
| }
 | |
| 
 | |
| impl ViewClusterBindings {
 | |
|     pub const MAX_OFFSETS: usize = 16384 / 4;
 | |
|     const MAX_UNIFORM_ITEMS: usize = Self::MAX_OFFSETS / 4;
 | |
|     pub const MAX_INDICES: usize = 16384;
 | |
| 
 | |
|     pub fn new(buffer_binding_type: BufferBindingType) -> Self {
 | |
|         Self {
 | |
|             n_indices: 0,
 | |
|             n_offsets: 0,
 | |
|             buffers: ViewClusterBuffers::new(buffer_binding_type),
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     pub fn clear(&mut self) {
 | |
|         match &mut self.buffers {
 | |
|             ViewClusterBuffers::Uniform {
 | |
|                 cluster_light_index_lists,
 | |
|                 cluster_offsets_and_counts,
 | |
|             } => {
 | |
|                 *cluster_light_index_lists.get_mut().data = [UVec4::ZERO; Self::MAX_UNIFORM_ITEMS];
 | |
|                 *cluster_offsets_and_counts.get_mut().data = [UVec4::ZERO; Self::MAX_UNIFORM_ITEMS];
 | |
|             }
 | |
|             ViewClusterBuffers::Storage {
 | |
|                 cluster_light_index_lists,
 | |
|                 cluster_offsets_and_counts,
 | |
|                 ..
 | |
|             } => {
 | |
|                 cluster_light_index_lists.get_mut().data.clear();
 | |
|                 cluster_offsets_and_counts.get_mut().data.clear();
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     pub fn push_offset_and_counts(&mut self, offset: usize, point_count: usize, spot_count: usize) {
 | |
|         match &mut self.buffers {
 | |
|             ViewClusterBuffers::Uniform {
 | |
|                 cluster_offsets_and_counts,
 | |
|                 ..
 | |
|             } => {
 | |
|                 let array_index = self.n_offsets >> 2; // >> 2 is equivalent to / 4
 | |
|                 if array_index >= Self::MAX_UNIFORM_ITEMS {
 | |
|                     warn!("cluster offset and count out of bounds!");
 | |
|                     return;
 | |
|                 }
 | |
|                 let component = self.n_offsets & ((1 << 2) - 1);
 | |
|                 let packed = pack_offset_and_counts(offset, point_count, spot_count);
 | |
| 
 | |
|                 cluster_offsets_and_counts.get_mut().data[array_index][component] = packed;
 | |
|             }
 | |
|             ViewClusterBuffers::Storage {
 | |
|                 cluster_offsets_and_counts,
 | |
|                 ..
 | |
|             } => {
 | |
|                 cluster_offsets_and_counts.get_mut().data.push(UVec4::new(
 | |
|                     offset as u32,
 | |
|                     point_count as u32,
 | |
|                     spot_count as u32,
 | |
|                     0,
 | |
|                 ));
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         self.n_offsets += 1;
 | |
|     }
 | |
| 
 | |
|     pub fn n_indices(&self) -> usize {
 | |
|         self.n_indices
 | |
|     }
 | |
| 
 | |
|     pub fn push_index(&mut self, index: usize) {
 | |
|         match &mut self.buffers {
 | |
|             ViewClusterBuffers::Uniform {
 | |
|                 cluster_light_index_lists,
 | |
|                 ..
 | |
|             } => {
 | |
|                 let array_index = self.n_indices >> 4; // >> 4 is equivalent to / 16
 | |
|                 let component = (self.n_indices >> 2) & ((1 << 2) - 1);
 | |
|                 let sub_index = self.n_indices & ((1 << 2) - 1);
 | |
|                 let index = index as u32;
 | |
| 
 | |
|                 cluster_light_index_lists.get_mut().data[array_index][component] |=
 | |
|                     index << (8 * sub_index);
 | |
|             }
 | |
|             ViewClusterBuffers::Storage {
 | |
|                 cluster_light_index_lists,
 | |
|                 ..
 | |
|             } => {
 | |
|                 cluster_light_index_lists.get_mut().data.push(index as u32);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         self.n_indices += 1;
 | |
|     }
 | |
| 
 | |
|     pub fn write_buffers(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) {
 | |
|         match &mut self.buffers {
 | |
|             ViewClusterBuffers::Uniform {
 | |
|                 cluster_light_index_lists,
 | |
|                 cluster_offsets_and_counts,
 | |
|             } => {
 | |
|                 cluster_light_index_lists.write_buffer(render_device, render_queue);
 | |
|                 cluster_offsets_and_counts.write_buffer(render_device, render_queue);
 | |
|             }
 | |
|             ViewClusterBuffers::Storage {
 | |
|                 cluster_light_index_lists,
 | |
|                 cluster_offsets_and_counts,
 | |
|             } => {
 | |
|                 cluster_light_index_lists.write_buffer(render_device, render_queue);
 | |
|                 cluster_offsets_and_counts.write_buffer(render_device, render_queue);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     pub fn light_index_lists_binding(&self) -> Option<BindingResource> {
 | |
|         match &self.buffers {
 | |
|             ViewClusterBuffers::Uniform {
 | |
|                 cluster_light_index_lists,
 | |
|                 ..
 | |
|             } => cluster_light_index_lists.binding(),
 | |
|             ViewClusterBuffers::Storage {
 | |
|                 cluster_light_index_lists,
 | |
|                 ..
 | |
|             } => cluster_light_index_lists.binding(),
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     pub fn offsets_and_counts_binding(&self) -> Option<BindingResource> {
 | |
|         match &self.buffers {
 | |
|             ViewClusterBuffers::Uniform {
 | |
|                 cluster_offsets_and_counts,
 | |
|                 ..
 | |
|             } => cluster_offsets_and_counts.binding(),
 | |
|             ViewClusterBuffers::Storage {
 | |
|                 cluster_offsets_and_counts,
 | |
|                 ..
 | |
|             } => cluster_offsets_and_counts.binding(),
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     pub fn min_size_cluster_light_index_lists(
 | |
|         buffer_binding_type: BufferBindingType,
 | |
|     ) -> NonZeroU64 {
 | |
|         match buffer_binding_type {
 | |
|             BufferBindingType::Storage { .. } => GpuClusterLightIndexListsStorage::min_size(),
 | |
|             BufferBindingType::Uniform => GpuClusterLightIndexListsUniform::min_size(),
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     pub fn min_size_cluster_offsets_and_counts(
 | |
|         buffer_binding_type: BufferBindingType,
 | |
|     ) -> NonZeroU64 {
 | |
|         match buffer_binding_type {
 | |
|             BufferBindingType::Storage { .. } => GpuClusterOffsetsAndCountsStorage::min_size(),
 | |
|             BufferBindingType::Uniform => GpuClusterOffsetsAndCountsUniform::min_size(),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| pub fn prepare_clusters(
 | |
|     mut commands: Commands,
 | |
|     render_device: Res<RenderDevice>,
 | |
|     render_queue: Res<RenderQueue>,
 | |
|     mesh_pipeline: Res<MeshPipeline>,
 | |
|     global_light_meta: Res<GlobalLightMeta>,
 | |
|     views: Query<(Entity, &ExtractedClustersPointLights), With<RenderPhase<Transparent3d>>>,
 | |
| ) {
 | |
|     let render_device = render_device.into_inner();
 | |
|     let supports_storage_buffers = matches!(
 | |
|         mesh_pipeline.clustered_forward_buffer_binding_type,
 | |
|         BufferBindingType::Storage { .. }
 | |
|     );
 | |
|     for (entity, extracted_clusters) in &views {
 | |
|         let mut view_clusters_bindings =
 | |
|             ViewClusterBindings::new(mesh_pipeline.clustered_forward_buffer_binding_type);
 | |
|         view_clusters_bindings.clear();
 | |
| 
 | |
|         for record in &extracted_clusters.data {
 | |
|             match record {
 | |
|                 ExtractedClustersPointLightsElement::ClusterHeader(
 | |
|                     point_light_count,
 | |
|                     spot_light_count,
 | |
|                 ) => {
 | |
|                     let offset = view_clusters_bindings.n_indices();
 | |
|                     view_clusters_bindings.push_offset_and_counts(
 | |
|                         offset,
 | |
|                         *point_light_count as usize,
 | |
|                         *spot_light_count as usize,
 | |
|                     );
 | |
|                 }
 | |
|                 ExtractedClustersPointLightsElement::LightEntity(entity) => {
 | |
|                     if let Some(light_index) = global_light_meta.entity_to_index.get(entity) {
 | |
|                         if view_clusters_bindings.n_indices() >= ViewClusterBindings::MAX_INDICES
 | |
|                             && !supports_storage_buffers
 | |
|                         {
 | |
|                             warn!("Cluster light index lists is full! The PointLights in the view are affecting too many clusters.");
 | |
|                             break;
 | |
|                         }
 | |
|                         view_clusters_bindings.push_index(*light_index);
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         view_clusters_bindings.write_buffers(render_device, &render_queue);
 | |
| 
 | |
|         commands.get_or_spawn(entity).insert(view_clusters_bindings);
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[allow(clippy::too_many_arguments)]
 | |
| pub fn queue_shadows<M: Material>(
 | |
|     shadow_draw_functions: Res<DrawFunctions<Shadow>>,
 | |
|     prepass_pipeline: Res<PrepassPipeline<M>>,
 | |
|     render_meshes: Res<RenderAssets<Mesh>>,
 | |
|     render_mesh_instances: Res<RenderMeshInstances>,
 | |
|     render_materials: Res<RenderMaterials<M>>,
 | |
|     render_material_instances: Res<RenderMaterialInstances<M>>,
 | |
|     mut pipelines: ResMut<SpecializedMeshPipelines<PrepassPipeline<M>>>,
 | |
|     pipeline_cache: Res<PipelineCache>,
 | |
|     render_lightmaps: Res<RenderLightmaps>,
 | |
|     view_lights: Query<(Entity, &ViewLightEntities)>,
 | |
|     mut view_light_shadow_phases: Query<(&LightEntity, &mut RenderPhase<Shadow>)>,
 | |
|     point_light_entities: Query<&CubemapVisibleEntities, With<ExtractedPointLight>>,
 | |
|     directional_light_entities: Query<&CascadesVisibleEntities, With<ExtractedDirectionalLight>>,
 | |
|     spot_light_entities: Query<&VisibleEntities, With<ExtractedPointLight>>,
 | |
| ) where
 | |
|     M::Data: PartialEq + Eq + Hash + Clone,
 | |
| {
 | |
|     for (entity, view_lights) in &view_lights {
 | |
|         let draw_shadow_mesh = shadow_draw_functions.read().id::<DrawPrepass<M>>();
 | |
|         for view_light_entity in view_lights.lights.iter().copied() {
 | |
|             let (light_entity, mut shadow_phase) =
 | |
|                 view_light_shadow_phases.get_mut(view_light_entity).unwrap();
 | |
|             let is_directional_light = matches!(light_entity, LightEntity::Directional { .. });
 | |
|             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
 | |
|             for entity in visible_entities.iter().copied() {
 | |
|                 let Some(mesh_instance) = render_mesh_instances.get(&entity) else {
 | |
|                     continue;
 | |
|                 };
 | |
|                 if !mesh_instance.shadow_caster {
 | |
|                     continue;
 | |
|                 }
 | |
|                 let Some(material_asset_id) = render_material_instances.get(&entity) else {
 | |
|                     continue;
 | |
|                 };
 | |
|                 let Some(material) = render_materials.get(material_asset_id) else {
 | |
|                     continue;
 | |
|                 };
 | |
|                 let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {
 | |
|                     continue;
 | |
|                 };
 | |
| 
 | |
|                 let mut mesh_key =
 | |
|                     MeshPipelineKey::from_primitive_topology(mesh.primitive_topology)
 | |
|                         | MeshPipelineKey::DEPTH_PREPASS;
 | |
|                 if mesh.morph_targets.is_some() {
 | |
|                     mesh_key |= MeshPipelineKey::MORPH_TARGETS;
 | |
|                 }
 | |
|                 if is_directional_light {
 | |
|                     mesh_key |= MeshPipelineKey::DEPTH_CLAMP_ORTHO;
 | |
|                 }
 | |
| 
 | |
|                 // 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(&entity) {
 | |
|                     mesh_key |= MeshPipelineKey::LIGHTMAPPED;
 | |
|                 }
 | |
| 
 | |
|                 mesh_key |= match material.properties.alpha_mode {
 | |
|                     AlphaMode::Mask(_)
 | |
|                     | AlphaMode::Blend
 | |
|                     | AlphaMode::Premultiplied
 | |
|                     | AlphaMode::Add => MeshPipelineKey::MAY_DISCARD,
 | |
|                     _ => MeshPipelineKey::NONE,
 | |
|                 };
 | |
|                 let pipeline_id = pipelines.specialize(
 | |
|                     &pipeline_cache,
 | |
|                     &prepass_pipeline,
 | |
|                     MaterialPipelineKey {
 | |
|                         mesh_key,
 | |
|                         bind_group_data: material.key.clone(),
 | |
|                     },
 | |
|                     &mesh.layout,
 | |
|                 );
 | |
| 
 | |
|                 let pipeline_id = match pipeline_id {
 | |
|                     Ok(id) => id,
 | |
|                     Err(err) => {
 | |
|                         error!("{}", err);
 | |
|                         continue;
 | |
|                     }
 | |
|                 };
 | |
| 
 | |
|                 mesh_instance
 | |
|                     .material_bind_group_id
 | |
|                     .set(material.get_bind_group_id());
 | |
| 
 | |
|                 shadow_phase.add(Shadow {
 | |
|                     draw_function: draw_shadow_mesh,
 | |
|                     pipeline: pipeline_id,
 | |
|                     entity,
 | |
|                     distance: 0.0, // TODO: sort front-to-back
 | |
|                     batch_range: 0..1,
 | |
|                     dynamic_offset: None,
 | |
|                 });
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| pub struct Shadow {
 | |
|     pub distance: f32,
 | |
|     pub entity: Entity,
 | |
|     pub pipeline: CachedRenderPipelineId,
 | |
|     pub draw_function: DrawFunctionId,
 | |
|     pub batch_range: Range<u32>,
 | |
|     pub dynamic_offset: Option<NonMaxU32>,
 | |
| }
 | |
| 
 | |
| impl PhaseItem for Shadow {
 | |
|     type SortKey = usize;
 | |
| 
 | |
|     #[inline]
 | |
|     fn entity(&self) -> Entity {
 | |
|         self.entity
 | |
|     }
 | |
| 
 | |
|     #[inline]
 | |
|     fn sort_key(&self) -> Self::SortKey {
 | |
|         self.pipeline.id()
 | |
|     }
 | |
| 
 | |
|     #[inline]
 | |
|     fn draw_function(&self) -> DrawFunctionId {
 | |
|         self.draw_function
 | |
|     }
 | |
| 
 | |
|     #[inline]
 | |
|     fn sort(items: &mut [Self]) {
 | |
|         // The shadow phase is sorted by pipeline id for performance reasons.
 | |
|         // Grouping all draw commands using the same pipeline together performs
 | |
|         // better than rebinding everything at a high rate.
 | |
|         radsort::sort_by_key(items, |item| item.sort_key());
 | |
|     }
 | |
| 
 | |
|     #[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 dynamic_offset(&self) -> Option<NonMaxU32> {
 | |
|         self.dynamic_offset
 | |
|     }
 | |
| 
 | |
|     #[inline]
 | |
|     fn dynamic_offset_mut(&mut self) -> &mut Option<NonMaxU32> {
 | |
|         &mut self.dynamic_offset
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl CachedRenderPipelinePhaseItem for Shadow {
 | |
|     #[inline]
 | |
|     fn cached_pipeline(&self) -> CachedRenderPipelineId {
 | |
|         self.pipeline
 | |
|     }
 | |
| }
 | |
| 
 | |
| pub struct ShadowPassNode {
 | |
|     main_view_query: QueryState<&'static ViewLightEntities>,
 | |
|     view_light_query: QueryState<(&'static ShadowView, &'static RenderPhase<Shadow>)>,
 | |
| }
 | |
| 
 | |
| impl ShadowPassNode {
 | |
|     pub fn new(world: &mut World) -> Self {
 | |
|         Self {
 | |
|             main_view_query: QueryState::new(world),
 | |
|             view_light_query: QueryState::new(world),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl Node for ShadowPassNode {
 | |
|     fn update(&mut self, world: &mut World) {
 | |
|         self.main_view_query.update_archetypes(world);
 | |
|         self.view_light_query.update_archetypes(world);
 | |
|     }
 | |
| 
 | |
|     fn run<'w>(
 | |
|         &self,
 | |
|         graph: &mut RenderGraphContext,
 | |
|         render_context: &mut RenderContext<'w>,
 | |
|         world: &'w World,
 | |
|     ) -> Result<(), NodeRunError> {
 | |
|         let view_entity = graph.view_entity();
 | |
|         if let Ok(view_lights) = self.main_view_query.get_manual(world, view_entity) {
 | |
|             for view_light_entity in view_lights.lights.iter().copied() {
 | |
|                 let (view_light, shadow_phase) = self
 | |
|                     .view_light_query
 | |
|                     .get_manual(world, view_light_entity)
 | |
|                     .unwrap();
 | |
| 
 | |
|                 let depth_stencil_attachment =
 | |
|                     Some(view_light.depth_attachment.get_attachment(StoreOp::Store));
 | |
| 
 | |
|                 render_context.add_command_buffer_generation_task(move |render_device| {
 | |
|                     #[cfg(feature = "trace")]
 | |
|                     let _shadow_pass_span = info_span!("shadow_pass").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);
 | |
| 
 | |
|                     shadow_phase.render(&mut render_pass, world, view_light_entity);
 | |
| 
 | |
|                     drop(render_pass);
 | |
|                     command_encoder.finish()
 | |
|                 });
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         Ok(())
 | |
|     }
 | |
| }
 |