Skip to main content

alice_physics/
sdf_adaptive.rs

1//! Adaptive SDF Resolution
2//!
3//! Evaluates SDF at variable resolution based on collision proximity.
4//! Near-collision regions get high-res evaluation; distant regions use cached coarse values.
5//!
6//! # Strategy
7//!
8//! - BVH broad-phase determines which bodies are near SDF surfaces
9//! - Bodies near SDF → full-resolution evaluation
10//! - Bodies far from SDF → cached distance (skip re-evaluation)
11//! - Bodies very far → skip entirely
12//!
13//! Author: Moroya Sakamoto
14
15use crate::math::Vec3Fix;
16use crate::sdf_collider::SdfCollider;
17
18#[cfg(not(feature = "std"))]
19use alloc::vec::Vec;
20
21// ============================================================================
22// Adaptive SDF Configuration
23// ============================================================================
24
25/// Adaptive evaluation configuration
26#[derive(Clone, Copy, Debug, PartialEq)]
27pub struct AdaptiveConfig {
28    /// Distance threshold for high-resolution evaluation
29    pub high_res_threshold: f32,
30    /// Distance threshold for cached evaluation (beyond = skip)
31    pub cache_threshold: f32,
32    /// Maximum age (frames) before cache entry expires
33    pub cache_max_age: u32,
34    /// Reserved: **not read**. The high-res normal refinement uses a fixed
35    /// six-sample central difference (step 0.0005); this field is kept for
36    /// compatibility and has no effect.
37    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// ============================================================================
52// Cached SDF Entry
53// ============================================================================
54
55/// Cached SDF evaluation result
56#[derive(Clone, Copy, Debug, PartialEq)]
57struct CacheEntry {
58    /// Cached distance value
59    distance: f32,
60    /// Cached normal (world space)
61    normal: (f32, f32, f32),
62    /// Position where this was evaluated
63    position: Vec3Fix,
64    /// Frame when this was last updated
65    last_update: u32,
66    /// Whether this entry is valid
67    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// ============================================================================
83// Evaluation Level
84// ============================================================================
85
86/// Resolution level for SDF evaluation
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum EvalLevel {
89    /// Full evaluation with refinement (near collision)
90    HighRes,
91    /// Single evaluation (moderate distance)
92    Standard,
93    /// Use cached value (far from surface)
94    Cached,
95    /// Skip evaluation (very far)
96    Skip,
97}
98
99// ============================================================================
100// Adaptive SDF Evaluator
101// ============================================================================
102
103/// Adaptive SDF evaluation with distance-based LOD
104///
105/// The cache is kept per body index, not per SDF: evaluating one body against
106/// several colliders in the same frame returns the first collider's cached
107/// value for the others (use one evaluator per collider, or `invalidate`).
108pub struct AdaptiveSdfEvaluator {
109    /// Per-body cache entries
110    cache: Vec<CacheEntry>,
111    /// Current frame counter
112    frame: u32,
113    /// Configuration
114    pub config: AdaptiveConfig,
115    /// Statistics: evaluations saved this frame
116    pub stats_saved: usize,
117    /// Statistics: total evaluations this frame
118    pub stats_total: usize,
119}
120
121impl AdaptiveSdfEvaluator {
122    /// Create evaluator for N bodies
123    #[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    /// Begin a new frame
135    pub fn begin_frame(&mut self) {
136        self.frame += 1;
137        self.stats_saved = 0;
138        self.stats_total = 0;
139    }
140
141    /// Resize cache for new body count
142    pub fn resize(&mut self, num_bodies: usize) {
143        self.cache.resize(num_bodies, CacheEntry::default());
144    }
145
146    /// Determine evaluation level for a body
147    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        // Check how far the body has moved since last evaluation
163        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    /// Evaluate SDF for a body with adaptive resolution
182    ///
183    /// Returns the distance in world metres and the unit normal in world space.
184    #[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    /// Evaluate SDF and update cache
223    #[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            // Refined normal: use tighter finite differences
237            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        // The field reports a local-space normal; callers work in world space.
252        // An unrotated collider needs no transform (and keeps its value bit
253        // for bit), a rotated one needs the same rotation `sphere_trace_sdf`
254        // and the contact queries apply.
255        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        // Update cache
263        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    /// Get evaluation statistics as (saved, total) counts
277    #[must_use]
278    pub const fn stats(&self) -> (usize, usize) {
279        (self.stats_saved, self.stats_total)
280    }
281
282    /// Invalidate cache for a specific body (e.g., after teleport)
283    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    /// Invalidate all cache entries
290    pub fn invalidate_all(&mut self) {
291        for entry in &mut self.cache {
292            entry.valid = false;
293        }
294    }
295}
296
297// ============================================================================
298// Tests
299// ============================================================================
300
301#[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        // Second evaluation should potentially use cache
333        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        // Fresh cache should return Standard
342        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        // 同じ球を +X に 1 ずらした SDF: 位置 (5,0,0) の距離は 4 → 3 に変わる
370        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        // 全 body が cache 済 (距離 4 > high_res 2、移動 0) → 次 frame は Cached
388        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        // 対照: invalidate しないと shifted SDF に対しても stale な 4 が返る
397        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        // 全 body が再評価され、shifted SDF の値が返る (saved は増えない)
410        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        // 再評価後は再び valid (Cached に戻る)
422        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        // 空 evaluator でも panic しない
431        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}