1use crate::{frustum::validate_sphere, BoundingSphere, Frustum, RenderError};
12#[derive(Debug, Clone, Copy)]
14pub struct LodLevel {
15 pub index_count: u32,
17 pub first_index: u32,
19 pub base_vertex: u32,
21 pub geometric_error: f32,
24 pub resident: bool,
26}
27#[derive(Debug, Clone, Copy)]
29pub struct LodObject {
30 pub bounds: BoundingSphere,
32 pub max_scale: f32,
34}
35impl LodObject {
36 pub(crate) fn validate(self) -> Result<(), RenderError> {
37 validate_sphere(self.bounds)?;
38 if !self.max_scale.is_finite() || !(0.000001..=10000.0).contains(&self.max_scale) {
39 return Err(RenderError::InvalidLimits);
40 }
41 Ok(())
42 }
43}
44#[derive(Debug, Clone, Copy)]
46pub struct LodView {
47 pub eye: [f32; 3],
49 pub forward: [f32; 3],
51 pub viewport_half_diagonal: f32,
53 pub near_distance: f32,
55 pub focal_pixels: f32,
57 pub maximum_error_pixels: f32,
59}
60impl LodView {
61 pub fn validate(mut self) -> Result<Self, RenderError> {
63 if !self.eye.iter().all(|v| v.is_finite() && v.abs() <= 1e12)
64 || !self.near_distance.is_finite()
65 || !(0.0001..=1000000.0).contains(&self.near_distance)
66 || !self.focal_pixels.is_finite()
67 || !(0.5..=32768.0).contains(&self.focal_pixels)
68 || !self.maximum_error_pixels.is_finite()
69 || !(0.125..=1024.0).contains(&self.maximum_error_pixels)
70 {
71 return Err(RenderError::InvalidLimits);
72 }
73 if !self.forward.iter().all(|v| v.is_finite() && v.abs() <= 1e6)
74 || !self.viewport_half_diagonal.is_finite()
75 || !(0.5..=32768.0).contains(&self.viewport_half_diagonal)
76 {
77 return Err(RenderError::InvalidLimits);
78 }
79 let length = self
80 .forward
81 .iter()
82 .map(|v| f64::from(*v).powi(2))
83 .sum::<f64>()
84 .sqrt();
85 if length == 0.0 {
86 return Err(RenderError::InvalidLimits);
87 }
88 self.forward = self.forward.map(|v| (f64::from(v) / length) as f32);
89 Ok(self)
90 }
91}
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub struct LodChoice {
95 pub level: u32,
97 pub degraded: bool,
99}
100pub(crate) fn validate_levels(levels: &[LodLevel]) -> Result<(), RenderError> {
101 if levels.is_empty()
102 || levels.len() > 8
103 || levels[0].geometric_error != 0.0
104 || !levels.last().is_some_and(|l| l.resident)
105 {
106 return Err(RenderError::InvalidLimits);
107 }
108 let mut previous_error = 0.0;
109 let mut previous_end = 0;
110 for level in levels {
111 if level.index_count == 0
112 || level.index_count > 196608
113 || !level.index_count.is_multiple_of(3)
114 || level.first_index < previous_end
115 || level.base_vertex > 524288
116 || !level.geometric_error.is_finite()
117 || !(previous_error..=1000000.0).contains(&level.geometric_error)
118 {
119 return Err(RenderError::InvalidLimits);
120 }
121 previous_end = level
122 .first_index
123 .checked_add(level.index_count)
124 .filter(|end| *end <= 1572864)
125 .ok_or(RenderError::InvalidLimits)?;
126 previous_error = level.geometric_error;
127 }
128 Ok(())
129}
130pub fn choose_lod(
134 levels: &[LodLevel],
135 object: LodObject,
136 view: LodView,
137 frustum: Frustum,
138) -> Result<Option<LodChoice>, RenderError> {
139 validate_levels(levels)?;
140 object.validate()?;
141 let view = view.validate()?;
142 if !frustum.intersects(object.bounds)? {
143 return Ok(None);
144 }
145 let clipped = frustum.planes().iter().any(|plane| {
146 let distance = (0..3)
147 .map(|i| f64::from(plane[i]) * f64::from(object.bounds.center[i]))
148 .sum::<f64>()
149 + f64::from(plane[3]);
150 let magnitude = (0..3)
151 .map(|i| (f64::from(plane[i]) * f64::from(object.bounds.center[i])).abs())
152 .sum::<f64>()
153 + f64::from(plane[3]).abs()
154 + f64::from(object.bounds.radius)
155 + 1.0;
156 distance <= f64::from(object.bounds.radius) + magnitude * 0.00001
157 });
158 let delta = std::array::from_fn::<_, 3, _>(|i| {
159 f64::from(object.bounds.center[i]) - f64::from(view.eye[i])
160 });
161 let magnitude = (0..3)
162 .map(|i| (delta[i] * f64::from(view.forward[i])).abs())
163 .sum::<f64>()
164 + f64::from(object.bounds.radius)
165 + 1.0;
166 let depth = (0..3)
167 .map(|i| delta[i] * f64::from(view.forward[i]))
168 .sum::<f64>()
169 - f64::from(object.bounds.radius)
170 - magnitude * 0.00001;
171 let mut desired = 0;
172 if !clipped && depth > f64::from(view.near_distance) {
173 for (index, level) in levels.iter().enumerate().skip(1) {
174 let error = f64::from(level.geometric_error) * f64::from(object.max_scale);
175 if depth - error <= f64::from(view.near_distance) {
176 continue;
177 }
178 let projected = (f64::from(view.focal_pixels) + f64::from(view.viewport_half_diagonal))
179 * error
180 / (depth - error);
181 if projected * (1.0 + 0.00001) <= f64::from(view.maximum_error_pixels) {
182 desired = index;
183 }
184 }
185 }
186 let selected = (desired..levels.len())
187 .find(|i| levels[*i].resident)
188 .ok_or(RenderError::InvalidResource)?;
189 Ok(Some(LodChoice {
190 level: selected as u32,
191 degraded: selected != desired,
192 }))
193}