feat(physics): add Capsule collider variant
Add Capsule { radius, half_height } to the Collider enum, with AABB
computation and ray-vs-capsule intersection support. The capsule is
oriented along the Y axis (cylinder + two hemisphere caps).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ use voltex_math::{Vec3, AABB};
|
||||
pub enum Collider {
|
||||
Sphere { radius: f32 },
|
||||
Box { half_extents: Vec3 },
|
||||
Capsule { radius: f32, half_height: f32 },
|
||||
}
|
||||
|
||||
impl Collider {
|
||||
@@ -16,6 +17,10 @@ impl Collider {
|
||||
Collider::Box { half_extents } => {
|
||||
AABB::new(position - *half_extents, position + *half_extents)
|
||||
}
|
||||
Collider::Capsule { radius, half_height } => {
|
||||
let r = Vec3::new(*radius, *half_height + *radius, *radius);
|
||||
AABB::new(position - r, position + r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,4 +44,12 @@ mod tests {
|
||||
assert_eq!(aabb.min, Vec3::new(-1.0, -2.0, -3.0));
|
||||
assert_eq!(aabb.max, Vec3::new(1.0, 2.0, 3.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capsule_aabb() {
|
||||
let c = Collider::Capsule { radius: 0.5, half_height: 1.0 };
|
||||
let aabb = c.aabb(Vec3::new(1.0, 2.0, 3.0));
|
||||
assert_eq!(aabb.min, Vec3::new(0.5, 0.5, 2.5));
|
||||
assert_eq!(aabb.max, Vec3::new(1.5, 3.5, 3.5));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,70 @@ pub fn ray_vs_box(ray: &Ray, center: Vec3, half_extents: Vec3) -> Option<(f32, V
|
||||
Some((t, normal))
|
||||
}
|
||||
|
||||
/// Ray vs Capsule (Y-axis aligned). Returns (t, normal) or None.
|
||||
/// Capsule = cylinder of given half_height along Y + hemisphere caps of given radius.
|
||||
pub fn ray_vs_capsule(ray: &Ray, center: Vec3, radius: f32, half_height: f32) -> Option<(f32, Vec3)> {
|
||||
// Test the two hemisphere caps
|
||||
let top_center = Vec3::new(center.x, center.y + half_height, center.z);
|
||||
let bot_center = Vec3::new(center.x, center.y - half_height, center.z);
|
||||
|
||||
let mut best: Option<(f32, Vec3)> = None;
|
||||
|
||||
// Test infinite cylinder (ignore Y component for the 2D circle test)
|
||||
// Ray in XZ plane relative to capsule center
|
||||
let ox = ray.origin.x - center.x;
|
||||
let oz = ray.origin.z - center.z;
|
||||
let dx = ray.direction.x;
|
||||
let dz = ray.direction.z;
|
||||
|
||||
let a = dx * dx + dz * dz;
|
||||
let b = 2.0 * (ox * dx + oz * dz);
|
||||
let c = ox * ox + oz * oz - radius * radius;
|
||||
|
||||
if a > 1e-10 {
|
||||
let disc = b * b - 4.0 * a * c;
|
||||
if disc >= 0.0 {
|
||||
let sqrt_d = disc.sqrt();
|
||||
for sign in [-1.0f32, 1.0] {
|
||||
let t = (-b + sign * sqrt_d) / (2.0 * a);
|
||||
if t >= 0.0 {
|
||||
let hit_y = ray.origin.y + ray.direction.y * t;
|
||||
// Check if hit is within the cylinder portion
|
||||
if hit_y >= center.y - half_height && hit_y <= center.y + half_height {
|
||||
let hit = ray.at(t);
|
||||
let normal = Vec3::new(hit.x - center.x, 0.0, hit.z - center.z).normalize();
|
||||
if best.is_none() || t < best.unwrap().0 {
|
||||
best = Some((t, normal));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test top hemisphere
|
||||
if let Some((t, normal)) = ray_vs_sphere(ray, top_center, radius) {
|
||||
let hit = ray.at(t);
|
||||
if hit.y >= center.y + half_height {
|
||||
if best.is_none() || t < best.unwrap().0 {
|
||||
best = Some((t, normal));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test bottom hemisphere
|
||||
if let Some((t, normal)) = ray_vs_sphere(ray, bot_center, radius) {
|
||||
let hit = ray.at(t);
|
||||
if hit.y <= center.y - half_height {
|
||||
if best.is_none() || t < best.unwrap().0 {
|
||||
best = Some((t, normal));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
best
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -50,6 +50,9 @@ pub fn raycast(world: &World, ray: &Ray, max_dist: f32) -> Option<RayHit> {
|
||||
Collider::Box { half_extents } => {
|
||||
ray_tests::ray_vs_box(ray, *pos, *half_extents)
|
||||
}
|
||||
Collider::Capsule { radius, half_height } => {
|
||||
ray_tests::ray_vs_capsule(ray, *pos, *radius, *half_height)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some((t, normal)) = result {
|
||||
|
||||
Reference in New Issue
Block a user