Spotlights (#4715)

# Objective

add spotlight support

## Solution / Changelog

- add spotlight angles (inner, outer) to ``PointLight`` struct. emitted light is linearly attenuated from 100% to 0% as angle tends from inner to outer. Direction is taken from the existing transform rotation.
- add spotlight direction (vec3) and angles (f32,f32) to ``GpuPointLight`` struct (60 bytes -> 80 bytes) in ``pbr/render/lights.rs`` and ``mesh_view_bind_group.wgsl``
- reduce no-buffer-support max point light count to 204 due to above
- use spotlight data to attenuate light in ``pbr.wgsl``
- do additional cluster culling on spotlights to minimise cost in ``assign_lights_to_clusters``
- changed one of the lights in the lighting demo to a spotlight
- also added a ``spotlight`` demo - probably not justified but so reviewers can see it more easily

## notes

increasing the size of the GpuPointLight struct on my machine reduces the FPS of ``many_lights -- sphere`` from ~150fps to 140fps. 

i thought this was a reasonable tradeoff, and felt better than handling spotlights separately which is possible but would mean introducing a new bind group, refactoring light-assignment code and adding new spotlight-specific code in pbr.wgsl. the FPS impact for smaller numbers of lights should be very small.

the cluster culling strategy reintroduces the cluster aabb code which was recently removed... sorry. the aabb is used to get a cluster bounding sphere, which can then be tested fairly efficiently using the strategy described at the end of https://bartwronski.com/2017/04/13/cull-that-cone/. this works well with roughly cubic clusters (where the cluster z size is close to the same as x/y size), less well for other cases like single Z slice / tiled forward rendering. In the worst case we will end up just keeping the culling of the equivalent point light.

Co-authored-by: François <mockersf@gmail.com>
This commit is contained in:
robtfm 2022-07-08 19:57:43 +00:00
parent 4c35ecf71f
commit 132950cd55
15 changed files with 1028 additions and 127 deletions

View file

@ -281,6 +281,16 @@ description = "Illustrates various lighting options in a simple scene"
category = "3D Rendering"
wasm = true
[[example]]
name = "spotlight"
path = "examples/3d/spotlight.rs"
[package.metadata.example.spotlight]
name = "Spotlight"
description = "Illustrates spot lights"
category = "3D Rendering"
wasm = true
[[example]]
name = "load_gltf"
path = "examples/3d/load_gltf.rs"

View file

@ -10,7 +10,7 @@ use bevy_log::warn;
use bevy_math::{Mat4, Vec3};
use bevy_pbr::{
AlphaMode, DirectionalLight, DirectionalLightBundle, PbrBundle, PointLight, PointLightBundle,
StandardMaterial,
SpotLight, SpotLightBundle, StandardMaterial,
};
use bevy_render::{
camera::{
@ -862,9 +862,33 @@ fn load_node(
}
}
gltf::khr_lights_punctual::Kind::Spot {
inner_cone_angle: _inner_cone_angle,
outer_cone_angle: _outer_cone_angle,
} => warn!("Spot lights are not yet supported."),
inner_cone_angle,
outer_cone_angle,
} => {
let mut entity = parent.spawn_bundle(SpotLightBundle {
spot_light: SpotLight {
color: Color::from(light.color()),
// NOTE: KHR_punctual_lights defines the intensity units for spot lights in
// candela (lm/sr) which is luminous intensity and we need luminous power.
// For a spot light, we map luminous power = 4 * pi * luminous intensity
intensity: light.intensity() * std::f32::consts::PI * 4.0,
range: light.range().unwrap_or(20.0),
radius: light.range().unwrap_or(0.0),
inner_angle: inner_cone_angle,
outer_angle: outer_cone_angle,
..Default::default()
},
..Default::default()
});
if let Some(name) = light.name() {
entity.insert(Name::new(name.to_string()));
}
if let Some(extras) = light.extras() {
entity.insert(super::GltfExtras {
value: extras.get().to_string(),
});
}
}
}
}

View file

