use glam::Vec3;
use super::{child_center, octant_for_point, SparseVoxelOctree, SvoNode};
#[derive(Debug, Clone, Copy)]
pub struct SvoRayHit {
pub distance: f32,
pub position: Vec3,
pub normal: Vec3,
pub depth: u32,
}
pub fn svo_query_point(svo: &SparseVoxelOctree, point: Vec3) -> f32 {
if svo.nodes.is_empty() {
return f32::MAX;
}
let bounds_min = svo.bounds.min();
let bounds_max = svo.bounds.max();
if point.x < bounds_min.x
|| point.x > bounds_max.x
|| point.y < bounds_min.y
|| point.y > bounds_max.y
|| point.z < bounds_min.z
|| point.z > bounds_max.z
{
return svo.bounds.distance_to_point(point);
}
let center = svo.bounds.center();
let half_size = svo.bounds.half_size();
query_descent(svo, 0, center, half_size, point)
}
fn query_descent(
svo: &SparseVoxelOctree,
node_idx: usize,
center: Vec3,
half_size: Vec3,
point: Vec3,
) -> f32 {
if node_idx >= svo.nodes.len() {
return f32::MAX;
}
let node = &svo.nodes[node_idx];
if node.is_leaf == 1 {
return node.distance;
}
let octant = octant_for_point(point, center);
node.child_index(octant).map_or(node.distance, |child_idx| {
let child_c = child_center(center, half_size, octant);
let child_half = half_size * 0.5;
query_descent(svo, child_idx as usize, child_c, child_half, point)
})
}
pub fn svo_ray_query(
svo: &SparseVoxelOctree,
origin: Vec3,
direction: Vec3,
max_distance: f32,
) -> Option<SvoRayHit> {
if svo.nodes.is_empty() {
return None;
}
let dir = direction.normalize();
let (t_enter, t_exit) = ray_aabb_intersect(origin, dir, svo.bounds.min(), svo.bounds.max())?;
let t_start = t_enter.max(0.0);
if t_start > max_distance || t_start >= t_exit {
return None;
}
let center = svo.bounds.center();
let half_size = svo.bounds.half_size();
let mut t = t_start + 0.001; let max_steps = 256u32;
for _ in 0..max_steps {
if t > max_distance.min(t_exit) {
break;
}
let p = origin + dir * t;
let (dist, depth) = query_descent_with_depth(svo, 0, center, half_size, p, 0);
if dist.abs() < 0.001 {
let n = estimate_svo_normal(svo, p, center, half_size);
return Some(SvoRayHit {
distance: t,
position: p,
normal: n,
depth,
});
}
t += dist.abs().max(0.001);
}
None
}
fn query_descent_with_depth(
svo: &SparseVoxelOctree,
node_idx: usize,
center: Vec3,
half_size: Vec3,
point: Vec3,
depth: u32,
) -> (f32, u32) {
if node_idx >= svo.nodes.len() {
return (f32::MAX, depth);
}
let node = &svo.nodes[node_idx];
if node.is_leaf == 1 {
return (node.distance, depth);
}
let octant = octant_for_point(point, center);
node.child_index(octant)
.map_or((node.distance, depth), |child_idx| {
let child_c = child_center(center, half_size, octant);
let child_half = half_size * 0.5;
query_descent_with_depth(
svo,
child_idx as usize,
child_c,
child_half,
point,
depth + 1,
)
})
}
fn estimate_svo_normal(
svo: &SparseVoxelOctree,
point: Vec3,
center: Vec3,
half_size: Vec3,
) -> Vec3 {
let e = 0.002;
let dx = query_descent(svo, 0, center, half_size, point + Vec3::new(e, 0.0, 0.0))
- query_descent(svo, 0, center, half_size, point - Vec3::new(e, 0.0, 0.0));
let dy = query_descent(svo, 0, center, half_size, point + Vec3::new(0.0, e, 0.0))
- query_descent(svo, 0, center, half_size, point - Vec3::new(0.0, e, 0.0));
let dz = query_descent(svo, 0, center, half_size, point + Vec3::new(0.0, 0.0, e))
- query_descent(svo, 0, center, half_size, point - Vec3::new(0.0, 0.0, e));
Vec3::new(dx, dy, dz).normalize_or_zero()
}
pub fn svo_nearest_surface(svo: &SparseVoxelOctree, point: Vec3) -> (f32, Vec3) {
if svo.nodes.is_empty() {
return (f32::MAX, point);
}
let center = svo.bounds.center();
let half_size = svo.bounds.half_size();
let (dist, best_node) = nearest_descent(svo, 0, center, half_size, point);
let n = best_node.normal();
let surface_point = if n.length_squared() > 0.01 {
point - n * dist
} else {
point
};
(dist, surface_point)
}
fn nearest_descent(
svo: &SparseVoxelOctree,
node_idx: usize,
center: Vec3,
half_size: Vec3,
point: Vec3,
) -> (f32, SvoNode) {
if node_idx >= svo.nodes.len() {
return (f32::MAX, SvoNode::default());
}
let node = svo.nodes[node_idx];
if node.is_leaf == 1 {
return (node.distance, node);
}
let octant = octant_for_point(point, center);
node.child_index(octant)
.map_or((node.distance, node), |child_idx| {
let child_c = child_center(center, half_size, octant);
let child_half = half_size * 0.5;
nearest_descent(svo, child_idx as usize, child_c, child_half, point)
})
}
fn ray_aabb_intersect(
origin: Vec3,
dir: Vec3,
aabb_min: Vec3,
aabb_max: Vec3,
) -> Option<(f32, f32)> {
let inv_dir = Vec3::new(
if dir.x.abs() > 1e-10 {
1.0 / dir.x
} else {
f32::MAX * dir.x.signum()
},
if dir.y.abs() > 1e-10 {
1.0 / dir.y
} else {
f32::MAX * dir.y.signum()
},
if dir.z.abs() > 1e-10 {
1.0 / dir.z
} else {
f32::MAX * dir.z.signum()
},
);
let t1 = (aabb_min - origin) * inv_dir;
let t2 = (aabb_max - origin) * inv_dir;
let t_min = t1.min(t2);
let t_max = t1.max(t2);
let t_enter = t_min.x.max(t_min.y).max(t_min.z);
let t_exit = t_max.x.min(t_max.y).min(t_max.z);
if t_enter <= t_exit && t_exit >= 0.0 {
Some((t_enter, t_exit))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::svo::SvoBuildConfig;
use crate::types::SdfNode;
fn make_test_svo() -> SparseVoxelOctree {
let sphere = SdfNode::sphere(1.0);
let config = SvoBuildConfig {
max_depth: 4,
bounds_min: Vec3::splat(-2.0),
bounds_max: Vec3::splat(2.0),
use_compiled: false,
..Default::default()
};
super::super::build::build_svo(&sphere, &config)
}
#[test]
fn test_point_query_center() {
let svo = make_test_svo();
let d = svo_query_point(&svo, Vec3::ZERO);
assert!(d < 0.0, "Center should be inside sphere, got {}", d);
}
#[test]
fn test_point_query_outside() {
let svo = make_test_svo();
let d = svo_query_point(&svo, Vec3::new(3.0, 0.0, 0.0));
assert!(d > 0.0, "Outside should be positive, got {}", d);
}
#[test]
fn test_point_query_surface() {
let svo = make_test_svo();
let d = svo_query_point(&svo, Vec3::new(1.0, 0.0, 0.0));
assert!(d.abs() < 0.5, "Surface should be near zero, got {}", d);
}
#[test]
fn test_ray_query_hit() {
let svo = make_test_svo();
let hit = svo_ray_query(&svo, Vec3::new(-5.0, 0.0, 0.0), Vec3::X, 10.0);
if let Some(h) = hit {
assert!(
(h.distance - 4.0).abs() < 0.5,
"Hit distance should be ~4.0, got {}",
h.distance
);
assert!(h.position.x < 0.0, "Hit should be on -X side");
}
}
#[test]
fn test_ray_query_miss() {
let svo = make_test_svo();
let hit = svo_ray_query(&svo, Vec3::new(-5.0, 5.0, 0.0), Vec3::X, 20.0);
assert!(hit.is_none(), "Ray should miss the sphere");
}
#[test]
fn test_nearest_surface() {
let svo = make_test_svo();
let (dist, _surface_point) = svo_nearest_surface(&svo, Vec3::ZERO);
assert!(dist < 0.0, "Center should be inside, got dist={}", dist);
}
#[test]
fn test_ray_aabb_intersect() {
let min = Vec3::new(-1.0, -1.0, -1.0);
let max = Vec3::new(1.0, 1.0, 1.0);
let hit = ray_aabb_intersect(Vec3::new(-5.0, 0.0, 0.0), Vec3::X, min, max);
assert!(hit.is_some());
let (t_enter, t_exit) = hit.unwrap();
assert!((t_enter - 4.0).abs() < 0.01);
assert!((t_exit - 6.0).abs() < 0.01);
let miss = ray_aabb_intersect(Vec3::new(-5.0, 5.0, 0.0), Vec3::X, min, max);
assert!(miss.is_none());
}
}