Path: blob/main/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs
9395 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 = mesh49.try_attribute(Mesh::ATTRIBUTE_POSITION)50.ok()?51.as_float3()?;5253// Normals are optional54let normals = mesh55.try_attribute(Mesh::ATTRIBUTE_NORMAL)56.ok()57.and_then(|normal_values| normal_values.as_float3());5859let uvs = mesh60.try_attribute(Mesh::ATTRIBUTE_UV_0)61.ok()62.and_then(|uvs| match uvs {63VertexAttributeValues::Float32x2(uvs) => Some(uvs.as_slice()),64_ => None,65});6667match mesh.try_indices().ok() {68Some(Indices::U16(indices)) => {69ray_mesh_intersection(ray, transform, positions, normals, Some(indices), uvs, cull)70}71Some(Indices::U32(indices)) => {72ray_mesh_intersection(ray, transform, positions, normals, Some(indices), uvs, cull)73}74None => ray_mesh_intersection::<u32>(ray, transform, positions, normals, None, uvs, cull),75}76}7778/// Checks if a ray intersects a mesh, and returns the nearest intersection if one exists.79pub fn ray_mesh_intersection<I>(80ray: Ray3d,81mesh_transform: &Affine3A,82positions: &[[f32; 3]],83vertex_normals: Option<&[[f32; 3]]>,84indices: Option<&[I]>,85uvs: Option<&[[f32; 2]]>,86backface_culling: Backfaces,87) -> Option<RayMeshHit>88where89I: TryInto<usize> + Clone + Copy,90{91let world_to_mesh = mesh_transform.inverse();9293let ray = Ray3d::new(94world_to_mesh.transform_point3(ray.origin),95Dir3::new(world_to_mesh.transform_vector3(*ray.direction)).ok()?,96);9798let closest_hit = if let Some(indices) = indices {99// The index list must be a multiple of three. If not, the mesh is malformed and the raycast100// result might be nonsensical.101if indices.len() % 3 != 0 {102return None;103}104105indices106.chunks_exact(3)107.enumerate()108.fold(109(f32::MAX, None),110|(closest_distance, closest_hit), (tri_idx, triangle)| {111let [Ok(a), Ok(b), Ok(c)] = [112triangle[0].try_into(),113triangle[1].try_into(),114triangle[2].try_into(),115] else {116return (closest_distance, closest_hit);117};118119let tri_vertices = match [positions.get(a), positions.get(b), positions.get(c)]120{121[Some(a), Some(b), Some(c)] => {122[Vec3::from(*a), Vec3::from(*b), Vec3::from(*c)]123}124_ => return (closest_distance, closest_hit),125};126127match ray_triangle_intersection(&ray, &tri_vertices, backface_culling) {128Some(hit) if hit.distance >= 0. && hit.distance < closest_distance => {129(hit.distance, Some((tri_idx, hit)))130}131_ => (closest_distance, closest_hit),132}133},134)135.1136} else {137positions138.chunks_exact(3)139.enumerate()140.fold(141(f32::MAX, None),142|(closest_distance, closest_hit), (tri_idx, triangle)| {143let tri_vertices = [144Vec3::from(triangle[0]),145Vec3::from(triangle[1]),146Vec3::from(triangle[2]),147];148149match ray_triangle_intersection(&ray, &tri_vertices, backface_culling) {150Some(hit) if hit.distance >= 0. && hit.distance < closest_distance => {151(hit.distance, Some((tri_idx, hit)))152}153_ => (closest_distance, closest_hit),154}155},156)157.1158};159160closest_hit.and_then(|(tri_idx, hit)| {161let [a, b, c] = match indices {162Some(indices) => {163let [i, j, k] = [tri_idx * 3, tri_idx * 3 + 1, tri_idx * 3 + 2];164[165indices.get(i).copied()?.try_into().ok()?,166indices.get(j).copied()?.try_into().ok()?,167indices.get(k).copied()?.try_into().ok()?,168]169}170None => [tri_idx * 3, tri_idx * 3 + 1, tri_idx * 3 + 2],171};172173let tri_vertices = match [positions.get(a), positions.get(b), positions.get(c)] {174[Some(a), Some(b), Some(c)] => [Vec3::from(*a), Vec3::from(*b), Vec3::from(*c)],175_ => return None,176};177178let tri_normals = vertex_normals.and_then(|normals| {179let [Some(a), Some(b), Some(c)] = [normals.get(a), normals.get(b), normals.get(c)]180else {181return None;182};183Some([Vec3::from(*a), Vec3::from(*b), Vec3::from(*c)])184});185186let point = ray.get_point(hit.distance);187// Note that we need to convert from the Möller-Trumbore convention to the more common188// P = uA + vB + (1 - u - v)C convention.189let u = hit.barycentric_coords.0;190let v = hit.barycentric_coords.1;191let w = 1.0 - u - v;192let barycentric = Vec3::new(w, u, v);193194let normal = if let Some(normals) = tri_normals {195normals[1] * u + normals[2] * v + normals[0] * w196} else {197(tri_vertices[1] - tri_vertices[0])198.cross(tri_vertices[2] - tri_vertices[0])199.normalize()200};201202let uv = uvs.and_then(|uvs| {203let tri_uvs = if let Some(indices) = indices {204let i = tri_idx * 3;205[206uvs[indices[i].try_into().ok()?],207uvs[indices[i + 1].try_into().ok()?],208uvs[indices[i + 2].try_into().ok()?],209]210} else {211let i = tri_idx * 3;212[uvs[i], uvs[i + 1], uvs[i + 2]]213};214Some(215barycentric.x * Vec2::from(tri_uvs[0])216+ barycentric.y * Vec2::from(tri_uvs[1])217+ barycentric.z * Vec2::from(tri_uvs[2]),218)219});220221Some(RayMeshHit {222point: mesh_transform.transform_point3(point),223normal: mesh_transform.transform_vector3(normal),224uv,225barycentric_coords: barycentric,226distance: mesh_transform227.transform_vector3(ray.direction * hit.distance)228.length(),229triangle: Some(tri_vertices.map(|v| mesh_transform.transform_point3(v))),230triangle_index: Some(tri_idx),231})232})233}234235/// Takes a ray and triangle and computes the intersection.236#[inline]237fn ray_triangle_intersection(238ray: &Ray3d,239triangle: &[Vec3; 3],240backface_culling: Backfaces,241) -> Option<RayTriangleHit> {242// Source: https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-rendering-a-triangle/moller-trumbore-ray-triangle-intersection243let vector_v0_to_v1: Vec3 = triangle[1] - triangle[0];244let vector_v0_to_v2: Vec3 = triangle[2] - triangle[0];245let p_vec: Vec3 = ray.direction.cross(vector_v0_to_v2);246let determinant: f32 = vector_v0_to_v1.dot(p_vec);247248match backface_culling {249Backfaces::Cull => {250// if the determinant is negative the triangle is back facing251// if the determinant is close to 0, the ray misses the triangle252// This test checks both cases253if determinant < f32::EPSILON {254return None;255}256}257Backfaces::Include => {258// ray and triangle are parallel if det is close to 0259if determinant.abs() < f32::EPSILON {260return None;261}262}263}264265let determinant_inverse = 1.0 / determinant;266267let t_vec = ray.origin - triangle[0];268let u = t_vec.dot(p_vec) * determinant_inverse;269if !(0.0..=1.0).contains(&u) {270return None;271}272273let q_vec = t_vec.cross(vector_v0_to_v1);274let v = (*ray.direction).dot(q_vec) * determinant_inverse;275if v < 0.0 || u + v > 1.0 {276return None;277}278279// The distance between ray origin and intersection is t.280let t: f32 = vector_v0_to_v2.dot(q_vec) * determinant_inverse;281282Some(RayTriangleHit {283distance: t,284barycentric_coords: (u, v),285})286}287288// TODO: It'd be nice to reuse `RayCast3d::aabb_intersection_at`, but it assumes a normalized ray.289// In our case, the ray is transformed to model space, which could involve scaling.290/// Checks if the ray intersects with the AABB of a mesh, returning the distance to the point of intersection.291/// The distance is zero if the ray starts inside the AABB.292pub fn ray_aabb_intersection_3d(293ray: Ray3d,294aabb: &Aabb3d,295model_to_world: &Affine3A,296) -> Option<f32> {297// Transform the ray to model space298let world_to_model = model_to_world.inverse();299let ray_direction: Vec3A = world_to_model.transform_vector3a((*ray.direction).into());300let ray_direction_recip = ray_direction.recip();301let ray_origin: Vec3A = world_to_model.transform_point3a(ray.origin.into());302303// Check if the ray intersects the mesh's AABB. It's useful to work in model space304// because we can do an AABB intersection test, instead of an OBB intersection test.305306// NOTE: This is largely copied from `RayCast3d::aabb_intersection_at`.307let positive = ray_direction.signum().cmpgt(Vec3A::ZERO);308let min = Vec3A::select(positive, aabb.min, aabb.max);309let max = Vec3A::select(positive, aabb.max, aabb.min);310311// Calculate the minimum/maximum time for each axis based on how much the direction goes that312// way. These values can get arbitrarily large, or even become NaN, which is handled by the313// min/max operations below314let tmin = (min - ray_origin) * ray_direction_recip;315let tmax = (max - ray_origin) * ray_direction_recip;316317// An axis that is not relevant to the ray direction will be NaN. When one of the arguments318// to min/max is NaN, the other argument is used.319// An axis for which the direction is the wrong way will return an arbitrarily large320// negative value.321let tmin = tmin.max_element().max(0.0);322let tmax = tmax.min_element();323324if tmin <= tmax {325Some(tmin)326} else {327None328}329}330331#[cfg(test)]332mod tests {333use bevy_math::Vec3;334use bevy_transform::components::GlobalTransform;335336use super::*;337338// Triangle vertices to be used in a left-hand coordinate system339const V0: [f32; 3] = [1.0, -1.0, 2.0];340const V1: [f32; 3] = [1.0, 2.0, -1.0];341const V2: [f32; 3] = [1.0, -1.0, -1.0];342343#[test]344fn ray_cast_triangle_mt() {345let triangle = [V0.into(), V1.into(), V2.into()];346let ray = Ray3d::new(Vec3::ZERO, Dir3::X);347let result = ray_triangle_intersection(&ray, &triangle, Backfaces::Include);348assert!(result.unwrap().distance - 1.0 <= f32::EPSILON);349}350351#[test]352fn ray_cast_triangle_mt_culling() {353let triangle = [V2.into(), V1.into(), V0.into()];354let ray = Ray3d::new(Vec3::ZERO, Dir3::X);355let result = ray_triangle_intersection(&ray, &triangle, Backfaces::Cull);356assert!(result.is_none());357}358359#[test]360fn ray_mesh_intersection_simple() {361let ray = Ray3d::new(Vec3::ZERO, Dir3::X);362let mesh_transform = GlobalTransform::IDENTITY.affine();363let positions = &[V0, V1, V2];364let vertex_normals = None;365let indices: Option<&[u16]> = None;366let backface_culling = Backfaces::Cull;367368let result = ray_mesh_intersection(369ray,370&mesh_transform,371positions,372vertex_normals,373indices,374None,375backface_culling,376);377378assert!(result.is_some());379}380381#[test]382fn ray_mesh_intersection_indices() {383let ray = Ray3d::new(Vec3::ZERO, Dir3::X);384let mesh_transform = GlobalTransform::IDENTITY.affine();385let positions = &[V0, V1, V2];386let vertex_normals = None;387let indices: Option<&[u16]> = Some(&[0, 1, 2]);388let backface_culling = Backfaces::Cull;389390let result = ray_mesh_intersection(391ray,392&mesh_transform,393positions,394vertex_normals,395indices,396None,397backface_culling,398);399400assert!(result.is_some());401}402403#[test]404fn ray_mesh_intersection_indices_vertex_normals() {405let ray = Ray3d::new(Vec3::ZERO, Dir3::X);406let mesh_transform = GlobalTransform::IDENTITY.affine();407let positions = &[V0, V1, V2];408let vertex_normals: Option<&[[f32; 3]]> =409Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]);410let indices: Option<&[u16]> = Some(&[0, 1, 2]);411let backface_culling = Backfaces::Cull;412413let result = ray_mesh_intersection(414ray,415&mesh_transform,416positions,417vertex_normals,418indices,419None,420backface_culling,421);422423assert!(result.is_some());424}425426#[test]427fn ray_mesh_intersection_vertex_normals() {428let ray = Ray3d::new(Vec3::ZERO, Dir3::X);429let mesh_transform = GlobalTransform::IDENTITY.affine();430let positions = &[V0, V1, V2];431let vertex_normals: Option<&[[f32; 3]]> =432Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]);433let indices: Option<&[u16]> = None;434let backface_culling = Backfaces::Cull;435436let result = ray_mesh_intersection(437ray,438&mesh_transform,439positions,440vertex_normals,441indices,442None,443backface_culling,444);445446assert!(result.is_some());447}448449#[test]450fn ray_mesh_intersection_missing_vertex_normals() {451let ray = Ray3d::new(Vec3::ZERO, Dir3::X);452let mesh_transform = GlobalTransform::IDENTITY.affine();453let positions = &[V0, V1, V2];454let vertex_normals: Option<&[[f32; 3]]> = Some(&[]);455let indices: Option<&[u16]> = None;456let backface_culling = Backfaces::Cull;457458let result = ray_mesh_intersection(459ray,460&mesh_transform,461positions,462vertex_normals,463indices,464None,465backface_culling,466);467468assert!(result.is_some());469}470471#[test]472fn ray_mesh_intersection_indices_missing_vertex_normals() {473let ray = Ray3d::new(Vec3::ZERO, Dir3::X);474let mesh_transform = GlobalTransform::IDENTITY.affine();475let positions = &[V0, V1, V2];476let vertex_normals: Option<&[[f32; 3]]> = Some(&[]);477let indices: Option<&[u16]> = Some(&[0, 1, 2]);478let backface_culling = Backfaces::Cull;479480let result = ray_mesh_intersection(481ray,482&mesh_transform,483positions,484vertex_normals,485indices,486None,487backface_culling,488);489490assert!(result.is_some());491}492493#[test]494fn ray_mesh_intersection_not_enough_indices() {495let ray = Ray3d::new(Vec3::ZERO, Dir3::X);496let mesh_transform = GlobalTransform::IDENTITY.affine();497let positions = &[V0, V1, V2];498let vertex_normals = None;499let indices: Option<&[u16]> = Some(&[0]);500let backface_culling = Backfaces::Cull;501502let result = ray_mesh_intersection(503ray,504&mesh_transform,505positions,506vertex_normals,507indices,508None,509backface_culling,510);511512assert!(result.is_none());513}514515#[test]516fn ray_mesh_intersection_bad_indices() {517let ray = Ray3d::new(Vec3::ZERO, Dir3::X);518let mesh_transform = GlobalTransform::IDENTITY.affine();519let positions = &[V0, V1, V2];520let vertex_normals = None;521let indices: Option<&[u16]> = Some(&[0, 1, 3]);522let backface_culling = Backfaces::Cull;523524let result = ray_mesh_intersection(525ray,526&mesh_transform,527positions,528vertex_normals,529indices,530None,531backface_culling,532);533534assert!(result.is_none());535}536}537538539