From 1b5da4d0d5aa008d3575dacb5afa6917f0e434d7 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Thu, 26 Mar 2026 15:48:57 +0900 Subject: [PATCH] feat(physics): add ray vs mesh raycasting with MeshCollider Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/voltex_physics/src/lib.rs | 4 +- crates/voltex_physics/src/mesh_collider.rs | 143 +++++++++++++++++++++ 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 crates/voltex_physics/src/mesh_collider.rs diff --git a/crates/voltex_physics/src/lib.rs b/crates/voltex_physics/src/lib.rs index d549fd5..84e4e31 100644 --- a/crates/voltex_physics/src/lib.rs +++ b/crates/voltex_physics/src/lib.rs @@ -10,9 +10,10 @@ pub mod integrator; pub mod solver; pub mod raycast; pub mod ccd; +pub mod mesh_collider; pub use bvh::BvhTree; -pub use collider::Collider; +pub use collider::{Collider, ConvexHull}; pub use contact::ContactPoint; pub use collision::detect_collisions; pub use rigid_body::{RigidBody, PhysicsConfig}; @@ -21,3 +22,4 @@ pub use solver::{resolve_collisions, physics_step}; pub use raycast::{RayHit, raycast, raycast_all}; pub use ray::ray_vs_triangle; pub use ccd::swept_sphere_vs_aabb; +pub use mesh_collider::{MeshCollider, MeshHit, ray_vs_mesh, ray_vs_mesh_all}; diff --git a/crates/voltex_physics/src/mesh_collider.rs b/crates/voltex_physics/src/mesh_collider.rs new file mode 100644 index 0000000..ddeabbf --- /dev/null +++ b/crates/voltex_physics/src/mesh_collider.rs @@ -0,0 +1,143 @@ +use voltex_math::{Vec3, Ray}; +use crate::ray::ray_vs_triangle; + +/// A triangle mesh collider defined by vertices and triangle indices. +#[derive(Debug, Clone)] +pub struct MeshCollider { + pub vertices: Vec, + pub indices: Vec<[u32; 3]>, +} + +/// Result of a ray-mesh intersection test. +#[derive(Debug, Clone, Copy)] +pub struct MeshHit { + pub distance: f32, + pub point: Vec3, + pub normal: Vec3, + pub triangle_index: usize, +} + +/// Cast a ray against a triangle mesh. Returns the closest hit, if any. +pub fn ray_vs_mesh(ray: &Ray, mesh: &MeshCollider) -> Option { + let mut closest: Option = None; + + for (i, tri) in mesh.indices.iter().enumerate() { + let v0 = mesh.vertices[tri[0] as usize]; + let v1 = mesh.vertices[tri[1] as usize]; + let v2 = mesh.vertices[tri[2] as usize]; + + if let Some((t, normal)) = ray_vs_triangle(ray, v0, v1, v2) { + let is_closer = closest.as_ref().map_or(true, |c| t < c.distance); + if is_closer { + let point = ray.at(t); + closest = Some(MeshHit { + distance: t, + point, + normal, + triangle_index: i, + }); + } + } + } + + closest +} + +/// Cast a ray against a triangle mesh. Returns all hits sorted by distance. +pub fn ray_vs_mesh_all(ray: &Ray, mesh: &MeshCollider) -> Vec { + let mut hits = Vec::new(); + + for (i, tri) in mesh.indices.iter().enumerate() { + let v0 = mesh.vertices[tri[0] as usize]; + let v1 = mesh.vertices[tri[1] as usize]; + let v2 = mesh.vertices[tri[2] as usize]; + + if let Some((t, normal)) = ray_vs_triangle(ray, v0, v1, v2) { + let point = ray.at(t); + hits.push(MeshHit { + distance: t, + point, + normal, + triangle_index: i, + }); + } + } + + hits.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap()); + hits +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ray_vs_mesh_hit() { + // Simple quad (2 triangles) + let mesh = MeshCollider { + vertices: vec![ + Vec3::new(-1.0, 0.0, -1.0), + Vec3::new(1.0, 0.0, -1.0), + Vec3::new(1.0, 0.0, 1.0), + Vec3::new(-1.0, 0.0, 1.0), + ], + indices: vec![[0, 1, 2], [0, 2, 3]], + }; + let ray = Ray::new(Vec3::new(0.0, 1.0, 0.0), Vec3::new(0.0, -1.0, 0.0)); + let hit = ray_vs_mesh(&ray, &mesh); + assert!(hit.is_some()); + assert!((hit.unwrap().distance - 1.0).abs() < 0.01); + } + + #[test] + fn test_ray_vs_mesh_miss() { + let mesh = MeshCollider { + vertices: vec![ + Vec3::new(-1.0, 0.0, -1.0), + Vec3::new(1.0, 0.0, -1.0), + Vec3::new(0.0, 0.0, 1.0), + ], + indices: vec![[0, 1, 2]], + }; + let ray = Ray::new(Vec3::new(5.0, 1.0, 0.0), Vec3::new(0.0, -1.0, 0.0)); + assert!(ray_vs_mesh(&ray, &mesh).is_none()); + } + + #[test] + fn test_ray_vs_mesh_closest() { + // Two triangles at different heights + let mesh = MeshCollider { + vertices: vec![ + Vec3::new(-1.0, 0.0, -1.0), + Vec3::new(1.0, 0.0, -1.0), + Vec3::new(0.0, 0.0, 1.0), + Vec3::new(-1.0, 2.0, -1.0), + Vec3::new(1.0, 2.0, -1.0), + Vec3::new(0.0, 2.0, 1.0), + ], + indices: vec![[0, 1, 2], [3, 4, 5]], + }; + let ray = Ray::new(Vec3::new(0.0, 5.0, 0.0), Vec3::new(0.0, -1.0, 0.0)); + let hit = ray_vs_mesh(&ray, &mesh).unwrap(); + assert!((hit.distance - 3.0).abs() < 0.01); // hits y=2 first + } + + #[test] + fn test_ray_vs_mesh_all() { + let mesh = MeshCollider { + vertices: vec![ + Vec3::new(-1.0, 0.0, -1.0), + Vec3::new(1.0, 0.0, -1.0), + Vec3::new(0.0, 0.0, 1.0), + Vec3::new(-1.0, 2.0, -1.0), + Vec3::new(1.0, 2.0, -1.0), + Vec3::new(0.0, 2.0, 1.0), + ], + indices: vec![[0, 1, 2], [3, 4, 5]], + }; + let ray = Ray::new(Vec3::new(0.0, 5.0, 0.0), Vec3::new(0.0, -1.0, 0.0)); + let hits = ray_vs_mesh_all(&ray, &mesh); + assert_eq!(hits.len(), 2); + assert!(hits[0].distance < hits[1].distance); // sorted + } +}