Implement volumetric fog support for both point lights and spotlights (#15361)

# Objective
- Fixes: https://github.com/bevyengine/bevy/issues/14451

## Solution
- Adding volumetric fog sampling code for both point lights and
spotlights.

## Testing
- I have modified the example of volumetric_fog.rs by adding a
volumetric point light and a volumetric spotlight.


https://github.com/user-attachments/assets/3eeb77a0-f22d-40a6-a48a-2dd75d55a877
This commit is contained in:
Sou1gh0st 2024-09-30 05:30:53 +08:00 committed by GitHub
parent 9cc7e7c080
commit 39d96ef0fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 444 additions and 42 deletions

View file

@ -17,10 +17,12 @@ use bevy_utils::tracing::warn;
use crate::{
ClusterConfig, ClusterFarZMode, Clusters, GlobalVisibleClusterableObjects, PointLight,
SpotLight, ViewClusterBindings, VisibleClusterableObjects,
SpotLight, ViewClusterBindings, VisibleClusterableObjects, VolumetricLight,
CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS,
};
use super::ClusterableObjectOrderData;
const NDC_MIN: Vec2 = Vec2::NEG_ONE;
const NDC_MAX: Vec2 = Vec2::ONE;
@ -34,6 +36,7 @@ pub(crate) struct ClusterableObjectAssignmentData {
transform: GlobalTransform,
range: f32,
shadows_enabled: bool,
volumetric: bool,
spot_light_angle: Option<f32>,
render_layers: RenderLayers,
}
@ -67,6 +70,7 @@ pub(crate) fn assign_objects_to_clusters(
&GlobalTransform,
&PointLight,
Option<&RenderLayers>,
Option<&VolumetricLight>,
&ViewVisibility,
)>,
spot_lights_query: Query<(
@ -74,6 +78,7 @@ pub(crate) fn assign_objects_to_clusters(
&GlobalTransform,
&SpotLight,
Option<&RenderLayers>,
Option<&VolumetricLight>,
&ViewVisibility,
)>,
mut clusterable_objects: Local<Vec<ClusterableObjectAssignmentData>>,
@ -93,11 +98,12 @@ pub(crate) fn assign_objects_to_clusters(
.iter()
.filter(|(.., visibility)| visibility.get())
.map(
|(entity, transform, point_light, maybe_layers, _visibility)| {
|(entity, transform, point_light, maybe_layers, volumetric, _visibility)| {
ClusterableObjectAssignmentData {
entity,
transform: GlobalTransform::from_translation(transform.translation()),
shadows_enabled: point_light.shadows_enabled,
volumetric: volumetric.is_some(),
range: point_light.range,
spot_light_angle: None,
render_layers: maybe_layers.unwrap_or_default().clone(),
@ -110,11 +116,12 @@ pub(crate) fn assign_objects_to_clusters(
.iter()
.filter(|(.., visibility)| visibility.get())
.map(
|(entity, transform, spot_light, maybe_layers, _visibility)| {
|(entity, transform, spot_light, maybe_layers, volumetric, _visibility)| {
ClusterableObjectAssignmentData {
entity,
transform: *transform,
shadows_enabled: spot_light.shadows_enabled,
volumetric: volumetric.is_some(),
range: spot_light.range,
spot_light_angle: Some(spot_light.outer_angle),
render_layers: maybe_layers.unwrap_or_default().clone(),
@ -134,16 +141,18 @@ pub(crate) fn assign_objects_to_clusters(
{
clusterable_objects.sort_by(|clusterable_object_1, clusterable_object_2| {
crate::clusterable_object_order(
(
&clusterable_object_1.entity,
&clusterable_object_1.shadows_enabled,
&clusterable_object_1.spot_light_angle.is_some(),
),
(
&clusterable_object_2.entity,
&clusterable_object_2.shadows_enabled,
&clusterable_object_2.spot_light_angle.is_some(),
),
ClusterableObjectOrderData {
entity: &clusterable_object_1.entity,
shadows_enabled: &clusterable_object_1.shadows_enabled,
is_volumetric_light: &clusterable_object_1.volumetric,
is_spot_light: &clusterable_object_1.spot_light_angle.is_some(),
},
ClusterableObjectOrderData {
entity: &clusterable_object_2.entity,
shadows_enabled: &clusterable_object_2.shadows_enabled,
is_volumetric_light: &clusterable_object_2.volumetric,
is_spot_light: &clusterable_object_2.spot_light_angle.is_some(),
},
)
});

View file

@ -491,6 +491,13 @@ impl Default for GpuClusterableObjectsUniform {
}
}
pub(crate) struct ClusterableObjectOrderData<'a> {
pub(crate) entity: &'a Entity,
pub(crate) shadows_enabled: &'a bool,
pub(crate) is_volumetric_light: &'a bool,
pub(crate) is_spot_light: &'a bool,
}
#[allow(clippy::too_many_arguments)]
// Sort clusterable objects by:
//
@ -505,13 +512,14 @@ impl Default for GpuClusterableObjectsUniform {
// clusterable objects are chosen if the clusterable object count limit is
// exceeded.
pub(crate) fn clusterable_object_order(
(entity_1, shadows_enabled_1, is_spot_light_1): (&Entity, &bool, &bool),
(entity_2, shadows_enabled_2, is_spot_light_2): (&Entity, &bool, &bool),
a: ClusterableObjectOrderData,
b: ClusterableObjectOrderData,
) -> core::cmp::Ordering {
is_spot_light_1
.cmp(is_spot_light_2) // pointlights before spot lights
.then_with(|| shadows_enabled_2.cmp(shadows_enabled_1)) // shadow casters before non-casters
.then_with(|| entity_1.cmp(entity_2)) // stable
a.is_spot_light
.cmp(b.is_spot_light) // pointlights before spot lights
.then_with(|| b.shadows_enabled.cmp(a.shadows_enabled)) // shadow casters before non-casters
.then_with(|| b.is_volumetric_light.cmp(a.is_volumetric_light)) // volumetric lights before non-volumetric lights
.then_with(|| a.entity.cmp(b.entity)) // stable
}
/// Extracts clusters from the main world from the render world.

View file

@ -45,6 +45,7 @@ pub struct ExtractedPointLight {
pub shadow_normal_bias: f32,
pub shadow_map_near_z: f32,
pub spot_light_angles: Option<(f32, f32)>,
pub volumetric: bool,
}
#[derive(Component, Debug)]
@ -69,6 +70,7 @@ bitflags::bitflags! {
struct PointLightFlags: u32 {
const SHADOWS_ENABLED = 1 << 0;
const SPOT_LIGHT_Y_NEGATIVE = 1 << 1;
const VOLUMETRIC = 1 << 2;
const NONE = 0;
const UNINITIALIZED = 0xFFFF;
}
@ -195,6 +197,7 @@ pub fn extract_lights(
&GlobalTransform,
&ViewVisibility,
&CubemapFrusta,
Option<&VolumetricLight>,
)>,
>,
spot_lights: Extract<
@ -204,6 +207,7 @@ pub fn extract_lights(
&GlobalTransform,
&ViewVisibility,
&Frustum,
Option<&VolumetricLight>,
)>,
>,
directional_lights: Extract<
@ -245,8 +249,14 @@ pub fn extract_lights(
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)
let Ok((
point_light,
cubemap_visible_entities,
transform,
view_visibility,
frusta,
volumetric_light,
)) = point_lights.get(entity)
else {
continue;
};
@ -274,6 +284,7 @@ pub fn extract_lights(
* core::f32::consts::SQRT_2,
shadow_map_near_z: point_light.shadow_map_near_z,
spot_light_angles: None,
volumetric: volumetric_light.is_some(),
};
point_lights_values.push((
entity,
@ -289,8 +300,14 @@ pub fn extract_lights(
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 let Ok((
spot_light,
visible_entities,
transform,
view_visibility,
frustum,
volumetric_light,
)) = spot_lights.get(entity)
{
if !view_visibility.get() {
continue;
@ -325,6 +342,7 @@ pub fn extract_lights(
* core::f32::consts::SQRT_2,
shadow_map_near_z: spot_light.shadow_map_near_z,
spot_light_angles: Some((spot_light.inner_angle, spot_light.outer_angle)),
volumetric: volumetric_light.is_some(),
},
render_visible_entities,
*frustum,
@ -621,6 +639,12 @@ pub fn prepare_lights(
.filter(|light| light.1.spot_light_angles.is_none())
.count();
let point_light_volumetric_enabled_count = point_lights
.iter()
.filter(|(_, light, _)| light.volumetric && light.spot_light_angles.is_none())
.count()
.min(max_texture_cubes);
let point_light_shadow_maps_count = point_lights
.iter()
.filter(|light| light.1.shadows_enabled && light.1.spot_light_angles.is_none())
@ -641,6 +665,12 @@ pub fn prepare_lights(
.count()
.min(max_texture_array_layers / MAX_CASCADES_PER_LIGHT);
let spot_light_volumetric_enabled_count = point_lights
.iter()
.filter(|(_, light, _)| light.volumetric && light.spot_light_angles.is_some())
.count()
.min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT);
let spot_light_shadow_maps_count = point_lights
.iter()
.filter(|(_, light, _)| light.shadows_enabled && light.spot_light_angles.is_some())
@ -654,16 +684,18 @@ pub fn prepare_lights(
// - 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, _)| {
clusterable_object_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(),
),
ClusterableObjectOrderData {
entity: entity_1,
shadows_enabled: &light_1.shadows_enabled,
is_volumetric_light: &light_1.volumetric,
is_spot_light: &light_1.spot_light_angles.is_some(),
},
ClusterableObjectOrderData {
entity: entity_2,
shadows_enabled: &light_2.shadows_enabled,
is_volumetric_light: &light_2.volumetric,
is_spot_light: &light_2.spot_light_angles.is_some(),
},
)
});
@ -706,6 +738,14 @@ pub fn prepare_lights(
1.0,
light.shadow_map_near_z,
);
if light.shadows_enabled
&& light.volumetric
&& (index < point_light_volumetric_enabled_count
|| (light.spot_light_angles.is_some()
&& index - point_light_count < spot_light_volumetric_enabled_count))
{
flags |= PointLightFlags::VOLUMETRIC;
}
let (light_custom_data, spot_light_tan_angle) = match light.spot_light_angles {
Some((inner, outer)) => {

View file

@ -19,6 +19,7 @@ struct ClusterableObject {
const POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u;
const POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE: u32 = 2u;
const POINT_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 4u;
struct DirectionalCascade {
clip_from_world: mat4x4<f32>,

View file

@ -15,9 +15,20 @@
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
#import bevy_pbr::mesh_functions::{get_world_from_local, mesh_position_local_to_clip}
#import bevy_pbr::mesh_view_bindings::{globals, lights, view}
#import bevy_pbr::mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT
#import bevy_pbr::shadow_sampling::sample_shadow_map_hardware
#import bevy_pbr::mesh_view_bindings::{globals, lights, view, clusterable_objects}
#import bevy_pbr::mesh_view_types::{
DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT,
POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT,
POINT_LIGHT_FLAGS_VOLUMETRIC_BIT,
POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE,
ClusterableObject
}
#import bevy_pbr::shadow_sampling::{
sample_shadow_map_hardware,
sample_shadow_cubemap,
sample_shadow_map,
SPOT_SHADOW_TEXEL_SIZE
}
#import bevy_pbr::shadows::{get_cascade_index, world_to_directional_light_local}
#import bevy_pbr::utils::interleaved_gradient_noise
#import bevy_pbr::view_transformations::{
@ -27,6 +38,8 @@
position_ndc_to_world,
position_view_to_world
}
#import bevy_pbr::clustered_forward as clustering
#import bevy_pbr::lighting::getDistanceAttenuation;
// The GPU version of [`VolumetricFog`]. See the comments in
// `volumetric_fog/mod.rs` for descriptions of the fields here.
@ -292,7 +305,180 @@ fn fragment(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
}
}
// Point lights and Spot lights
let view_z = view_start_pos.z;
let is_orthographic = view.clip_from_view[3].w == 1.0;
let cluster_index = clustering::fragment_cluster_index(frag_coord.xy, view_z, is_orthographic);
let offset_and_counts = clustering::unpack_offset_and_counts(cluster_index);
let spot_light_start_index = offset_and_counts[0] + offset_and_counts[1];
for (var i: u32 = offset_and_counts[0]; i < offset_and_counts[0] + offset_and_counts[1] + offset_and_counts[2]; i = i + 1u) {
let light_id = clustering::get_clusterable_object_id(i);
let light = &clusterable_objects.data[light_id];
if (((*light).flags & POINT_LIGHT_FLAGS_VOLUMETRIC_BIT) == 0) {
continue;
}
// Reset `background_alpha` for a new raymarch.
background_alpha = 1.0;
// Start raymarching.
for (var step = 0u; step < step_count; step += 1u) {
// As an optimization, break if we've gotten too dark.
if (background_alpha < 0.001) {
break;
}
// Calculate where we are in the ray.
let P_world = Ro_world + Rd_world * f32(step) * step_size_world;
let P_view = Rd_view * f32(step) * step_size_world;
var density = density_factor;
let light_to_frag = (*light).position_radius.xyz - P_world;
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);
var local_light_attenuation = distance_atten;
if (i < spot_light_start_index) {
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));
}
local_light_attenuation *= shadow;
} else {
// spot light attenuation
// reconstruct spot dir from x/z and y-direction flag
var spot_dir = vec3<f32>((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y);
spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z));
if ((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u {
spot_dir.y = -spot_dir.y;
}
let light_to_frag = (*light).position_radius.xyz - P_world;
// calculate attenuation based on filament formula https://google.github.io/filament/Filament.html#listing_glslpunctuallight
// spot_scale and spot_offset have been precomputed
// note we normalize here to get "l" from the filament listing. spot_dir is already normalized
let cd = dot(-spot_dir, normalize(light_to_frag));
let attenuation = saturate(cd * (*light).light_custom_data.z + (*light).light_custom_data.w);
let spot_attenuation = attenuation * attenuation;
var shadow: f32 = 1.0;
if (((*light).flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
shadow = fetch_spot_shadow_without_normal(light_id, vec4(P_world, 1.0));
}
local_light_attenuation *= spot_attenuation * shadow;
}
// Calculate absorption (amount of light absorbed by the fog) and
// out-scattering (amount of light the fog scattered away).
let sample_attenuation = exp(-step_size_world * density * (absorption + scattering));
// Process absorption and out-scattering.
background_alpha *= sample_attenuation;
let light_attenuation = exp(-density * bounding_radius * (absorption + scattering));
let light_factors_per_step = fog_color * light_tint * light_attenuation *
scattering * density * step_size_world * light_intensity * 0.1;
// Modulate the factor we calculated above by the phase, fog color,
// light color, light tint.
let light_color_per_step = (*light).color_inverse_square_range.rgb * light_factors_per_step;
// Accumulate the light.
accumulated_color += light_color_per_step * local_light_attenuation *
background_alpha;
}
}
// We're done! Return the color with alpha so it can be blended onto the
// render target.
return vec4(accumulated_color, 1.0 - background_alpha);
}
fn fetch_point_shadow_without_normal(light_id: u32, frag_position: vec4<f32>) -> f32 {
let light = &clusterable_objects.data[light_id];
// because the shadow maps align with the axes and the frustum planes are at 45 degrees
// we can get the worldspace depth by taking the largest absolute axis
let surface_to_light = (*light).position_radius.xyz - frag_position.xyz;
let surface_to_light_abs = abs(surface_to_light);
let distance_to_light = max(surface_to_light_abs.x, max(surface_to_light_abs.y, surface_to_light_abs.z));
// The normal bias here is already scaled by the texel size at 1 world unit from the light.
// The texel size increases proportionally with distance from the light so multiplying by
// distance to light scales the normal bias to the texel size at the fragment distance.
let depth_offset = (*light).shadow_depth_bias * normalize(surface_to_light.xyz);
let offset_position = frag_position.xyz + depth_offset;
// similar largest-absolute-axis trick as above, but now with the offset fragment position
let frag_ls = offset_position.xyz - (*light).position_radius.xyz ;
let abs_position_ls = abs(frag_ls);
let major_axis_magnitude = max(abs_position_ls.x, max(abs_position_ls.y, abs_position_ls.z));
// NOTE: These simplifications come from multiplying:
// projection * vec4(0, 0, -major_axis_magnitude, 1.0)
// and keeping only the terms that have any impact on the depth.
// Projection-agnostic approach:
let zw = -major_axis_magnitude * (*light).light_custom_data.xy + (*light).light_custom_data.zw;
let depth = zw.x / zw.y;
// Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed coordinate space,
// so we have to flip the z-axis when sampling.
let flip_z = vec3(1.0, 1.0, -1.0);
return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_id);
}
fn fetch_spot_shadow_without_normal(light_id: u32, frag_position: vec4<f32>) -> f32 {
let light = &clusterable_objects.data[light_id];
let surface_to_light = (*light).position_radius.xyz - frag_position.xyz;
// construct the light view matrix
var spot_dir = vec3<f32>((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y);
// reconstruct spot dir from x/z and y-direction flag
spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z));
if (((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u) {
spot_dir.y = -spot_dir.y;
}
// view matrix z_axis is the reverse of transform.forward()
let fwd = -spot_dir;
let offset_position =
-surface_to_light
+ ((*light).shadow_depth_bias * normalize(surface_to_light));
// the construction of the up and right vectors needs to precisely mirror the code
// in render/light.rs:spot_light_view_matrix
var sign = -1.0;
if (fwd.z >= 0.0) {
sign = 1.0;
}
let a = -1.0 / (fwd.z + sign);
let b = fwd.x * fwd.y * a;
let up_dir = vec3<f32>(1.0 + sign * fwd.x * fwd.x * a, sign * b, -sign * fwd.x);
let right_dir = vec3<f32>(-b, -sign - fwd.y * fwd.y * a, fwd.y);
let light_inv_rot = mat3x3<f32>(right_dir, up_dir, fwd);
// because the matrix is a pure rotation matrix, the inverse is just the transpose, and to calculate
// the product of the transpose with a vector we can just post-multiply instead of pre-multiplying.
// this allows us to keep the matrix construction code identical between CPU and GPU.
let projected_position = offset_position * light_inv_rot;
// divide xy by perspective matrix "f" and by -projected.z (projected.z is -projection matrix's w)
// to get ndc coordinates
let f_div_minus_z = 1.0 / ((*light).spot_light_tan_angle * -projected_position.z);
let shadow_xy_ndc = projected_position.xy * f_div_minus_z;
// convert to uv coordinates
let shadow_uv = shadow_xy_ndc * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5, 0.5);
// 0.1 must match POINT_LIGHT_NEAR_Z
let depth = 0.1 / -projected_position.z;
return sample_shadow_map(
shadow_uv,
depth,
i32(light_id) + lights.spot_light_shadowmap_offset,
SPOT_SHADOW_TEXEL_SIZE
);
}

View file

@ -1,6 +1,7 @@
//! Demonstrates volumetric fog and lighting (light shafts or god rays).
use bevy::{
color::palettes::css::RED,
core_pipeline::{bloom::Bloom, tonemapping::Tonemapping, Skybox},
math::vec3,
pbr::{FogVolumeBundle, VolumetricFog, VolumetricLight},
@ -9,6 +10,32 @@ use bevy::{
const DIRECTIONAL_LIGHT_MOVEMENT_SPEED: f32 = 0.02;
/// The current settings that the user has chosen.
#[derive(Resource)]
struct AppSettings {
/// Whether volumetric spot light is on.
volumetric_spotlight: bool,
/// Whether volumetric point light is on.
volumetric_pointlight: bool,
}
impl Default for AppSettings {
fn default() -> Self {
Self {
volumetric_spotlight: true,
volumetric_pointlight: true,
}
}
}
// Define a struct to store parameters for the point light's movement.
#[derive(Component)]
struct MoveBackAndForthHorizontally {
min_x: f32,
max_x: f32,
speed: f32,
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
@ -19,14 +46,16 @@ fn main() {
alpha: 1.0,
})))
.insert_resource(AmbientLight::NONE)
.init_resource::<AppSettings>()
.add_systems(Startup, setup)
.add_systems(Update, tweak_scene)
.add_systems(Update, move_directional_light)
.add_systems(Update, (move_directional_light, move_point_light))
.add_systems(Update, adjust_app_settings)
.run();
}
/// Initializes the scene.
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_settings: Res<AppSettings>) {
// Spawn the glTF scene.
commands.spawn(SceneBundle {
scene: asset_server.load(
@ -60,6 +89,44 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
});
// Add the point light
commands
.spawn((
PointLightBundle {
point_light: PointLight {
shadows_enabled: true,
range: 150.0,
color: RED.into(),
intensity: 1000.0,
..default()
},
transform: Transform::from_xyz(-0.4, 1.9, 1.0),
..default()
},
MoveBackAndForthHorizontally {
min_x: -1.93,
max_x: -0.4,
speed: -0.2,
},
))
.insert(VolumetricLight);
// Add the spot light
commands
.spawn(SpotLightBundle {
transform: Transform::from_xyz(-1.8, 3.9, -2.7).looking_at(Vec3::ZERO, Vec3::Y),
spot_light: SpotLight {
intensity: 5000.0, // lumens
color: Color::WHITE,
shadows_enabled: true,
inner_angle: 0.76,
outer_angle: 0.94,
..default()
},
..default()
})
.insert(VolumetricLight);
// Add the fog volume.
commands.spawn(FogVolumeBundle {
transform: Transform::from_scale(Vec3::splat(35.0)),
@ -69,10 +136,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Add the help text.
commands.spawn(
TextBundle {
text: Text::from_section(
"Press WASD or the arrow keys to change the light direction",
TextStyle::default(),
),
text: create_text(&app_settings),
..default()
}
.with_style(Style {
@ -84,6 +148,26 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
);
}
fn create_text(app_settings: &AppSettings) -> Text {
Text::from_section(
format!(
"{}\n{}\n{}",
"Press WASD or the arrow keys to change the direction of the directional light",
if app_settings.volumetric_pointlight {
"Press P to turn volumetric point light off"
} else {
"Press P to turn volumetric point light on"
},
if app_settings.volumetric_spotlight {
"Press L to turn volumetric spot light off"
} else {
"Press L to turn volumetric spot light on"
}
),
TextStyle::default(),
)
}
/// A system that makes directional lights in the glTF scene into volumetric
/// lights with shadows.
fn tweak_scene(
@ -125,3 +209,77 @@ fn move_directional_light(
transform.rotate(delta_quat);
}
}
// Toggle point light movement between left and right.
fn move_point_light(
timer: Res<Time>,
mut objects: Query<(&mut Transform, &mut MoveBackAndForthHorizontally)>,
) {
for (mut transform, mut move_data) in objects.iter_mut() {
let mut translation = transform.translation;
let mut need_toggle = false;
translation.x += move_data.speed * timer.delta_seconds();
if translation.x > move_data.max_x {
translation.x = move_data.max_x;
need_toggle = true;
} else if translation.x < move_data.min_x {
translation.x = move_data.min_x;
need_toggle = true;
}
if need_toggle {
move_data.speed = -move_data.speed;
}
transform.translation = translation;
}
}
// Adjusts app settings per user input.
fn adjust_app_settings(
mut commands: Commands,
keyboard_input: Res<ButtonInput<KeyCode>>,
mut app_settings: ResMut<AppSettings>,
mut point_lights: Query<Entity, With<PointLight>>,
mut spot_lights: Query<Entity, With<SpotLight>>,
mut text: Query<&mut Text>,
) {
// If there are no changes, we're going to bail for efficiency. Record that
// here.
let mut any_changes = false;
// If the user pressed P, toggle volumetric state of the point light.
if keyboard_input.just_pressed(KeyCode::KeyP) {
app_settings.volumetric_pointlight = !app_settings.volumetric_pointlight;
any_changes = true;
}
// If the user pressed L, toggle volumetric state of the spot light.
if keyboard_input.just_pressed(KeyCode::KeyL) {
app_settings.volumetric_spotlight = !app_settings.volumetric_spotlight;
any_changes = true;
}
// If there were no changes, bail out.
if !any_changes {
return;
}
// Update volumetric settings.
for point_light in point_lights.iter_mut() {
if app_settings.volumetric_pointlight {
commands.entity(point_light).insert(VolumetricLight);
} else {
commands.entity(point_light).remove::<VolumetricLight>();
}
}
for spot_light in spot_lights.iter_mut() {
if app_settings.volumetric_spotlight {
commands.entity(spot_light).insert(VolumetricLight);
} else {
commands.entity(spot_light).remove::<VolumetricLight>();
}
}
// Update the help text.
for mut text in text.iter_mut() {
*text = create_text(&app_settings);
}
}