Files
game_engine/crates/voltex_physics/src/ccd.rs
tolelom abd6f5cf6e feat(physics): add angular dynamics, sequential impulse solver, sleep system, BVH improvements, CCD, and ray extensions
- Angular velocity integration with diagonal inertia tensor (sphere/box/capsule)
- Angular impulse in collision solver (torque from off-center contacts)
- Sequential impulse solver with configurable iterations (default 4)
- Sleep/island system: bodies sleep after velocity threshold timeout, wake on collision
- Ray vs triangle intersection (Moller-Trumbore algorithm)
- raycast_all returning all hits sorted by distance
- BVH query_pairs replaced N^2 brute force with recursive tree traversal
- BVH query_ray for accelerated raycasting
- BVH refit for incremental AABB updates
- Swept sphere vs AABB continuous collision detection (CCD)
- Updated lib.rs exports for all new public APIs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 20:57:55 +09:00

119 lines
3.8 KiB
Rust

use voltex_math::{Vec3, AABB, Ray};
use crate::ray::ray_vs_aabb;
/// Swept sphere vs AABB continuous collision detection.
/// Expands the AABB by the sphere radius, then tests a ray from start to end.
/// Returns t in [0,1] of first contact, or None if no contact.
pub fn swept_sphere_vs_aabb(start: Vec3, end: Vec3, radius: f32, aabb: &AABB) -> Option<f32> {
// Expand AABB by sphere radius
let r = Vec3::new(radius, radius, radius);
let expanded = AABB::new(aabb.min - r, aabb.max + r);
let direction = end - start;
let sweep_len = direction.length();
if sweep_len < 1e-10 {
// No movement — check if already inside
if expanded.contains_point(start) {
return Some(0.0);
}
return None;
}
let ray = Ray::new(start, direction * (1.0 / sweep_len));
match ray_vs_aabb(&ray, &expanded) {
Some(t) => {
let parametric_t = t / sweep_len;
if parametric_t <= 1.0 {
Some(parametric_t)
} else {
None
}
}
None => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-3
}
#[test]
fn test_swept_sphere_hits_aabb() {
let start = Vec3::new(-10.0, 0.0, 0.0);
let end = Vec3::new(10.0, 0.0, 0.0);
let radius = 0.5;
let aabb = AABB::new(Vec3::new(4.0, -1.0, -1.0), Vec3::new(6.0, 1.0, 1.0));
let t = swept_sphere_vs_aabb(start, end, radius, &aabb).unwrap();
// Expanded AABB min.x = 3.5, start.x = -10, direction = 20
// t = (3.5 - (-10)) / 20 = 13.5 / 20 = 0.675
assert!(t > 0.0 && t < 1.0);
assert!(approx(t, 0.675));
}
#[test]
fn test_swept_sphere_misses_aabb() {
let start = Vec3::new(-10.0, 10.0, 0.0);
let end = Vec3::new(10.0, 10.0, 0.0);
let radius = 0.5;
let aabb = AABB::new(Vec3::new(4.0, -1.0, -1.0), Vec3::new(6.0, 1.0, 1.0));
assert!(swept_sphere_vs_aabb(start, end, radius, &aabb).is_none());
}
#[test]
fn test_swept_sphere_starts_inside() {
let start = Vec3::new(5.0, 0.0, 0.0);
let end = Vec3::new(10.0, 0.0, 0.0);
let radius = 0.5;
let aabb = AABB::new(Vec3::new(4.0, -1.0, -1.0), Vec3::new(6.0, 1.0, 1.0));
let t = swept_sphere_vs_aabb(start, end, radius, &aabb).unwrap();
assert!(approx(t, 0.0));
}
#[test]
fn test_swept_sphere_tunneling_detection() {
// Fast sphere that would tunnel through a thin wall
let start = Vec3::new(-100.0, 0.0, 0.0);
let end = Vec3::new(100.0, 0.0, 0.0);
let radius = 0.1;
// Thin wall at x=0
let aabb = AABB::new(Vec3::new(-0.05, -10.0, -10.0), Vec3::new(0.05, 10.0, 10.0));
let t = swept_sphere_vs_aabb(start, end, radius, &aabb);
assert!(t.is_some(), "should detect tunneling through thin wall");
let t = t.unwrap();
assert!(t > 0.0 && t < 1.0);
}
#[test]
fn test_swept_sphere_no_movement() {
let start = Vec3::new(5.0, 0.0, 0.0);
let end = Vec3::new(5.0, 0.0, 0.0);
let radius = 0.5;
let aabb = AABB::new(Vec3::new(4.0, -1.0, -1.0), Vec3::new(6.0, 1.0, 1.0));
// Inside expanded AABB, so should return 0
let t = swept_sphere_vs_aabb(start, end, radius, &aabb).unwrap();
assert!(approx(t, 0.0));
}
#[test]
fn test_swept_sphere_beyond_range() {
let start = Vec3::new(-10.0, 0.0, 0.0);
let end = Vec3::new(-5.0, 0.0, 0.0);
let radius = 0.5;
let aabb = AABB::new(Vec3::new(4.0, -1.0, -1.0), Vec3::new(6.0, 1.0, 1.0));
// AABB is at x=4..6, moving from -10 to -5 won't reach it
assert!(swept_sphere_vs_aabb(start, end, radius, &aabb).is_none());
}
}