1use serde::Serialize;
10use serde_json::Value;
11
12use crate::{Component, Entity, Instance, JsonError, Resource, World};
13
14#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
17pub struct LodBoundary {
18 pub border: f32,
19 pub play: f32,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
24#[serde(rename_all = "snake_case")]
25pub enum LodBand {
26 CrossFade,
29 Hysteresis,
32}
33
34#[derive(Clone, Debug, PartialEq, Serialize)]
45pub struct LodSwitch {
46 pub boundaries: Vec<LodBoundary>,
47 pub scale: f32,
49 pub band: LodBand,
50}
51
52impl LodSwitch {
53 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 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 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 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
171pub struct LodSwitchView {
172 pub reference_fov_y: f32,
174}
175
176impl LodSwitchView {
177 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 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 #[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 #[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 assert_eq!([21.9, 22.0, 60.0].map(|d| s.next(0, d)), [0, 1, 2]);
243 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 #[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}