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