1use crate::math::Vec3Fix;
16use crate::sdf_collider::SdfCollider;
17
18#[cfg(not(feature = "std"))]
19use alloc::vec::Vec;
20
21#[derive(Clone, Copy, Debug, PartialEq)]
27pub struct AdaptiveConfig {
28 pub high_res_threshold: f32,
30 pub cache_threshold: f32,
32 pub cache_max_age: u32,
34 pub refinement_samples: usize,
38}
39
40impl Default for AdaptiveConfig {
41 fn default() -> Self {
42 Self {
43 high_res_threshold: 2.0,
44 cache_threshold: 10.0,
45 cache_max_age: 8,
46 refinement_samples: 4,
47 }
48 }
49}
50
51#[derive(Clone, Copy, Debug, PartialEq)]
57struct CacheEntry {
58 distance: f32,
60 normal: (f32, f32, f32),
62 position: Vec3Fix,
64 last_update: u32,
66 valid: bool,
68}
69
70impl Default for CacheEntry {
71 fn default() -> Self {
72 Self {
73 distance: f32::MAX,
74 normal: (0.0, 1.0, 0.0),
75 position: Vec3Fix::ZERO,
76 last_update: 0,
77 valid: false,
78 }
79 }
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum EvalLevel {
89 HighRes,
91 Standard,
93 Cached,
95 Skip,
97}
98
99pub struct AdaptiveSdfEvaluator {
109 cache: Vec<CacheEntry>,
111 frame: u32,
113 pub config: AdaptiveConfig,
115 pub stats_saved: usize,
117 pub stats_total: usize,
119}
120
121impl AdaptiveSdfEvaluator {
122 #[must_use]
124 pub fn new(num_bodies: usize, config: AdaptiveConfig) -> Self {
125 Self {
126 cache: vec![CacheEntry::default(); num_bodies],
127 frame: 0,
128 config,
129 stats_saved: 0,
130 stats_total: 0,
131 }
132 }
133
134 pub fn begin_frame(&mut self) {
136 self.frame += 1;
137 self.stats_saved = 0;
138 self.stats_total = 0;
139 }
140
141 pub fn resize(&mut self, num_bodies: usize) {
143 self.cache.resize(num_bodies, CacheEntry::default());
144 }
145
146 fn determine_level(&self, body_idx: usize, position: Vec3Fix) -> EvalLevel {
148 if body_idx >= self.cache.len() {
149 return EvalLevel::Standard;
150 }
151
152 let entry = &self.cache[body_idx];
153 if !entry.valid {
154 return EvalLevel::Standard;
155 }
156
157 let age = self.frame.saturating_sub(entry.last_update);
158 if age > self.config.cache_max_age {
159 return EvalLevel::Standard;
160 }
161
162 let movement = (position - entry.position).length().to_f32();
164
165 if entry.distance > self.config.cache_threshold && movement < self.config.high_res_threshold
166 {
167 return EvalLevel::Skip;
168 }
169
170 if entry.distance > self.config.high_res_threshold && movement < entry.distance * 0.5 {
171 return EvalLevel::Cached;
172 }
173
174 if entry.distance < self.config.high_res_threshold {
175 return EvalLevel::HighRes;
176 }
177
178 EvalLevel::Standard
179 }
180
181 #[cfg(feature = "std")]
185 pub fn evaluate(
186 &mut self,
187 body_idx: usize,
188 position: Vec3Fix,
189 sdf: &SdfCollider,
190 ) -> (f32, (f32, f32, f32)) {
191 self.stats_total += 1;
192
193 let level = self.determine_level(body_idx, position);
194
195 match level {
196 EvalLevel::Skip => {
197 self.stats_saved += 1;
198 if body_idx < self.cache.len() {
199 let entry = &self.cache[body_idx];
200 (entry.distance, entry.normal)
201 } else {
202 (f32::MAX, (0.0, 1.0, 0.0))
203 }
204 }
205
206 EvalLevel::Cached => {
207 self.stats_saved += 1;
208 if body_idx < self.cache.len() && self.cache[body_idx].valid {
209 let entry = &self.cache[body_idx];
210 (entry.distance, entry.normal)
211 } else {
212 self.evaluate_and_cache(body_idx, position, sdf, false)
213 }
214 }
215
216 EvalLevel::Standard => self.evaluate_and_cache(body_idx, position, sdf, false),
217
218 EvalLevel::HighRes => self.evaluate_and_cache(body_idx, position, sdf, true),
219 }
220 }
221
222 #[cfg(feature = "std")]
224 fn evaluate_and_cache(
225 &mut self,
226 body_idx: usize,
227 position: Vec3Fix,
228 sdf: &SdfCollider,
229 high_res: bool,
230 ) -> (f32, (f32, f32, f32)) {
231 let (lx, ly, lz) = sdf.world_to_local(position);
232 let (dist, normal) = sdf.field.distance_and_normal(lx, ly, lz);
233 let world_dist = dist * sdf.scale_f32;
234
235 let final_normal = if high_res && world_dist.abs() < self.config.high_res_threshold {
236 let eps = 0.0005;
238 let dx = sdf.field.distance(lx + eps, ly, lz) - sdf.field.distance(lx - eps, ly, lz);
239 let dy = sdf.field.distance(lx, ly + eps, lz) - sdf.field.distance(lx, ly - eps, lz);
240 let dz = sdf.field.distance(lx, ly, lz + eps) - sdf.field.distance(lx, ly, lz - eps);
241 let len = (dx * dx + dy * dy + dz * dz).sqrt();
242 if len > 1e-10 {
243 (dx / len, dy / len, dz / len)
244 } else {
245 normal
246 }
247 } else {
248 normal
249 };
250
251 let final_normal = if sdf.rotation == crate::math::QuatFix::IDENTITY {
256 final_normal
257 } else {
258 sdf.local_normal_to_world(final_normal.0, final_normal.1, final_normal.2)
259 .to_f32()
260 };
261
262 if body_idx < self.cache.len() {
264 self.cache[body_idx] = CacheEntry {
265 distance: world_dist,
266 normal: final_normal,
267 position,
268 last_update: self.frame,
269 valid: true,
270 };
271 }
272
273 (world_dist, final_normal)
274 }
275
276 #[must_use]
278 pub const fn stats(&self) -> (usize, usize) {
279 (self.stats_saved, self.stats_total)
280 }
281
282 pub fn invalidate(&mut self, body_idx: usize) {
284 if body_idx < self.cache.len() {
285 self.cache[body_idx].valid = false;
286 }
287 }
288
289 pub fn invalidate_all(&mut self) {
291 for entry in &mut self.cache {
292 entry.valid = false;
293 }
294 }
295}
296
297#[cfg(test)]
302mod tests {
303 use super::*;
304 use crate::math::QuatFix;
305 use crate::sdf_collider::{ClosureSdf, SdfCollider};
306
307 fn unit_sphere() -> ClosureSdf {
308 ClosureSdf::new(
309 |x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
310 |x, y, z| {
311 let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
312 if len < 1e-10 {
313 (0.0, 1.0, 0.0)
314 } else {
315 (x / len, y / len, z / len)
316 }
317 },
318 )
319 }
320
321 #[test]
322 fn test_adaptive_evaluator() {
323 let sdf =
324 SdfCollider::new_static(Box::new(unit_sphere()), Vec3Fix::ZERO, QuatFix::IDENTITY);
325
326 let mut evaluator = AdaptiveSdfEvaluator::new(2, AdaptiveConfig::default());
327
328 evaluator.begin_frame();
329 let (dist, _) = evaluator.evaluate(0, Vec3Fix::from_f32(5.0, 0.0, 0.0), &sdf);
330 assert!(dist > 0.0, "Should be outside sphere");
331
332 evaluator.begin_frame();
334 let (dist2, _) = evaluator.evaluate(0, Vec3Fix::from_f32(5.0, 0.0, 0.0), &sdf);
335 assert!((dist2 - dist).abs() < 0.1, "Cached value should be close");
336 }
337
338 #[test]
339 fn test_eval_levels() {
340 let evaluator = AdaptiveSdfEvaluator::new(1, AdaptiveConfig::default());
341 let level = evaluator.determine_level(0, Vec3Fix::ZERO);
343 assert_eq!(level, EvalLevel::Standard);
344 }
345
346 #[test]
347 fn test_invalidation() {
348 let sdf =
349 SdfCollider::new_static(Box::new(unit_sphere()), Vec3Fix::ZERO, QuatFix::IDENTITY);
350
351 let mut evaluator = AdaptiveSdfEvaluator::new(1, AdaptiveConfig::default());
352 evaluator.begin_frame();
353 evaluator.evaluate(0, Vec3Fix::from_f32(5.0, 0.0, 0.0), &sdf);
354
355 evaluator.invalidate(0);
356 let level = evaluator.determine_level(0, Vec3Fix::from_f32(5.0, 0.0, 0.0));
357 assert_eq!(
358 level,
359 EvalLevel::Standard,
360 "After invalidation should use Standard"
361 );
362 }
363
364 #[cfg(feature = "std")]
365 #[test]
366 fn invalidate_all_forces_fresh_evaluation_for_every_body() {
367 let sdf =
368 SdfCollider::new_static(Box::new(unit_sphere()), Vec3Fix::ZERO, QuatFix::IDENTITY);
369 let shifted = SdfCollider::new_static(
371 Box::new(unit_sphere()),
372 Vec3Fix::from_int(1, 0, 0),
373 QuatFix::IDENTITY,
374 );
375 let positions = [
376 Vec3Fix::from_f32(5.0, 0.0, 0.0),
377 Vec3Fix::from_f32(0.0, 5.0, 0.0),
378 Vec3Fix::from_f32(0.0, 0.0, 5.0),
379 ];
380
381 let mut evaluator = AdaptiveSdfEvaluator::new(3, AdaptiveConfig::default());
382 evaluator.begin_frame();
383 for (i, p) in positions.iter().enumerate() {
384 let (d, _) = evaluator.evaluate(i, *p, &sdf);
385 assert!((d - 4.0).abs() < 1e-5, "body {i} dist {d}");
386 }
387 evaluator.begin_frame();
389 for (i, p) in positions.iter().enumerate() {
390 assert_eq!(
391 evaluator.determine_level(i, *p),
392 EvalLevel::Cached,
393 "body {i}"
394 );
395 }
396 let (stale, _) = evaluator.evaluate(0, positions[0], &shifted);
398 assert!((stale - 4.0).abs() < 1e-5, "stale cached dist {stale}");
399 assert_eq!(evaluator.stats(), (1, 1));
400
401 evaluator.invalidate_all();
402 for (i, p) in positions.iter().enumerate() {
403 assert_eq!(
404 evaluator.determine_level(i, *p),
405 EvalLevel::Standard,
406 "body {i}"
407 );
408 }
409 evaluator.begin_frame();
411 let expected = [3.0_f32, (26.0_f32).sqrt() - 1.0, (26.0_f32).sqrt() - 1.0];
412 for (i, p) in positions.iter().enumerate() {
413 let (d, _) = evaluator.evaluate(i, *p, &shifted);
414 assert!(
415 (d - expected[i]).abs() < 1e-4,
416 "body {i} dist {d}, want {}",
417 expected[i]
418 );
419 }
420 assert_eq!(evaluator.stats(), (0, 3));
421 evaluator.begin_frame();
423 for (i, p) in positions.iter().enumerate() {
424 assert_eq!(
425 evaluator.determine_level(i, *p),
426 EvalLevel::Cached,
427 "body {i}"
428 );
429 }
430 let mut empty = AdaptiveSdfEvaluator::new(0, AdaptiveConfig::default());
432 empty.invalidate_all();
433 assert_eq!(empty.determine_level(0, Vec3Fix::ZERO), EvalLevel::Standard);
434 }
435}