Skip to main content

primase_core/
environment.rs

1//! What a source says about its scene's environment (`core.environment`, a world resource).
2
3use serde::Serialize;
4use serde_json::Value;
5
6use crate::{Entity, JsonError, Light, LightKind, Resource, World};
7
8/// The scene's environment as the source authored it. `sun`: the entity whose directional `core.light` the source
9/// names as its sun (a scene setting naming one light, not the brightest light a writer picks); `None` when the source
10/// names none, or in a part of a world that does not hold the sun (a subset or a cell). `ambient`: the scene's ambient
11/// light ([`Ambient`]); `None` when the source states none or a lowering has not stated its meaning. Sky and fog are
12/// not here until their meaning and units are established for a source.
13#[derive(Clone, Debug, Default, PartialEq, Serialize)]
14pub struct Environment {
15    pub sun: Option<Entity>,
16    pub ambient: Option<Ambient>,
17}
18
19/// Ambient light: light every surface receives from no particular place, by the direction it faces alone, unshadowed.
20/// Its colours are linear RGB, each scaled by `intensity`: a colour of 1 at intensity 1 lights a surface as a
21/// directional `core.light` of intensity 1 (lux) shining straight onto it does. A source whose ambient is relative (to
22/// its sun, say) states it at that scale and reports the approximation; this is not a measured illuminance.
23#[derive(Clone, Debug, PartialEq, Serialize)]
24pub struct Ambient {
25    pub intensity: f32,
26    pub light: AmbientLight,
27}
28
29/// How [`Ambient`]'s light varies with the direction a surface faces.
30#[derive(Clone, Debug, PartialEq, Serialize)]
31pub enum AmbientLight {
32    /// The same colour whatever a surface faces.
33    Flat([f32; 3]),
34    /// A gradient over the direction a surface faces: `sky` facing up (+Y), `equator` facing sideways, `ground` facing
35    /// down.
36    Gradient {
37        sky: [f32; 3],
38        equator: [f32; 3],
39        ground: [f32; 3],
40    },
41}
42
43impl AmbientLight {
44    /// Its colours: the flat one, or the gradient's sky, equator and ground.
45    pub fn colours(&self) -> Vec<[f32; 3]> {
46        match self {
47            AmbientLight::Flat(c) => vec![*c],
48            AmbientLight::Gradient { sky, equator, ground } => vec![*sky, *equator, *ground],
49        }
50    }
51}
52
53impl Ambient {
54    /// Finite and not negative, its intensity and every colour.
55    fn verify(&self) -> Result<(), String> {
56        let ok = |x: f32| x.is_finite() && x >= 0.0;
57        if !ok(self.intensity) {
58            return Err(format!(
59                "ambient intensity {} is not finite and at least 0",
60                self.intensity
61            ));
62        }
63        match self
64            .light
65            .colours()
66            .into_iter()
67            .find(|c| !c.iter().all(|&x| ok(x)))
68        {
69            Some(c) => Err(format!("ambient colour {c:?} is not finite and at least 0")),
70            None => Ok(()),
71        }
72    }
73}
74
75/// The hour of the day the scene shows (`core.time_of_day`, a world resource), 0 to 24 (24 is midnight again): the hour a
76/// lowering chose for a scene whose source varies over the day (its environment stored at several hours, a program's
77/// material keyed by the hour), the program's hour where it chose one, else the source's stated default, counted where
78/// it is a default. A program of the source reads it as [`crate::shader::FrameParameter::TimeOfDay`]. A world no
79/// lowering chose an hour for has none, and a consumer gives such a program no hour.
80#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
81pub struct TimeOfDay {
82    /// Hours since midnight; finite, 0 to 24.
83    pub hour: f32,
84}
85
86impl Resource for TimeOfDay {
87    const NAME: &'static str = "core.time_of_day";
88    fn to_json(&self) -> Result<Option<Value>, JsonError> {
89        crate::json::to_value(self).map(Some)
90    }
91    /// A finite hour from 0 to 24.
92    fn verify(&self, _w: &World) -> Result<(), String> {
93        if !(self.hour.is_finite() && (0.0..=24.0).contains(&self.hour)) {
94            return Err(format!("hour {} is not a finite number from 0 to 24", self.hour));
95        }
96        Ok(())
97    }
98}
99
100impl Resource for Environment {
101    const NAME: &'static str = "core.environment";
102    /// 2: `ambient`.
103    const VERSION: u32 = 2;
104    fn to_json(&self) -> Result<Option<Value>, JsonError> {
105        crate::json::to_value(self).map(Some)
106    }
107    /// The sun is a directional light; the ambient is finite and not negative.
108    fn verify(&self, w: &World) -> Result<(), String> {
109        match self.sun {
110            Some(e) if !matches!(w.get::<Light>(e), Some(l) if l.kind == LightKind::Directional) => {
111                return Err(format!("sun {e:?} is not a directional core.light"));
112            }
113            _ => {}
114        }
115        self.ambient.as_ref().map_or(Ok(()), Ambient::verify)
116    }
117    fn references(&self, out: &mut Vec<Entity>) {
118        out.extend(self.sun);
119    }
120    fn retain_references(&mut self, keep: &dyn Fn(Entity) -> bool) {
121        if self.sun.is_some_and(|e| !keep(e)) {
122            self.sun = None;
123        }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::Transform;
131    use std::collections::BTreeSet;
132
133    /// The hour is held to the day: finite, 0 to 24, both ends included.
134    #[test]
135    fn the_time_of_day_is_an_hour_of_the_day() {
136        let w = World::new();
137        for good in [0.0, 12.5, 24.0] {
138            assert_eq!(TimeOfDay { hour: good }.verify(&w), Ok(()), "{good}");
139        }
140        for bad in [-0.5, 24.5, f32::NAN, f32::INFINITY] {
141            assert!(TimeOfDay { hour: bad }.verify(&w).is_err(), "{bad}");
142        }
143    }
144
145    /// A sun, a point light and a mesh-less placement-like entity; the environment names the sun.
146    fn world() -> (World, Entity, Entity) {
147        let mut w = World::new();
148        let sun = w.spawn();
149        w.insert(
150            sun,
151            Light {
152                kind: LightKind::Directional,
153                color: [1.0, 0.95, 0.9],
154                intensity: 3.0,
155                range: None,
156                cast_shadows: Some(true),
157                radius: None,
158                specular: None,
159            },
160        );
161        w.insert(sun, Transform::IDENTITY);
162        let lamp = w.spawn();
163        w.insert(
164            lamp,
165            Light {
166                kind: LightKind::Point,
167                color: [1.0; 3],
168                intensity: 10.0,
169                range: Some(5.0),
170                cast_shadows: None,
171                radius: None,
172                specular: None,
173            },
174        );
175        w.insert_resource(Environment {
176            sun: Some(sun),
177            ambient: None,
178        });
179        (w, sun, lamp)
180    }
181
182    fn sun_of(w: &World) -> Option<Entity> {
183        w.resource::<Environment>().unwrap().sun
184    }
185
186    #[test]
187    fn the_sun_survives_a_subset_that_keeps_it_and_is_forgotten_by_one_that_does_not() {
188        let (w, sun, lamp) = world();
189        assert!(w.verify_resources().is_empty());
190        let mut out = Vec::new();
191        Resource::references(w.resource::<Environment>().unwrap(), &mut out);
192        assert_eq!(out, [sun]);
193        let with = w.subset(&w.closure([sun]));
194        assert_eq!(sun_of(&with), Some(sun));
195        let without = w.subset(&BTreeSet::from([lamp]));
196        assert_eq!(sun_of(&without), None, "no reference to an entity the part lacks");
197        assert!(without.verify_resources().is_empty());
198        assert_eq!(sun_of(&w), Some(sun), "the source world keeps its sun");
199        // a split part as a subset; absorbing it back restores the world as it was
200        let mut split = w.clone();
201        let part = split.split_off(&BTreeSet::from([lamp]));
202        assert_eq!((sun_of(&part), sun_of(&split)), (None, Some(sun)));
203        let part = split.split_off(&BTreeSet::from([sun]));
204        assert_eq!(sun_of(&part), Some(sun));
205        split.absorb(part).unwrap();
206        assert!(split.verify_resources().is_empty());
207    }
208
209    #[test]
210    fn despawning_or_dropping_the_sun_forgets_it() {
211        let (mut w, sun, lamp) = world();
212        w.retain(&BTreeSet::from([sun, lamp]));
213        assert_eq!(sun_of(&w), Some(sun));
214        w.retain(&BTreeSet::from([lamp]));
215        assert_eq!(sun_of(&w), None);
216        let (mut w, sun, _) = world();
217        w.despawn(sun);
218        assert_eq!(sun_of(&w), None);
219        assert!(w.verify_resources().is_empty());
220    }
221
222    #[test]
223    fn the_sun_must_be_a_directional_light() {
224        let (mut w, _, lamp) = world();
225        w.insert_resource(Environment {
226            sun: Some(lamp),
227            ambient: None,
228        });
229        assert_eq!(
230            w.verify_resources(),
231            [(
232                "core.environment",
233                format!("sun {lamp:?} is not a directional core.light")
234            )]
235        );
236        assert_eq!(
237            Environment {
238                sun: Some(lamp),
239                ambient: None
240            }
241            .to_json()
242            .unwrap(),
243            Some(serde_json::json!({"sun": lamp.index(), "ambient": null}))
244        );
245    }
246
247    #[test]
248    fn the_ambient_is_kept_by_every_part_and_must_be_finite_and_not_negative() {
249        let (mut w, sun, lamp) = world();
250        let gradient = Ambient {
251            intensity: 1.0,
252            light: AmbientLight::Gradient {
253                sky: [0.3, 0.3, 0.4],
254                equator: [0.5; 3],
255                ground: [0.25; 3],
256            },
257        };
258        w.insert_resource(Environment {
259            sun: Some(sun),
260            ambient: Some(gradient.clone()),
261        });
262        assert!(w.verify_resources().is_empty());
263        // a scene-wide fact: a part without the sun keeps it
264        let part = w.subset(&BTreeSet::from([lamp]));
265        assert_eq!(
266            part.resource::<Environment>().unwrap().ambient,
267            Some(gradient.clone())
268        );
269        assert_eq!(gradient.light.colours(), [[0.3, 0.3, 0.4], [0.5; 3], [0.25; 3]]);
270        assert_eq!(AmbientLight::Flat([0.2; 3]).colours(), [[0.2; 3]]);
271        assert_eq!(
272            w.resource::<Environment>().unwrap().to_json().unwrap(),
273            Some(
274                serde_json::json!({"sun": sun.index(), "ambient": {"intensity": 1.0, "light": {"Gradient": {
275                "sky": [0.30000001192092896, 0.30000001192092896, 0.4000000059604645],
276                "equator": [0.5, 0.5, 0.5], "ground": [0.25, 0.25, 0.25]}}}})
277            )
278        );
279        for (intensity, colour, bad) in [
280            (
281                -1.0,
282                [0.5; 3],
283                "ambient intensity -1 is not finite and at least 0",
284            ),
285            (
286                f32::NAN,
287                [0.5; 3],
288                "ambient intensity NaN is not finite and at least 0",
289            ),
290            (
291                1.0,
292                [0.5, -0.1, 0.5],
293                "ambient colour [0.5, -0.1, 0.5] is not finite and at least 0",
294            ),
295            (
296                1.0,
297                [f32::INFINITY; 3],
298                "ambient colour [inf, inf, inf] is not finite and at least 0",
299            ),
300        ] {
301            w.insert_resource(Environment {
302                sun: None,
303                ambient: Some(Ambient {
304                    intensity,
305                    light: AmbientLight::Flat(colour),
306                }),
307            });
308            assert_eq!(w.verify_resources(), [("core.environment", bad.to_owned())]);
309        }
310    }
311}