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