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}