1use serde::{Deserialize, Serialize};
6
7use super::{LightTableKind, ShadowedLight};
8use crate::{Entity, Light, Transform, World};
9
10#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
16#[serde(deny_unknown_fields)]
17pub struct LocalShadowChoice {
18 pub enabled: bool,
19 #[serde(deserialize_with = "Option::deserialize")]
20 pub max_distance: Option<f32>,
21 #[serde(deserialize_with = "Option::deserialize")]
22 pub max_lights: Option<usize>,
23 pub order: ShadowOrder,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum ShadowOrder {
31 NearestEye,
34}
35
36#[derive(Clone, Copy, Debug, PartialEq)]
39pub struct ShadowView {
40 pub view_projection: [[f32; 4]; 4],
41 pub eye: [f32; 3],
42}
43
44#[derive(Clone, Debug, Default, PartialEq)]
47pub struct ChosenShadows {
48 pub lights: Vec<ShadowedLight>,
49 pub casters: usize,
51 pub in_view: usize,
53}
54
55fn casters(world: &World, kind: LightTableKind) -> impl Iterator<Item = (Entity, [f32; 3], f32)> + '_ {
57 world.iter::<Light>().filter_map(move |(e, l)| {
58 let range = l.range.filter(|r| *r > 0.0 && r.is_finite())?;
59 let t = world.get::<Transform>(e)?;
60 (kind.holds(l) && l.cast_shadows == Some(true)).then_some((e, t.cols[3], range))
61 })
62}
63
64impl LocalShadowChoice {
65 pub fn most(&self, world: &World, kind: LightTableKind, capacity: usize) -> usize {
68 if !self.enabled {
69 return 0;
70 }
71 capacity
72 .min(casters(world, kind).count())
73 .min(self.max_lights.unwrap_or(usize::MAX))
74 }
75
76 pub fn describe(&self, capacity: usize) -> String {
78 if !self.enabled {
79 return "local shadows: off; every shadow-casting light is lit without one".into();
80 }
81 format!(
82 "local shadows: lights with cast_shadows and a range, their range sphere in the view's frustum{}, {}, up to {}",
83 self.max_distance
84 .map(|d| format!(" and within {d} m of the eye"))
85 .unwrap_or_default(),
86 match self.order {
87 ShadowOrder::NearestEye => "nearest the eye first",
88 },
89 match self.max_lights {
90 Some(n) if n < capacity => format!("{n} (the atlas holds {capacity})"),
91 _ => format!("the atlas's capacity, {capacity}"),
92 }
93 )
94 }
95
96 pub fn choose(
98 &self,
99 world: &World,
100 kind: LightTableKind,
101 view: &ShadowView,
102 capacity: usize,
103 ) -> ChosenShadows {
104 let mut chosen = ChosenShadows::default();
105 if !self.enabled {
106 return chosen;
107 }
108 let planes = frustum(&view.view_projection);
109 let mut found = Vec::new();
110 for (light, centre, range) in casters(world, kind) {
111 chosen.casters += 1;
112 let to_centre = distance(centre, view.eye);
113 let gap = (to_centre - range).max(0.0);
114 if !meets(&planes, centre, range) || self.max_distance.is_some_and(|d| gap > d) {
115 continue;
116 }
117 chosen.in_view += 1;
118 found.push((gap, to_centre, light, centre, range));
119 }
120 match self.order {
121 ShadowOrder::NearestEye => {
122 found.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.total_cmp(&b.1)).then(a.2.cmp(&b.2)));
123 }
124 }
125 let limit = self.max_lights.map_or(capacity, |n| n.min(capacity));
126 chosen.lights = found
127 .into_iter()
128 .take(limit)
129 .map(|(_, _, light, centre, range)| ShadowedLight { light, centre, range })
130 .collect();
131 chosen
132 }
133}
134
135fn distance(a: [f32; 3], b: [f32; 3]) -> f32 {
136 ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)).sqrt()
137}
138
139fn frustum(m: &[[f32; 4]; 4]) -> [[f32; 4]; 6] {
142 let row = |r: usize| m[r];
143 let add = |a: [f32; 4], b: [f32; 4], s: f32| std::array::from_fn::<f32, 4, _>(|k| a[k] + s * b[k]);
144 let (x, y, z, w) = (row(0), row(1), row(2), row(3));
145 [
146 add(w, x, 1.0),
147 add(w, x, -1.0),
148 add(w, y, 1.0),
149 add(w, y, -1.0),
150 z,
151 add(w, z, -1.0),
152 ]
153 .map(|p| {
154 let length = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
155 if length > 0.0 { p.map(|v| v / length) } else { p }
156 })
157}
158
159fn meets(planes: &[[f32; 4]; 6], centre: [f32; 3], radius: f32) -> bool {
161 planes
162 .iter()
163 .all(|p| p[0] * centre[0] + p[1] * centre[1] + p[2] * centre[2] + p[3] >= -radius)
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::LightKind;
170
171 const ALL: LocalShadowChoice = LocalShadowChoice {
173 enabled: true,
174 max_distance: None,
175 max_lights: None,
176 order: ShadowOrder::NearestEye,
177 };
178
179 #[test]
181 fn every_option_is_required() -> Result<(), serde_json::Error> {
182 let all: LocalShadowChoice = serde_json::from_str(
183 r#"{"enabled":true,"max_distance":null,"max_lights":null,"order":"nearest_eye"}"#,
184 )?;
185 assert_eq!(all, ALL);
186 let limited: LocalShadowChoice = serde_json::from_str(
187 r#"{"enabled":true,"max_distance":20.0,"max_lights":4,"order":"nearest_eye"}"#,
188 )?;
189 assert_eq!((limited.max_distance, limited.max_lights), (Some(20.0), Some(4)));
190 for partial in [
191 r#"{"max_distance":null,"max_lights":null,"order":"nearest_eye"}"#,
192 r#"{"enabled":true,"max_lights":null,"order":"nearest_eye"}"#,
193 r#"{"enabled":true,"max_distance":null,"order":"nearest_eye"}"#,
194 r#"{"enabled":true,"max_distance":null,"max_lights":null}"#,
195 r#"{"enabled":true,"max_distance":null,"max_lights":null,"order":"brightest"}"#,
196 r#"{}"#,
197 ] {
198 assert!(
199 serde_json::from_str::<LocalShadowChoice>(partial).is_err(),
200 "{partial}"
201 );
202 }
203 Ok(())
204 }
205
206 fn world(lights: &[(f32, Option<bool>)]) -> (World, Vec<Entity>) {
208 let mut w = World::new();
209 let mut out = Vec::new();
210 for &(z, casts) in lights {
211 let e = w.spawn();
212 let mut t = Transform::IDENTITY;
213 t.cols[3] = [0.0, 0.0, -z];
214 w.insert(e, t);
215 w.insert(
216 e,
217 Light {
218 kind: LightKind::Point,
219 color: [1.0; 3],
220 intensity: 1.0,
221 range: Some(2.0),
222 cast_shadows: casts,
223 radius: None,
224 specular: None,
225 },
226 );
227 out.push(e);
228 }
229 (w, out)
230 }
231
232 fn view() -> ShadowView {
234 let (n, f) = (0.1f32, 100.0f32);
235 ShadowView {
236 view_projection: [
237 [1.0, 0.0, 0.0, 0.0],
238 [0.0, 1.0, 0.0, 0.0],
239 [0.0, 0.0, f / (n - f), n * f / (n - f)],
240 [0.0, 0.0, -1.0, 0.0],
241 ],
242 eye: [0.0; 3],
243 }
244 }
245
246 #[test]
248 fn the_choice_takes_the_nearest_shadow_casting_lights_in_view() {
249 let (w, e) = world(&[
251 (30.0, Some(true)),
252 (-10.0, Some(true)),
253 (5.0, Some(false)),
254 (6.0, None),
255 (10.0, Some(true)),
256 (20.0, Some(true)),
257 ]);
258 let choice = ALL;
259 let chosen = choice.choose(&w, LightTableKind::Point, &view(), 8);
260 assert_eq!((chosen.casters, chosen.in_view), (4, 3));
261 assert_eq!(
262 chosen.lights.iter().map(|l| l.light).collect::<Vec<_>>(),
263 [e[4], e[5], e[0]]
264 );
265 assert_eq!(chosen.lights[0].range, 2.0);
266 assert_eq!(chosen.lights[0].centre, [0.0, 0.0, -10.0]);
267 assert_eq!(
268 choice.choose(&w, LightTableKind::Point, &view(), 2).lights.len(),
269 2
270 );
271 let near = LocalShadowChoice {
272 max_distance: Some(20.0),
273 ..choice
274 };
275 assert_eq!(near.choose(&w, LightTableKind::Point, &view(), 8).lights.len(), 2);
276 let one = LocalShadowChoice {
277 max_lights: Some(1),
278 ..choice
279 };
280 assert_eq!(one.choose(&w, LightTableKind::Point, &view(), 8).lights.len(), 1);
281 let off = LocalShadowChoice {
282 enabled: false,
283 ..choice
284 };
285 assert!(
286 off.choose(&w, LightTableKind::Point, &view(), 8)
287 .lights
288 .is_empty()
289 );
290 assert!(
291 choice
292 .choose(&w, LightTableKind::Spot, &view(), 8)
293 .lights
294 .is_empty()
295 );
296 }
297
298 #[test]
300 fn a_light_whose_range_reaches_the_view_is_in_view() {
301 let (mut w, e) = world(&[(1.0, Some(true))]);
302 let mut t = Transform::IDENTITY;
305 t.cols[3] = [2.5, 0.0, -1.0];
306 w.insert(e[0], t);
307 assert_eq!(ALL.choose(&w, LightTableKind::Point, &view(), 8).in_view, 1);
308 t.cols[3] = [5.0, 0.0, -1.0];
309 w.insert(e[0], t);
310 assert_eq!(ALL.choose(&w, LightTableKind::Point, &view(), 8).in_view, 0);
311 }
312
313 #[test]
316 fn the_most_a_choice_shadows_is_bounded_by_the_atlas_the_casters_and_the_limit() {
317 let (w, _) = world(&[(10.0, Some(true)), (20.0, Some(true)), (30.0, Some(false))]);
318 let choice = ALL;
319 assert_eq!(choice.most(&w, LightTableKind::Point, 170), 2);
320 assert_eq!(choice.most(&w, LightTableKind::Point, 1), 1);
321 assert_eq!(
322 LocalShadowChoice {
323 max_lights: Some(1),
324 ..choice
325 }
326 .most(&w, LightTableKind::Point, 170),
327 1
328 );
329 assert_eq!(
330 LocalShadowChoice {
331 enabled: false,
332 ..choice
333 }
334 .most(&w, LightTableKind::Point, 170),
335 0
336 );
337 }
338}