Skip to main content

primase_core/
lod_switch.rs

1//! Which level of detail a placement draws, by distance ([`LodSwitch`], `core.lod_switch`), and the view its distances
2//! are stated for ([`LodSwitchView`], `core.lod_switch_view`).
3//!
4//! The levels are the prototype's [`crate::Lods`] (level `k` the form at detail `k`). A source that chooses among them
5//! per placement while it draws states its rule here, and a renderer applies it every frame from the placement's
6//! [`crate::box_distance`] scaled by [`LodSwitchView::factor`]. A writer, which cannot switch per frame, keeps one form
7//! with `select-lod`.
8
9use serde::Serialize;
10use serde_json::Value;
11
12use crate::{Component, Entity, Instance, JsonError, Resource, World};
13
14/// One boundary between two neighbouring levels: level `k + 1` begins at `border` (times [`LodSwitch::scale`]), and
15/// `play` is a band past it ([`LodBand`]), not scaled.
16#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
17pub struct LodBoundary {
18    pub border: f32,
19    pub play: f32,
20}
21
22/// What a boundary's `play` is.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
24#[serde(rename_all = "snake_case")]
25pub enum LodBand {
26    /// The level follows the distance alone: level `k + 1` from `border * scale + play` on, the finer level fading into
27    /// it from `border * scale` ([`LodSwitch::fade`]).
28    CrossFade,
29    /// The level moves with hysteresis from the one drawn before ([`LodSwitch::next`]): the boundary above it is crossed
30    /// at `border * scale + play`, the one below it at `border * scale - play`, any other at `border * scale`.
31    Hysteresis,
32}
33
34/// When a placement draws each level of its prototype's levels of detail (`core.lod_switch`), on the placement (an
35/// entity with `core.instance`). Distances are in the world's units, from the camera to the placement's world box,
36/// scaled by [`LodSwitchView::factor`]. `boundaries[k]` is the boundary between levels `k` and `k + 1`, in order (a
37/// border that is never reached, such as a source's 9999 m, is stated as it is).
38///
39/// Elden Ring (eldenring.exe 1.17.3, read from the executable's code, not checked against frames of the running game):
40/// the game evaluates the hysteresis rule from the level remembered between frames, and it remembers only that level's
41/// low two bits, so a placement at level 4 or 5 is next evaluated from level 0 or 1; and from level 4 it compares the
42/// first border unscaled. Both are the game's quirks, observed by decompilation, and [`LodSwitch::next`] does not
43/// reproduce them: it states the rule the game's code applies at levels 0 to 3.
44#[derive(Clone, Debug, PartialEq, Serialize)]
45pub struct LodSwitch {
46    pub boundaries: Vec<LodBoundary>,
47    /// A factor on every border, positive (a source's per-kind scale of its draw distances).
48    pub scale: f32,
49    pub band: LodBand,
50}
51
52impl LodSwitch {
53    /// The level at scaled distance `distance` with no level drawn before: the largest `k` whose boundary below it is
54    /// passed, at `border * scale + play` with [`LodBand::CrossFade`] and at `border * scale` with
55    /// [`LodBand::Hysteresis`] (the plain boundaries: a view with no history takes no side of a band).
56    pub fn level_at(&self, distance: f32) -> u8 {
57        let passed = |b: &LodBoundary| match self.band {
58            LodBand::CrossFade => distance >= b.border * self.scale + b.play,
59            LodBand::Hysteresis => distance >= b.border * self.scale,
60        };
61        level(self.boundaries.iter().take_while(|b| passed(b)).count())
62    }
63
64    /// The level after one evaluation at `distance` from level `current` drawn before: with [`LodBand::Hysteresis`] the
65    /// boundary just above `current` is passed at `border * scale + play`, the one just below at `border * scale -
66    /// play`, every other at `border * scale`; with [`LodBand::CrossFade`], [`LodSwitch::level_at`].
67    pub fn next(&self, current: u8, distance: f32) -> u8 {
68        if self.band == LodBand::CrossFade {
69            return self.level_at(distance);
70        }
71        let current = usize::from(current);
72        let passed = |(k, b): (usize, &LodBoundary)| {
73            let border = b.border * self.scale;
74            let threshold = if k == current {
75                border + b.play
76            } else if k + 1 == current {
77                border - b.play
78            } else {
79                border
80            };
81            distance >= threshold
82        };
83        level(
84            self.boundaries
85                .iter()
86                .enumerate()
87                .take_while(|&kb| passed(kb))
88                .count(),
89        )
90    }
91
92    /// With [`LodBand::Hysteresis`], whether the level at `distance` depends on the level drawn before: `distance` lies
93    /// within `play` of a scaled border, where [`LodSwitch::next`] can give either side. Never with
94    /// [`LodBand::CrossFade`].
95    pub fn in_band(&self, distance: f32) -> bool {
96        self.band == LodBand::Hysteresis
97            && self.boundaries.iter().any(|b| {
98                let border = b.border * self.scale;
99                b.play > 0.0 && distance >= border - b.play && distance < border + b.play
100            })
101    }
102
103    /// With [`LodBand::CrossFade`], how far the level drawn at `distance` has faded into the next: `(distance - border *
104    /// scale) / play` within the band past its boundary above, 0 elsewhere.
105    pub fn fade(&self, distance: f32) -> f32 {
106        if self.band != LodBand::CrossFade {
107            return 0.0;
108        }
109        let at = usize::from(self.level_at(distance));
110        match self.boundaries.get(at) {
111            Some(b) if b.play > 0.0 && distance >= b.border * self.scale => {
112                ((distance - b.border * self.scale) / b.play).clamp(0.0, 1.0)
113            }
114            _ => 0.0,
115        }
116    }
117}
118
119fn level(count: usize) -> u8 {
120    u8::try_from(count).unwrap_or(u8::MAX)
121}
122
123impl Component for LodSwitch {
124    const NAME: &'static str = "core.lod_switch";
125    const GEOMETRY: crate::Geometry = crate::Geometry::None;
126    /// World distances of the placement, not local-space geometry.
127    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
128        crate::world::Linearity::Invariant
129    }
130    fn to_json(&self) -> Result<Option<Value>, JsonError> {
131        crate::json::to_value(self).map(Some)
132    }
133    /// On a placement; at least one boundary, at most 254; `scale` finite and positive; every border finite and not
134    /// negative, not below the one before it; every play finite and not negative.
135    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
136        if !w.has::<Instance>(e) {
137            return Err(
138                "a level-of-detail switch on an entity that is not a placement (no core.instance)".into(),
139            );
140        }
141        if self.boundaries.is_empty() || self.boundaries.len() > 254 {
142            return Err(format!("{} boundaries, not 1 to 254", self.boundaries.len()));
143        }
144        if !(self.scale.is_finite() && self.scale > 0.0) {
145            return Err(format!("scale {} is not a finite positive number", self.scale));
146        }
147        let mut before = 0.0f32;
148        for (k, b) in self.boundaries.iter().enumerate() {
149            if !(b.border.is_finite() && b.border >= before) {
150                return Err(format!(
151                    "boundary {k}'s border {} is not finite and at least {before}",
152                    b.border
153                ));
154            }
155            if !(b.play.is_finite() && b.play >= 0.0) {
156                return Err(format!(
157                    "boundary {k}'s play {} is not a finite number of 0 or more",
158                    b.play
159                ));
160            }
161            before = b.border;
162        }
163        Ok(())
164    }
165}
166
167/// The view the distances of every [`LodSwitch`] are stated for (`core.lod_switch_view`, a world resource): a camera
168/// of vertical field of view `reference_fov_y` measures them as they are, a narrower one as nearer. A world with level
169/// switches has it.
170#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
171pub struct LodSwitchView {
172    /// Radians, more than 0 and less than pi.
173    pub reference_fov_y: f32,
174}
175
176impl LodSwitchView {
177    /// What a camera of vertical field of view `fov_y` (radians) multiplies its distances by before comparing them to
178    /// the borders: `tan(fov_y / 2) / tan(reference_fov_y / 2)`.
179    pub fn factor(&self, fov_y: f32) -> f32 {
180        (fov_y / 2.0).tan() / (self.reference_fov_y / 2.0).tan()
181    }
182}
183
184impl Resource for LodSwitchView {
185    const NAME: &'static str = "core.lod_switch_view";
186    fn to_json(&self) -> Result<Option<Value>, JsonError> {
187        crate::json::to_value(self).map(Some)
188    }
189    /// A field of view strictly between 0 and pi.
190    fn verify(&self, _w: &World) -> Result<(), String> {
191        let f = self.reference_fov_y;
192        if !(f.is_finite() && f > 0.0 && f < std::f32::consts::PI) {
193            return Err(format!(
194                "reference vertical field of view {f} is not between 0 and pi radians"
195            ));
196        }
197        Ok(())
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    fn switch(band: LodBand) -> LodSwitch {
206        LodSwitch {
207            boundaries: vec![
208                LodBoundary {
209                    border: 10.0,
210                    play: 2.0,
211                },
212                LodBoundary {
213                    border: 30.0,
214                    play: 4.0,
215                },
216                LodBoundary {
217                    border: 9999.0,
218                    play: 0.0,
219                },
220            ],
221            scale: 2.0,
222            band,
223        }
224    }
225
226    /// With no history the borders are plain: 20 m and 60 m at scale 2.
227    #[test]
228    fn with_no_history_the_plain_borders_choose() {
229        let s = switch(LodBand::Hysteresis);
230        assert_eq!(
231            [19.9, 20.0, 59.9, 60.0, 1e6].map(|d| s.level_at(d)),
232            [0, 1, 1, 2, 3]
233        );
234    }
235
236    /// From level 1 the boundary above is passed at 60 + 4 and the one below at 20 - 2; others at their border.
237    #[test]
238    fn the_boundaries_next_to_the_level_drawn_take_the_play() {
239        let s = switch(LodBand::Hysteresis);
240        assert_eq!([17.9, 18.0, 63.9, 64.0].map(|d| s.next(1, d)), [0, 1, 1, 2]);
241        // from 0: up at 20 + 2; the next boundary plain
242        assert_eq!([21.9, 22.0, 60.0].map(|d| s.next(0, d)), [0, 1, 2]);
243        // from 2: down below 60 - 4, and the first boundary plain
244        assert_eq!([56.0, 55.9, 19.9].map(|d| s.next(2, d)), [2, 1, 0]);
245        assert!(s.in_band(19.0) && s.in_band(63.0) && !s.in_band(30.0) && !s.in_band(64.0));
246    }
247
248    /// With cross-fade the level is the distance's alone, from border * scale + play, and the band fades.
249    #[test]
250    fn a_cross_fade_follows_the_distance_and_fades_across_the_band() {
251        let s = switch(LodBand::CrossFade);
252        assert_eq!([21.9, 22.0, 63.9, 64.0].map(|d| s.level_at(d)), [0, 1, 1, 2]);
253        assert_eq!(s.next(2, 21.0), 0);
254        assert_eq!([19.0, 21.0, 62.0].map(|d| s.fade(d)), [0.0, 0.5, 0.5]);
255        assert!(!s.in_band(21.0));
256    }
257
258    #[test]
259    fn verify_refuses_what_it_cannot_apply() {
260        let mut w = World::new();
261        let placed = w.spawn();
262        let prototype = w.spawn();
263        w.insert(
264            placed,
265            Instance {
266                prototype,
267                variant: 0,
268            },
269        );
270        assert_eq!(switch(LodBand::Hysteresis).verify(placed, &w), Ok(()));
271        assert!(switch(LodBand::Hysteresis).verify(prototype, &w).is_err());
272        for bad in [
273            LodSwitch {
274                boundaries: vec![],
275                ..switch(LodBand::Hysteresis)
276            },
277            LodSwitch {
278                scale: 0.0,
279                ..switch(LodBand::Hysteresis)
280            },
281            LodSwitch {
282                boundaries: vec![
283                    LodBoundary {
284                        border: 30.0,
285                        play: 0.0,
286                    },
287                    LodBoundary {
288                        border: 10.0,
289                        play: 0.0,
290                    },
291                ],
292                ..switch(LodBand::Hysteresis)
293            },
294            LodSwitch {
295                boundaries: vec![LodBoundary {
296                    border: 10.0,
297                    play: -1.0,
298                }],
299                ..switch(LodBand::Hysteresis)
300            },
301        ] {
302            assert!(bad.verify(placed, &w).is_err(), "{bad:?}");
303        }
304        let view = LodSwitchView {
305            reference_fov_y: 43f32.to_radians(),
306        };
307        assert!((view.factor(43f32.to_radians()) - 1.0).abs() < 1e-6);
308        assert!(LodSwitchView { reference_fov_y: 0.0 }.verify(&w).is_err());
309    }
310}