diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs index d4ec97e1f3549..317310f48c48b 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs @@ -66,160 +66,134 @@ pub fn ray_mesh_intersection + Clone + Copy>( indices: Option<&[I]>, backface_culling: Backfaces, ) -> Option { - // The ray cast can hit the same mesh many times, so we need to track which hit is - // closest to the camera, and record that. - let mut closest_hit_distance = f32::MAX; - let mut closest_hit = None; - let world_to_mesh = mesh_transform.inverse(); - let mesh_space_ray = Ray3d::new( + let ray = Ray3d::new( world_to_mesh.transform_point3(ray.origin), Dir3::new(world_to_mesh.transform_vector3(*ray.direction)).ok()?, ); - if let Some(indices) = indices { + let closest_hit = if let Some(indices) = indices { // The index list must be a multiple of three. If not, the mesh is malformed and the raycast // result might be nonsensical. if indices.len() % 3 != 0 { return None; } - for triangle in indices.chunks_exact(3) { - let [a, b, c] = [ - triangle[0].try_into().ok()?, - triangle[1].try_into().ok()?, - triangle[2].try_into().ok()?, - ]; - - let triangle_index = Some(a); - let tri_vertex_positions = &[ - Vec3::from(positions[a]), - Vec3::from(positions[b]), - Vec3::from(positions[c]), - ]; - let tri_normals = vertex_normals.map(|normals| { - [ - Vec3::from(normals[a]), - Vec3::from(normals[b]), - Vec3::from(normals[c]), - ] - }); - - let Some(hit) = triangle_intersection( - tri_vertex_positions, - tri_normals.as_ref(), - closest_hit_distance, - &mesh_space_ray, - backface_culling, - ) else { - continue; - }; - - closest_hit = Some(RayMeshHit { - point: mesh_transform.transform_point3(hit.point), - normal: mesh_transform.transform_vector3(hit.normal), - barycentric_coords: hit.barycentric_coords, - distance: mesh_transform - .transform_vector3(mesh_space_ray.direction * hit.distance) - .length(), - triangle: hit.triangle.map(|tri| { - [ - mesh_transform.transform_point3(tri[0]), - mesh_transform.transform_point3(tri[1]), - mesh_transform.transform_point3(tri[2]), - ] - }), - triangle_index, - }); - closest_hit_distance = hit.distance; - } + indices + .chunks_exact(3) + .fold( + (f32::MAX, None), + |(closest_distance, closest_hit), triangle| { + let [Ok(a), Ok(b), Ok(c)] = [ + triangle[0].try_into(), + triangle[1].try_into(), + triangle[2].try_into(), + ] else { + return (closest_distance, closest_hit); + }; + + let tri_vertices = match [positions.get(a), positions.get(b), positions.get(c)] + { + [Some(a), Some(b), Some(c)] => { + [Vec3::from(*a), Vec3::from(*b), Vec3::from(*c)] + } + _ => return (closest_distance, closest_hit), + }; + + match ray_triangle_intersection(&ray, &tri_vertices, backface_culling) { + Some(hit) if hit.distance >= 0. && hit.distance < closest_distance => { + (hit.distance, Some((a, hit))) + } + _ => (closest_distance, closest_hit), + } + }, + ) + .1 } else { - for (i, triangle) in positions.chunks_exact(3).enumerate() { - let &[a, b, c] = triangle else { - continue; - }; - let triangle_index = Some(i); - let tri_vertex_positions = &[Vec3::from(a), Vec3::from(b), Vec3::from(c)]; - let tri_normals = vertex_normals.map(|normals| { - [ - Vec3::from(normals[i]), - Vec3::from(normals[i + 1]), - Vec3::from(normals[i + 2]), - ] - }); - - let Some(hit) = triangle_intersection( - tri_vertex_positions, - tri_normals.as_ref(), - closest_hit_distance, - &mesh_space_ray, - backface_culling, - ) else { - continue; - }; - - closest_hit = Some(RayMeshHit { - point: mesh_transform.transform_point3(hit.point), - normal: mesh_transform.transform_vector3(hit.normal), - barycentric_coords: hit.barycentric_coords, - distance: mesh_transform - .transform_vector3(mesh_space_ray.direction * hit.distance) - .length(), - triangle: hit.triangle.map(|tri| { - [ - mesh_transform.transform_point3(tri[0]), - mesh_transform.transform_point3(tri[1]), - mesh_transform.transform_point3(tri[2]), - ] - }), - triangle_index, - }); - closest_hit_distance = hit.distance; - } - } + positions + .chunks_exact(3) + .enumerate() + .fold( + (f32::MAX, None), + |(closest_distance, closest_hit), (tri_idx, triangle)| { + let tri_vertices = [ + Vec3::from(triangle[0]), + Vec3::from(triangle[1]), + Vec3::from(triangle[2]), + ]; + + match ray_triangle_intersection(&ray, &tri_vertices, backface_culling) { + Some(hit) if hit.distance >= 0. && hit.distance < closest_distance => { + (hit.distance, Some((tri_idx, hit))) + } + _ => (closest_distance, closest_hit), + } + }, + ) + .1 + }; - closest_hit -} + closest_hit.and_then(|(tri_idx, hit)| { + let [a, b, c] = match indices { + Some(indices) => { + let triangle = indices.get((tri_idx * 3)..(tri_idx * 3 + 3))?; -fn triangle_intersection( - tri_vertices: &[Vec3; 3], - tri_normals: Option<&[Vec3; 3]>, - max_distance: f32, - ray: &Ray3d, - backface_culling: Backfaces, -) -> Option { - let hit = ray_triangle_intersection(ray, tri_vertices, backface_culling)?; + let [Ok(a), Ok(b), Ok(c)] = [ + triangle[0].try_into(), + triangle[1].try_into(), + triangle[2].try_into(), + ] else { + return None; + }; - if hit.distance < 0.0 || hit.distance > max_distance { - return None; - }; + [a, b, c] + } + None => [tri_idx * 3, tri_idx * 3 + 1, tri_idx * 3 + 2], + }; - let point = ray.get_point(hit.distance); - let u = hit.barycentric_coords.0; - let v = hit.barycentric_coords.1; - let w = 1.0 - u - v; - let barycentric = Vec3::new(u, v, w); + let tri_vertices = match [positions.get(a), positions.get(b), positions.get(c)] { + [Some(a), Some(b), Some(c)] => [Vec3::from(*a), Vec3::from(*b), Vec3::from(*c)], + _ => return None, + }; - let normal = if let Some(normals) = tri_normals { - normals[1] * u + normals[2] * v + normals[0] * w - } else { - (tri_vertices[1] - tri_vertices[0]) - .cross(tri_vertices[2] - tri_vertices[0]) - .normalize() - }; - - Some(RayMeshHit { - point, - normal, - barycentric_coords: barycentric, - distance: hit.distance, - triangle: Some(*tri_vertices), - triangle_index: None, + let tri_normals = vertex_normals.and_then(|normals| { + let [Some(a), Some(b), Some(c)] = [normals.get(a), normals.get(b), normals.get(c)] + else { + return None; + }; + Some([Vec3::from(*a), Vec3::from(*b), Vec3::from(*c)]) + }); + + let point = ray.get_point(hit.distance); + let u = hit.barycentric_coords.0; + let v = hit.barycentric_coords.1; + let w = 1.0 - u - v; + let barycentric = Vec3::new(u, v, w); + + let normal = if let Some(normals) = tri_normals { + normals[1] * u + normals[2] * v + normals[0] * w + } else { + (tri_vertices[1] - tri_vertices[0]) + .cross(tri_vertices[2] - tri_vertices[0]) + .normalize() + }; + + Some(RayMeshHit { + point: mesh_transform.transform_point3(point), + normal: mesh_transform.transform_vector3(normal), + barycentric_coords: barycentric, + distance: mesh_transform + .transform_vector3(ray.direction * hit.distance) + .length(), + triangle: Some(tri_vertices.map(|v| mesh_transform.transform_point3(v))), + triangle_index: Some(a), + }) }) } /// Takes a ray and triangle and computes the intersection. +#[inline] fn ray_triangle_intersection( ray: &Ray3d, triangle: &[Vec3; 3], @@ -313,6 +287,7 @@ pub fn ray_aabb_intersection_3d(ray: Ray3d, aabb: &Aabb3d, model_to_world: &Mat4 #[cfg(test)] mod tests { use bevy_math::Vec3; + use bevy_transform::components::GlobalTransform; use super::*; @@ -336,4 +311,174 @@ mod tests { let result = ray_triangle_intersection(&ray, &triangle, Backfaces::Cull); assert!(result.is_none()); } + + #[test] + fn ray_mesh_intersection_simple() { + let ray = Ray3d::new(Vec3::ZERO, Dir3::X); + let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let positions = &[V0, V1, V2]; + let vertex_normals = None; + let indices: Option<&[u16]> = None; + let backface_culling = Backfaces::Cull; + + let result = ray_mesh_intersection( + ray, + &mesh_transform, + positions, + vertex_normals, + indices, + backface_culling, + ); + + assert!(result.is_some()); + } + + #[test] + fn ray_mesh_intersection_indices() { + let ray = Ray3d::new(Vec3::ZERO, Dir3::X); + let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let positions = &[V0, V1, V2]; + let vertex_normals = None; + let indices: Option<&[u16]> = Some(&[0, 1, 2]); + let backface_culling = Backfaces::Cull; + + let result = ray_mesh_intersection( + ray, + &mesh_transform, + positions, + vertex_normals, + indices, + backface_culling, + ); + + assert!(result.is_some()); + } + + #[test] + fn ray_mesh_intersection_indices_vertex_normals() { + let ray = Ray3d::new(Vec3::ZERO, Dir3::X); + let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let positions = &[V0, V1, V2]; + let vertex_normals: Option<&[[f32; 3]]> = + Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]); + let indices: Option<&[u16]> = Some(&[0, 1, 2]); + let backface_culling = Backfaces::Cull; + + let result = ray_mesh_intersection( + ray, + &mesh_transform, + positions, + vertex_normals, + indices, + backface_culling, + ); + + assert!(result.is_some()); + } + + #[test] + fn ray_mesh_intersection_vertex_normals() { + let ray = Ray3d::new(Vec3::ZERO, Dir3::X); + let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let positions = &[V0, V1, V2]; + let vertex_normals: Option<&[[f32; 3]]> = + Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]); + let indices: Option<&[u16]> = None; + let backface_culling = Backfaces::Cull; + + let result = ray_mesh_intersection( + ray, + &mesh_transform, + positions, + vertex_normals, + indices, + backface_culling, + ); + + assert!(result.is_some()); + } + + #[test] + fn ray_mesh_intersection_missing_vertex_normals() { + let ray = Ray3d::new(Vec3::ZERO, Dir3::X); + let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let positions = &[V0, V1, V2]; + let vertex_normals: Option<&[[f32; 3]]> = Some(&[]); + let indices: Option<&[u16]> = None; + let backface_culling = Backfaces::Cull; + + let result = ray_mesh_intersection( + ray, + &mesh_transform, + positions, + vertex_normals, + indices, + backface_culling, + ); + + assert!(result.is_some()); + } + + #[test] + fn ray_mesh_intersection_indices_missing_vertex_normals() { + let ray = Ray3d::new(Vec3::ZERO, Dir3::X); + let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let positions = &[V0, V1, V2]; + let vertex_normals: Option<&[[f32; 3]]> = Some(&[]); + let indices: Option<&[u16]> = Some(&[0, 1, 2]); + let backface_culling = Backfaces::Cull; + + let result = ray_mesh_intersection( + ray, + &mesh_transform, + positions, + vertex_normals, + indices, + backface_culling, + ); + + assert!(result.is_some()); + } + + #[test] + fn ray_mesh_intersection_not_enough_indices() { + let ray = Ray3d::new(Vec3::ZERO, Dir3::X); + let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let positions = &[V0, V1, V2]; + let vertex_normals = None; + let indices: Option<&[u16]> = Some(&[0]); + let backface_culling = Backfaces::Cull; + + let result = ray_mesh_intersection( + ray, + &mesh_transform, + positions, + vertex_normals, + indices, + backface_culling, + ); + + assert!(result.is_none()); + } + + #[test] + fn ray_mesh_intersection_bad_indices() { + let ray = Ray3d::new(Vec3::ZERO, Dir3::X); + let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let positions = &[V0, V1, V2]; + let vertex_normals = None; + let indices: Option<&[u16]> = Some(&[0, 1, 3]); + let backface_culling = Backfaces::Cull; + + let result = ray_mesh_intersection( + ray, + &mesh_transform, + positions, + vertex_normals, + indices, + backface_culling, + ); + + assert!(result.is_none()); + } }