From 12d98c3ae4a17154a719eb0df38efc7577eb2131 Mon Sep 17 00:00:00 2001 From: GHowe <116420022+GageHowe@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:25:34 -0500 Subject: [PATCH 1/2] fmt From 6964585d6882243b404d18eff261accd316bf8fa Mon Sep 17 00:00:00 2001 From: Gage Howe Date: Tue, 12 May 2026 14:12:30 -0500 Subject: [PATCH 2/2] init --- Cargo.toml | 11 + crates/bevy_light/src/cluster/assign.rs | 49 +- crates/bevy_light/src/cluster/mod.rs | 50 +- crates/bevy_light/src/lib.rs | 5 +- crates/bevy_light/src/light_falloff.rs | 35 ++ crates/bevy_light/src/point_light.rs | 6 +- crates/bevy_light/src/spot_light.rs | 6 +- .../src/cluster/cluster_allocate.wgsl | 7 +- .../bevy_pbr/src/cluster/cluster_raster.wgsl | 112 +++- crates/bevy_pbr/src/cluster/mod.rs | 25 +- .../src/render/clustered_forward.wgsl | 63 ++- crates/bevy_pbr/src/render/light.rs | 18 +- .../bevy_pbr/src/render/mesh_view_types.wgsl | 6 +- crates/bevy_pbr/src/render/pbr_functions.wgsl | 366 +++++++++++++- crates/bevy_pbr/src/render/pbr_lighting.wgsl | 127 ++++- .../src/volumetric_fog/volumetric_fog.wgsl | 39 +- examples/3d/light_falloff.rs | 478 ++++++++++++++++++ 17 files changed, 1315 insertions(+), 88 deletions(-) create mode 100644 crates/bevy_light/src/light_falloff.rs create mode 100644 examples/3d/light_falloff.rs diff --git a/Cargo.toml b/Cargo.toml index bd63f5eb560d8..37fabf084fd04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1272,6 +1272,17 @@ description = "Illustrates various lighting options in a simple scene" category = "3D Rendering" wasm = true +[[example]] +name = "light_falloff" +path = "examples/3d/light_falloff.rs" +doc-scrape-examples = true + +[package.metadata.example.light_falloff] +name = "Light Falloff" +description = "Compares inverse-square, linear, and exponential falloff for point and spot lights" +category = "3D Rendering" +wasm = true + [[example]] name = "lines" path = "examples/3d/lines.rs" diff --git a/crates/bevy_light/src/cluster/assign.rs b/crates/bevy_light/src/cluster/assign.rs index 7ddc9f32b218e..a65189f94efba 100644 --- a/crates/bevy_light/src/cluster/assign.rs +++ b/crates/bevy_light/src/cluster/assign.rs @@ -20,7 +20,8 @@ use tracing::{error, warn}; use super::{ClusterConfig, ClusterFarZMode, ClusteredDecal, Clusters, GlobalClusterSettings}; use crate::{ - cluster::ClusterableObjects, EnvironmentMapLight, LightProbe, PointLight, SpotLight, + cluster::ClusterableObjects, EnvironmentMapLight, LightFalloff, LightProbe, PointLight, + SpotLight, VolumetricLight, }; @@ -73,6 +74,9 @@ pub enum ClusterableObjectType { /// /// This is used for sorting the light list. volumetric: bool, + + /// The distance falloff mode for this light. + falloff: LightFalloff, }, /// Data needed to assign spot lights to clusters. @@ -87,6 +91,9 @@ pub enum ClusterableObjectType { /// This is used for sorting the light list. volumetric: bool, + /// The distance falloff mode for this light. + falloff: LightFalloff, + /// The outer angle of the light cone in radians. outer_angle: f32, }, @@ -114,15 +121,21 @@ impl ClusterableObjectType { ClusterableObjectType::PointLight { shadow_maps_enabled, volumetric, - } => (0, !shadow_maps_enabled, !volumetric), + falloff, + } => (falloff.bucket_index() as u8, !shadow_maps_enabled, !volumetric), ClusterableObjectType::SpotLight { shadow_maps_enabled, volumetric, + falloff, .. - } => (1, !shadow_maps_enabled, !volumetric), - ClusterableObjectType::ReflectionProbe => (2, false, false), - ClusterableObjectType::IrradianceVolume => (3, false, false), - ClusterableObjectType::Decal => (4, false, false), + } => ( + LightFalloff::VARIANT_COUNT as u8 + falloff.bucket_index() as u8, + !shadow_maps_enabled, + !volumetric, + ), + ClusterableObjectType::ReflectionProbe => (6, false, false), + ClusterableObjectType::IrradianceVolume => (7, false, false), + ClusterableObjectType::Decal => (8, false, false), } } } @@ -189,6 +202,7 @@ pub(crate) fn assign_objects_to_clusters( object_type: ClusterableObjectType::PointLight { shadow_maps_enabled: point_light.shadow_maps_enabled, volumetric: volumetric.is_some(), + falloff: point_light.falloff, }, render_layers: maybe_layers.unwrap_or_default().clone(), }) @@ -208,6 +222,7 @@ pub(crate) fn assign_objects_to_clusters( outer_angle: spot_light.outer_angle, shadow_maps_enabled: spot_light.shadow_maps_enabled, volumetric: volumetric.is_some(), + falloff: spot_light.falloff, }, render_layers: maybe_layers.unwrap_or_default().clone(), }) @@ -264,17 +279,17 @@ pub(crate) fn assign_objects_to_clusters( )); } + clusterable_objects.sort_by_cached_key(|clusterable_object| { + ( + clusterable_object.object_type.ordering(), + clusterable_object.entity, + ) + }); + if clusterable_objects.len() > global_cluster_settings.max_uniform_buffer_clusterable_objects && !global_cluster_settings.supports_storage_buffers { - clusterable_objects.sort_by_cached_key(|clusterable_object| { - ( - clusterable_object.object_type.ordering(), - clusterable_object.entity, - ) - }); - if clusterable_objects.len() > global_cluster_settings.max_uniform_buffer_clusterable_objects && !*max_clusterable_objects_warning_emitted @@ -647,7 +662,7 @@ pub(crate) fn assign_objects_to_clusters( + z) as usize; match clusterable_object.object_type { - ClusterableObjectType::SpotLight { .. } => { + ClusterableObjectType::SpotLight { falloff, .. } => { let (view_light_direction, angle_sin, angle_cos) = spot_light_dir_sin_cos.unwrap(); for x in min_x..=max_x { @@ -699,18 +714,18 @@ pub(crate) fn assign_objects_to_clusters( if !angle_cull && !front_cull && !back_cull { // this cluster is affected by the spot light clusterable_objects[cluster_index] - .add_spot_light(clusterable_object.entity); + .add_spot_light(clusterable_object.entity, falloff); total_cluster_index_count += 1; } cluster_index += clusters.dimensions.z as usize; } } - ClusterableObjectType::PointLight { .. } => { + ClusterableObjectType::PointLight { falloff, .. } => { for _ in min_x..=max_x { // all clusters within range are affected by point lights clusterable_objects[cluster_index] - .add_point_light(clusterable_object.entity); + .add_point_light(clusterable_object.entity, falloff); cluster_index += clusters.dimensions.z as usize; } total_cluster_index_count += (max_x - min_x + 1) as usize; diff --git a/crates/bevy_light/src/cluster/mod.rs b/crates/bevy_light/src/cluster/mod.rs index 4a3f02b6d28d5..f711e29e59022 100644 --- a/crates/bevy_light/src/cluster/mod.rs +++ b/crates/bevy_light/src/cluster/mod.rs @@ -20,7 +20,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_transform::components::Transform; use tracing::warn; -use crate::LightProbe; +use crate::{LightFalloff, LightProbe}; pub mod assign; @@ -198,10 +198,18 @@ pub struct ObjectsInClusterCpu { /// fewer than 3 SSBOs are available, which usually means on WebGL 2. #[derive(Clone, Copy, Default, Debug)] pub struct ClusterableObjectCounts { - /// The number of point lights in the cluster. - pub point_lights: u32, - /// The number of spot lights in the cluster. - pub spot_lights: u32, + /// The number of inverse-square point lights in the cluster. + pub point_lights_inverse_square: u32, + /// The number of linear point lights in the cluster. + pub point_lights_linear: u32, + /// The number of exponential point lights in the cluster. + pub point_lights_exponential: u32, + /// The number of inverse-square spot lights in the cluster. + pub spot_lights_inverse_square: u32, + /// The number of linear spot lights in the cluster. + pub spot_lights_linear: u32, + /// The number of exponential spot lights in the cluster. + pub spot_lights_exponential: u32, /// The number of reflection probes in the cluster. pub reflection_probes: u32, /// The number of irradiance volumes in the cluster. @@ -210,6 +218,22 @@ pub struct ClusterableObjectCounts { pub decals: u32, } +impl ClusterableObjectCounts { + /// Returns the total number of point lights in the cluster. + pub fn total_point_lights(self) -> u32 { + self.point_lights_inverse_square + + self.point_lights_linear + + self.point_lights_exponential + } + + /// Returns the total number of spot lights in the cluster. + pub fn total_spot_lights(self) -> u32 { + self.spot_lights_inverse_square + + self.spot_lights_linear + + self.spot_lights_exponential + } +} + /// An object that projects a decal onto surfaces within its bounds. /// /// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It @@ -474,15 +498,23 @@ impl ObjectsInClusterCpu { } /// Adds a spot light to the list. - pub fn add_spot_light(&mut self, entity: Entity) { + pub fn add_spot_light(&mut self, entity: Entity, falloff: LightFalloff) { self.clusterables.push(entity); - self.counts.spot_lights += 1; + match falloff { + LightFalloff::InverseSquare => self.counts.spot_lights_inverse_square += 1, + LightFalloff::Linear => self.counts.spot_lights_linear += 1, + LightFalloff::Exponential => self.counts.spot_lights_exponential += 1, + } } /// Adds a point light to the list. - pub fn add_point_light(&mut self, entity: Entity) { + pub fn add_point_light(&mut self, entity: Entity, falloff: LightFalloff) { self.clusterables.push(entity); - self.counts.point_lights += 1; + match falloff { + LightFalloff::InverseSquare => self.counts.point_lights_inverse_square += 1, + LightFalloff::Linear => self.counts.point_lights_linear += 1, + LightFalloff::Exponential => self.counts.point_lights_exponential += 1, + } } /// Adds a reflection probe to the list. diff --git a/crates/bevy_light/src/lib.rs b/crates/bevy_light/src/lib.rs index c5d523f7615f9..702e3c4a80bff 100644 --- a/crates/bevy_light/src/lib.rs +++ b/crates/bevy_light/src/lib.rs @@ -31,6 +31,8 @@ pub use cluster::ClusteredDecal; mod ambient_light; pub use ambient_light::{AmbientLight, GlobalAmbientLight}; use bevy_camera::visibility::SetViewVisibility; +mod light_falloff; +pub use light_falloff::LightFalloff; mod probe; pub use probe::{ @@ -70,7 +72,8 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ light_consts, AmbientLight, DirectionalLight, EnvironmentMapLight, - GeneratedEnvironmentMapLight, GlobalAmbientLight, LightProbe, PointLight, SpotLight, + GeneratedEnvironmentMapLight, GlobalAmbientLight, LightFalloff, LightProbe, PointLight, + SpotLight, }; #[doc(hidden)] diff --git a/crates/bevy_light/src/light_falloff.rs b/crates/bevy_light/src/light_falloff.rs new file mode 100644 index 0000000000000..1fa79808719ec --- /dev/null +++ b/crates/bevy_light/src/light_falloff.rs @@ -0,0 +1,35 @@ +use bevy_reflect::prelude::*; + +/// Controls how a punctual light's intensity falls off over distance. +/// +/// All modes are clamped to zero at the configured light range. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)] +#[reflect(Default, Debug, Clone, PartialEq, Hash)] +pub enum LightFalloff { + /// Uses Bevy's existing physically-motivated inverse-square attenuation. + #[default] + InverseSquare, + /// Decreases intensity linearly from the light origin to its range. + Linear, + /// Uses an exponential falloff curve over the light's range. + Exponential, +} + +impl LightFalloff { + /// The number of built-in falloff modes. + pub const VARIANT_COUNT: usize = 3; + + /// Returns the value encoded into GPU-side flag bits for this mode. + pub const fn shader_index(self) -> u32 { + match self { + Self::InverseSquare => 0, + Self::Linear => 1, + Self::Exponential => 2, + } + } + + /// Returns the stable ordering bucket used for clustered light sorting. + pub const fn bucket_index(self) -> usize { + self.shader_index() as usize + } +} diff --git a/crates/bevy_light/src/point_light.rs b/crates/bevy_light/src/point_light.rs index f0c12f8ef19e4..cd4a2dc8348c5 100644 --- a/crates/bevy_light/src/point_light.rs +++ b/crates/bevy_light/src/point_light.rs @@ -10,7 +10,7 @@ use bevy_math::{primitives::ViewFrustum, Mat4}; use bevy_reflect::prelude::*; use bevy_transform::components::{GlobalTransform, Transform}; -use crate::{cluster::ClusterVisibilityClass, light_consts}; +use crate::{cluster::ClusterVisibilityClass, light_consts, LightFalloff}; /// A light that emits light in all directions from a central point. /// @@ -57,6 +57,9 @@ pub struct PointLight { /// lighting cut-offs. pub range: f32, + /// Controls how this light attenuates over distance within its range. + pub falloff: LightFalloff, + /// Simulates a light source coming from a spherical volume with the given /// radius. /// @@ -131,6 +134,7 @@ impl Default for PointLight { color: Color::WHITE, intensity: light_consts::lumens::VERY_LARGE_CINEMA_LIGHT, range: 20.0, + falloff: LightFalloff::InverseSquare, radius: 0.0, shadow_maps_enabled: false, contact_shadows_enabled: false, diff --git a/crates/bevy_light/src/spot_light.rs b/crates/bevy_light/src/spot_light.rs index f9e4710c48b71..2964c0ec6eef5 100644 --- a/crates/bevy_light/src/spot_light.rs +++ b/crates/bevy_light/src/spot_light.rs @@ -10,7 +10,7 @@ use bevy_math::{primitives::ViewFrustum, Affine3A, Dir3, Mat3, Mat4, Vec3}; use bevy_reflect::prelude::*; use bevy_transform::components::{GlobalTransform, Transform}; -use crate::cluster::ClusterVisibilityClass; +use crate::{cluster::ClusterVisibilityClass, LightFalloff}; /// A light that emits light in a given direction from a central point. /// @@ -39,6 +39,9 @@ pub struct SpotLight { /// Consequently, you should set this value to be only the size that you need. pub range: f32, + /// Controls how this light attenuates over distance within its range. + pub falloff: LightFalloff, + /// Simulates a light source coming from a spherical volume with the given /// radius. /// @@ -148,6 +151,7 @@ impl Default for SpotLight { // this would be way too bright. intensity: 1_000_000.0, range: 20.0, + falloff: LightFalloff::InverseSquare, radius: 0.0, shadow_maps_enabled: false, contact_shadows_enabled: false, diff --git a/crates/bevy_pbr/src/cluster/cluster_allocate.wgsl b/crates/bevy_pbr/src/cluster/cluster_allocate.wgsl index 433e32ab91df0..a7b5e8ca741f0 100644 --- a/crates/bevy_pbr/src/cluster/cluster_allocate.wgsl +++ b/crates/bevy_pbr/src/cluster/cluster_allocate.wgsl @@ -83,6 +83,7 @@ fn allocate_local_main( // of the rasterizer. scratchpad_offsets_and_counts.data[global_id.x][0u] = vec4(0u); scratchpad_offsets_and_counts.data[global_id.x][1u] = vec4(0u); + scratchpad_offsets_and_counts.data[global_id.x][2u] = vec4(0u); } } @@ -123,5 +124,9 @@ fn cluster_object_count(cluster_index: u32) -> u32 { offsets_and_counts.data[cluster_index][0].z + offsets_and_counts.data[cluster_index][0].w + offsets_and_counts.data[cluster_index][1].x + - offsets_and_counts.data[cluster_index][1].y; + offsets_and_counts.data[cluster_index][1].y + + offsets_and_counts.data[cluster_index][1].z + + offsets_and_counts.data[cluster_index][1].w + + offsets_and_counts.data[cluster_index][2].x + + offsets_and_counts.data[cluster_index][2].y; } diff --git a/crates/bevy_pbr/src/cluster/cluster_raster.wgsl b/crates/bevy_pbr/src/cluster/cluster_raster.wgsl index dbd623a3383d4..35ceee51d2f3f 100644 --- a/crates/bevy_pbr/src/cluster/cluster_raster.wgsl +++ b/crates/bevy_pbr/src/cluster/cluster_raster.wgsl @@ -8,7 +8,8 @@ #import bevy_pbr::light_probes::transpose_affine_matrix #import bevy_pbr::mesh_view_types::{ ClusterOffsetsAndCounts, ClusterableObjectIndexLists, ClusteredDecals, ClusteredLights, - LightProbes, Lights, POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE + LightProbes, Lights, LIGHT_FALLOFF_EXPONENTIAL, LIGHT_FALLOFF_INVERSE_SQUARE, + LIGHT_FALLOFF_LINEAR, POINT_LIGHT_FLAGS_FALLOFF_SHIFT, POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE } #import bevy_render::view::View @@ -53,8 +54,12 @@ struct ClusterOffsetsAndCountsAtomic { // fields so that we can write to it. struct ClusterOffsetsAndCountsElementAtomic { offset: atomic, - point_lights: atomic, - spot_lights: atomic, + point_lights_inverse_square: atomic, + point_lights_linear: atomic, + point_lights_exponential: atomic, + spot_lights_inverse_square: atomic, + spot_lights_linear: atomic, + spot_lights_exponential: atomic, reflection_probes: atomic, irradiance_volumes: atomic, decals: atomic, @@ -221,12 +226,12 @@ fn fragment_main(varyings: Varyings) -> @location(0) vec4 { // object index. Otherwise, if this is the count pass, just bump the // appropriate counter. #ifdef POPULATE_PASS - let output_index = allocate_list_entry(cluster_index, object_type); + let output_index = allocate_list_entry(cluster_index, object_index, object_type); if (output_index < arrayLength(&index_lists.data)) { index_lists.data[output_index] = object_index; } #else // POPULATE_PASS - increment_object_count(cluster_index, object_type); + increment_object_count(cluster_index, object_index, object_type); #endif // POPULATE_PASS return vec4(0.0); @@ -435,24 +440,72 @@ fn sin_atan(tan_theta: f32) -> f32 { } #ifndef VERTEX_SHADER +fn light_falloff_mode(object_index: u32) -> u32 { + return (clustered_lights.data[object_index].flags >> POINT_LIGHT_FLAGS_FALLOFF_SHIFT) & 0x3u; +} + #ifdef POPULATE_PASS // Allocates space in the appropriate list and returns the global index that the // object index should be written to. -fn allocate_list_entry(cluster_index: u32, object_type: u32) -> u32 { +fn allocate_list_entry(cluster_index: u32, object_index: u32, object_type: u32) -> u32 { switch (object_type) { case CLUSTERABLE_OBJECT_TYPE_POINT_LIGHT: { + let falloff = light_falloff_mode(object_index); + if (falloff == LIGHT_FALLOFF_LINEAR) { + return offsets_and_counts.data[cluster_index][0u].x + + offsets_and_counts.data[cluster_index][0u].y + + atomicAdd(&scratchpad_offsets_and_counts.data[cluster_index].point_lights_linear, 1u); + } + if (falloff == LIGHT_FALLOFF_EXPONENTIAL) { + return offsets_and_counts.data[cluster_index][0u].x + + offsets_and_counts.data[cluster_index][0u].y + + offsets_and_counts.data[cluster_index][0u].z + + atomicAdd(&scratchpad_offsets_and_counts.data[cluster_index].point_lights_exponential, 1u); + } return offsets_and_counts.data[cluster_index][0u].x + - atomicAdd(&scratchpad_offsets_and_counts.data[cluster_index].point_lights, 1u); + atomicAdd( + &scratchpad_offsets_and_counts.data[cluster_index].point_lights_inverse_square, + 1u + ); } case CLUSTERABLE_OBJECT_TYPE_SPOT_LIGHT: { + let falloff = light_falloff_mode(object_index); + if (falloff == LIGHT_FALLOFF_LINEAR) { + return offsets_and_counts.data[cluster_index][0u].x + + offsets_and_counts.data[cluster_index][0u].y + + offsets_and_counts.data[cluster_index][0u].z + + offsets_and_counts.data[cluster_index][0u].w + + offsets_and_counts.data[cluster_index][1u].x + + atomicAdd(&scratchpad_offsets_and_counts.data[cluster_index].spot_lights_linear, 1u); + } + if (falloff == LIGHT_FALLOFF_EXPONENTIAL) { + return offsets_and_counts.data[cluster_index][0u].x + + offsets_and_counts.data[cluster_index][0u].y + + offsets_and_counts.data[cluster_index][0u].z + + offsets_and_counts.data[cluster_index][0u].w + + offsets_and_counts.data[cluster_index][1u].x + + offsets_and_counts.data[cluster_index][1u].y + + atomicAdd( + &scratchpad_offsets_and_counts.data[cluster_index].spot_lights_exponential, + 1u + ); + } return offsets_and_counts.data[cluster_index][0u].x + offsets_and_counts.data[cluster_index][0u].y + - atomicAdd(&scratchpad_offsets_and_counts.data[cluster_index].spot_lights, 1u); + offsets_and_counts.data[cluster_index][0u].z + + offsets_and_counts.data[cluster_index][0u].w + + atomicAdd( + &scratchpad_offsets_and_counts.data[cluster_index].spot_lights_inverse_square, + 1u + ); } case CLUSTERABLE_OBJECT_TYPE_REFLECTION_PROBE: { return offsets_and_counts.data[cluster_index][0u].x + offsets_and_counts.data[cluster_index][0u].y + offsets_and_counts.data[cluster_index][0u].z + + offsets_and_counts.data[cluster_index][0u].w + + offsets_and_counts.data[cluster_index][1u].x + + offsets_and_counts.data[cluster_index][1u].y + atomicAdd(&scratchpad_offsets_and_counts.data[cluster_index].reflection_probes, 1u); } case CLUSTERABLE_OBJECT_TYPE_IRRADIANCE_VOLUME: { @@ -460,6 +513,9 @@ fn allocate_list_entry(cluster_index: u32, object_type: u32) -> u32 { offsets_and_counts.data[cluster_index][0u].y + offsets_and_counts.data[cluster_index][0u].z + offsets_and_counts.data[cluster_index][0u].w + + offsets_and_counts.data[cluster_index][1u].x + + offsets_and_counts.data[cluster_index][1u].y + + offsets_and_counts.data[cluster_index][1u].z + atomicAdd( &scratchpad_offsets_and_counts.data[cluster_index].irradiance_volumes, 1u @@ -471,6 +527,8 @@ fn allocate_list_entry(cluster_index: u32, object_type: u32) -> u32 { offsets_and_counts.data[cluster_index][0u].z + offsets_and_counts.data[cluster_index][0u].w + offsets_and_counts.data[cluster_index][1u].x + + offsets_and_counts.data[cluster_index][1u].y + + offsets_and_counts.data[cluster_index][1u].z + atomicAdd(&scratchpad_offsets_and_counts.data[cluster_index].decals, 1u); } default: {} @@ -479,13 +537,45 @@ fn allocate_list_entry(cluster_index: u32, object_type: u32) -> u32 { } #else // POPULATE_PASS // Increments the count of objects of the given type for the given cluster. -fn increment_object_count(cluster_index: u32, object_type: u32) { +fn increment_object_count(cluster_index: u32, object_index: u32, object_type: u32) { switch (object_type) { case CLUSTERABLE_OBJECT_TYPE_POINT_LIGHT: { - atomicAdd(&offsets_and_counts.data[cluster_index].point_lights, 1u); + switch light_falloff_mode(object_index) { + case LIGHT_FALLOFF_LINEAR: { + atomicAdd(&offsets_and_counts.data[cluster_index].point_lights_linear, 1u); + } + case LIGHT_FALLOFF_EXPONENTIAL: { + atomicAdd( + &offsets_and_counts.data[cluster_index].point_lights_exponential, + 1u + ); + } + default: { + atomicAdd( + &offsets_and_counts.data[cluster_index].point_lights_inverse_square, + 1u + ); + } + } } case CLUSTERABLE_OBJECT_TYPE_SPOT_LIGHT: { - atomicAdd(&offsets_and_counts.data[cluster_index].spot_lights, 1u); + switch light_falloff_mode(object_index) { + case LIGHT_FALLOFF_LINEAR: { + atomicAdd(&offsets_and_counts.data[cluster_index].spot_lights_linear, 1u); + } + case LIGHT_FALLOFF_EXPONENTIAL: { + atomicAdd( + &offsets_and_counts.data[cluster_index].spot_lights_exponential, + 1u + ); + } + default: { + atomicAdd( + &offsets_and_counts.data[cluster_index].spot_lights_inverse_square, + 1u + ); + } + } } case CLUSTERABLE_OBJECT_TYPE_REFLECTION_PROBE: { atomicAdd(&offsets_and_counts.data[cluster_index].reflection_probes, 1u); diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index 959089758b07c..3e505353af1dd 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -219,15 +219,15 @@ struct GpuClusterableObjectIndexListsStorage { #[derive(ShaderType, Default)] struct GpuClusterOffsetsAndCountsStorage { - /// The starting offset, followed by the number of point lights, spot - /// lights, reflection probes, and irradiance volumes in each cluster, in - /// that order. The remaining fields are filled with zeroes. + /// The starting offset, followed by clustered object counts for each + /// cluster. Storage-buffer targets keep separate point and spot light + /// counts for each supported distance falloff mode. #[shader(size(runtime))] data: Vec, } /// The type we use for the offset and counts for each cluster. -type GpuClusterOffsetAndCounts = [UVec4; 2]; +type GpuClusterOffsetAndCounts = [UVec4; 3]; enum ViewClusterBuffers { Uniform { @@ -614,8 +614,11 @@ impl ViewClusterBindings { return; } let component = self.n_offsets & ((1 << 2) - 1); - let packed = - pack_offset_and_counts(offset, counts.point_lights, counts.spot_lights); + let packed = pack_offset_and_counts( + offset, + counts.total_point_lights(), + counts.total_spot_lights(), + ); cluster_offsets_and_counts.get_mut().data[array_index][component] = packed; } @@ -626,8 +629,14 @@ impl ViewClusterBindings { cluster_offsets_and_counts.get_mut().data.push([ uvec4( offset as u32, - counts.point_lights, - counts.spot_lights, + counts.point_lights_inverse_square, + counts.point_lights_linear, + counts.point_lights_exponential, + ), + uvec4( + counts.spot_lights_inverse_square, + counts.spot_lights_linear, + counts.spot_lights_exponential, counts.reflection_probes, ), uvec4(counts.irradiance_volumes, counts.decals, 0, 0), diff --git a/crates/bevy_pbr/src/render/clustered_forward.wgsl b/crates/bevy_pbr/src/render/clustered_forward.wgsl index 4bdf29494f85a..a23f35af6e3e5 100644 --- a/crates/bevy_pbr/src/render/clustered_forward.wgsl +++ b/crates/bevy_pbr/src/render/clustered_forward.wgsl @@ -12,9 +12,31 @@ // Offsets within the `cluster_offsets_and_counts` buffer for a single cluster. // +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 // These offsets must be monotonically nondecreasing. That is, indices are -// always sorted into the following order: point lights, spot lights, reflection -// probes, irradiance volumes. +// always sorted into the following order: point lights bucketed by distance +// falloff mode, then spot lights bucketed by distance falloff mode, followed +// by reflection probes, irradiance volumes, and decals. +struct ClusterableObjectIndexRanges { + first_point_light_inverse_square_index_offset: u32, + first_point_light_linear_index_offset: u32, + first_point_light_exponential_index_offset: u32, + first_spot_light_inverse_square_index_offset: u32, + first_spot_light_linear_index_offset: u32, + first_spot_light_exponential_index_offset: u32, + // The offset of the index of the first reflection probe, which also + // terminates the list of spot lights. + first_reflection_probe_index_offset: u32, + // The offset of the index of the first irradiance volumes, which also + // terminates the list of reflection probes. + first_irradiance_volume_index_offset: u32, + first_decal_offset: u32, + // One past the offset of the index of the final clusterable object for this + // cluster. + last_clusterable_object_index_offset: u32, +} +#else +// Compact fallback used on targets without storage-buffer clustering support. struct ClusterableObjectIndexRanges { // The offset of the index of the first point light. first_point_light_index_offset: u32, @@ -32,6 +54,7 @@ struct ClusterableObjectIndexRanges { // cluster. last_clusterable_object_index_offset: u32, } +#endif // NOTE: Keep in sync with bevy_pbr/src/light.rs fn view_z_to_z_slice( @@ -87,21 +110,36 @@ fn unpack_clusterable_object_index_ranges(cluster_index: u32) -> ClusterableObje let offset_and_counts_a = bindings::cluster_offsets_and_counts.data[cluster_index][0]; let offset_and_counts_b = bindings::cluster_offsets_and_counts.data[cluster_index][1]; + let offset_and_counts_c = bindings::cluster_offsets_and_counts.data[cluster_index][2]; // Sum up the counts to produce the range brackets. // // We could have stored the range brackets in `cluster_offsets_and_counts` // directly, but doing it this way makes the logic in this path more // consistent with the WebGL 2 path below. - let point_light_offset = offset_and_counts_a.x; - let spot_light_offset = point_light_offset + offset_and_counts_a.y; - let reflection_probe_offset = spot_light_offset + offset_and_counts_a.z; - let irradiance_volume_offset = reflection_probe_offset + offset_and_counts_a.w; - let decal_offset = irradiance_volume_offset + offset_and_counts_b.x; - let last_clusterable_offset = decal_offset + offset_and_counts_b.y; + let point_light_inverse_square_offset = offset_and_counts_a.x; + let point_light_linear_offset = + point_light_inverse_square_offset + offset_and_counts_a.y; + let point_light_exponential_offset = + point_light_linear_offset + offset_and_counts_a.z; + let spot_light_inverse_square_offset = + point_light_exponential_offset + offset_and_counts_a.w; + let spot_light_linear_offset = + spot_light_inverse_square_offset + offset_and_counts_b.x; + let spot_light_exponential_offset = + spot_light_linear_offset + offset_and_counts_b.y; + let reflection_probe_offset = + spot_light_exponential_offset + offset_and_counts_b.z; + let irradiance_volume_offset = reflection_probe_offset + offset_and_counts_b.w; + let decal_offset = irradiance_volume_offset + offset_and_counts_c.x; + let last_clusterable_offset = decal_offset + offset_and_counts_c.y; return ClusterableObjectIndexRanges( - point_light_offset, - spot_light_offset, + point_light_inverse_square_offset, + point_light_linear_offset, + point_light_exponential_offset, + spot_light_inverse_square_offset, + spot_light_linear_offset, + spot_light_exponential_offset, reflection_probe_offset, irradiance_volume_offset, decal_offset, @@ -190,8 +228,13 @@ fn cluster_debug_visualization( // complexity measure. let cluster_overlay_alpha = 0.1; let max_complexity_per_cluster = 64.0; +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + let object_count = clusterable_object_index_ranges.first_reflection_probe_index_offset - + clusterable_object_index_ranges.first_point_light_inverse_square_index_offset; +#else let object_count = clusterable_object_index_ranges.first_reflection_probe_index_offset - clusterable_object_index_ranges.first_point_light_index_offset; +#endif output_color.r = (1.0 - cluster_overlay_alpha) * output_color.r + cluster_overlay_alpha * smoothstep(0.0, max_complexity_per_cluster, f32(object_count)); output_color.g = (1.0 - cluster_overlay_alpha) * output_color.g + cluster_overlay_alpha * diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 2b8b1b31e6704..b3d8b283e16dc 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -22,8 +22,8 @@ use bevy_light::cluster::assign::{calculate_cluster_factors, ClusterableObjectTy use bevy_light::SunDisk; use bevy_light::{ spot_light_clip_from_view, spot_light_world_from_view, AmbientLight, CascadeShadowConfig, - Cascades, DirectionalLight, DirectionalLightShadowMap, GlobalAmbientLight, PointLight, - PointLightShadowMap, ShadowFilteringMethod, SpotLight, VolumetricLight, + Cascades, DirectionalLight, DirectionalLightShadowMap, GlobalAmbientLight, LightFalloff, + PointLight, PointLightShadowMap, ShadowFilteringMethod, SpotLight, VolumetricLight, }; use bevy_material::{ key::{ErasedMaterialPipelineKey, ErasedMeshPipelineKey}, @@ -79,6 +79,7 @@ pub struct ExtractedPointLight { /// luminous intensity in lumens per steradian pub intensity: f32, pub range: f32, + pub falloff: LightFalloff, pub radius: f32, pub transform: GlobalTransform, pub shadow_maps_enabled: bool, @@ -132,6 +133,8 @@ bitflags::bitflags! { } } +const POINT_LIGHT_FLAGS_FALLOFF_SHIFT: u32 = 6; + #[derive(Copy, Clone, ShaderType, Default, Debug)] pub struct GpuDirectionalCascade { clip_from_world: Mat4, @@ -487,6 +490,7 @@ pub fn extract_lights( // for details. intensity: point_light.intensity / (4.0 * core::f32::consts::PI), range: point_light.range, + falloff: point_light.falloff, radius: point_light.radius, transform: *transform, shadow_maps_enabled: point_light.shadow_maps_enabled, @@ -594,6 +598,7 @@ pub fn extract_lights( // which seems least surprising for users intensity: spot_light.intensity / (4.0 * core::f32::consts::PI), range: spot_light.range, + falloff: spot_light.falloff, radius: spot_light.radius, transform: *transform, shadow_maps_enabled: spot_light.shadow_maps_enabled, @@ -1073,7 +1078,8 @@ pub fn prepare_lights( .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, + // - point-light vs spot-light and then by falloff mode, so that we can iterate matching + // 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. @@ -1174,6 +1180,8 @@ pub fn prepare_lights( } }; + let flags = flags.bits() | (light.falloff.shader_index() << POINT_LIGHT_FLAGS_FALLOFF_SHIFT); + global_clusterable_object_meta .gpu_clustered_lights .add(GpuClusteredLight { @@ -1185,7 +1193,7 @@ pub fn prepare_lights( .xyz() .extend(1.0 / (light.range * light.range)), position_radius: light.transform.translation().extend(light.radius), - flags: flags.bits(), + flags, shadow_depth_bias: light.shadow_depth_bias, shadow_normal_bias: light.shadow_normal_bias, shadow_map_near_z: light.shadow_map_near_z, @@ -2571,10 +2579,12 @@ fn point_or_spot_light_to_clusterable(point_light: &ExtractedPointLight) -> Clus outer_angle, shadow_maps_enabled: point_light.shadow_maps_enabled, volumetric: point_light.volumetric, + falloff: point_light.falloff, }, None => ClusterableObjectType::PointLight { shadow_maps_enabled: point_light.shadow_maps_enabled, volumetric: point_light.volumetric, + falloff: point_light.falloff, }, } } diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index bacde26fb99f8..a7db39720ed88 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -23,6 +23,10 @@ const POINT_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 1u << 2u; const POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 1u << 3u; const POINT_LIGHT_FLAGS_CONTACT_SHADOWS_ENABLED_BIT: u32 = 1u << 4u; const POINT_LIGHT_FLAGS_SPOT_LIGHT_BIT: u32 = 1u << 5u; +const POINT_LIGHT_FLAGS_FALLOFF_SHIFT: u32 = 6u; +const LIGHT_FALLOFF_INVERSE_SQUARE: u32 = 0u; +const LIGHT_FALLOFF_LINEAR: u32 = 1u; +const LIGHT_FALLOFF_EXPONENTIAL: u32 = 2u; struct DirectionalCascade { clip_from_world: mat4x4, @@ -107,7 +111,7 @@ struct ClusterableObjectIndexLists { data: array, }; struct ClusterOffsetsAndCounts { - data: array, 2>>, + data: array, 3>>, }; #else struct ClusteredLights { diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 8a08c7cdb0292..3f296f4bf18ec 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -455,6 +455,329 @@ fn apply_pbr_lighting( let contact_shadow_enabled = contact_shadow_steps > 0u; // Point lights (direct) +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + for (var i: u32 = clusterable_object_index_ranges.first_point_light_inverse_square_index_offset; + i < clusterable_object_index_ranges.first_point_light_linear_index_offset; + i = i + 1u) { + let light_id = clustering::get_clusterable_object_id(i); + +#ifdef LIGHTMAP + let enable_diffuse = + (view_bindings::clustered_lights.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT) != 0u; +#else + let enable_diffuse = true; +#endif + + var shadow: f32 = 1.0; + if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u + && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = shadows::fetch_point_shadow(light_id, in.world_position, in.world_normal, in.frag_coord.xy); + } + +#ifdef DEPTH_PREPASS + if contact_shadow_enabled && (in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u && shadow > 0.0 && + (view_bindings::clustered_lights.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_CONTACT_SHADOWS_ENABLED_BIT) != 0u { + let L = normalize(view_bindings::clustered_lights.data[light_id].position_radius.xyz - in.world_position.xyz); + shadow *= calculate_contact_shadow(in.world_position.xyz, in.frag_coord.xy, L, contact_shadow_steps); + } +#endif + + let light_contrib = + lighting::point_light_inverse_square(light_id, &lighting_input, enable_diffuse, true); + direct_light += light_contrib * shadow; + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + var transmitted_shadow: f32 = 1.0; + if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) + && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + transmitted_shadow = shadows::fetch_point_shadow(light_id, diffuse_transmissive_lobe_world_position, -in.world_normal, in.frag_coord.xy); + } + + let transmitted_light_contrib = lighting::point_light_inverse_square( + light_id, + &transmissive_lighting_input, + enable_diffuse, + true, + ); + transmitted_light += transmitted_light_contrib * transmitted_shadow; +#endif + } + + for (var i: u32 = clusterable_object_index_ranges.first_point_light_linear_index_offset; + i < clusterable_object_index_ranges.first_point_light_exponential_index_offset; + i = i + 1u) { + let light_id = clustering::get_clusterable_object_id(i); + +#ifdef LIGHTMAP + let enable_diffuse = + (view_bindings::clustered_lights.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT) != 0u; +#else + let enable_diffuse = true; +#endif + + var shadow: f32 = 1.0; + if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u + && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = shadows::fetch_point_shadow(light_id, in.world_position, in.world_normal, in.frag_coord.xy); + } + +#ifdef DEPTH_PREPASS + if contact_shadow_enabled && (in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u && shadow > 0.0 && + (view_bindings::clustered_lights.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_CONTACT_SHADOWS_ENABLED_BIT) != 0u { + let L = normalize(view_bindings::clustered_lights.data[light_id].position_radius.xyz - in.world_position.xyz); + shadow *= calculate_contact_shadow(in.world_position.xyz, in.frag_coord.xy, L, contact_shadow_steps); + } +#endif + + let light_contrib = + lighting::point_light_linear(light_id, &lighting_input, enable_diffuse, true); + direct_light += light_contrib * shadow; + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + var transmitted_shadow: f32 = 1.0; + if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) + && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + transmitted_shadow = shadows::fetch_point_shadow(light_id, diffuse_transmissive_lobe_world_position, -in.world_normal, in.frag_coord.xy); + } + + let transmitted_light_contrib = lighting::point_light_linear( + light_id, + &transmissive_lighting_input, + enable_diffuse, + true, + ); + transmitted_light += transmitted_light_contrib * transmitted_shadow; +#endif + } + + for (var i: u32 = clusterable_object_index_ranges.first_point_light_exponential_index_offset; + i < clusterable_object_index_ranges.first_spot_light_inverse_square_index_offset; + i = i + 1u) { + let light_id = clustering::get_clusterable_object_id(i); + +#ifdef LIGHTMAP + let enable_diffuse = + (view_bindings::clustered_lights.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT) != 0u; +#else + let enable_diffuse = true; +#endif + + var shadow: f32 = 1.0; + if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u + && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = shadows::fetch_point_shadow(light_id, in.world_position, in.world_normal, in.frag_coord.xy); + } + +#ifdef DEPTH_PREPASS + if contact_shadow_enabled && (in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u && shadow > 0.0 && + (view_bindings::clustered_lights.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_CONTACT_SHADOWS_ENABLED_BIT) != 0u { + let L = normalize(view_bindings::clustered_lights.data[light_id].position_radius.xyz - in.world_position.xyz); + shadow *= calculate_contact_shadow(in.world_position.xyz, in.frag_coord.xy, L, contact_shadow_steps); + } +#endif + + let light_contrib = + lighting::point_light_exponential(light_id, &lighting_input, enable_diffuse, true); + direct_light += light_contrib * shadow; + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + var transmitted_shadow: f32 = 1.0; + if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) + && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + transmitted_shadow = shadows::fetch_point_shadow(light_id, diffuse_transmissive_lobe_world_position, -in.world_normal, in.frag_coord.xy); + } + + let transmitted_light_contrib = lighting::point_light_exponential( + light_id, + &transmissive_lighting_input, + enable_diffuse, + true, + ); + transmitted_light += transmitted_light_contrib * transmitted_shadow; +#endif + } + + // Spot lights (direct) + for (var i: u32 = clusterable_object_index_ranges.first_spot_light_inverse_square_index_offset; + i < clusterable_object_index_ranges.first_spot_light_linear_index_offset; + i = i + 1u) { + let light_id = clustering::get_clusterable_object_id(i); + +#ifdef LIGHTMAP + let enable_diffuse = + (view_bindings::clustered_lights.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT) != 0u; +#else + let enable_diffuse = true; +#endif + + var shadow: f32 = 1.0; + if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u + && (view_bindings::clustered_lights.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = shadows::fetch_spot_shadow( + light_id, + in.world_position, + in.world_normal, + view_bindings::clustered_lights.data[light_id].shadow_map_near_z, + in.frag_coord.xy, + ); + } + +#ifdef DEPTH_PREPASS + if contact_shadow_enabled && (in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u && shadow > 0.0 && + (view_bindings::clustered_lights.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_CONTACT_SHADOWS_ENABLED_BIT) != 0u { + let L = normalize(view_bindings::clustered_lights.data[light_id].position_radius.xyz - in.world_position.xyz); + shadow *= calculate_contact_shadow(in.world_position.xyz, in.frag_coord.xy, L, contact_shadow_steps); + } +#endif + + let light_contrib = + lighting::spot_light_inverse_square(light_id, &lighting_input, enable_diffuse); + direct_light += light_contrib * shadow; + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + var transmitted_shadow: f32 = 1.0; + if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) + && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + transmitted_shadow = shadows::fetch_spot_shadow( + light_id, + diffuse_transmissive_lobe_world_position, + -in.world_normal, + view_bindings::clustered_lights.data[light_id].shadow_map_near_z, + in.frag_coord.xy, + ); + } + + let transmitted_light_contrib = + lighting::spot_light_inverse_square(light_id, &transmissive_lighting_input, enable_diffuse); + transmitted_light += transmitted_light_contrib * transmitted_shadow; +#endif + } + + for (var i: u32 = clusterable_object_index_ranges.first_spot_light_linear_index_offset; + i < clusterable_object_index_ranges.first_spot_light_exponential_index_offset; + i = i + 1u) { + let light_id = clustering::get_clusterable_object_id(i); + +#ifdef LIGHTMAP + let enable_diffuse = + (view_bindings::clustered_lights.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT) != 0u; +#else + let enable_diffuse = true; +#endif + + var shadow: f32 = 1.0; + if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u + && (view_bindings::clustered_lights.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = shadows::fetch_spot_shadow( + light_id, + in.world_position, + in.world_normal, + view_bindings::clustered_lights.data[light_id].shadow_map_near_z, + in.frag_coord.xy, + ); + } + +#ifdef DEPTH_PREPASS + if contact_shadow_enabled && (in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u && shadow > 0.0 && + (view_bindings::clustered_lights.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_CONTACT_SHADOWS_ENABLED_BIT) != 0u { + let L = normalize(view_bindings::clustered_lights.data[light_id].position_radius.xyz - in.world_position.xyz); + shadow *= calculate_contact_shadow(in.world_position.xyz, in.frag_coord.xy, L, contact_shadow_steps); + } +#endif + + let light_contrib = + lighting::spot_light_linear(light_id, &lighting_input, enable_diffuse); + direct_light += light_contrib * shadow; + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + var transmitted_shadow: f32 = 1.0; + if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) + && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + transmitted_shadow = shadows::fetch_spot_shadow( + light_id, + diffuse_transmissive_lobe_world_position, + -in.world_normal, + view_bindings::clustered_lights.data[light_id].shadow_map_near_z, + in.frag_coord.xy, + ); + } + + let transmitted_light_contrib = + lighting::spot_light_linear(light_id, &transmissive_lighting_input, enable_diffuse); + transmitted_light += transmitted_light_contrib * transmitted_shadow; +#endif + } + + for (var i: u32 = clusterable_object_index_ranges.first_spot_light_exponential_index_offset; + i < clusterable_object_index_ranges.first_reflection_probe_index_offset; + i = i + 1u) { + let light_id = clustering::get_clusterable_object_id(i); + +#ifdef LIGHTMAP + let enable_diffuse = + (view_bindings::clustered_lights.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT) != 0u; +#else + let enable_diffuse = true; +#endif + + var shadow: f32 = 1.0; + if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u + && (view_bindings::clustered_lights.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = shadows::fetch_spot_shadow( + light_id, + in.world_position, + in.world_normal, + view_bindings::clustered_lights.data[light_id].shadow_map_near_z, + in.frag_coord.xy, + ); + } + +#ifdef DEPTH_PREPASS + if contact_shadow_enabled && (in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u && shadow > 0.0 && + (view_bindings::clustered_lights.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_CONTACT_SHADOWS_ENABLED_BIT) != 0u { + let L = normalize(view_bindings::clustered_lights.data[light_id].position_radius.xyz - in.world_position.xyz); + shadow *= calculate_contact_shadow(in.world_position.xyz, in.frag_coord.xy, L, contact_shadow_steps); + } +#endif + + let light_contrib = + lighting::spot_light_exponential(light_id, &lighting_input, enable_diffuse); + direct_light += light_contrib * shadow; + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + var transmitted_shadow: f32 = 1.0; + if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) + && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + transmitted_shadow = shadows::fetch_spot_shadow( + light_id, + diffuse_transmissive_lobe_world_position, + -in.world_normal, + view_bindings::clustered_lights.data[light_id].shadow_map_near_z, + in.frag_coord.xy, + ); + } + + let transmitted_light_contrib = + lighting::spot_light_exponential(light_id, &transmissive_lighting_input, enable_diffuse); + transmitted_light += transmitted_light_contrib * transmitted_shadow; +#endif + } +#else for (var i: u32 = clusterable_object_index_ranges.first_point_light_index_offset; i < clusterable_object_index_ranges.first_spot_light_index_offset; i = i + 1u) { @@ -485,7 +808,16 @@ fn apply_pbr_lighting( } #endif - let light_contrib = lighting::point_light(light_id, &lighting_input, enable_diffuse, true); + let light_falloff_mode = (view_bindings::clustered_lights.data[light_id].flags >> + mesh_view_types::POINT_LIGHT_FLAGS_FALLOFF_SHIFT) & 0x3u; + var light_contrib: vec3; + if light_falloff_mode == mesh_view_types::LIGHT_FALLOFF_LINEAR { + light_contrib = lighting::point_light_linear(light_id, &lighting_input, enable_diffuse, true); + } else if light_falloff_mode == mesh_view_types::LIGHT_FALLOFF_EXPONENTIAL { + light_contrib = lighting::point_light_exponential(light_id, &lighting_input, enable_diffuse, true); + } else { + light_contrib = lighting::point_light_inverse_square(light_id, &lighting_input, enable_diffuse, true); + } direct_light += light_contrib * shadow; #ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION @@ -504,8 +836,14 @@ fn apply_pbr_lighting( transmitted_shadow = shadows::fetch_point_shadow(light_id, diffuse_transmissive_lobe_world_position, -in.world_normal, in.frag_coord.xy); } - let transmitted_light_contrib = - lighting::point_light(light_id, &transmissive_lighting_input, enable_diffuse, true); + var transmitted_light_contrib: vec3; + if light_falloff_mode == mesh_view_types::LIGHT_FALLOFF_LINEAR { + transmitted_light_contrib = lighting::point_light_linear(light_id, &transmissive_lighting_input, enable_diffuse, true); + } else if light_falloff_mode == mesh_view_types::LIGHT_FALLOFF_EXPONENTIAL { + transmitted_light_contrib = lighting::point_light_exponential(light_id, &transmissive_lighting_input, enable_diffuse, true); + } else { + transmitted_light_contrib = lighting::point_light_inverse_square(light_id, &transmissive_lighting_input, enable_diffuse, true); + } transmitted_light += transmitted_light_contrib * transmitted_shadow; #endif } @@ -548,7 +886,16 @@ fn apply_pbr_lighting( } #endif - let light_contrib = lighting::spot_light(light_id, &lighting_input, enable_diffuse); + let light_falloff_mode = (view_bindings::clustered_lights.data[light_id].flags >> + mesh_view_types::POINT_LIGHT_FLAGS_FALLOFF_SHIFT) & 0x3u; + var light_contrib: vec3; + if light_falloff_mode == mesh_view_types::LIGHT_FALLOFF_LINEAR { + light_contrib = lighting::spot_light_linear(light_id, &lighting_input, enable_diffuse); + } else if light_falloff_mode == mesh_view_types::LIGHT_FALLOFF_EXPONENTIAL { + light_contrib = lighting::spot_light_exponential(light_id, &lighting_input, enable_diffuse); + } else { + light_contrib = lighting::spot_light_inverse_square(light_id, &lighting_input, enable_diffuse); + } direct_light += light_contrib * shadow; #ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION @@ -573,11 +920,18 @@ fn apply_pbr_lighting( ); } - let transmitted_light_contrib = - lighting::spot_light(light_id, &transmissive_lighting_input, enable_diffuse); + var transmitted_light_contrib: vec3; + if light_falloff_mode == mesh_view_types::LIGHT_FALLOFF_LINEAR { + transmitted_light_contrib = lighting::spot_light_linear(light_id, &transmissive_lighting_input, enable_diffuse); + } else if light_falloff_mode == mesh_view_types::LIGHT_FALLOFF_EXPONENTIAL { + transmitted_light_contrib = lighting::spot_light_exponential(light_id, &transmissive_lighting_input, enable_diffuse); + } else { + transmitted_light_contrib = lighting::spot_light_inverse_square(light_id, &transmissive_lighting_input, enable_diffuse); + } transmitted_light += transmitted_light_contrib * transmitted_shadow; #endif } +#endif // directional lights (direct) let n_directional_lights = view_bindings::lights.n_directional_lights; diff --git a/crates/bevy_pbr/src/render/pbr_lighting.wgsl b/crates/bevy_pbr/src/render/pbr_lighting.wgsl index fba80a5da687b..cab70bca1889f 100644 --- a/crates/bevy_pbr/src/render/pbr_lighting.wgsl +++ b/crates/bevy_pbr/src/render/pbr_lighting.wgsl @@ -123,18 +123,29 @@ struct DerivedLightingInput { LdotH: f32, } -// distanceAttenuation is simply the square falloff of light intensity -// combined with a smooth attenuation at the edge of the light radius +// Distance attenuation for the default inverse-square falloff combined with a +// smooth attenuation at the edge of the light radius. // -// light radius is a non-physical construct for efficiency purposes, -// because otherwise every light affects every fragment in the scene -fn getDistanceAttenuation(distanceSquare: f32, inverseRangeSquared: f32) -> f32 { +// Light radius is a non-physical construct for efficiency purposes, because +// otherwise every light affects every fragment in the scene. +fn getDistanceAttenuationInverseSquare(distanceSquare: f32, inverseRangeSquared: f32) -> f32 { let factor = distanceSquare * inverseRangeSquared; let smoothFactor = saturate(1.0 - factor * factor); let attenuation = smoothFactor * smoothFactor; return attenuation * 1.0 / max(distanceSquare, 0.0001); } +fn getDistanceAttenuationLinear(distance: f32, range: f32) -> f32 { + return saturate(1.0 - distance / max(range, 0.0001)); +} + +fn getDistanceAttenuationExponential(distance: f32, range: f32) -> f32 { + let factor = distance / max(range, 0.0001); + let smoothFactor = saturate(1.0 - factor * factor); + let attenuation = smoothFactor * smoothFactor; + return attenuation * exp2(-8.0 * factor); +} + // Normal distribution function (specular D) // Based on https://google.github.io/filament/Filament.html#citation-walter07 @@ -623,11 +634,14 @@ fn specular_fix_remap(a: f32) -> f32 { return 1.0 - inv_a_sq * inv_a_sq; } -fn point_light( +fn point_light_with_range_attenuation( light_id: u32, input: ptr, enable_diffuse: bool, enable_texture: bool, + light_to_frag: vec3, + distance: f32, + rangeAttenuation: f32, ) -> vec3 { // Unpack. let diffuse_color = (*input).diffuse_color; @@ -636,11 +650,7 @@ fn point_light( let V = (*input).V; let light = &view_bindings::clustered_lights.data[light_id]; - let light_to_frag = (*light).position_radius.xyz - P; let L = normalize(light_to_frag); - let distance_square = dot(light_to_frag, light_to_frag); - let distance = sqrt(distance_square); - let rangeAttenuation = getDistanceAttenuation(distance_square, (*light).color_inverse_square_range.w); // Base layer @@ -776,14 +786,76 @@ fn point_light( rangeAttenuation * texture_sample; } -fn spot_light( +fn point_light_inverse_square( light_id: u32, input: ptr, - enable_diffuse: bool + enable_diffuse: bool, + enable_texture: bool, +) -> vec3 { + let light = &view_bindings::clustered_lights.data[light_id]; + let light_to_frag = (*light).position_radius.xyz - (*input).P; + let distance_square = dot(light_to_frag, light_to_frag); + let distance = sqrt(distance_square); + let rangeAttenuation = + getDistanceAttenuationInverseSquare(distance_square, (*light).color_inverse_square_range.w); + return point_light_with_range_attenuation( + light_id, + input, + enable_diffuse, + enable_texture, + light_to_frag, + distance, + rangeAttenuation, + ); +} + +fn point_light_linear( + light_id: u32, + input: ptr, + enable_diffuse: bool, + enable_texture: bool, +) -> vec3 { + let light = &view_bindings::clustered_lights.data[light_id]; + let light_to_frag = (*light).position_radius.xyz - (*input).P; + let distance = length(light_to_frag); + let rangeAttenuation = getDistanceAttenuationLinear(distance, (*light).range); + return point_light_with_range_attenuation( + light_id, + input, + enable_diffuse, + enable_texture, + light_to_frag, + distance, + rangeAttenuation, + ); +} + +fn point_light_exponential( + light_id: u32, + input: ptr, + enable_diffuse: bool, + enable_texture: bool, ) -> vec3 { - // reuse the point light calculations - let point_light = point_light(light_id, input, enable_diffuse, false); + let light = &view_bindings::clustered_lights.data[light_id]; + let light_to_frag = (*light).position_radius.xyz - (*input).P; + let distance = length(light_to_frag); + let rangeAttenuation = getDistanceAttenuationExponential(distance, (*light).range); + return point_light_with_range_attenuation( + light_id, + input, + enable_diffuse, + enable_texture, + light_to_frag, + distance, + rangeAttenuation, + ); +} +fn spot_light_with_point_light( + light_id: u32, + input: ptr, + point_light: vec3, +) -> vec3 { let light = &view_bindings::clustered_lights.data[light_id]; // reconstruct spot dir from x/z and y-direction flag @@ -824,6 +896,33 @@ fn spot_light( return point_light * spot_attenuation * texture_sample; } +fn spot_light_inverse_square( + light_id: u32, + input: ptr, + enable_diffuse: bool, +) -> vec3 { + let point_light = point_light_inverse_square(light_id, input, enable_diffuse, false); + return spot_light_with_point_light(light_id, input, point_light); +} + +fn spot_light_linear( + light_id: u32, + input: ptr, + enable_diffuse: bool, +) -> vec3 { + let point_light = point_light_linear(light_id, input, enable_diffuse, false); + return spot_light_with_point_light(light_id, input, point_light); +} + +fn spot_light_exponential( + light_id: u32, + input: ptr, + enable_diffuse: bool, +) -> vec3 { + let point_light = point_light_exponential(light_id, input, enable_diffuse, false); + return spot_light_with_point_light(light_id, input, point_light); +} + fn directional_light( light_id: u32, input: ptr, diff --git a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl index 41018555bc666..81398ac773df9 100644 --- a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl +++ b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl @@ -24,6 +24,9 @@ atmosphere_data, atmosphere_transmittance_texture, atmosphere_transmittance_sampler } #import bevy_pbr::mesh_view_types::{ + LIGHT_FALLOFF_EXPONENTIAL, + LIGHT_FALLOFF_LINEAR, + POINT_LIGHT_FLAGS_FALLOFF_SHIFT, DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT, POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT, POINT_LIGHT_FLAGS_VOLUMETRIC_BIT, @@ -45,7 +48,11 @@ position_view_to_world } #import bevy_pbr::clustered_forward as clustering -#import bevy_pbr::lighting::getDistanceAttenuation; +#import bevy_pbr::lighting::{ + getDistanceAttenuationExponential, + getDistanceAttenuationInverseSquare, + getDistanceAttenuationLinear, +}; // The GPU version of [`VolumetricFog`]. See the comments in // `volumetric_fog/mod.rs` for descriptions of the fields here. @@ -341,7 +348,18 @@ fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { let cluster_index = clustering::view_fragment_cluster_index(frag_coord.xy, view_z, is_orthographic); var clusterable_object_index_ranges = clustering::unpack_clusterable_object_index_ranges(cluster_index); - for (var i: u32 = clusterable_object_index_ranges.first_point_light_index_offset; +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + let first_point_light_index_offset = + clusterable_object_index_ranges.first_point_light_inverse_square_index_offset; + let first_spot_light_index_offset = + clusterable_object_index_ranges.first_spot_light_inverse_square_index_offset; +#else + let first_point_light_index_offset = + clusterable_object_index_ranges.first_point_light_index_offset; + let first_spot_light_index_offset = + clusterable_object_index_ranges.first_spot_light_index_offset; +#endif + for (var i: u32 = first_point_light_index_offset; i < clusterable_object_index_ranges.first_reflection_probe_index_offset; i = i + 1u) { let light_id = clustering::get_clusterable_object_id(i); @@ -370,9 +388,22 @@ fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { let V = Rd_world; let L = normalize(light_to_frag); let distance_square = dot(light_to_frag, light_to_frag); - let distance_atten = getDistanceAttenuation(distance_square, (*light).color_inverse_square_range.w); + let light_falloff_mode = + ((*light).flags >> POINT_LIGHT_FLAGS_FALLOFF_SHIFT) & 0x3u; + let distance = sqrt(distance_square); + var distance_atten: f32; + if light_falloff_mode == LIGHT_FALLOFF_LINEAR { + distance_atten = getDistanceAttenuationLinear(distance, (*light).range); + } else if light_falloff_mode == LIGHT_FALLOFF_EXPONENTIAL { + distance_atten = getDistanceAttenuationExponential(distance, (*light).range); + } else { + distance_atten = getDistanceAttenuationInverseSquare( + distance_square, + (*light).color_inverse_square_range.w, + ); + } var local_light_attenuation = distance_atten; - if (i < clusterable_object_index_ranges.first_spot_light_index_offset) { + if (i < first_spot_light_index_offset) { var shadow: f32 = 1.0; if (((*light).flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { shadow = fetch_point_shadow_without_normal(light_id, vec4(P_world, 1.0), position.xy); diff --git a/examples/3d/light_falloff.rs b/examples/3d/light_falloff.rs new file mode 100644 index 0000000000000..90c2960d39845 --- /dev/null +++ b/examples/3d/light_falloff.rs @@ -0,0 +1,478 @@ +//! Demonstrates the built-in light falloff modes for point and spot lights. + +use std::f32::consts::PI; + +use bevy::{ + color::palettes::css::{BLUE, SILVER, YELLOW}, + prelude::*, +}; + +const INSTRUCTIONS: &str = "Light Falloff"; + +const POINT_LIGHT_INTENSITY: f32 = 520_000.0; +const SPOT_LIGHT_INTENSITY: f32 = 760_000.0; +const LIGHT_RANGE: f32 = 21.0; +const RUNWAY_LENGTH: f32 = 20.0; +const INTENSITY_SCALE: f32 = 1.1; +const KEY_REPEAT_DELAY: f32 = 0.3; +const KEY_REPEAT_INTERVAL: f32 = 0.06; + +fn main() { + App::new() + .insert_resource(ClearColor(Color::srgb(0.04, 0.04, 0.05))) + .insert_resource(GlobalAmbientLight { + brightness: 20.0, + ..default() + }) + .init_resource::() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, (handle_keyboard_controls, sync_light_status_text)) + .run(); +} + +#[derive(Resource)] +struct LightControls { + selected: SelectedLight, +} + +impl Default for LightControls { + fn default() -> Self { + Self { + selected: SelectedLight::Point, + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum SelectedLight { + Point, + Spot, +} + +#[derive(Component)] +struct PointDemoLight; + +#[derive(Component)] +struct SpotDemoLight; + +#[derive(Component)] +struct SelectedLightLabel; + +#[derive(Clone, Copy)] +enum UiAction { + CycleFalloff, + IntensityDown, + IntensityUp, +} + +#[derive(Component)] +struct LightStateLabel; + +#[derive(Default)] +struct HeldKeyState { + key: Option, + timer: Option, +} + +trait DemoLight { + fn falloff(&self) -> LightFalloff; + fn set_falloff(&mut self, falloff: LightFalloff); + fn intensity(&self) -> f32; + fn set_intensity(&mut self, intensity: f32); +} + +impl DemoLight for PointLight { + fn falloff(&self) -> LightFalloff { + self.falloff + } + + fn set_falloff(&mut self, falloff: LightFalloff) { + self.falloff = falloff; + } + + fn intensity(&self) -> f32 { + self.intensity + } + + fn set_intensity(&mut self, intensity: f32) { + self.intensity = intensity; + } +} + +impl DemoLight for SpotLight { + fn falloff(&self) -> LightFalloff { + self.falloff + } + + fn set_falloff(&mut self, falloff: LightFalloff) { + self.falloff = falloff; + } + + fn intensity(&self) -> f32 { + self.intensity + } + + fn set_intensity(&mut self, intensity: f32) { + self.intensity = intensity; + } +} + +impl DemoLight for Mut<'_, PointLight> { + fn falloff(&self) -> LightFalloff { + self.falloff + } + + fn set_falloff(&mut self, falloff: LightFalloff) { + self.falloff = falloff; + } + + fn intensity(&self) -> f32 { + self.intensity + } + + fn set_intensity(&mut self, intensity: f32) { + self.intensity = intensity; + } +} + +impl DemoLight for Mut<'_, SpotLight> { + fn falloff(&self) -> LightFalloff { + self.falloff + } + + fn set_falloff(&mut self, falloff: LightFalloff) { + self.falloff = falloff; + } + + fn intensity(&self) -> f32 { + self.intensity + } + + fn set_intensity(&mut self, intensity: f32) { + self.intensity = intensity; + } +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + controls: Res, +) { + let runway_mesh = meshes.add(Cuboid::new(RUNWAY_LENGTH, 0.2, 3.0)); + let end_wall_mesh = meshes.add(Cuboid::new(0.2, 2.8, 3.0)); + let marker_mesh = meshes.add(Cuboid::new(0.3, 0.06, 1.9)); + let pillar_mesh = meshes.add(Cuboid::new(0.6, 1.4, 0.6)); + let sphere_mesh = meshes.add(Sphere::new(0.55).mesh().uv(32, 18)); + let runway_material = materials.add(StandardMaterial { + base_color: Color::srgb(0.12, 0.12, 0.13), + perceptual_roughness: 1.0, + ..default() + }); + let wall_material = materials.add(StandardMaterial { + base_color: Color::srgb(0.82, 0.82, 0.84), + perceptual_roughness: 0.92, + ..default() + }); + let marker_material = materials.add(StandardMaterial { + base_color: Color::srgb(0.42, 0.43, 0.48), + perceptual_roughness: 0.8, + ..default() + }); + let pillar_material = materials.add(StandardMaterial { + base_color: Color::srgb(0.48, 0.50, 0.56), + perceptual_roughness: 0.72, + ..default() + }); + let sphere_material = materials.add(StandardMaterial { + base_color: SILVER.into(), + metallic: 0.05, + perceptual_roughness: 0.25, + ..default() + }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 8.8, 20.0).looking_at(Vec3::new(0.0, 1.4, 0.0), Vec3::Y), + )); + + spawn_runway( + &mut commands, + &runway_mesh, + &end_wall_mesh, + &marker_mesh, + &pillar_mesh, + &sphere_mesh, + &runway_material, + &wall_material, + &marker_material, + &pillar_material, + &sphere_material, + Vec3::new(0.0, 0.0, -4.2), + ); + spawn_runway( + &mut commands, + &runway_mesh, + &end_wall_mesh, + &marker_mesh, + &pillar_mesh, + &sphere_mesh, + &runway_material, + &wall_material, + &marker_material, + &pillar_material, + &sphere_material, + Vec3::new(0.0, 0.0, 4.2), + ); + + commands.spawn(( + PointLight { + color: YELLOW.into(), + intensity: POINT_LIGHT_INTENSITY, + range: LIGHT_RANGE, + falloff: LightFalloff::InverseSquare, + shadow_maps_enabled: true, + ..default() + }, + PointDemoLight, + Transform::from_xyz(-9.2, 1.6, -4.2), + )); + + commands.spawn(( + SpotLight { + color: BLUE.into(), + intensity: SPOT_LIGHT_INTENSITY, + range: LIGHT_RANGE, + falloff: LightFalloff::InverseSquare, + inner_angle: PI / 14.0, + outer_angle: PI / 10.0, + shadow_maps_enabled: true, + ..default() + }, + SpotDemoLight, + Transform::from_xyz(-9.2, 1.8, 5.1).looking_at(Vec3::new(9.2, 0.35, 3.8), Vec3::Y), + )); + + commands.spawn(( + Text2d::new("Point light"), + TextFont { + font_size: FontSize::Px(28.0), + ..default() + }, + TextColor(YELLOW.into()), + Transform::from_xyz(-11.5, 2.8, -4.2), + )); + commands.spawn(( + Text2d::new("Spot light"), + TextFont { + font_size: FontSize::Px(28.0), + ..default() + }, + TextColor(BLUE.into()), + Transform::from_xyz(-11.0, 2.8, 4.2), + )); + + commands.spawn(( + Text::new(INSTRUCTIONS), + Node { + position_type: PositionType::Absolute, + top: px(12), + left: px(12), + ..default() + }, + )); + + spawn_status_ui(&mut commands, controls.selected); +} + +fn spawn_runway( + commands: &mut Commands, + runway_mesh: &Handle, + end_wall_mesh: &Handle, + marker_mesh: &Handle, + pillar_mesh: &Handle, + sphere_mesh: &Handle, + runway_material: &Handle, + wall_material: &Handle, + marker_material: &Handle, + pillar_material: &Handle, + sphere_material: &Handle, + origin: Vec3, +) { + commands.spawn(( + Mesh3d(runway_mesh.clone()), + MeshMaterial3d(runway_material.clone()), + Transform::from_translation(origin), + )); + + commands.spawn(( + Mesh3d(end_wall_mesh.clone()), + MeshMaterial3d(wall_material.clone()), + Transform::from_translation(origin + Vec3::new(10.1, 1.4, 0.0)), + )); + + for marker_x in [-6.0, -2.0, 2.0, 6.0] { + commands.spawn(( + Mesh3d(marker_mesh.clone()), + MeshMaterial3d(marker_material.clone()), + Transform::from_translation(origin + Vec3::new(marker_x, 0.14, 0.0)), + )); + } + + commands.spawn(( + Mesh3d(pillar_mesh.clone()), + MeshMaterial3d(pillar_material.clone()), + Transform::from_translation(origin + Vec3::new(2.8, 0.7, -0.8)), + )); + + commands.spawn(( + Mesh3d(sphere_mesh.clone()), + MeshMaterial3d(sphere_material.clone()), + Transform::from_translation(origin + Vec3::new(6.0, 0.8, 0.6)), + )); +} + +fn spawn_status_ui(commands: &mut Commands, selected: SelectedLight) { + commands + .spawn(( + Node { + position_type: PositionType::Absolute, + right: px(16), + top: px(16), + width: px(320), + padding: UiRect::all(px(14)), + flex_direction: FlexDirection::Column, + row_gap: px(10), + border_radius: BorderRadius::all(px(12)), + ..default() + }, + BackgroundColor(Color::srgba(0.07, 0.07, 0.09, 0.88)), + )) + .with_children(|parent| { + parent.spawn(( + Text::new("Selected"), + TextFont { + font_size: FontSize::Px(24.0), + ..default() + }, + )); + parent.spawn(( + Text::new(match selected { + SelectedLight::Point => "Point light", + SelectedLight::Spot => "Spot light", + }), + SelectedLightLabel, + )); + parent.spawn((Text::new(""), LightStateLabel)); + }); +} + +fn handle_keyboard_controls( + time: Res