@ -1,4 +1,4 @@
use crate::{DirectionalLight, Material, PointLight, StandardMaterial};
use crate::{DirectionalLight, Material, PointLight, SpotLight, StandardMaterial};
use bevy_asset::Handle;
use bevy_ecs::{bundle::Bundle, component::Component, reflect::ReflectComponent};
use bevy_reflect::Reflect;
@ -75,6 +75,18 @@ pub struct PointLightBundle {
pub visibility: Visibility,
}
/// A component bundle for spot light entities
#[derive(Debug, Bundle, Default)]
pub struct SpotLightBundle {
pub spot_light: SpotLight,
pub visible_entities: VisibleEntities,
pub frustum: Frustum,
pub transform: Transform,
pub global_transform: GlobalTransform,
/// Enables or disables the light
pub visibility: Visibility,
}
/// A component bundle for [`DirectionalLight`] entities.
#[derive(Debug, Bundle, Default)]
pub struct DirectionalLightBundle {

View file

@ -20,8 +20,11 @@ pub mod prelude {
#[doc(hidden)]
pub use crate::{
alpha::AlphaMode,
bundle::{DirectionalLightBundle, MaterialMeshBundle, PbrBundle, PointLightBundle},
light::{AmbientLight, DirectionalLight, PointLight},
bundle::{
DirectionalLightBundle, MaterialMeshBundle, PbrBundle, PointLightBundle,
SpotLightBundle,
},
light::{AmbientLight, DirectionalLight, PointLight, SpotLight},
material::{Material, MaterialPlugin},
pbr_material::StandardMaterial,
};
@ -123,6 +126,7 @@ impl Plugin for PbrPlugin {
app.register_type::<CubemapVisibleEntities>()
.register_type::<DirectionalLight>()
.register_type::<PointLight>()
.register_type::<SpotLight>()
.add_plugin(MeshRenderPlugin)
.add_plugin(MaterialPlugin::<StandardMaterial>::default())
.register_type::<AmbientLight>()
@ -152,13 +156,20 @@ impl Plugin for PbrPlugin {
.add_system_to_stage(
CoreStage::PostUpdate,
update_directional_light_frusta
.label(SimulationLightSystems::UpdateDirectionalLightFrusta)
.label(SimulationLightSystems::UpdateLightFrusta)
.after(TransformSystem::TransformPropagate),
)
.add_system_to_stage(
CoreStage::PostUpdate,
update_point_light_frusta
.label(SimulationLightSystems::UpdatePointLightFrusta)
.label(SimulationLightSystems::UpdateLightFrusta)
.after(TransformSystem::TransformPropagate)
.after(SimulationLightSystems::AssignLightsToClusters),
)
.add_system_to_stage(
CoreStage::PostUpdate,
update_spot_light_frusta
.label(SimulationLightSystems::UpdateLightFrusta)
.after(TransformSystem::TransformPropagate)
.after(SimulationLightSystems::AssignLightsToClusters),
)
@ -168,8 +179,7 @@ impl Plugin for PbrPlugin {
.label(SimulationLightSystems::CheckLightVisibility)
.after(TransformSystem::TransformPropagate)
.after(VisibilitySystems::CalculateBounds)
.after(SimulationLightSystems::UpdateDirectionalLightFrusta)
.after(SimulationLightSystems::UpdatePointLightFrusta)
.after(SimulationLightSystems::UpdateLightFrusta)
// NOTE: This MUST be scheduled AFTER the core renderer visibility check
// because that resets entity ComputedVisibility for the first view
// which would override any results from this otherwise

View file

@ -1,7 +1,7 @@
use std::collections::HashSet;
use bevy_ecs::prelude::*;
use bevy_math::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles};
use bevy_math::{Mat4, Quat, UVec2, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles};
use bevy_reflect::prelude::*;
use bevy_render::{
camera::{Camera, CameraProjection, OrthographicProjection},
@ -16,9 +16,9 @@ use bevy_transform::components::GlobalTransform;
use bevy_utils::tracing::warn;
use crate::{
calculate_cluster_factors, CubeMapFace, CubemapVisibleEntities, ViewClusterBindings,
CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, CUBE_MAP_FACES, MAX_UNIFORM_BUFFER_POINT_LIGHTS,
POINT_LIGHT_NEAR_Z,
calculate_cluster_factors, spot_light_projection_matrix, spot_light_view_matrix, CubeMapFace,
CubemapVisibleEntities, ViewClusterBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT,
CUBE_MAP_FACES, MAX_UNIFORM_BUFFER_POINT_LIGHTS, POINT_LIGHT_NEAR_Z,
};
/// A light that emits light in all directions from a central point.
@ -85,6 +85,59 @@ impl Default for PointLightShadowMap {
}
}
/// A light that emits light in a given direction from a central point.
/// Behaves like a point light in a perfectly absorbant housing that
/// shines light only in a given direction. The direction is taken from
/// the transform, and can be specified with [`Transform::looking_at`](bevy_transform::components::Transform::looking_at).
#[derive(Component, Debug, Clone, Copy, Reflect)]
#[reflect(Component, Default)]
pub struct SpotLight {
pub color: Color,
pub intensity: f32,
pub range: f32,
pub radius: f32,
pub shadows_enabled: bool,
pub shadow_depth_bias: f32,
/// A bias applied along the direction of the fragment's surface normal. It is scaled to the
/// shadow map's texel size so that it can be small close to the camera and gets larger further
/// away.
pub shadow_normal_bias: f32,
/// Angle defining the distance from the spot light direction to the outer limit
/// of the light's cone of effect.
/// `outer_angle` should be < `PI / 2.0`.
/// `PI / 2.0` defines a hemispherical spot light, but shadows become very blocky as the angle
/// approaches this limit.
pub outer_angle: f32,
/// Angle defining the distance from the spot light direction to the inner limit
/// of the light's cone of effect.
/// Light is attenuated from `inner_angle` to `outer_angle` to give a smooth falloff.
/// `inner_angle` should be <= `outer_angle`
pub inner_angle: f32,
}
impl SpotLight {
pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02;
pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6;
}
impl Default for SpotLight {
fn default() -> Self {
// a quarter arc attenuating from the centre
Self {
color: Color::rgb(1.0, 1.0, 1.0),
/// Luminous power in lumens
intensity: 800.0, // Roughly a 60W non-halogen incandescent bulb
range: 20.0,
radius: 0.0,
shadows_enabled: false,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
inner_angle: 0.0,
outer_angle: std::f32::consts::FRAC_PI_4,
}
}
}
/// A Directional light.
///
/// Directional lights don't exist in reality but they are a good
@ -198,8 +251,7 @@ pub struct NotShadowReceiver;
pub enum SimulationLightSystems {
AddClusters,
AssignLightsToClusters,
UpdateDirectionalLightFrusta,
UpdatePointLightFrusta,
UpdateLightFrusta,
CheckLightVisibility,
}
@ -419,6 +471,8 @@ pub fn add_clusters(
#[derive(Clone, Component, Debug, Default)]
pub struct VisiblePointLights {
pub(crate) entities: Vec<Entity>,
pub point_light_count: usize,
pub spot_light_count: usize,
}
impl VisiblePointLights {
@ -588,20 +642,113 @@ fn cluster_space_light_aabb(
)
}
fn screen_to_view(screen_size: Vec2, inverse_projection: Mat4, screen: Vec2, ndc_z: f32) -> Vec4 {
let tex_coord = screen / screen_size;
let clip = Vec4::new(
tex_coord.x * 2.0 - 1.0,
(1.0 - tex_coord.y) * 2.0 - 1.0,
ndc_z,
1.0,
);
clip_to_view(inverse_projection, clip)
}
const NDC_MIN: Vec2 = Vec2::NEG_ONE;
const NDC_MAX: Vec2 = Vec2::ONE;
// Sort point lights with shadows enabled first, then by a stable key so that the index
// can be used to limit the number of point light shadows to render based on the device and
// we keep a stable set of lights visible
// Calculate the intersection of a ray from the eye through the view space position to a z plane
fn line_intersection_to_z_plane(origin: Vec3, p: Vec3, z: f32) -> Vec3 {
let v = p - origin;
let t = (z - Vec3::Z.dot(origin)) / Vec3::Z.dot(v);
origin + t * v
}
#[allow(clippy::too_many_arguments)]
fn compute_aabb_for_cluster(
z_near: f32,
z_far: f32,
tile_size: Vec2,
screen_size: Vec2,
inverse_projection: Mat4,
is_orthographic: bool,
cluster_dimensions: UVec3,
ijk: UVec3,
) -> Aabb {
let ijk = ijk.as_vec3();
// Calculate the minimum and maximum points in screen space
let p_min = ijk.xy() * tile_size;
let p_max = p_min + tile_size;
let cluster_min;
let cluster_max;
if is_orthographic {
// Use linear depth slicing for orthographic
// Convert to view space at the cluster near and far planes
// NOTE: 1.0 is the near plane due to using reverse z projections
let p_min = screen_to_view(
screen_size,
inverse_projection,
p_min,
1.0 - (ijk.z / cluster_dimensions.z as f32),
)
.xyz();
let p_max = screen_to_view(
screen_size,
inverse_projection,
p_max,
1.0 - ((ijk.z + 1.0) / cluster_dimensions.z as f32),
)
.xyz();
cluster_min = p_min.min(p_max);
cluster_max = p_min.max(p_max);
} else {
// Convert to view space at the near plane
// NOTE: 1.0 is the near plane due to using reverse z projections
let p_min = screen_to_view(screen_size, inverse_projection, p_min, 1.0);
let p_max = screen_to_view(screen_size, inverse_projection, p_max, 1.0);
let z_far_over_z_near = -z_far / -z_near;
let cluster_near = if ijk.z == 0.0 {
0.0
} else {
-z_near * z_far_over_z_near.powf((ijk.z - 1.0) / (cluster_dimensions.z - 1) as f32)
};
// NOTE: This could be simplified to:
// cluster_far = cluster_near * z_far_over_z_near;
let cluster_far = if cluster_dimensions.z == 1 {
-z_far
} else {
-z_near * z_far_over_z_near.powf(ijk.z / (cluster_dimensions.z - 1) as f32)
};
// Calculate the four intersection points of the min and max points with the cluster near and far planes
let p_min_near = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_near);
let p_min_far = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_far);
let p_max_near = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_near);
let p_max_far = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_far);
cluster_min = p_min_near.min(p_min_far).min(p_max_near.min(p_max_far));
cluster_max = p_min_near.max(p_min_far).max(p_max_near.max(p_max_far));
}
Aabb::from_min_max(cluster_min, cluster_max)
}
// Sort lights by
// - point-light vs spot-light, so that we can iterate point lights and spot lights in contiguous blocks in the fragment shader,
// - then those with shadows enabled first, so that the index can be used to render at most `point_light_shadow_maps_count`
// point light shadows and `spot_light_shadow_maps_count` spot light shadow maps,
// - then by entity as a stable key to ensure that a consistent set of lights are chosen if the light count limit is exceeded.
pub(crate) fn point_light_order(
(entity_1, shadows_enabled_1): (&Entity, &bool),
(entity_2, shadows_enabled_2): (&Entity, &bool),
(entity_1, shadows_enabled_1, is_spot_light_1): (&Entity, &bool, &bool),
(entity_2, shadows_enabled_2, is_spot_light_2): (&Entity, &bool, &bool),
) -> std::cmp::Ordering {
shadows_enabled_1
.cmp(shadows_enabled_2)
.reverse()
.then_with(|| entity_1.cmp(entity_2))
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
}
#[derive(Clone, Copy)]
@ -609,8 +756,10 @@ pub(crate) fn point_light_order(
pub(crate) struct PointLightAssignmentData {
entity: Entity,
translation: Vec3,
rotation: Quat,
range: f32,
shadows_enabled: bool,
spot_light_angle: Option<f32>,
}
#[derive(Default)]
@ -644,8 +793,10 @@ pub(crate) fn assign_lights_to_clusters(
&mut Clusters,
Option<&mut VisiblePointLights>,
)>,
lights_query: Query<(Entity, &GlobalTransform, &PointLight, &Visibility)>,
point_lights_query: Query<(Entity, &GlobalTransform, &PointLight, &Visibility)>,
spot_lights_query: Query<(Entity, &GlobalTransform, &SpotLight, &Visibility)>,
mut lights: Local<Vec<PointLightAssignmentData>>,
mut cluster_aabb_spheres: Local<Vec<Option<Sphere>>>,
mut max_point_lights_warning_emitted: Local<bool>,
render_device: Option<Res<RenderDevice>>,
) {
@ -658,15 +809,32 @@ pub(crate) fn assign_lights_to_clusters(
lights.clear();
// collect just the relevant light query data into a persisted vec to avoid reallocating each frame
lights.extend(
lights_query
point_lights_query
.iter()
.filter(|(.., visibility)| visibility.is_visible)
.map(
|(entity, transform, light, _visibility)| PointLightAssignmentData {
|(entity, transform, point_light, _visibility)| PointLightAssignmentData {
entity,
translation: transform.translation,
shadows_enabled: light.shadows_enabled,
range: light.range,
rotation: Quat::default(),
shadows_enabled: point_light.shadows_enabled,
range: point_light.range,
spot_light_angle: None,
},
),
);
lights.extend(
spot_lights_query
.iter()
.filter(|(.., visibility)| visibility.is_visible)
.map(
|(entity, transform, spot_light, _visibility)| PointLightAssignmentData {
entity,
translation: transform.translation,
rotation: transform.rotation,
shadows_enabled: spot_light.shadows_enabled,
range: spot_light.range,
spot_light_angle: Some(spot_light.outer_angle),
},
),
);
@ -680,8 +848,16 @@ pub(crate) fn assign_lights_to_clusters(
if lights.len() > MAX_UNIFORM_BUFFER_POINT_LIGHTS && !supports_storage_buffers {
lights.sort_by(|light_1, light_2| {
point_light_order(
(&light_1.entity, &light_1.shadows_enabled),
(&light_2.entity, &light_2.shadows_enabled),
(
&light_1.entity,
&light_1.shadows_enabled,
&light_1.spot_light_angle.is_some(),
),
(
&light_2.entity,
&light_2.shadows_enabled,
&light_2.spot_light_angle.is_some(),
),
)
});
@ -876,11 +1052,18 @@ pub(crate) fn assign_lights_to_clusters(
for lights in &mut clusters.lights {
lights.entities.clear();
lights.point_light_count = 0;
lights.spot_light_count = 0;
}
clusters.lights.resize_with(
(clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z) as usize,
VisiblePointLights::default,
);
let cluster_count =
(clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z) as usize;
clusters
.lights
.resize_with(cluster_count, VisiblePointLights::default);
// initialize empty cluster bounding spheres
cluster_aabb_spheres.clear();
cluster_aabb_spheres.extend(std::iter::repeat(None).take(cluster_count));
// Calculate the x/y/z cluster frustum planes in view space
let mut x_planes = Vec::with_capacity(clusters.dimensions.x as usize + 1);
@ -991,6 +1174,15 @@ pub(crate) fn assign_lights_to_clusters(
center: Vec3A::from(inverse_view_transform * light_sphere.center.extend(1.0)),
radius: light_sphere.radius,
};
let spot_light_dir_sin_cos = light.spot_light_angle.map(|angle| {
let (angle_sin, angle_cos) = angle.sin_cos();
(
(inverse_view_transform * (light.rotation * Vec3::Z).extend(0.0))
.truncate(),
angle_sin,
angle_cos,
)
});
let light_center_clip =
camera.projection_matrix() * view_light_sphere.center.extend(1.0);
let light_center_ndc = light_center_clip.xyz() / light_center_clip.w;
@ -1084,10 +1276,66 @@ pub(crate) fn assign_lights_to_clusters(
let mut cluster_index = ((y * clusters.dimensions.x + min_x)
* clusters.dimensions.z
+ z) as usize;
// Mark the clusters in the range as affected
for _ in min_x..=max_x {
clusters.lights[cluster_index].entities.push(light.entity);
cluster_index += clusters.dimensions.z as usize;
if let Some((view_light_direction, angle_sin, angle_cos)) =
spot_light_dir_sin_cos
{
for x in min_x..=max_x {
// further culling for spot lights
// get or initialize cluster bounding sphere
let cluster_aabb_sphere = &mut cluster_aabb_spheres[cluster_index];
let cluster_aabb_sphere = if let Some(sphere) = cluster_aabb_sphere
{
&*sphere
} else {
let aabb = compute_aabb_for_cluster(
first_slice_depth,
far_z,
clusters.tile_size.as_vec2(),
screen_size.as_vec2(),
inverse_projection,
is_orthographic,
clusters.dimensions,
UVec3::new(x, y, z),
);
let sphere = Sphere {
center: aabb.center,
radius: aabb.half_extents.length(),
};
*cluster_aabb_sphere = Some(sphere);
cluster_aabb_sphere.as_ref().unwrap()
};
// test -- based on https://bartwronski.com/2017/04/13/cull-that-cone/
let spot_light_offset = Vec3::from(
view_light_sphere.center - cluster_aabb_sphere.center,
);
let spot_light_dist_sq = spot_light_offset.length_squared();
let v1_len = spot_light_offset.dot(view_light_direction);
let distance_closest_point = (angle_cos
* (spot_light_dist_sq - v1_len * v1_len).sqrt())
- v1_len * angle_sin;
let angle_cull =
distance_closest_point > cluster_aabb_sphere.radius;
let front_cull = v1_len > cluster_aabb_sphere.radius + light.range;
let back_cull = v1_len < -cluster_aabb_sphere.radius;
if !angle_cull && !front_cull && !back_cull {
// this cluster is affected by the spot light
clusters.lights[cluster_index].entities.push(light.entity);
clusters.lights[cluster_index].spot_light_count += 1;
}
cluster_index += clusters.dimensions.z as usize;
}
} else {
for _ in min_x..=max_x {
// all clusters within range are affected by point lights
clusters.lights[cluster_index].entities.push(light.entity);
clusters.lights[cluster_index].point_light_count += 1;
cluster_index += clusters.dimensions.z as usize;
}
}
}
}
@ -1101,9 +1349,10 @@ pub(crate) fn assign_lights_to_clusters(
} else {
let mut entities = Vec::new();
update_from_light_intersections(&mut entities);
commands
.entity(view_entity)
.insert(VisiblePointLights { entities });
commands.entity(view_entity).insert(VisiblePointLights {
entities,
..Default::default()
});
}
}
}
@ -1235,6 +1484,41 @@ pub fn update_point_light_frusta(
}
}
pub fn update_spot_light_frusta(
global_lights: Res<GlobalVisiblePointLights>,
mut views: Query<
(Entity, &GlobalTransform, &SpotLight, &mut Frustum),
Or<(Changed<GlobalTransform>, Changed<SpotLight>)>,
>,
) {
for (entity, transform, spot_light, mut frustum) in views.iter_mut() {
// The frusta are used for culling meshes to the light for shadow mapping
// so if shadow mapping is disabled for this light, then the frusta are
// not needed.
// Also, if the light is not relevant for any cluster, it will not be in the
// global lights set and so there is no need to update its frusta.
if !spot_light.shadows_enabled || !global_lights.entities.contains(&entity) {
continue;
}
// ignore scale because we don't want to effectively scale light radius and range
// by applying those as a view transform to shadow map rendering of objects
let view_translation = GlobalTransform::from_translation(transform.translation);
let view_backward = transform.back();
let spot_view = spot_light_view_matrix(transform);
let spot_projection = spot_light_projection_matrix(spot_light.outer_angle);
let view_projection = spot_projection * spot_view.inverse();
*frustum = Frustum::from_view_projection(
&view_projection,
&view_translation.translation,
&view_backward,
spot_light.range,
);
}
}
pub fn check_light_mesh_visibility(
visible_point_lights: Query<&VisiblePointLights>,
mut point_lights: Query<(
@ -1244,13 +1528,23 @@ pub fn check_light_mesh_visibility(
&mut CubemapVisibleEntities,
Option<&RenderLayers>,
)>,
mut directional_lights: Query<(
&DirectionalLight,
mut spot_lights: Query<(
&SpotLight,
&GlobalTransform,
&Frustum,
&mut VisibleEntities,
Option<&RenderLayers>,
&Visibility,
)>,
mut directional_lights: Query<
(
&DirectionalLight,
&Frustum,
&mut VisibleEntities,
Option<&RenderLayers>,
&Visibility,
),
Without<SpotLight>,
>,
mut visible_entity_query: Query<
(
Entity,
@ -1309,9 +1603,9 @@ pub fn check_light_mesh_visibility(
// to prevent holding unneeded memory
}
// Point lights
for visible_lights in visible_point_lights.iter() {
for light_entity in visible_lights.entities.iter().copied() {
// Point lights
if let Ok((
point_light,
transform,
@ -1360,6 +1654,7 @@ pub fn check_light_mesh_visibility(
if !light_sphere.intersects_obb(aabb, &model_to_world) {
continue;
}
for (frustum, visible_entities) in cubemap_frusta
.iter()
.zip(cubemap_visible_entities.iter_mut())
@ -1380,6 +1675,63 @@ pub fn check_light_mesh_visibility(
// TODO: check for big changes in visible entities len() vs capacity() (ex: 2x) and resize
// to prevent holding unneeded memory
}
// spot lights
if let Ok((point_light, transform, frustum, mut visible_entities, maybe_view_mask)) =
spot_lights.get_mut(light_entity)
{
visible_entities.entities.clear();
// NOTE: If shadow mapping is disabled for the light then it must have no visible entities
if !point_light.shadows_enabled {
continue;
}
let view_mask = maybe_view_mask.copied().unwrap_or_default();
let light_sphere = Sphere {
center: Vec3A::from(transform.translation),
radius: point_light.range,
};
for (
entity,
visibility,
mut computed_visibility,
maybe_entity_mask,
maybe_aabb,
maybe_transform,
) in visible_entity_query.iter_mut()
{
if !visibility.is_visible {
continue;
}
let entity_mask = maybe_entity_mask.copied().unwrap_or_default();
if !view_mask.intersects(&entity_mask) {
continue;
}
// If we have an aabb and transform, do frustum culling
if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
let model_to_world = transform.compute_matrix();
// Do a cheap sphere vs obb test to prune out most meshes outside the sphere of the light
if !light_sphere.intersects_obb(aabb, &model_to_world) {
continue;
}
if frustum.intersects_obb(aabb, &model_to_world, true) {
computed_visibility.is_visible = true;
visible_entities.entities.push(entity);
}
} else {
computed_visibility.is_visible = true;
visible_entities.entities.push(entity);
}
}
// TODO: check for big changes in visible entities len() vs capacity() (ex: 2x) and resize
// to prevent holding unneeded memory
}
}
}
}

View file

@ -27,18 +27,19 @@ fn fragment_cluster_index(frag_coord: vec2<f32>, view_z: f32, is_orthographic: b
}
// this must match CLUSTER_COUNT_SIZE in light.rs
let CLUSTER_COUNT_SIZE = 13u;
fn unpack_offset_and_count(cluster_index: u32) -> vec2<u32> {
let CLUSTER_COUNT_SIZE = 9u;
fn unpack_offset_and_counts(cluster_index: u32) -> vec3<u32> {
#ifdef NO_STORAGE_BUFFERS_SUPPORT
let offset_and_count = cluster_offsets_and_counts.data[cluster_index >> 2u][cluster_index & ((1u << 2u) - 1u)];
return vec2<u32>(
// The offset is stored in the upper 32 - CLUSTER_COUNT_SIZE = 19 bits
(offset_and_count >> CLUSTER_COUNT_SIZE) & ((1u << 32u - CLUSTER_COUNT_SIZE) - 1u),
// The count is stored in the lower CLUSTER_COUNT_SIZE = 13 bits
offset_and_count & ((1u << CLUSTER_COUNT_SIZE) - 1u)
let offset_and_counts = cluster_offsets_and_counts.data[cluster_index >> 2u][cluster_index & ((1u << 2u) - 1u)];
// [ 31 .. 18 | 17 .. 9 | 8 .. 0 ]
// [ offset | point light count | spot light count ]
return vec3<u32>(
(offset_and_counts >> (CLUSTER_COUNT_SIZE * 2u)) & ((1u << (32u - (CLUSTER_COUNT_SIZE * 2u))) - 1u),
(offset_and_counts >> CLUSTER_COUNT_SIZE) & ((1u << CLUSTER_COUNT_SIZE) - 1u),
offset_and_counts & ((1u << CLUSTER_COUNT_SIZE) - 1u),
);
#else
return cluster_offsets_and_counts.data[cluster_index];
return cluster_offsets_and_counts.data[cluster_index].xyz;
#endif
}
@ -58,7 +59,7 @@ fn cluster_debug_visualization(
output_color: vec4<f32>,
view_z: f32,
is_orthographic: bool,
offset_and_count: vec2<u32>,
offset_and_counts: vec3<u32>,
cluster_index: u32,
) -> vec4<f32> {
// Cluster allocation debug (using 'over' alpha blending)
@ -82,9 +83,9 @@ fn cluster_debug_visualization(
let cluster_overlay_alpha = 0.1;
let max_light_complexity_per_cluster = 64.0;
output_color.r = (1.0 - cluster_overlay_alpha) * output_color.r
+ cluster_overlay_alpha * smoothStep(0.0, max_light_complexity_per_cluster, f32(offset_and_count[1]));
+ cluster_overlay_alpha * smoothStep(0.0, max_light_complexity_per_cluster, f32(offset_and_counts[1] + offset_and_counts[2]));
output_color.g = (1.0 - cluster_overlay_alpha) * output_color.g
+ cluster_overlay_alpha * (1.0 - smoothStep(0.0, max_light_complexity_per_cluster, f32(offset_and_count[1])));
+ cluster_overlay_alpha * (1.0 - smoothStep(0.0, max_light_complexity_per_cluster, f32(offset_and_counts[1] + offset_and_counts[2])));
#endif // CLUSTERED_FORWARD_DEBUG_CLUSTER_LIGHT_COMPLEXITY
#ifdef CLUSTERED_FORWARD_DEBUG_CLUSTER_COHERENCY
// NOTE: Visualizes the cluster to which the fragment belongs

View file

@ -1,7 +1,8 @@
use crate::{
point_light_order, AmbientLight, Clusters, CubemapVisibleEntities, DirectionalLight,
DirectionalLightShadowMap, DrawMesh, GlobalVisiblePointLights, MeshPipeline, NotShadowCaster,
PointLight, PointLightShadowMap, SetMeshBindGroup, VisiblePointLights, SHADOW_SHADER_HANDLE,
PointLight, PointLightShadowMap, SetMeshBindGroup, SpotLight, VisiblePointLights,
SHADOW_SHADER_HANDLE,
};
use bevy_asset::Handle;
use bevy_core_pipeline::core_3d::Transparent3d;
@ -9,7 +10,7 @@ use bevy_ecs::{
prelude::*,
system::{lifetimeless::*, SystemParamItem},
};
use bevy_math::{Mat4, UVec2, UVec3, UVec4, Vec2, Vec3, Vec4, Vec4Swizzles};
use bevy_math::{Mat4, UVec3, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};
use bevy_render::{
camera::{Camera, CameraProjection},
color::Color,
@ -56,6 +57,7 @@ pub struct ExtractedPointLight {
shadows_enabled: bool,
shadow_depth_bias: f32,
shadow_normal_bias: f32,
spot_light_angles: Option<(f32, f32)>,
}
#[derive(Component)]
@ -71,13 +73,15 @@ pub struct ExtractedDirectionalLight {
#[derive(Copy, Clone, ShaderType, Default, Debug)]
pub struct GpuPointLight {
// The lower-right 2x2 values of the projection matrix 22 23 32 33
projection_lr: Vec4,
// For point lights: the lower-right 2x2 values of the projection matrix [2][2] [2][3] [3][2] [3][3]
// For spot lights: 2 components of the direction (x,z), spot_scale and spot_offset
light_custom_data: Vec4,
color_inverse_square_range: Vec4,
position_radius: Vec4,
flags: u32,
shadow_depth_bias: f32,
shadow_normal_bias: f32,
spot_light_tan_angle: f32,
}
#[derive(ShaderType)]
@ -162,6 +166,7 @@ bitflags::bitflags! {
#[repr(transparent)]
struct PointLightFlags: u32 {
const SHADOWS_ENABLED = (1 << 0);
const SPOT_LIGHT_Y_NEGATIVE = (1 << 1);
const NONE = 0;
const UNINITIALIZED = 0xFFFF;
}
@ -198,12 +203,13 @@ pub struct GpuLights {
// w is cluster_dimensions.z * log(near) / log(far / near)
cluster_factors: Vec4,
n_directional_lights: u32,
// offset from spot light's light index to spot light's shadow map index
spot_light_shadowmap_offset: i32,
}
// NOTE: this must be kept in sync with the same constants in pbr.frag
pub const MAX_UNIFORM_BUFFER_POINT_LIGHTS: usize = 256;
pub const MAX_DIRECTIONAL_LIGHTS: usize = 1;
pub const DIRECTIONAL_SHADOW_LAYERS: u32 = MAX_DIRECTIONAL_LIGHTS as u32;
pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float;
pub struct ShadowPipeline {
@ -402,14 +408,19 @@ pub fn extract_lights(
directional_light_shadow_map: Res<DirectionalLightShadowMap>,
global_point_lights: Res<GlobalVisiblePointLights>,
mut point_lights: Query<(&PointLight, &mut CubemapVisibleEntities, &GlobalTransform)>,
mut directional_lights: Query<(
Entity,
&DirectionalLight,
&mut VisibleEntities,
&GlobalTransform,
&Visibility,
)>,
mut spot_lights: Query<(&SpotLight, &mut VisibleEntities, &GlobalTransform)>,
mut directional_lights: Query<
(
Entity,
&DirectionalLight,
&mut VisibleEntities,
&GlobalTransform,
&Visibility,
),
Without<SpotLight>,
>,
mut previous_point_lights_len: Local<usize>,
mut previous_spot_lights_len: Local<usize>,
) {
// NOTE: These shadow map resources are extracted here as they are used here too so this avoids
// races between scheduling of ExtractResourceSystems and this system.
@ -452,6 +463,7 @@ pub fn extract_lights(
shadow_normal_bias: point_light.shadow_normal_bias
* point_light_texel_size
* std::f32::consts::SQRT_2,
spot_light_angles: None,
},
render_cubemap_visible_entities,
),
@ -461,6 +473,44 @@ pub fn extract_lights(
*previous_point_lights_len = point_lights_values.len();
commands.insert_or_spawn_batch(point_lights_values);
let mut spot_lights_values = Vec::with_capacity(*previous_spot_lights_len);
for entity in global_point_lights.iter().copied() {
if let Ok((spot_light, visible_entities, transform)) = spot_lights.get_mut(entity) {
let render_visible_entities = std::mem::take(visible_entities.into_inner());
let texel_size =
2.0 * spot_light.outer_angle.tan() / directional_light_shadow_map.size as f32;
spot_lights_values.push((
entity,
(
ExtractedPointLight {
color: spot_light.color,
// NOTE: Map from luminous power in lumens to luminous intensity in lumens per steradian
// for a point light. See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower
// for details.
// Note: Filament uses a divisor of PI for spot lights. We choose to use the same 4*PI divisor
// in both cases so that toggling between point light and spot light keeps lit areas lit equally,
// which seems least surprising for users
intensity: spot_light.intensity / (4.0 * std::f32::consts::PI),
range: spot_light.range,
radius: spot_light.radius,
transform: *transform,
shadows_enabled: spot_light.shadows_enabled,
shadow_depth_bias: spot_light.shadow_depth_bias,
// The factor of SQRT_2 is for the worst-case diagonal offset
shadow_normal_bias: spot_light.shadow_normal_bias
* texel_size
* std::f32::consts::SQRT_2,
spot_light_angles: Some((spot_light.inner_angle, spot_light.outer_angle)),
},
render_visible_entities,
),
));
}
}
*previous_spot_lights_len = spot_lights_values.len();
commands.insert_or_spawn_batch(spot_lights_values);
for (entity, directional_light, visible_entities, transform, visibility) in
directional_lights.iter_mut()
{
@ -619,6 +669,9 @@ pub enum LightEntity {
light_entity: Entity,
face_index: usize,
},
Spot {
light_entity: Entity,
},
}
pub fn calculate_cluster_factors(
near: f32,
@ -637,6 +690,38 @@ pub fn calculate_cluster_factors(
}
}
// this method of constructing a basis from a vec3 is used by glam::Vec3::any_orthonormal_pair
// we will also construct it in the fragment shader and need our implementations to match,
// so we reproduce it here to avoid a mismatch if glam changes. we also switch the handedness
// could move this onto transform but it's pretty niche
pub(crate) fn spot_light_view_matrix(transform: &GlobalTransform) -> Mat4 {
// the matrix z_local (opposite of transform.forward())
let fwd_dir = transform.local_z().extend(0.0);
let sign = 1f32.copysign(fwd_dir.z);
let a = -1.0 / (fwd_dir.z + sign);
let b = fwd_dir.x * fwd_dir.y * a;
let up_dir = Vec4::new(
1.0 + sign * fwd_dir.x * fwd_dir.x * a,
sign * b,
-sign * fwd_dir.x,
0.0,
);
let right_dir = Vec4::new(-b, -sign - fwd_dir.y * fwd_dir.y * a, fwd_dir.y, 0.0);
Mat4::from_cols(
right_dir,
up_dir,
fwd_dir,
transform.translation.extend(1.0),
)
}
pub(crate) fn spot_light_projection_matrix(angle: f32) -> Mat4 {
// spot light projection FOV is 2x the angle from spot light centre to outer edge
Mat4::perspective_infinite_reverse_rh(angle * 2.0, 1.0, POINT_LIGHT_NEAR_Z)
}
#[allow(clippy::too_many_arguments)]
pub fn prepare_lights(
mut commands: Commands,
@ -670,20 +755,50 @@ pub fn prepare_lights(
let mut point_lights: Vec<_> = point_lights.iter().collect::<Vec<_>>();
#[cfg(not(feature = "webgl"))]
let max_point_light_shadow_maps = point_lights
.iter()
.filter(|light| light.1.shadows_enabled)
.count()
.min((render_device.limits().max_texture_array_layers / 6) as usize);
let max_texture_array_layers = render_device.limits().max_texture_array_layers as usize;
#[cfg(not(feature = "webgl"))]
let max_texture_cubes = max_texture_array_layers / 6;
#[cfg(feature = "webgl")]
let max_point_light_shadow_maps = 1;
let max_texture_array_layers = 1;
#[cfg(feature = "webgl")]
let max_texture_cubes = 1;
// Sort point lights with shadows enabled first, then by a stable key so that the index can be used
// to render at most `max_point_light_shadow_maps` point light shadows.
let point_light_count = point_lights
.iter()
.filter(|light| light.1.shadows_enabled && light.1.spot_light_angles.is_none())
.count();
let point_light_shadow_maps_count = point_light_count.min(max_texture_cubes);
let directional_shadow_maps_count = directional_lights
.iter()
.filter(|(_, light)| light.shadows_enabled)
.count()
.min(max_texture_array_layers);
let spot_light_shadow_maps_count = point_lights
.iter()
.filter(|(_, light)| light.shadows_enabled && light.spot_light_angles.is_some())
.count()
.min(max_texture_array_layers - directional_shadow_maps_count);
// Sort lights by
// - point-light vs spot-light, so that we can iterate point lights and spot lights in contiguous blocks in the fragment shader,
// - then those with shadows enabled first, so that the index can be used to render at most `point_light_shadow_maps_count`
// point light shadows and `spot_light_shadow_maps_count` spot light shadow maps,
// - then by entity as a stable key to ensure that a consistent set of lights are chosen if the light count limit is exceeded.
point_lights.sort_by(|(entity_1, light_1), (entity_2, light_2)| {
point_light_order(
(entity_1, &light_1.shadows_enabled),
(entity_2, &light_2.shadows_enabled),
(
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(),
),
)
});
@ -696,17 +811,50 @@ pub fn prepare_lights(
let mut gpu_point_lights = Vec::new();
for (index, &(entity, light)) in point_lights.iter().enumerate() {
let mut flags = PointLightFlags::NONE;
// Lights are sorted, shadow enabled lights are first
if light.shadows_enabled && index < max_point_light_shadow_maps {
if light.shadows_enabled
&& (index < point_light_shadow_maps_count
|| (light.spot_light_angles.is_some()
&& index - point_light_count < spot_light_shadow_maps_count))
{
flags |= PointLightFlags::SHADOWS_ENABLED;
}
let (light_custom_data, spot_light_tan_angle) = match light.spot_light_angles {
Some((inner, outer)) => {
let light_direction = light.transform.forward();
if light_direction.y.is_sign_negative() {
flags |= PointLightFlags::SPOT_LIGHT_Y_NEGATIVE;
}
let cos_outer = outer.cos();
let spot_scale = 1.0 / f32::max(inner.cos() - cos_outer, 1e-4);
let spot_offset = -cos_outer * spot_scale;
(
// For spot lights: the direction (x,z), spot_scale and spot_offset
light_direction.xz().extend(spot_scale).extend(spot_offset),
outer.tan(),
)
}
None => {
(
// For point lights: the lower-right 2x2 values of the projection matrix [2][2] [2][3] [3][2] [3][3]
Vec4::new(
cube_face_projection.z_axis.z,
cube_face_projection.z_axis.w,
cube_face_projection.w_axis.z,
cube_face_projection.w_axis.w,
),
// unused
0.0,
)
}
};
gpu_point_lights.push(GpuPointLight {
projection_lr: Vec4::new(
cube_face_projection.z_axis.z,
cube_face_projection.z_axis.w,
cube_face_projection.w_axis.z,
cube_face_projection.w_axis.w,
),
light_custom_data,
// premultiply color by intensity
// we don't use the alpha at all, so no reason to multiply only [0..3]
color_inverse_square_range: (Vec4::from_slice(&light.color.as_linear_rgba_f32())
@ -717,6 +865,7 @@ pub fn prepare_lights(
flags: flags.bits,
shadow_depth_bias: light.shadow_depth_bias,
shadow_normal_bias: light.shadow_normal_bias,
spot_light_tan_angle,
});
global_light_meta.entity_to_index.insert(entity, index);
}
@ -734,7 +883,7 @@ pub fn prepare_lights(
size: Extent3d {
width: point_light_shadow_map.size as u32,
height: point_light_shadow_map.size as u32,
depth_or_array_layers: max_point_light_shadow_maps.max(1) as u32 * 6,
depth_or_array_layers: point_light_shadow_maps_count.max(1) as u32 * 6,
},
mip_level_count: 1,
sample_count: 1,
@ -752,7 +901,9 @@ pub fn prepare_lights(
.min(render_device.limits().max_texture_dimension_2d),
height: (directional_light_shadow_map.size as u32)
.min(render_device.limits().max_texture_dimension_2d),
depth_or_array_layers: DIRECTIONAL_SHADOW_LAYERS,
depth_or_array_layers: (directional_shadow_maps_count
+ spot_light_shadow_maps_count)
.max(1) as u32,
},
mip_level_count: 1,
sample_count: 1,
@ -785,13 +936,18 @@ pub fn prepare_lights(
),
cluster_dimensions: clusters.dimensions.extend(n_clusters),
n_directional_lights: directional_lights.iter().len() as u32,
// spotlight shadow maps are stored in the directional light array, starting at directional_shadow_maps_count.
// the spot lights themselves start in the light array at point_light_count. so to go from light
// index to shadow map index, we need to subtract point light shadowmap count and add directional shadowmap count.
spot_light_shadowmap_offset: directional_shadow_maps_count as i32
- point_light_count as i32,
};
// TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query
for &(light_entity, light) in point_lights
.iter()
// Lights are sorted, shadow enabled lights are first
.take(max_point_light_shadow_maps)
.take(point_light_shadow_maps_count)
.filter(|(_, light)| light.shadows_enabled)
{
let light_index = *global_light_meta
@ -846,6 +1002,55 @@ pub fn prepare_lights(
}
}
// spot lights
for (light_index, &(light_entity, light)) in point_lights
.iter()
.skip(point_light_count)
.take(spot_light_shadow_maps_count)
.enumerate()
{
let spot_view_matrix = spot_light_view_matrix(&light.transform);
let spot_view_transform = GlobalTransform::from_matrix(spot_view_matrix);
let angle = light.spot_light_angles.expect("lights should be sorted so that \
[point_light_shadow_maps_count..point_light_shadow_maps_count + spot_light_shadow_maps_count] are spot lights").1;
let spot_projection = spot_light_projection_matrix(angle);
let depth_texture_view =
directional_light_depth_texture
.texture
.create_view(&TextureViewDescriptor {
label: Some("spot_light_shadow_map_texture_view"),
format: None,
dimension: Some(TextureViewDimension::D2),
aspect: TextureAspect::All,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: (directional_shadow_maps_count + light_index) as u32,
array_layer_count: NonZeroU32::new(1),
});
let view_light_entity = commands
.spawn()
.insert_bundle((
ShadowView {
depth_texture_view,
pass_name: format!("shadow pass spot light {}", light_index,),
},
ExtractedView {
width: directional_light_shadow_map.size as u32,
height: directional_light_shadow_map.size as u32,
transform: spot_view_transform,
projection: spot_projection,
},
RenderPhase::<Shadow>::default(),
LightEntity::Spot { light_entity },
))
.id();
view_lights.push(view_light_entity);
}
for (i, (light_entity, light)) in directional_lights
.iter()
.enumerate()
@ -980,28 +1185,29 @@ pub fn prepare_lights(
// this must match CLUSTER_COUNT_SIZE in pbr.wgsl
// and must be large enough to contain MAX_UNIFORM_BUFFER_POINT_LIGHTS
const CLUSTER_COUNT_SIZE: u32 = 13;
const CLUSTER_COUNT_SIZE: u32 = 9;
const CLUSTER_OFFSET_MASK: u32 = (1 << (32 - CLUSTER_COUNT_SIZE)) - 1;
const CLUSTER_OFFSET_MASK: u32 = (1 << (32 - (CLUSTER_COUNT_SIZE * 2))) - 1;
const CLUSTER_COUNT_MASK: u32 = (1 << CLUSTER_COUNT_SIZE) - 1;
const POINT_LIGHT_INDEX_MASK: u32 = (1 << 8) - 1;
// NOTE: With uniform buffer max binding size as 16384 bytes
// that means we can fit say 256 point lights in one uniform
// that means we can fit 256 point lights in one uniform
// buffer, which means the count can be at most 256 so it
// needs 9 bits.
// The array of indices can also use u8 and that means the
// offset in to the array of indices needs to be able to address
// 16384 values. log2(16384) = 14 bits.
// We use 32 bits to store the pair, so we choose to divide the
// remaining 9 bits proportionally to give some future room.
// This means we can pack the offset into the upper 19 bits of a u32
// and the count into the lower 13 bits.
// We use 32 bits to store the offset and counts so
// we pack the offset into the upper 14 bits of a u32,
// the point light count into bits 9-17, and the spot light count into bits 0-8.
// [ 31 .. 18 | 17 .. 9 | 8 .. 0 ]
// [ offset | point light count | spot light count ]
// NOTE: This assumes CPU and GPU endianness are the same which is true
// for all common and tested x86/ARM CPUs and AMD/NVIDIA/Intel/Apple/etc GPUs
fn pack_offset_and_count(offset: usize, count: usize) -> u32 {
((offset as u32 & CLUSTER_OFFSET_MASK) << CLUSTER_COUNT_SIZE)
| (count as u32 & CLUSTER_COUNT_MASK)
fn pack_offset_and_counts(offset: usize, point_count: usize, spot_count: usize) -> u32 {
((offset as u32 & CLUSTER_OFFSET_MASK) << (CLUSTER_COUNT_SIZE * 2))
| (point_count as u32 & CLUSTER_COUNT_MASK) << CLUSTER_COUNT_SIZE
| (spot_count as u32 & CLUSTER_COUNT_MASK)
}
#[derive(ShaderType)]
@ -1043,7 +1249,7 @@ struct GpuClusterLightIndexListsStorage {
#[derive(ShaderType, Default)]
struct GpuClusterOffsetsAndCountsStorage {
#[size(runtime)]
data: Vec<UVec2>,
data: Vec<UVec4>,
}
enum ViewClusterBuffers {
@ -1122,7 +1328,7 @@ impl ViewClusterBindings {
}
}
pub fn push_offset_and_count(&mut self, offset: usize, count: usize) {
pub fn push_offset_and_counts(&mut self, offset: usize, point_count: usize, spot_count: usize) {
match &mut self.buffers {
ViewClusterBuffers::Uniform {
cluster_offsets_and_counts,
@ -1134,7 +1340,7 @@ impl ViewClusterBindings {
return;
}
let component = self.n_offsets & ((1 << 2) - 1);
let packed = pack_offset_and_count(offset, count);
let packed = pack_offset_and_counts(offset, point_count, spot_count);
cluster_offsets_and_counts.get_mut().data[array_index][component] = packed;
}
@ -1142,10 +1348,12 @@ impl ViewClusterBindings {
cluster_offsets_and_counts,
..
} => {
cluster_offsets_and_counts
.get_mut()
.data
.push(UVec2::new(offset as u32, count as u32));
cluster_offsets_and_counts.get_mut().data.push(UVec4::new(
offset as u32,
point_count as u32,
spot_count as u32,
0,
));
}
}
@ -1165,7 +1373,7 @@ impl ViewClusterBindings {
let array_index = self.n_indices >> 4; // >> 4 is equivalent to / 16
let component = (self.n_indices >> 2) & ((1 << 2) - 1);
let sub_index = self.n_indices & ((1 << 2) - 1);
let index = index as u32 & POINT_LIGHT_INDEX_MASK;
let index = index as u32;
cluster_light_index_lists.get_mut().data[array_index][component] |=
index << (8 * sub_index);
@ -1278,8 +1486,11 @@ pub fn prepare_clusters(
for _z in 0..cluster_config.dimensions.z {
let offset = view_clusters_bindings.n_indices();
let cluster_lights = &extracted_clusters.data[cluster_index];
let count = cluster_lights.len();
view_clusters_bindings.push_offset_and_count(offset, count);
view_clusters_bindings.push_offset_and_counts(
offset,
cluster_lights.point_light_count,
cluster_lights.spot_light_count,
);
if !indices_full {
for entity in cluster_lights.iter() {
@ -1340,6 +1551,7 @@ pub fn queue_shadows(
mut view_light_shadow_phases: Query<(&LightEntity, &mut RenderPhase<Shadow>)>,
point_light_entities: Query<&CubemapVisibleEntities, With<ExtractedPointLight>>,
directional_light_entities: Query<&VisibleEntities, With<ExtractedDirectionalLight>>,
spot_light_entities: Query<&VisibleEntities, With<ExtractedPointLight>>,
) {
for view_lights in view_lights.iter() {
let draw_shadow_mesh = shadow_draw_functions
@ -1360,6 +1572,9 @@ pub fn queue_shadows(
.get(*light_entity)
.expect("Failed to get point light visible entities")
.get(*face_index),
LightEntity::Spot { light_entity } => spot_light_entities
.get(*light_entity)
.expect("Failed to get spot light visible entities"),
};
// NOTE: Lights with shadow mapping disabled will have no visible entities
// so no meshes will be queued

View file

@ -13,17 +13,20 @@ struct View {
};
struct PointLight {
// NOTE: [2][2] [2][3] [3][2] [3][3]
projection_lr: vec4<f32>;
// For point lights: the lower-right 2x2 values of the projection matrix [2][2] [2][3] [3][2] [3][3]
// For spot lights: the direction (x,z), spot_scale and spot_offset
light_custom_data: vec4<f32>;
color_inverse_square_range: vec4<f32>;
position_radius: vec4<f32>;
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32;
shadow_depth_bias: f32;
shadow_normal_bias: f32;
spot_light_tan_angle: f32;
};
let POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u;
let POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u;
let POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE: u32 = 2u;
struct DirectionalLight {
view_projection: mat4x4<f32>;
@ -55,6 +58,7 @@ struct Lights {
// w is cluster_dimensions.z / (-far - -near)
cluster_factors: vec4<f32>;
n_directional_lights: u32;
spot_light_shadowmap_offset: i32;
};
#ifdef NO_STORAGE_BUFFERS_SUPPORT
@ -78,6 +82,6 @@ struct ClusterLightIndexLists {
data: array<u32>;
};
struct ClusterOffsetsAndCounts {
data: array<vec2<u32>>;
data: array<vec4<u32>>;
};
#endif

View file

@ -169,8 +169,10 @@ fn pbr(
view.inverse_view[3].z
), in.world_position);
let cluster_index = fragment_cluster_index(in.frag_coord.xy, view_z, in.is_orthographic);
let offset_and_count = unpack_offset_and_count(cluster_index);
for (var i: u32 = offset_and_count[0]; i < offset_and_count[0] + offset_and_count[1]; i = i + 1u) {
let offset_and_counts = unpack_offset_and_counts(cluster_index);
// point lights
for (var i: u32 = offset_and_counts[0]; i < offset_and_counts[0] + offset_and_counts[1]; i = i + 1u) {
let light_id = get_light_id(i);
let light = point_lights.data[light_id];
var shadow: f32 = 1.0;
@ -182,6 +184,19 @@ fn pbr(
light_accum = light_accum + light_contrib * shadow;
}
// spot lights
for (var i: u32 = offset_and_counts[0] + offset_and_counts[1]; i < offset_and_counts[0] + offset_and_counts[1] + offset_and_counts[2]; i = i + 1u) {
let light_id = get_light_id(i);
let light = point_lights.data[light_id];
var shadow: f32 = 1.0;
if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u
&& (light.flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
shadow = fetch_spot_shadow(light_id, in.world_position, in.world_normal);
}
let light_contrib = spot_light(in.world_position.xyz, light, roughness, NdotV, in.N, in.V, R, F0, diffuse_color);
light_accum = light_accum + light_contrib * shadow;
}
let n_directional_lights = lights.n_directional_lights;
for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) {
let light = lights.directional_lights[i];
@ -207,7 +222,7 @@ fn pbr(
output_color,
view_z,
in.is_orthographic,
offset_and_count,
offset_and_counts,
cluster_index,
);

View file

@ -239,6 +239,31 @@ fn point_light(
return ((diffuse + specular_light) * light.color_inverse_square_range.rgb) * (rangeAttenuation * NoL);
}
fn spot_light(
world_position: vec3<f32>, light: PointLight, roughness: f32, NdotV: f32, N: vec3<f32>, V: vec3<f32>,
R: vec3<f32>, F0: vec3<f32>, diffuseColor: vec3<f32>
) -> vec3<f32> {
// reuse the point light calculations
let point = point_light(world_position, light, roughness, NdotV, N, V, R, F0, diffuseColor);
// 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(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 - world_position.xyz;
// 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;
return point * spot_attenuation;
}
fn directional_light(light: DirectionalLight, roughness: f32, NdotV: f32, normal: vec3<f32>, view: vec3<f32>, R: vec3<f32>, F0: vec3<f32>, diffuseColor: vec3<f32>) -> vec3<f32> {
let incident_light = light.direction_to_light.xyz;

View file

@ -25,7 +25,7 @@ fn fetch_point_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: v
// 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.projection_lr.xy + light.projection_lr.zw;
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
@ -41,6 +41,63 @@ fn fetch_point_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: v
#endif
}
fn fetch_spot_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: vec3<f32>) -> f32 {
let light = point_lights.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(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 distance_to_light = dot(fwd, surface_to_light);
let offset_position =
-surface_to_light
+ (light.shadow_depth_bias * normalize(surface_to_light))
+ (surface_normal.xyz * light.shadow_normal_bias) * distance_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-multplying.
// 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;
#ifdef NO_ARRAY_TEXTURES_SUPPORT
return textureSampleCompare(directional_shadow_textures, directional_shadow_textures_sampler,
shadow_uv, depth);
#else
return textureSampleCompareLevel(directional_shadow_textures, directional_shadow_textures_sampler,
shadow_uv, i32(light_id) + lights.spot_light_shadowmap_offset, depth);
#endif
}
fn fetch_directional_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: vec3<f32>) -> f32 {
let light = lights.directional_lights[light_id];

View file

@ -123,6 +123,9 @@ impl Plane {
}
}
/// A frustum defined by the 6 containing planes
/// Planes are ordered left, right, top, bottom, near, far
/// Normals point into the contained volume
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct Frustum {

View file

@ -121,22 +121,28 @@ fn setup(
});
});
// green point light
// green spot light
commands
.spawn_bundle(PointLightBundle {
// transform: Transform::from_xyz(5.0, 8.0, 2.0),
transform: Transform::from_xyz(-1.0, 2.0, 0.0),
point_light: PointLight {
.spawn_bundle(SpotLightBundle {
transform: Transform::from_xyz(-1.0, 2.0, 0.0)
.looking_at(Vec3::new(-1.0, 0.0, 0.0), Vec3::Z),
spot_light: SpotLight {
intensity: 1600.0, // lumens - roughly a 100W non-halogen incandescent bulb
color: Color::GREEN,
shadows_enabled: true,
inner_angle: 0.6,
outer_angle: 0.8,
..default()
},
..default()
})
.with_children(|builder| {
builder.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::UVSphere {
transform: Transform::from_rotation(Quat::from_rotation_x(
std::f32::consts::PI / 2.0,
)),
mesh: meshes.add(Mesh::from(shape::Capsule {
depth: 0.125,
radius: 0.1,
..default()
})),

166
examples/3d/spotlight.rs Normal file
View file

@ -0,0 +1,166 @@
use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
pbr::NotShadowCaster,
prelude::*,
};
use rand::{thread_rng, Rng};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(FrameTimeDiagnosticsPlugin::default())
.add_plugin(LogDiagnosticsPlugin::default())
.add_startup_system(setup)
.add_system(light_sway)
.add_system(movement)
.run();
}
#[derive(Component)]
struct Movable;
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// ground plane
commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Plane { size: 100.0 })),
material: materials.add(StandardMaterial {
base_color: Color::GREEN,
perceptual_roughness: 1.0,
..default()
}),
..default()
});
// cubes
let mut rng = thread_rng();
for _ in 0..100 {
let x = rng.gen_range(-5.0..5.0);
let y = rng.gen_range(-5.0..5.0);
let z = rng.gen_range(-5.0..5.0);
commands
.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 0.5 })),
material: materials.add(StandardMaterial {
base_color: Color::BLUE,
..default()
}),
transform: Transform::from_xyz(x, y, z),
..default()
})
.insert(Movable);
}
// ambient light
commands.insert_resource(AmbientLight {
color: Color::rgb(0.0, 1.0, 1.0),
brightness: 0.14,
});
for x in 0..4 {
for z in 0..4 {
let x = x as f32 - 2.0;
let z = z as f32 - 2.0;
// red spot_light
commands
.spawn_bundle(SpotLightBundle {
transform: Transform::from_xyz(1.0 + x, 2.0, z)
.looking_at(Vec3::new(1.0 + x, 0.0, z), Vec3::X),
spot_light: SpotLight {
intensity: 200.0, // lumens
color: Color::WHITE,
shadows_enabled: true,
inner_angle: std::f32::consts::PI / 4.0 * 0.85,
outer_angle: std::f32::consts::PI / 4.0,
..default()
},
..default()
})
.with_children(|builder| {
builder.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::UVSphere {
radius: 0.05,
..default()
})),
material: materials.add(StandardMaterial {
base_color: Color::RED,
emissive: Color::rgba_linear(1.0, 0.0, 0.0, 0.0),
..default()
}),
..default()
});
builder
.spawn_bundle(PbrBundle {
transform: Transform::from_translation(Vec3::Z * -0.1),
mesh: meshes.add(Mesh::from(shape::UVSphere {
radius: 0.1,
..default()
})),
material: materials.add(StandardMaterial {
base_color: Color::MAROON,
emissive: Color::rgba_linear(0.125, 0.0, 0.0, 0.0),
..default()
}),
..default()
})
.insert(NotShadowCaster);
});
}
}
// camera
commands.spawn_bundle(Camera3dBundle {
transform: Transform::from_xyz(-4.0, 5.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
}
fn light_sway(time: Res<Time>, mut query: Query<(&mut Transform, &mut SpotLight)>) {
for (mut transform, mut angles) in query.iter_mut() {
transform.rotation = Quat::from_euler(
EulerRot::XYZ,
-std::f32::consts::FRAC_PI_2
+ (time.seconds_since_startup() * 0.67 * 3.0).sin() as f32 * 0.5,
(time.seconds_since_startup() * 3.0).sin() as f32 * 0.5,
0.0,
);
let angle = ((time.seconds_since_startup() * 1.2).sin() as f32 + 1.0)
* (std::f32::consts::FRAC_PI_4 - 0.1);
angles.inner_angle = angle * 0.8;
angles.outer_angle = angle;
}
}
fn movement(
input: Res<Input<KeyCode>>,
time: Res<Time>,
mut query: Query<&mut Transform, With<Movable>>,
) {
for mut transform in query.iter_mut() {
let mut direction = Vec3::ZERO;
if input.pressed(KeyCode::Up) {
direction.z -= 1.0;
}
if input.pressed(KeyCode::Down) {
direction.z += 1.0;
}
if input.pressed(KeyCode::Left) {
direction.x -= 1.0;
}
if input.pressed(KeyCode::Right) {
direction.x += 1.0;
}
if input.pressed(KeyCode::PageUp) {
direction.y += 1.0;
}
if input.pressed(KeyCode::PageDown) {
direction.y -= 1.0;
}
transform.translation += time.delta_seconds() * 2.0 * direction;
}
}

View file

@ -117,6 +117,7 @@ Example | Description
[Shadow Caster and Receiver](../examples/3d/shadow_caster_receiver.rs) | Demonstrates how to prevent meshes from casting/receiving shadows in a 3d scene
[Spherical Area Lights](../examples/3d/spherical_area_lights.rs) | Demonstrates how point light radius values affect light behavior
[Split Screen](../examples/3d/split_screen.rs) | Demonstrates how to render two cameras to the same window to accomplish "split screen"
[Spotlight](../examples/3d/spotlight.rs) | Illustrates spot lights
[Texture](../examples/3d/texture.rs) | Shows configuration of texture materials
[Transparency in 3D](../examples/3d/transparency_3d.rs) | Demonstrates transparency in 3d
[Two Passes](../examples/3d/two_passes.rs) | Renders two 3d passes to the same window from different perspectives