From 534838b7b956a19908583690ecc700b5b6c5ad59 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Wed, 25 Mar 2026 18:25:20 +0900 Subject: [PATCH] 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) --- crates/voltex_physics/src/collider.rs | 13 ++++++ crates/voltex_physics/src/ray.rs | 64 +++++++++++++++++++++++++++ crates/voltex_physics/src/raycast.rs | 3 ++ 3 files changed, 80 insertions(+) diff --git a/crates/voltex_physics/src/collider.rs b/crates/voltex_physics/src/collider.rs index faaebf2..668d8a0 100644 --- a/crates/voltex_physics/src/collider.rs +++ b/crates/voltex_physics/src/collider.rs @@ -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)); + } } diff --git a/crates/voltex_physics/src/ray.rs b/crates/voltex_physics/src/ray.rs index 0e89a8d..88fba32 100644 --- a/crates/voltex_physics/src/ray.rs +++ b/crates/voltex_physics/src/ray.rs @@ -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::*; diff --git a/crates/voltex_physics/src/raycast.rs b/crates/voltex_physics/src/raycast.rs index 94775d4..c0a7219 100644 --- a/crates/voltex_physics/src/raycast.rs +++ b/crates/voltex_physics/src/raycast.rs @@ -50,6 +50,9 @@ pub fn raycast(world: &World, ray: &Ray, max_dist: f32) -> Option { 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 {