From d93a76da682f8d65dea12bd4ebe3eb67ac8b09aa Mon Sep 17 00:00:00 2001 From: Zach Loafman Date: Thu, 9 Apr 2026 19:42:28 -0700 Subject: [PATCH] feat: add Bvh::cast_ray_best for SIMD-accelerated generic ray queries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cast_local_ray_and_get_normal on CompositeShapeRef was calling find_best() directly with the non-SIMD BvhNode::cast_ray for AABB tests, missing the SIMD-accelerated path available on f32/dim3 builds. Add Bvh::cast_ray_best — a generic version of cast_ray that returns any BvhLeafCost type (not just Real) with proper SIMD/non-SIMD dispatch. Change cast_local_ray_and_get_normal to use it. This means TriMesh::cast_local_ray_and_get_normal (and all composite shapes using this code path) now benefits from SIMD AABB tests on supported platforms, matching the existing cast_local_ray behavior. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/partitioning/bvh/bvh_queries.rs | 72 +++++++++++++++++++++++++++- src/query/ray/ray_composite_shape.rs | 11 ++--- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/partitioning/bvh/bvh_queries.rs b/src/partitioning/bvh/bvh_queries.rs index 5b915a92..81d4daf2 100644 --- a/src/partitioning/bvh/bvh_queries.rs +++ b/src/partitioning/bvh/bvh_queries.rs @@ -1,4 +1,4 @@ -use super::{Bvh, BvhNode}; +use super::{Bvh, BvhLeafCost, BvhNode}; use crate::bounding_volume::{Aabb, BoundingVolume}; use crate::math::Real; use crate::math::Vector; @@ -250,6 +250,76 @@ impl Bvh { ) } + /// Like [`cast_ray`](Self::cast_ray), but returns an arbitrary [`BvhLeafCost`] + /// type from the leaf test rather than just [`Real`]. + /// + /// This lets callers collect richer per-hit data (e.g., a `RayIntersection` + /// which includes the surface normal) while still benefiting from + /// SIMD-accelerated AABB tests on supported platforms. + /// + /// # Example + /// + /// ``` + /// # #[cfg(all(feature = "dim3", feature = "f32"))] { + /// use parry3d::math::Vector; + /// use parry3d::query::{Ray, RayCast, RayIntersection}; + /// use parry3d::shape::TriMesh; + /// + /// let vertices = vec![ + /// Vector::new(-1.0, -1.0, 0.0), + /// Vector::new(1.0, -1.0, 0.0), + /// Vector::new(1.0, 1.0, 0.0), + /// Vector::new(-1.0, 1.0, 0.0), + /// ]; + /// let mesh = TriMesh::new(vertices, vec![[0, 1, 2], [0, 2, 3]]).unwrap(); + /// let ray = Ray::new(Vector::new(0.5, 0.3, -2.0), Vector::new(0.0, 0.0, 1.0)); + /// + /// // cast_ray_best returns RayIntersection (with normal) instead of just Real: + /// let hit = mesh.bvh().cast_ray_best(&ray, f32::MAX, |leaf_id, _best| { + /// let tri = mesh.triangle(leaf_id); + /// tri.cast_local_ray_and_get_normal(&ray, f32::MAX, false) + /// }); + /// + /// assert!(hit.is_some()); + /// let (_triangle_index, intersection) = hit.unwrap(); + /// assert!((intersection.time_of_impact - 2.0).abs() < 1e-5); + /// # } + /// ``` + #[cfg(not(all(feature = "simd-is-enabled", feature = "dim3", feature = "f32")))] + pub fn cast_ray_best( + &self, + ray: &Ray, + max_time_of_impact: Real, + primitive_check: impl Fn(u32, Real) -> Option, + ) -> Option<(u32, L)> { + self.find_best( + max_time_of_impact, + |node: &BvhNode, best_so_far| node.cast_ray(ray, best_so_far), + primitive_check, + ) + } + + /// Like [`cast_ray`](Self::cast_ray), but returns an arbitrary [`BvhLeafCost`] + /// type from the leaf test rather than just [`Real`]. + /// + /// This lets callers collect richer per-hit data (e.g., a `RayIntersection` + /// which includes the surface normal) while still benefiting from + /// SIMD-accelerated AABB tests on supported platforms. + #[cfg(all(feature = "simd-is-enabled", feature = "dim3", feature = "f32"))] + pub fn cast_ray_best( + &self, + ray: &Ray, + max_time_of_impact: Real, + primitive_check: impl Fn(u32, Real) -> Option, + ) -> Option<(u32, L)> { + let simd_inv_ray = SimdInvRay::from(*ray); + self.find_best( + max_time_of_impact, + |node: &BvhNode, _best_so_far| node.cast_inv_ray_simd(&simd_inv_ray), + primitive_check, + ) + } + /// Casts a ray on this BVH using the provided leaf ray-cast function. /// /// The `primitive_check` delegates the ray-casting task to an external function that diff --git a/src/query/ray/ray_composite_shape.rs b/src/query/ray/ray_composite_shape.rs index 82018592..56ad808c 100644 --- a/src/query/ray/ray_composite_shape.rs +++ b/src/query/ray/ray_composite_shape.rs @@ -1,5 +1,4 @@ use crate::math::Real; -use crate::partitioning::BvhNode; use crate::query::{Ray, RayCast, RayIntersection}; use crate::shape::{CompositeShapeRef, Compound, Polyline, TypedCompositeShape}; @@ -46,10 +45,9 @@ impl CompositeShapeRef<'_, S> { max_time_of_impact: Real, solid: bool, ) -> Option<(u32, RayIntersection)> { - self.0.bvh().find_best( - max_time_of_impact, - |node: &BvhNode, best_so_far| node.cast_ray(ray, best_so_far), - |primitive, best_so_far| { + self.0 + .bvh() + .cast_ray_best(ray, max_time_of_impact, |primitive, best_so_far| { self.0.map_typed_part_at(primitive, |pose, part, _| { if let Some(pose) = pose { part.cast_ray_and_get_normal(pose, ray, best_so_far, solid) @@ -57,8 +55,7 @@ impl CompositeShapeRef<'_, S> { part.cast_local_ray_and_get_normal(ray, best_so_far, solid) } })? - }, - ) + }) } }