mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 12:43:34 +00:00
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:
parent
9cc7e7c080
commit
39d96ef0fd
6 changed files with 444 additions and 42 deletions
|
@ -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(),
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)) => {
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue