use crate::math::Vec3Fix;
use crate::sdf_collider::SdfCollider;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AdaptiveConfig {
pub high_res_threshold: f32,
pub cache_threshold: f32,
pub cache_max_age: u32,
pub refinement_samples: usize,
}
impl Default for AdaptiveConfig {
fn default() -> Self {
Self {
high_res_threshold: 2.0,
cache_threshold: 10.0,
cache_max_age: 8,
refinement_samples: 4,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct CacheEntry {
distance: f32,
normal: (f32, f32, f32),
position: Vec3Fix,
last_update: u32,
valid: bool,
}
impl Default for CacheEntry {
fn default() -> Self {
Self {
distance: f32::MAX,
normal: (0.0, 1.0, 0.0),
position: Vec3Fix::ZERO,
last_update: 0,
valid: false,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EvalLevel {
HighRes,
Standard,
Cached,
Skip,
}
pub struct AdaptiveSdfEvaluator {
cache: Vec<CacheEntry>,
frame: u32,
pub config: AdaptiveConfig,
pub stats_saved: usize,
pub stats_total: usize,
}
impl AdaptiveSdfEvaluator {
#[must_use]
pub fn new(num_bodies: usize, config: AdaptiveConfig) -> Self {
Self {
cache: vec![CacheEntry::default(); num_bodies],
frame: 0,
config,
stats_saved: 0,
stats_total: 0,
}
}
pub fn begin_frame(&mut self) {
self.frame += 1;
self.stats_saved = 0;
self.stats_total = 0;
}
pub fn resize(&mut self, num_bodies: usize) {
self.cache.resize(num_bodies, CacheEntry::default());
}
fn determine_level(&self, body_idx: usize, position: Vec3Fix) -> EvalLevel {
if body_idx >= self.cache.len() {
return EvalLevel::Standard;
}
let entry = &self.cache[body_idx];
if !entry.valid {
return EvalLevel::Standard;
}
let age = self.frame.saturating_sub(entry.last_update);
if age > self.config.cache_max_age {
return EvalLevel::Standard;
}
let movement = (position - entry.position).length().to_f32();
if entry.distance > self.config.cache_threshold && movement < self.config.high_res_threshold
{
return EvalLevel::Skip;
}
if entry.distance > self.config.high_res_threshold && movement < entry.distance * 0.5 {
return EvalLevel::Cached;
}
if entry.distance < self.config.high_res_threshold {
return EvalLevel::HighRes;
}
EvalLevel::Standard
}
#[cfg(feature = "std")]
pub fn evaluate(
&mut self,
body_idx: usize,
position: Vec3Fix,
sdf: &SdfCollider,
) -> (f32, (f32, f32, f32)) {
self.stats_total += 1;
let level = self.determine_level(body_idx, position);
match level {
EvalLevel::Skip => {
self.stats_saved += 1;
if body_idx < self.cache.len() {
let entry = &self.cache[body_idx];
(entry.distance, entry.normal)
} else {
(f32::MAX, (0.0, 1.0, 0.0))
}
}
EvalLevel::Cached => {
self.stats_saved += 1;
if body_idx < self.cache.len() && self.cache[body_idx].valid {
let entry = &self.cache[body_idx];
(entry.distance, entry.normal)
} else {
self.evaluate_and_cache(body_idx, position, sdf, false)
}
}
EvalLevel::Standard => self.evaluate_and_cache(body_idx, position, sdf, false),
EvalLevel::HighRes => self.evaluate_and_cache(body_idx, position, sdf, true),
}
}
#[cfg(feature = "std")]
fn evaluate_and_cache(
&mut self,
body_idx: usize,
position: Vec3Fix,
sdf: &SdfCollider,
high_res: bool,
) -> (f32, (f32, f32, f32)) {
let (lx, ly, lz) = sdf.world_to_local(position);
let (dist, normal) = sdf.field.distance_and_normal(lx, ly, lz);
let world_dist = dist * sdf.scale_f32;
let final_normal = if high_res && world_dist.abs() < self.config.high_res_threshold {
let eps = 0.0005;
let dx = sdf.field.distance(lx + eps, ly, lz) - sdf.field.distance(lx - eps, ly, lz);
let dy = sdf.field.distance(lx, ly + eps, lz) - sdf.field.distance(lx, ly - eps, lz);
let dz = sdf.field.distance(lx, ly, lz + eps) - sdf.field.distance(lx, ly, lz - eps);
let len = (dx * dx + dy * dy + dz * dz).sqrt();
if len > 1e-10 {
(dx / len, dy / len, dz / len)
} else {
normal
}
} else {
normal
};
if body_idx < self.cache.len() {
self.cache[body_idx] = CacheEntry {
distance: world_dist,
normal: final_normal,
position,
last_update: self.frame,
valid: true,
};
}
(world_dist, final_normal)
}
#[must_use]
pub const fn stats(&self) -> (usize, usize) {
(self.stats_saved, self.stats_total)
}
pub fn invalidate(&mut self, body_idx: usize) {
if body_idx < self.cache.len() {
self.cache[body_idx].valid = false;
}
}
pub fn invalidate_all(&mut self) {
for entry in &mut self.cache {
entry.valid = false;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::QuatFix;
use crate::sdf_collider::{ClosureSdf, SdfCollider};
fn unit_sphere() -> ClosureSdf {
ClosureSdf::new(
|x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
|x, y, z| {
let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
if len < 1e-10 {
(0.0, 1.0, 0.0)
} else {
(x / len, y / len, z / len)
}
},
)
}
#[test]
fn test_adaptive_evaluator() {
let sdf =
SdfCollider::new_static(Box::new(unit_sphere()), Vec3Fix::ZERO, QuatFix::IDENTITY);
let mut evaluator = AdaptiveSdfEvaluator::new(2, AdaptiveConfig::default());
evaluator.begin_frame();
let (dist, _) = evaluator.evaluate(0, Vec3Fix::from_f32(5.0, 0.0, 0.0), &sdf);
assert!(dist > 0.0, "Should be outside sphere");
evaluator.begin_frame();
let (dist2, _) = evaluator.evaluate(0, Vec3Fix::from_f32(5.0, 0.0, 0.0), &sdf);
assert!((dist2 - dist).abs() < 0.1, "Cached value should be close");
}
#[test]
fn test_eval_levels() {
let evaluator = AdaptiveSdfEvaluator::new(1, AdaptiveConfig::default());
let level = evaluator.determine_level(0, Vec3Fix::ZERO);
assert_eq!(level, EvalLevel::Standard);
}
#[test]
fn test_invalidation() {
let sdf =
SdfCollider::new_static(Box::new(unit_sphere()), Vec3Fix::ZERO, QuatFix::IDENTITY);
let mut evaluator = AdaptiveSdfEvaluator::new(1, AdaptiveConfig::default());
evaluator.begin_frame();
evaluator.evaluate(0, Vec3Fix::from_f32(5.0, 0.0, 0.0), &sdf);
evaluator.invalidate(0);
let level = evaluator.determine_level(0, Vec3Fix::from_f32(5.0, 0.0, 0.0));
assert_eq!(
level,
EvalLevel::Standard,
"After invalidation should use Standard"
);
}
#[cfg(feature = "std")]
#[test]
fn invalidate_all_forces_fresh_evaluation_for_every_body() {
let sdf =
SdfCollider::new_static(Box::new(unit_sphere()), Vec3Fix::ZERO, QuatFix::IDENTITY);
let shifted = SdfCollider::new_static(
Box::new(unit_sphere()),
Vec3Fix::from_int(1, 0, 0),
QuatFix::IDENTITY,
);
let positions = [
Vec3Fix::from_f32(5.0, 0.0, 0.0),
Vec3Fix::from_f32(0.0, 5.0, 0.0),
Vec3Fix::from_f32(0.0, 0.0, 5.0),
];
let mut evaluator = AdaptiveSdfEvaluator::new(3, AdaptiveConfig::default());
evaluator.begin_frame();
for (i, p) in positions.iter().enumerate() {
let (d, _) = evaluator.evaluate(i, *p, &sdf);
assert!((d - 4.0).abs() < 1e-5, "body {i} dist {d}");
}
evaluator.begin_frame();
for (i, p) in positions.iter().enumerate() {
assert_eq!(
evaluator.determine_level(i, *p),
EvalLevel::Cached,
"body {i}"
);
}
let (stale, _) = evaluator.evaluate(0, positions[0], &shifted);
assert!((stale - 4.0).abs() < 1e-5, "stale cached dist {stale}");
assert_eq!(evaluator.stats(), (1, 1));
evaluator.invalidate_all();
for (i, p) in positions.iter().enumerate() {
assert_eq!(
evaluator.determine_level(i, *p),
EvalLevel::Standard,
"body {i}"
);
}
evaluator.begin_frame();
let expected = [3.0_f32, (26.0_f32).sqrt() - 1.0, (26.0_f32).sqrt() - 1.0];
for (i, p) in positions.iter().enumerate() {
let (d, _) = evaluator.evaluate(i, *p, &shifted);
assert!(
(d - expected[i]).abs() < 1e-4,
"body {i} dist {d}, want {}",
expected[i]
);
}
assert_eq!(evaluator.stats(), (0, 3));
evaluator.begin_frame();
for (i, p) in positions.iter().enumerate() {
assert_eq!(
evaluator.determine_level(i, *p),
EvalLevel::Cached,
"body {i}"
);
}
let mut empty = AdaptiveSdfEvaluator::new(0, AdaptiveConfig::default());
empty.invalidate_all();
assert_eq!(empty.determine_level(0, Vec3Fix::ZERO), EvalLevel::Standard);
}
}