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}