Skip to main content

renderling/
light.rs

1//! Lighting effects.
2//!
3//! This module includes support for various types of lights such as
4//! directional, point, and spot lights.
5//!
6//! Additionally, the module provides shadow mapping to create realistic
7//! shadows.
8//!
9//! Also provided is an implementation of light tiling, a technique that
10//! optimizes the rendering of thousands of analytical lights. If you find your
11//! scene performing poorly under the load of very many lights, [`LightTiling`]
12//! can speed things up.
13//!
14//! ## Analytical lights
15//!
16//! Analytical lights are a fundamental lighting effect in a scene.
17//! These lights can be created directly from the [`Stage`] using the methods
18//! * [`Stage::new_directional_light`]
19//! * [`Stage::new_point_light`]
20//! * [`Stage::new_spot_light`]
21//!
22//! Each of these methods returns an [`AnalyticalLight`] instance that can be
23//! manipulated to simulate different lighting conditions.
24//!
25//! Once created, these lights can be positioned and oriented using
26//! [`Transform`] or [`NestedTransform`] objects. The [`Transform`] allows you
27//! to set the position, rotation, and scale of the light, while
28//! [`NestedTransform`] enables hierarchical transformations, which is useful
29//! for complex scenes where lights need to follow specific objects or
30//! structures.
31//!
32//! By adjusting the properties of these lights, such as intensity, color, and
33//! direction, you can achieve a wide range of lighting effects, from simulating
34//! sunlight with directional lights to creating focused spotlights or ambient
35//! point lights. These lights can also be combined with shadow mapping
36//! techniques to enhance the realism of shadows in the scene.
37//!
38//! ## Shadow mapping
39//!
40//! Shadow mapping is a technique used to add realistic shadows to a scene by
41//! simulating the way light interacts with objects.
42//!
43//! To create a [`ShadowMap`], use the [`Stage::new_shadow_map`] method, passing
44//! in the light source and desired parameters such as the size of the shadow
45//! map and the near and far planes of the light's frustum.  Once created, the
46//! shadow map needs to be updated each frame (or as needed) using the
47//! [`ShadowMap::update`] method, which renders the scene from the light's
48//! perspective to determine which areas are in shadow.
49//!
50//! This technique allows for dynamic shadows that change with the movement of
51//! lights and objects, enhancing the realism of the scene. Proper
52//! configuration of shadow map parameters, such as bias and resolution, is
53//! crucial to achieving high-quality shadows without artifacts, and varies
54//! with each scene.
55//!
56//! ## Light tiling
57//!
58//! Light tiling is a technique used to optimize the rendering of scenes with a
59//! large number of lights.
60//!
61//! It divides the rendering surface into a grid of tiles, allowing for
62//! efficient computation of lighting effects by processing each tile
63//! independently and cutting down on the overall lighting calculations.
64//!
65//! To create a [`LightTiling`], use the [`Stage::new_light_tiling`] method,
66//! providing a [`LightTilingConfig`] to specify parameters such as tile size
67//! and maximum lights per tile.
68//!
69//! Once created, the [`LightTiling`] instance should be kept in sync with the
70//! scene by calling the [`LightTiling::run`] method each frame, or however you
71//! see fit. This method updates the lighting calculations for each tile based
72//! on the current scene configuration, ensuring optimal performance even with
73//! many lights.
74//!
75//! By using light tiling, you can significantly improve the performance of your
76//! rendering pipeline, especially in complex scenes with numerous light
77//! sources.
78
79#[cfg(doc)]
80use crate::{
81    stage::Stage,
82    transform::{NestedTransform, Transform},
83};
84
85#[cfg(cpu)]
86mod cpu;
87#[cfg(cpu)]
88pub use cpu::*;
89
90#[cfg(cpu)]
91mod shadow_map;
92#[cfg(cpu)]
93pub use shadow_map::*;
94
95#[cfg(cpu)]
96mod tiling;
97#[cfg(cpu)]
98pub use tiling::*;
99
100pub mod shader;
101
102#[cfg(test)]
103mod test {
104    use crabslab::Array;
105    use glam::{UVec2, UVec3, Vec2, Vec3};
106
107    use crate::math::{GpuRng, IsVector};
108
109    use super::shader::*;
110
111    #[cfg(feature = "gltf")]
112    #[test]
113    fn position_direction_sanity() {
114        // With GLTF, the direction of a light is given by the light's node's
115        // transform. Specifically we get the node's transform and use
116        // the rotation quaternion to rotate the vector Vec3::NEG_Z -
117        // the result is our direction.
118
119        use glam::{Mat4, Quat};
120        println!("{:#?}", std::env::current_dir());
121        let (document, _buffers, _images) = gltf::import("../../gltf/four_spotlights.glb").unwrap();
122        for node in document.nodes() {
123            use glam::Vec3;
124
125            println!("node: {} {:?}", node.index(), node.name());
126
127            let gltf_transform = node.transform();
128            let (translation, rotation, _scale) = gltf_transform.decomposed();
129            let position = Vec3::from_array(translation);
130            let direction =
131                Mat4::from_quat(Quat::from_array(rotation)).transform_vector3(Vec3::NEG_Z);
132            println!("position: {position}");
133            println!("direction: {direction}");
134
135            // In Blender, our lights are sitting at (0, 0, 1) pointing at -Z,
136            // +Z, +X and +Y. But alas, it is a bit more complicated
137            // than that because this file is exported with UP being
138            // +Y, so Z and Y have been flipped...
139            assert_eq!(Vec3::Y, position);
140            let expected_direction = match node.name() {
141                Some("light_negative_z") => Vec3::NEG_Y,
142                Some("light_positive_z") => Vec3::Y,
143                Some("light_positive_x") => Vec3::X,
144                Some("light_positive_y") => Vec3::NEG_Z,
145                n => panic!("unexpected node '{n:?}'"),
146            };
147            // And also there are rounding ... imprecisions...
148            assert_approx_eq::assert_approx_eq!(expected_direction.x, direction.x);
149            assert_approx_eq::assert_approx_eq!(expected_direction.y, direction.y);
150            assert_approx_eq::assert_approx_eq!(expected_direction.z, direction.z);
151        }
152    }
153
154    #[test]
155    /// Test that we can determine if a point is inside clip space or not.
156    fn clip_space_bounds_sanity() {
157        let inside = Vec3::ONE;
158        assert!(
159            crate::math::is_inside_clip_space(inside),
160            "should be inside"
161        );
162        let inside = Vec3::new(0.5, -0.5, 0.8);
163        assert!(
164            crate::math::is_inside_clip_space(inside),
165            "should be inside"
166        );
167        let outside_neg_z = Vec3::new(0.5, -0.5, -0.8);
168        assert!(
169            !crate::math::is_inside_clip_space(outside_neg_z),
170            "negative z should be outside (wgpu z range is [0, 1])"
171        );
172        let outside = Vec3::new(0.5, 0.0, 1.3);
173        assert!(
174            !crate::math::is_inside_clip_space(outside),
175            "should be outside"
176        );
177    }
178
179    #[test]
180    fn finding_orthogonal_vectors_sanity() {
181        const THRESHOLD: f32 = f32::EPSILON * 3.0;
182
183        let mut prng = GpuRng::new(0);
184        for _ in 0..100 {
185            let v2 = prng.gen_vec2(Vec2::splat(-100.0), Vec2::splat(100.0));
186            let v2_ortho = v2.orthonormal_vectors();
187            let v2_dot = v2.dot(v2_ortho);
188            if v2_dot.abs() >= THRESHOLD {
189                panic!("{v2} • {v2_ortho} < {THRESHOLD}, saw {v2_dot}")
190            }
191
192            let v3 = prng
193                .gen_vec3(Vec3::splat(-100.0), Vec3::splat(100.0))
194                .alt_norm_or_zero();
195            for v3_ortho in v3.orthonormal_vectors() {
196                let v3_dot = v3.dot(v3_ortho);
197                if v3_dot.abs() >= THRESHOLD {
198                    panic!("{v3} • {v3_ortho} < {THRESHOLD}, saw {v3_dot}");
199                }
200            }
201        }
202    }
203
204    #[test]
205    fn next_light_sanity() {
206        {
207            let lights_array = Array::new(0, 1);
208            // When there's only one light we only need one invocation to check
209            // that one light (per tile)
210            let mut next_light = NextLightIndex::new(UVec3::new(0, 0, 0), 16, lights_array);
211            assert_eq!(Some(0u32.into()), next_light.next());
212            assert_eq!(None, next_light.next());
213            // The next invocation won't check anything
214            let mut next_light = NextLightIndex::new(UVec3::new(1, 0, 0), 16, lights_array);
215            assert_eq!(None, next_light.next());
216            // Neither will the next row
217            let mut next_light = NextLightIndex::new(UVec3::new(0, 1, 0), 16, lights_array);
218            assert_eq!(None, next_light.next());
219        }
220        {
221            let lights_array = Array::new(0, 2);
222            // When there's two lights we need two invocations
223            let mut next_light = NextLightIndex::new(UVec3::new(0, 0, 0), 16, lights_array);
224            assert_eq!(Some(0u32.into()), next_light.next());
225            assert_eq!(None, next_light.next());
226            // The next invocation checks the second light
227            let mut next_light = NextLightIndex::new(UVec3::new(1, 0, 0), 16, lights_array);
228            assert_eq!(Some(1u32.into()), next_light.next());
229            assert_eq!(None, next_light.next());
230            // The next one doesn't check anything
231            let mut next_light = NextLightIndex::new(UVec3::new(2, 0, 0), 16, lights_array);
232            assert_eq!(None, next_light.next());
233        }
234        {
235            // With 256 lights (16*16), each fragment in the tile checks exactly
236            // one light
237            let lights_array = Array::new(0, 16 * 16);
238            let mut checked_lights = vec![];
239            for y in 0..16 {
240                for x in 0..16 {
241                    let mut next_light = NextLightIndex::new(UVec3::new(x, y, 0), 16, lights_array);
242                    let next_index = next_light.next_index();
243                    let checked_light = next_light.next().unwrap();
244                    assert_eq!(next_index, checked_light.index());
245                    checked_lights.push(checked_light);
246                    assert_eq!(None, next_light.next());
247                }
248            }
249            println!("checked_lights: {checked_lights:#?}");
250            assert_eq!(256, checked_lights.len());
251        }
252    }
253
254    #[test]
255    fn frag_coord_to_tile_index() {
256        let tiling_desc = LightTilingDescriptor {
257            depth_texture_size: UVec2::new(1024, 800),
258            ..Default::default()
259        };
260        for x in 0..16 {
261            let index = tiling_desc.tile_index_for_fragment(Vec2::new(x as f32, 0.0));
262            assert_eq!(0, index);
263        }
264        let index = tiling_desc.tile_index_for_fragment(Vec2::new(16.0, 0.0));
265        assert_eq!(1, index);
266        let index = tiling_desc.tile_index_for_fragment(Vec2::new(0.0, 16.0));
267        assert_eq!(1024 / 16, index);
268
269        let tiling_desc = LightTilingDescriptor {
270            depth_texture_size: UVec2::new(
271                (10.0 * 2.0f32.powi(8)) as u32,
272                (9.0 * 2.0f32.powi(8)) as u32,
273            ),
274            ..Default::default()
275        };
276        let frag_coord = Vec2::new(1917.0, 979.0);
277        let tile_coord = tiling_desc.tile_coord_for_fragment(frag_coord);
278        assert_eq!(UVec2::new(119, 61), tile_coord);
279    }
280}