Path: blob/main/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs
6600 views
use bevy_math::{bounding::Aabb3d, Affine3A, Dir3, Ray3d, Vec2, Vec3, Vec3A};1use bevy_mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues};2use bevy_reflect::Reflect;34use super::Backfaces;56/// Hit data for an intersection between a ray and a mesh.7#[derive(Debug, Clone, Reflect)]8#[reflect(Clone)]9pub struct RayMeshHit {10/// The point of intersection in world space.11pub point: Vec3,12/// The normal vector of the triangle at the point of intersection. Not guaranteed to be normalized for scaled meshes.13pub normal: Vec3,14/// The barycentric coordinates of the intersection.15pub barycentric_coords: Vec3,16/// The distance from the ray origin to the intersection point.17pub distance: f32,18/// The vertices of the triangle that was hit.19pub triangle: Option<[Vec3; 3]>,20/// UV coordinate of the hit, if the mesh has UV attributes.21pub uv: Option<Vec2>,22/// The index of the triangle that was hit.23pub triangle_index: Option<usize>,24}2526/// Hit data for an intersection between a ray and a triangle.27#[derive(Default, Debug)]28pub struct RayTriangleHit {29pub distance: f32,30/// Note this uses the convention from the Moller-Trumbore algorithm:31/// P = (1 - u - v)A + uB + vC32/// This is different from the more common convention of33/// P = uA + vB + (1 - u - v)C34pub barycentric_coords: (f32, f32),35}3637/// Casts a ray on a mesh, and returns the intersection.38pub(super) fn ray_intersection_over_mesh(39mesh: &Mesh,40transform: &Affine3A,41ray: Ray3d,42cull: Backfaces,43) -> Option<RayMeshHit> {44if mesh.primitive_topology() != PrimitiveTopology::TriangleList {45return None; // ray_mesh_intersection assumes vertices are laid out in a triangle list46}47// Vertex positions are required48let positions = mesh.attribute(Mesh::ATTRIBUTE_POSITION)?.as_float3()?;4950// Normals are optional51let normals = mesh52.attribute(Mesh::ATTRIBUTE_NORMAL)53.and_then(|normal_values| normal_values.as_float3());5455let uvs = mesh56.attribute(Mesh::ATTRIBUTE_UV_0)57.and_then(|uvs| match uvs {58VertexAttributeValues::Float32x2(uvs) => Some(uvs.as_slice()),59_ => None,60});6162match mesh.indices() {63Some(Indices::U16(indices)) => {64ray_mesh_intersection(ray, transform, positions, normals, Some(indices), uvs, cull)65}66Some(Indices::U32(indices)) => {67ray_mesh_intersection(ray, transform, positions, normals, Some(indices), uvs, cull)68}69None => ray_mesh_intersection::<usize>(ray, transform, positions, normals, None, uvs, cull),70}71}7273/// Checks if a ray intersects a mesh, and returns the nearest intersection if one exists.74pub fn ray_mesh_intersection<I>(75ray: Ray3d,76mesh_transform: &Affine3A,77positions: &[[f32; 3]],78vertex_normals: Option<&[[f32; 3]]>,79indices: Option<&[I]>,80uvs: Option<&[[f32; 2]]>,81backface_culling: Backfaces,82) -> Option<RayMeshHit>83where84I: TryInto<usize> + Clone + Copy,85{86let world_to_mesh = mesh_transform.inverse();8788let ray = Ray3d::new(89world_to_mesh.transform_point3(ray.origin),90Dir3::new(world_to_mesh.transform_vector3(*ray.direction)).ok()?,91);9293let closest_hit = if let Some(indices) = indices {94// The index list must be a multiple of three. If not, the mesh is malformed and the raycast95// result might be nonsensical.96if indices.len() % 3 != 0 {97return None;98}99100indices101.chunks_exact(3)102.enumerate()103.fold(104(f32::MAX, None),105|(closest_distance, closest_hit), (tri_idx, triangle)| {106let [Ok(a), Ok(b), Ok(c)] = [107triangle[0].try_into(),108triangle[1].try_into(),109triangle[2].try_into(),110] else {111return (closest_distance, closest_hit);112};113114let tri_vertices = match [positions.get(a), positions.get(b), positions.get(c)]115{116[Some(a), Some(b), Some(c)] => {117[Vec3::from(*a), Vec3::from(*b), Vec3::from(*c)]118}119_ => return (closest_distance, closest_hit),120};121122match ray_triangle_intersection(&ray, &tri_vertices, backface_culling) {123Some(hit) if hit.distance >= 0. && hit.distance < closest_distance => {124(hit.distance, Some((tri_idx, hit)))125}126_ => (closest_distance, closest_hit),127}128},129)130.1131} else {132positions133.chunks_exact(3)134.enumerate()135.fold(136(f32::MAX, None),137|(closest_distance, closest_hit), (tri_idx, triangle)| {138let tri_vertices = [139Vec3::from(triangle[0]),140Vec3::from(triangle[1]),141Vec3::from(triangle[2]),142];143144match ray_triangle_intersection(&ray, &tri_vertices, backface_culling) {145Some(hit) if hit.distance >= 0. && hit.distance < closest_distance => {146(hit.distance, Some((tri_idx, hit)))147}148_ => (closest_distance, closest_hit),149}150},151)152.1153};154155closest_hit.and_then(|(tri_idx, hit)| {156let [a, b, c] = match indices {157Some(indices) => {158let [i, j, k] = [tri_idx * 3, tri_idx * 3 + 1, tri_idx * 3 + 2];159[160indices.get(i).copied()?.try_into().ok()?,161indices.get(j).copied()?.try_into().ok()?,162indices.get(k).copied()?.try_into().ok()?,163]164}165None => [tri_idx * 3, tri_idx * 3 + 1, tri_idx * 3 + 2],166};167168let tri_vertices = match [positions.get(a), positions.get(b), positions.get(c)] {169[Some(a), Some(b), Some(c)] => [Vec3::from(*a), Vec3::from(*b), Vec3::from(*c)],170_ => return None,171};172173let tri_normals = vertex_normals.and_then(|normals| {174let [Some(a), Some(b), Some(c)] = [normals.get(a), normals.get(b), normals.get(c)]175else {176return None;177};178Some([Vec3::from(*a), Vec3::from(*b), Vec3::from(*c)])179});180181let point = ray.get_point(hit.distance);182// Note that we need to convert from the Möller-Trumbore convention to the more common183// P = uA + vB + (1 - u - v)C convention.184let u = hit.barycentric_coords.0;185let v = hit.barycentric_coords.1;186let w = 1.0 - u - v;187let barycentric = Vec3::new(w, u, v);188189let normal = if let Some(normals) = tri_normals {190normals[1] * u + normals[2] * v + normals[0] * w191} else {192(tri_vertices[1] - tri_vertices[0])193.cross(tri_vertices[2] - tri_vertices[0])194.normalize()195};196197let uv = uvs.and_then(|uvs| {198let tri_uvs = if let Some(indices) = indices {199let i = tri_idx * 3;200[201uvs[indices[i].try_into().ok()?],202uvs[indices[i + 1].try_into().ok()?],203uvs[indices[i + 2].try_into().ok()?],204]205} else {206let i = tri_idx * 3;207[uvs[i], uvs[i + 1], uvs[i + 2]]208};209Some(210barycentric.x * Vec2::from(tri_uvs[0])211+ barycentric.y * Vec2::from(tri_uvs[1])212+ barycentric.z * Vec2::from(tri_uvs[2]),213)214});215216Some(RayMeshHit {217point: mesh_transform.transform_point3(point),218normal: mesh_transform.transform_vector3(normal),219uv,220barycentric_coords: barycentric,221distance: mesh_transform222.transform_vector3(ray.direction * hit.distance)223.length(),224triangle: Some(tri_vertices.map(|v| mesh_transform.transform_point3(v))),225triangle_index: Some(tri_idx),226})227})228}229230/// Takes a ray and triangle and computes the intersection.231#[inline]232fn ray_triangle_intersection(233ray: &Ray3d,234triangle: &[Vec3; 3],235backface_culling: Backfaces,236) -> Option<RayTriangleHit> {237// Source: https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-rendering-a-triangle/moller-trumbore-ray-triangle-intersection238let vector_v0_to_v1: Vec3 = triangle[1] - triangle[0];239let vector_v0_to_v2: Vec3 = triangle[2] - triangle[0];240let p_vec: Vec3 = ray.direction.cross(vector_v0_to_v2);241let determinant: f32 = vector_v0_to_v1.dot(p_vec);242243match backface_culling {244Backfaces::Cull => {245// if the determinant is negative the triangle is back facing246// if the determinant is close to 0, the ray misses the triangle247// This test checks both cases248if determinant < f32::EPSILON {249return None;250}251}252Backfaces::Include => {253// ray and triangle are parallel if det is close to 0254if determinant.abs() < f32::EPSILON {255return None;256}257}258}259260let determinant_inverse = 1.0 / determinant;261262let t_vec = ray.origin - triangle[0];263let u = t_vec.dot(p_vec) * determinant_inverse;264if !(0.0..=1.0).contains(&u) {265return None;266}267268let q_vec = t_vec.cross(vector_v0_to_v1);269let v = (*ray.direction).dot(q_vec) * determinant_inverse;270if v < 0.0 || u + v > 1.0 {271return None;272}273274// The distance between ray origin and intersection is t.275let t: f32 = vector_v0_to_v2.dot(q_vec) * determinant_inverse;276277Some(RayTriangleHit {278distance: t,279barycentric_coords: (u, v),280})281}282283// TODO: It'd be nice to reuse `RayCast3d::aabb_intersection_at`, but it assumes a normalized ray.284// In our case, the ray is transformed to model space, which could involve scaling.285/// Checks if the ray intersects with the AABB of a mesh, returning the distance to the point of intersection.286/// The distance is zero if the ray starts inside the AABB.287pub fn ray_aabb_intersection_3d(288ray: Ray3d,289aabb: &Aabb3d,290model_to_world: &Affine3A,291) -> Option<f32> {292// Transform the ray to model space293let world_to_model = model_to_world.inverse();294let ray_direction: Vec3A = world_to_model.transform_vector3a((*ray.direction).into());295let ray_direction_recip = ray_direction.recip();296let ray_origin: Vec3A = world_to_model.transform_point3a(ray.origin.into());297298// Check if the ray intersects the mesh's AABB. It's useful to work in model space299// because we can do an AABB intersection test, instead of an OBB intersection test.300301// NOTE: This is largely copied from `RayCast3d::aabb_intersection_at`.302let positive = ray_direction.signum().cmpgt(Vec3A::ZERO);303let min = Vec3A::select(positive, aabb.min, aabb.max);304let max = Vec3A::select(positive, aabb.max, aabb.min);305306// Calculate the minimum/maximum time for each axis based on how much the direction goes that307// way. These values can get arbitrarily large, or even become NaN, which is handled by the308// min/max operations below309let tmin = (min - ray_origin) * ray_direction_recip;310let tmax = (max - ray_origin) * ray_direction_recip;311312// An axis that is not relevant to the ray direction will be NaN. When one of the arguments313// to min/max is NaN, the other argument is used.314// An axis for which the direction is the wrong way will return an arbitrarily large315// negative value.316let tmin = tmin.max_element().max(0.0);317let tmax = tmax.min_element();318319if tmin <= tmax {320Some(tmin)321} else {322None323}324}325326#[cfg(test)]327mod tests {328use bevy_math::Vec3;329use bevy_transform::components::GlobalTransform;330331use super::*;332333// Triangle vertices to be used in a left-hand coordinate system334const V0: [f32; 3] = [1.0, -1.0, 2.0];335const V1: [f32; 3] = [1.0, 2.0, -1.0];336const V2: [f32; 3] = [1.0, -1.0, -1.0];337338#[test]339fn ray_cast_triangle_mt() {340let triangle = [V0.into(), V1.into(), V2.into()];341let ray = Ray3d::new(Vec3::ZERO, Dir3::X);342let result = ray_triangle_intersection(&ray, &triangle, Backfaces::Include);343assert!(result.unwrap().distance - 1.0 <= f32::EPSILON);344}345346#[test]347fn ray_cast_triangle_mt_culling() {348let triangle = [V2.into(), V1.into(), V0.into()];349let ray = Ray3d::new(Vec3::ZERO, Dir3::X);350let result = ray_triangle_intersection(&ray, &triangle, Backfaces::Cull);351assert!(result.is_none());352}353354#[test]355fn ray_mesh_intersection_simple() {356let ray = Ray3d::new(Vec3::ZERO, Dir3::X);357let mesh_transform = GlobalTransform::IDENTITY.affine();358let positions = &[V0, V1, V2];359let vertex_normals = None;360let indices: Option<&[u16]> = None;361let backface_culling = Backfaces::Cull;362363let result = ray_mesh_intersection(364ray,365&mesh_transform,366positions,367vertex_normals,368indices,369None,370backface_culling,371);372373assert!(result.is_some());374}375376#[test]377fn ray_mesh_intersection_indices() {378let ray = Ray3d::new(Vec3::ZERO, Dir3::X);379let mesh_transform = GlobalTransform::IDENTITY.affine();380let positions = &[V0, V1, V2];381let vertex_normals = None;382let indices: Option<&[u16]> = Some(&[0, 1, 2]);383let backface_culling = Backfaces::Cull;384385let result = ray_mesh_intersection(386ray,387&mesh_transform,388positions,389vertex_normals,390indices,391None,392backface_culling,393);394395assert!(result.is_some());396}397398#[test]399fn ray_mesh_intersection_indices_vertex_normals() {400let ray = Ray3d::new(Vec3::ZERO, Dir3::X);401let mesh_transform = GlobalTransform::IDENTITY.affine();402let positions = &[V0, V1, V2];403let vertex_normals: Option<&[[f32; 3]]> =404Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]);405let indices: Option<&[u16]> = Some(&[0, 1, 2]);406let backface_culling = Backfaces::Cull;407408let result = ray_mesh_intersection(409ray,410&mesh_transform,411positions,412vertex_normals,413indices,414None,415backface_culling,416);417418assert!(result.is_some());419}420421#[test]422fn ray_mesh_intersection_vertex_normals() {423let ray = Ray3d::new(Vec3::ZERO, Dir3::X);424let mesh_transform = GlobalTransform::IDENTITY.affine();425let positions = &[V0, V1, V2];426let vertex_normals: Option<&[[f32; 3]]> =427Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]);428let indices: Option<&[u16]> = None;429let backface_culling = Backfaces::Cull;430431let result = ray_mesh_intersection(432ray,433&mesh_transform,434positions,435vertex_normals,436indices,437None,438backface_culling,439);440441assert!(result.is_some());442}443444#[test]445fn ray_mesh_intersection_missing_vertex_normals() {446let ray = Ray3d::new(Vec3::ZERO, Dir3::X);447let mesh_transform = GlobalTransform::IDENTITY.affine();448let positions = &[V0, V1, V2];449let vertex_normals: Option<&[[f32; 3]]> = Some(&[]);450let indices: Option<&[u16]> = None;451let backface_culling = Backfaces::Cull;452453let result = ray_mesh_intersection(454ray,455&mesh_transform,456positions,457vertex_normals,458indices,459None,460backface_culling,461);462463assert!(result.is_some());464}465466#[test]467fn ray_mesh_intersection_indices_missing_vertex_normals() {468let ray = Ray3d::new(Vec3::ZERO, Dir3::X);469let mesh_transform = GlobalTransform::IDENTITY.affine();470let positions = &[V0, V1, V2];471let vertex_normals: Option<&[[f32; 3]]> = Some(&[]);472let indices: Option<&[u16]> = Some(&[0, 1, 2]);473let backface_culling = Backfaces::Cull;474475let result = ray_mesh_intersection(476ray,477&mesh_transform,478positions,479vertex_normals,480indices,481None,482backface_culling,483);484485assert!(result.is_some());486}487488#[test]489fn ray_mesh_intersection_not_enough_indices() {490let ray = Ray3d::new(Vec3::ZERO, Dir3::X);491let mesh_transform = GlobalTransform::IDENTITY.affine();492let positions = &[V0, V1, V2];493let vertex_normals = None;494let indices: Option<&[u16]> = Some(&[0]);495let backface_culling = Backfaces::Cull;496497let result = ray_mesh_intersection(498ray,499&mesh_transform,500positions,501vertex_normals,502indices,503None,504backface_culling,505);506507assert!(result.is_none());508}509510#[test]511fn ray_mesh_intersection_bad_indices() {512let ray = Ray3d::new(Vec3::ZERO, Dir3::X);513let mesh_transform = GlobalTransform::IDENTITY.affine();514let positions = &[V0, V1, V2];515let vertex_normals = None;516let indices: Option<&[u16]> = Some(&[0, 1, 3]);517let backface_culling = Backfaces::Cull;518519let result = ray_mesh_intersection(520ray,521&mesh_transform,522positions,523vertex_normals,524indices,525None,526backface_culling,527);528529assert!(result.is_none());530}531}532533534