use alice_physics::bvh::{BvhPrimitive, LinearBvh};
use alice_physics::collider::AABB;
use alice_physics::math::{Fix128, Vec3Fix};
use alice_physics::query::{
batch_raycast, batch_sphere_cast, capsule_cast, overlap_aabb, overlap_aabb_bvh,
overlap_aabb_expanded, overlap_sphere, overlap_sphere_bvh, sphere_cast, BatchRayQuery,
OverlapResult, ShapeCastHit,
};
use alice_physics::raycast::{ray_sphere, Ray};
use alice_physics::solver::RigidBody;
fn overlap_scene() -> Vec<RigidBody> {
vec![
RigidBody::new_static(Vec3Fix::from_int(0, 0, 0)),
RigidBody::new_static(Vec3Fix::from_int(6, 0, 0)),
RigidBody::new_static(Vec3Fix::from_int(0, 6, 0)),
RigidBody::new_static(Vec3Fix::from_int(-4, 0, 0)),
]
}
fn build_bvh(bodies: &[RigidBody], body_radius: Fix128) -> LinearBvh {
let prims: Vec<BvhPrimitive> = bodies
.iter()
.enumerate()
.map(|(i, b)| BvhPrimitive {
aabb: AABB::new(
Vec3Fix::new(
b.position.x - body_radius,
b.position.y - body_radius,
b.position.z - body_radius,
),
Vec3Fix::new(
b.position.x + body_radius,
b.position.y + body_radius,
b.position.z + body_radius,
),
),
index: i as u32,
morton: 0,
})
.collect();
LinearBvh::build(prims)
}
fn sorted_indices(results: &[OverlapResult]) -> Vec<usize> {
let mut v: Vec<usize> = results.iter().map(|r| r.body_index).collect();
v.sort_unstable();
v
}
fn main() {
let bodies = overlap_scene();
let body_radius = Fix128::ONE;
let sphere_center = Vec3Fix::from_int(0, 0, 0);
let sphere_radius = Fix128::from_int(3);
let sphere_results: Vec<OverlapResult> =
overlap_sphere(sphere_center, sphere_radius, &bodies, body_radius);
assert_eq!(sorted_indices(&sphere_results), vec![0], "overlap_sphere");
assert_eq!(sphere_results[0].depth, Fix128::from_int(4));
println!(
"[spatial_queries] overlap_sphere: {} hit(s), body0 depth={}",
sphere_results.len(),
sphere_results[0].depth.to_f64()
);
let aabb = AABB::new(Vec3Fix::from_int(-1, -1, -1), Vec3Fix::from_int(7, 1, 1));
let aabb_results: Vec<OverlapResult> = overlap_aabb(&aabb, &bodies);
assert_eq!(sorted_indices(&aabb_results), vec![0, 1], "overlap_aabb");
println!(
"[spatial_queries] overlap_aabb: bodies {:?}",
sorted_indices(&aabb_results)
);
let expanded_zero = overlap_aabb_expanded(&aabb, &bodies, Fix128::ZERO);
assert_eq!(
expanded_zero, aabb_results,
"zero expansion must equal overlap_aabb exactly"
);
let aabb2 = AABB::new(Vec3Fix::from_int(2, -1, -1), Vec3Fix::from_int(4, 1, 1));
let aabb2_unexpanded = overlap_aabb(&aabb2, &bodies);
assert!(
aabb2_unexpanded.is_empty(),
"aabb2 should contain no body before expansion"
);
let margin = Fix128::from_int(2);
let aabb2_expanded = overlap_aabb_expanded(&aabb2, &bodies, margin);
assert_eq!(
sorted_indices(&aabb2_expanded),
vec![0, 1],
"margin of 2 should reach exactly bodies 0 and 1"
);
println!(
"[spatial_queries] overlap_aabb_expanded: margin 2 adds {:?}",
sorted_indices(&aabb2_expanded)
);
let bvh = build_bvh(&bodies, body_radius);
let sphere_bvh_results =
overlap_sphere_bvh(sphere_center, sphere_radius, &bodies, body_radius, &bvh);
let mut brute_sorted = sphere_results.clone();
brute_sorted.sort_by_key(|r| r.body_index);
let mut bvh_sorted = sphere_bvh_results.clone();
bvh_sorted.sort_by_key(|r| r.body_index);
assert_eq!(
bvh_sorted, brute_sorted,
"overlap_sphere_bvh must agree exactly with overlap_sphere"
);
let aabb_bvh_results = overlap_aabb_bvh(&aabb, &bodies, &bvh);
let mut aabb_bvh_sorted = aabb_bvh_results.clone();
aabb_bvh_sorted.sort_by_key(|r| r.body_index);
let mut aabb_brute_sorted = aabb_results.clone();
aabb_brute_sorted.sort_by_key(|r| r.body_index);
assert_eq!(
aabb_bvh_sorted, aabb_brute_sorted,
"overlap_aabb_bvh must agree exactly with overlap_aabb"
);
println!(
"[spatial_queries] overlap_*_bvh: BVH-accelerated and brute-force agree \
({} sphere hits, {} aabb hits)",
bvh_sorted.len(),
aabb_bvh_sorted.len()
);
let cast_bodies = vec![
RigidBody::new_static(Vec3Fix::from_int(5, 0, 0)),
RigidBody::new_static(Vec3Fix::from_int(15, 0, 0)),
];
let cast_body_radius = Fix128::ONE;
let cast_radius = Fix128::ONE;
let hit: Option<ShapeCastHit> = sphere_cast(
Vec3Fix::ZERO,
cast_radius,
Vec3Fix::UNIT_X,
Fix128::from_int(100),
&cast_bodies,
cast_body_radius,
);
let hit = hit.expect("sphere_cast should hit body0");
assert_eq!(hit.body_index, 0);
assert_eq!(hit.t, Fix128::from_int(3));
assert_eq!(hit.point, Vec3Fix::from_int(3, 0, 0));
assert_eq!(
hit.normal,
Vec3Fix::new(-Fix128::ONE, Fix128::ZERO, Fix128::ZERO)
);
println!(
"[spatial_queries] sphere_cast: body={} t={} point={:?}",
hit.body_index,
hit.t.to_f64(),
(
hit.point.x.to_f64(),
hit.point.y.to_f64(),
hit.point.z.to_f64()
)
);
let zero_radius_hit = sphere_cast(
Vec3Fix::ZERO,
Fix128::ZERO,
Vec3Fix::UNIT_X,
Fix128::from_int(100),
&cast_bodies,
cast_body_radius,
)
.expect("zero-radius sphere_cast should hit body0");
assert_eq!(zero_radius_hit.t, Fix128::from_int(4));
let ray = Ray::new(Vec3Fix::ZERO, Vec3Fix::UNIT_X);
let plain_ray_hit = ray_sphere(
&ray,
&alice_physics::collider::Sphere::new(cast_bodies[0].position, cast_body_radius),
Fix128::from_int(100),
)
.expect("plain ray_sphere should also hit body0");
assert_eq!(
zero_radius_hit.t, plain_ray_hit.t,
"zero-radius sphere_cast must equal a plain raycast"
);
println!(
"[spatial_queries] sphere_cast(radius=0) == raycast: t={}",
zero_radius_hit.t.to_f64()
);
let inside_body = vec![RigidBody::new_static(Vec3Fix::from_int(5, 0, 0))];
let inside_hit = sphere_cast(
Vec3Fix::from_int(4, 0, 0),
Fix128::ONE,
Vec3Fix::UNIT_X,
Fix128::from_int(100),
&inside_body,
Fix128::ONE,
)
.expect("sphere_cast moving deeper from inside the body reports the initial overlap");
assert_eq!(
inside_hit.t,
Fix128::ZERO,
"interior origin moving deeper resolves to an initial overlap at TOI=0, not the exit point"
);
assert_eq!(inside_hit.point, Vec3Fix::from_int(4, 0, 0));
assert_eq!(inside_hit.normal, Vec3Fix::from_int(-1, 0, 0));
println!(
"[spatial_queries] sphere_cast from inside a body, moving deeper: t={} normal={:?}",
inside_hit.t.to_f64(),
inside_hit.normal.to_f32()
);
let leaving = sphere_cast(
Vec3Fix::from_int(6, 0, 0),
Fix128::ONE,
Vec3Fix::UNIT_X,
Fix128::from_int(100),
&inside_body,
Fix128::ONE,
);
assert_eq!(
leaving, None,
"interior origin moving outward reports no contact"
);
println!("[spatial_queries] sphere_cast from inside a body, moving outward: no contact");
let capsule_body = vec![RigidBody::new_static(Vec3Fix::from_int(10, 0, 0))];
let capsule_hit = capsule_cast(
Vec3Fix::from_int(4, -2, 0),
Vec3Fix::from_int(4, 2, 0),
Fix128::ONE,
Vec3Fix::UNIT_X,
Fix128::from_int(100),
&capsule_body,
Fix128::ONE,
)
.expect("capsule_cast should hit");
assert_eq!(
capsule_hit.t,
Fix128::from_int(4),
"the midpoint sub-cast is the closest of the three"
);
assert_eq!(capsule_hit.point, Vec3Fix::from_int(8, 0, 0));
println!(
"[spatial_queries] capsule_cast: closest sub-cast t={} (midpoint)",
capsule_hit.t.to_f64()
);
let queries = vec![
BatchRayQuery {
origin: Vec3Fix::ZERO,
direction: Vec3Fix::UNIT_X,
max_distance: Fix128::from_int(100),
},
BatchRayQuery {
origin: Vec3Fix::ZERO,
direction: Vec3Fix::UNIT_Y,
max_distance: Fix128::from_int(100),
},
BatchRayQuery {
origin: Vec3Fix::from_int(20, 0, 0),
direction: -Vec3Fix::UNIT_X,
max_distance: Fix128::from_int(100),
},
];
let batch_results: Vec<Option<ShapeCastHit>> =
batch_raycast(&queries, &cast_bodies, cast_body_radius);
let loop_results: Vec<Option<ShapeCastHit>> = queries
.iter()
.map(|q| {
sphere_cast(
q.origin,
Fix128::ZERO,
q.direction,
q.max_distance,
&cast_bodies,
cast_body_radius,
)
})
.collect();
assert_eq!(
batch_results, loop_results,
"batch_raycast must equal calling sphere_cast once per ray"
);
assert!(batch_results[0].is_some());
assert!(batch_results[1].is_none());
let q2_hit = batch_results[2].expect("Q2 should hit body1");
assert_eq!(q2_hit.body_index, 1);
assert_eq!(q2_hit.t, Fix128::from_int(4));
assert_eq!(q2_hit.point, Vec3Fix::from_int(16, 0, 0));
println!(
"[spatial_queries] batch_raycast: {} queries, hits={:?}",
queries.len(),
batch_results
.iter()
.map(Option::is_some)
.collect::<Vec<_>>()
);
let origins = vec![Vec3Fix::ZERO, Vec3Fix::ZERO];
let directions = vec![Vec3Fix::UNIT_X, Vec3Fix::UNIT_Y];
let batch_sphere_results: Vec<Option<ShapeCastHit>> = batch_sphere_cast(
&origins,
cast_radius,
&directions,
Fix128::from_int(100),
&cast_bodies,
cast_body_radius,
);
let loop_sphere_results: Vec<Option<ShapeCastHit>> = origins
.iter()
.zip(directions.iter())
.map(|(o, d)| {
sphere_cast(
*o,
cast_radius,
*d,
Fix128::from_int(100),
&cast_bodies,
cast_body_radius,
)
})
.collect();
assert_eq!(
batch_sphere_results, loop_sphere_results,
"batch_sphere_cast must equal calling sphere_cast once per (origin, direction) pair"
);
assert!(batch_sphere_results[0].is_some());
assert!(batch_sphere_results[1].is_none());
println!(
"[spatial_queries] batch_sphere_cast: {} queries, hits={:?}",
origins.len(),
batch_sphere_results
.iter()
.map(Option::is_some)
.collect::<Vec<_>>()
);
let empty: Vec<RigidBody> = Vec::new();
assert!(overlap_sphere(sphere_center, sphere_radius, &empty, body_radius).is_empty());
assert!(overlap_aabb(&aabb, &empty).is_empty());
assert!(overlap_aabb_expanded(&aabb, &empty, margin).is_empty());
let empty_bvh = build_bvh(&empty, body_radius);
assert!(overlap_sphere_bvh(
sphere_center,
sphere_radius,
&empty,
body_radius,
&empty_bvh
)
.is_empty());
assert!(overlap_aabb_bvh(&aabb, &empty, &empty_bvh).is_empty());
assert!(sphere_cast(
Vec3Fix::ZERO,
cast_radius,
Vec3Fix::UNIT_X,
Fix128::from_int(100),
&empty,
cast_body_radius
)
.is_none());
assert!(capsule_cast(
Vec3Fix::from_int(0, -1, 0),
Vec3Fix::from_int(0, 1, 0),
Fix128::ONE,
Vec3Fix::UNIT_X,
Fix128::from_int(100),
&empty,
cast_body_radius
)
.is_none());
assert!(batch_raycast(&[], &empty, cast_body_radius).is_empty());
assert!(batch_sphere_cast(
&[],
cast_radius,
&[],
Fix128::from_int(100),
&empty,
cast_body_radius
)
.is_empty());
println!("[spatial_queries] degenerate: empty world returns empty results, no panic");
assert!(sphere_cast(
Vec3Fix::ZERO,
cast_radius,
Vec3Fix::ZERO,
Fix128::from_int(100),
&cast_bodies,
cast_body_radius
)
.is_none());
assert!(capsule_cast(
Vec3Fix::from_int(0, -1, 0),
Vec3Fix::from_int(0, 1, 0),
Fix128::ONE,
Vec3Fix::ZERO,
Fix128::from_int(100),
&cast_bodies,
cast_body_radius
)
.is_none());
println!("[spatial_queries] degenerate: zero-length direction returns None, no panic");
let point_aabb = AABB::new(Vec3Fix::from_int(6, 0, 0), Vec3Fix::from_int(6, 0, 0));
let point_hit = overlap_aabb(&point_aabb, &bodies);
assert_eq!(
sorted_indices(&point_hit),
vec![1],
"point AABB matches only body1"
);
let zero_zero = overlap_sphere(
Vec3Fix::from_int(0, 0, 0),
Fix128::ZERO,
&bodies,
Fix128::ZERO,
);
assert!(
zero_zero.is_empty(),
"zero-radius sphere query never matches, even an exact coincidence (strict <)"
);
println!(
"[spatial_queries] degenerate: zero-size queries -- point AABB matches 1, \
zero-radius sphere matches 0 even at exact coincidence"
);
let extreme = Vec3Fix::new(
Fix128 {
hi: i64::MAX,
lo: u64::MAX,
},
Fix128::ZERO,
Fix128::ZERO,
);
let extreme_bodies = vec![RigidBody::new_static(extreme)];
let outcome = std::panic::catch_unwind(|| {
let _ = overlap_sphere(extreme, sphere_radius, &extreme_bodies, body_radius);
let _ = sphere_cast(
Vec3Fix::ZERO,
cast_radius,
Vec3Fix::UNIT_X,
Fix128::from_int(100),
&extreme_bodies,
cast_body_radius,
);
});
assert!(
outcome.is_ok(),
"extreme coordinates wrap (Fix128 arithmetic is a mod-2^128 group), they must not panic"
);
println!("[spatial_queries] degenerate: extreme coordinates wrap, no panic");
println!("[spatial_queries] all 12 previously-unwired query items exercised");
}