Skip to main content

primase_core/
sky_anchor.rs

1//! Where a placement of the sky stands relative to the view (`core.sky_anchor`, on placement entities).
2
3use serde::Serialize;
4use serde_json::Value;
5
6use crate::{Component, Entity, JsonError, Light, LightKind, Transform, World};
7
8/// `core.sky_anchor`: a placement of the sky (its materials in [`crate::DrawLayer::Sky`]) that the source moves with
9/// the view every frame, and how. Its `core.transform` is where it stands for a view at the world's origin; a consumer
10/// computes, every frame and for every view (each eye of a stereo view), the transform it draws it with
11/// ([`SkyAnchor::placed`]). A consumer that does not move it draws it where its transform puts it, which is right for a
12/// view at the origin only.
13#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub enum SkyAnchor {
16    /// It follows the view across the ground and keeps its height and its rotation: its transform with the view's x
17    /// and z added to the translation (a sky dome, the far mountains around it).
18    Horizontal,
19    /// It stands `distance` from the view in the direction towards the directional light `light`, facing the view
20    /// (a moon or a sun's disc): `Transform::facing(view + distance * towards, -towards)`, so that its local +Z points
21    /// towards the light and its local +Y is the world's up made perpendicular to that, where `towards` is the light's
22    /// local +Z (the opposite of the direction it shines). Its own transform is not used.
23    TowardsLight { light: Entity, distance: f32 },
24}
25
26impl SkyAnchor {
27    /// The transform a placement with this anchor and its own `transform` is drawn with for a view at `view` (a
28    /// world position), `light` the transform of the anchor's light ([`SkyAnchor::TowardsLight`]; unused for
29    /// [`SkyAnchor::Horizontal`]). `None` when the light's transform is missing or its +Z is zero or not finite.
30    pub fn placed(
31        &self,
32        transform: &Transform,
33        view: [f32; 3],
34        light: Option<&Transform>,
35    ) -> Option<Transform> {
36        match *self {
37            SkyAnchor::Horizontal => {
38                let mut t = *transform;
39                t.cols[3][0] += view[0];
40                t.cols[3][2] += view[2];
41                Some(t)
42            }
43            SkyAnchor::TowardsLight { distance, .. } => {
44                let z = light?.cols[2];
45                let n = z.iter().map(|x| x * x).sum::<f32>().sqrt();
46                if !(n > 0.0 && n.is_finite()) {
47                    return None;
48                }
49                let towards = z.map(|x| x / n);
50                let position = std::array::from_fn(|i| view[i] + distance * towards[i]);
51                Transform::facing(position, towards.map(|x| -x))
52            }
53        }
54    }
55}
56
57impl Component for SkyAnchor {
58    const NAME: &'static str = "core.sky_anchor";
59    const GEOMETRY: crate::Geometry = crate::Geometry::None;
60    /// No local-space geometry: where the placement stands, in the world.
61    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
62        crate::world::Linearity::Invariant
63    }
64    fn to_json(&self) -> Result<Option<Value>, JsonError> {
65        crate::json::to_value(self).map(Some)
66    }
67    fn references(&self, out: &mut Vec<Entity>) {
68        if let SkyAnchor::TowardsLight { light, .. } = self {
69            out.push(*light);
70        }
71    }
72    /// A placement (it has a `core.transform`); a light anchor's light is a directional `core.light` with a
73    /// transform, and its distance finite and above 0.
74    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
75        if !w.has::<Transform>(e) {
76            return Err("a sky anchor on an entity without a core.transform".into());
77        }
78        if let SkyAnchor::TowardsLight { light, distance } = *self {
79            if !(distance.is_finite() && distance > 0.0) {
80                return Err(format!(
81                    "sky anchor distance {distance} is not finite and above 0"
82                ));
83            }
84            if !matches!(w.get::<Light>(light), Some(l) if l.kind == LightKind::Directional) {
85                return Err(format!(
86                    "sky anchor light {light:?} is not a directional core.light"
87                ));
88            }
89            if !w.has::<Transform>(light) {
90                return Err(format!("sky anchor light {light:?} has no core.transform"));
91            }
92        }
93        Ok(())
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    fn close(a: Transform, b: [[f32; 3]; 4]) -> bool {
102        a.cols
103            .iter()
104            .flatten()
105            .zip(b.iter().flatten())
106            .all(|(x, y)| (x - y).abs() < 1e-4)
107    }
108
109    /// A Sekiro frame (a recorded frame of Ashina castle at 18:00, the D3D11 recorder's constant buffers): the camera
110    /// at (-203.3207, -26.2268, 178.8822); the sky dome's object matrix the identity rotation at (-203.3207, 0,
111    /// 178.8822); the moon's at (-203.3206, -15.6614, 156.2245) with rows (-1, 0, 0), (0, 0.9063, 0.4226),
112    /// (0, 0.4226, -0.9063), its light set's direction towards the light (0, 0.4226, -0.9063), 25 units away. The
113    /// values are the game's own, in the game's frame.
114    #[test]
115    fn the_sky_of_a_recorded_frame_stands_where_the_game_drew_it() {
116        let view = [-203.3207, -26.2268, 178.8822];
117        let dome = SkyAnchor::Horizontal
118            .placed(&Transform::IDENTITY, view, None)
119            .unwrap();
120        assert!(close(
121            dome,
122            [
123                [1., 0., 0.],
124                [0., 1., 0.],
125                [0., 0., 1.],
126                [-203.3207, 0., 178.8822]
127            ]
128        ));
129        // a light shining along -towards: its +Z is towards
130        let towards = [0.0, 0.422618, -0.9063079];
131        let light = Transform::facing([0.; 3], towards.map(|x| -x)).unwrap();
132        let mut w = World::new();
133        let moon = SkyAnchor::TowardsLight {
134            light: w.spawn(),
135            distance: 25.0,
136        }
137        .placed(&Transform::IDENTITY, view, Some(&light))
138        .unwrap();
139        // columns: the images of local x, y, z (the game's rows, transposed), then the translation
140        assert!(close(
141            moon,
142            [
143                [-1., 0., 0.],
144                [0., 0.9063079, 0.422618],
145                [0., 0.422618, -0.9063079],
146                [-203.3207, -15.66135, 156.2245]
147            ]
148        ));
149    }
150
151    #[test]
152    fn a_light_anchor_needs_a_directional_light_and_a_distance() {
153        let mut w = World::new();
154        let sun = w.spawn();
155        w.insert(sun, Transform::IDENTITY);
156        w.insert(
157            sun,
158            Light {
159                kind: LightKind::Directional,
160                color: [1.; 3],
161                intensity: 1.,
162                range: None,
163                cast_shadows: None,
164                radius: None,
165                specular: None,
166            },
167        );
168        let moon = w.spawn();
169        w.insert(moon, Transform::IDENTITY);
170        let ok = SkyAnchor::TowardsLight {
171            light: sun,
172            distance: 25.,
173        };
174        assert_eq!(ok.verify(moon, &w), Ok(()));
175        let far = SkyAnchor::TowardsLight {
176            light: sun,
177            distance: 0.,
178        };
179        assert!(far.verify(moon, &w).unwrap_err().contains("distance"));
180        let lamp = SkyAnchor::TowardsLight {
181            light: moon,
182            distance: 25.,
183        };
184        assert!(lamp.verify(moon, &w).unwrap_err().contains("not a directional"));
185        let loose = w.spawn();
186        assert!(
187            SkyAnchor::Horizontal
188                .verify(loose, &w)
189                .unwrap_err()
190                .contains("core.transform")
191        );
192        let mut refs = Vec::new();
193        ok.references(&mut refs);
194        assert_eq!(refs, vec![sun]);
195        assert_eq!(
196            ok.to_json().unwrap(),
197            Some(serde_json::json!({"towards_light": {"light": sun, "distance": 25.0}}))
198        );
199        assert_eq!(
200            SkyAnchor::Horizontal.to_json().unwrap(),
201            Some(serde_json::json!("horizontal"))
202        );
203    }
204}