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 {
|
pub enum Collider {
|
||||||
Sphere { radius: f32 },
|
Sphere { radius: f32 },
|
||||||
Box { half_extents: Vec3 },
|
Box { half_extents: Vec3 },
|
||||||
|
Capsule { radius: f32, half_height: f32 },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collider {
|
impl Collider {
|
||||||
@@ -16,6 +17,10 @@ impl Collider {
|
|||||||
Collider::Box { half_extents } => {
|
Collider::Box { half_extents } => {
|
||||||
AABB::new(position - *half_extents, position + *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.min, Vec3::new(-1.0, -2.0, -3.0));
|
||||||
assert_eq!(aabb.max, 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))
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ pub fn raycast(world: &World, ray: &Ray, max_dist: f32) -> Option<RayHit> {
|
|||||||
Collider::Box { half_extents } => {
|
Collider::Box { half_extents } => {
|
||||||
ray_tests::ray_vs_box(ray, *pos, *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 {
|
if let Some((t, normal)) = result {
|
||||||
|
|||||||
Reference in New Issue
Block a user