diff --git a/benches/benches/bevy_picking/ray_mesh_intersection.rs b/benches/benches/bevy_picking/ray_mesh_intersection.rs index ee81f1ac7fd87..211f3cd5e4f63 100644 --- a/benches/benches/bevy_picking/ray_mesh_intersection.rs +++ b/benches/benches/bevy_picking/ray_mesh_intersection.rs @@ -2,9 +2,13 @@ use core::hint::black_box; use std::time::Duration; use benches::bench; -use bevy_math::{Dir3, Mat4, Ray3d, Vec3}; +use bevy_math::{Dir3, Mat4, Ray3d, Vec2, Vec3}; use bevy_picking::mesh_picking::ray_cast::{self, Backfaces}; -use criterion::{criterion_group, AxisScale, BenchmarkId, Criterion, PlotConfiguration}; +use bevy_render::mesh::{Indices, Mesh, MeshBuilder, PlaneMeshBuilder, VertexAttributeValues}; +use criterion::{ + criterion_group, measurement::WallTime, AxisScale, BenchmarkGroup, BenchmarkId, Criterion, + PlotConfiguration, Throughput, +}; criterion_group!(benches, bench); @@ -61,6 +65,375 @@ fn create_mesh(vertices_per_side: u32) -> SimpleMesh { } } +fn cull_intersect(mut group: BenchmarkGroup<'_, WallTime>, should_intersect: bool) { + for vertices_per_side in [10_u32, 100, 1000] { + let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y); + let mesh_to_world = Mat4::IDENTITY; + let mesh = create_mesh(vertices_per_side); + let tri_count = (mesh.indices.len() / 3) as u64; + + group.throughput(Throughput::Elements(tri_count)); + group.bench_with_input( + format!( + "{} triangles ({} positions, {} indices)", + underscore_separate_number(tri_count), + underscore_separate_number(mesh.positions.len()), + underscore_separate_number(mesh.indices.len()) + ), + &(ray, mesh_to_world, mesh), + |b, (ray, mesh_to_world, mesh)| { + b.iter(|| { + let intersection = ray_cast::ray_mesh_intersection( + *ray, + mesh_to_world, + &mesh.positions, + Some(&mesh.normals), + Some(&mesh.indices), + Backfaces::Cull, + ); + + #[cfg(test)] + assert_eq!(intersection.is_some(), should_intersect); + + intersection + }); + }, + ); + } +} + +fn no_cull_intersect(mut group: BenchmarkGroup<'_, WallTime>, should_intersect: bool) { + for vertices_per_side in [10_u32, 100, 1000] { + let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y); + let mesh_to_world = Mat4::IDENTITY; + let mesh = create_mesh(vertices_per_side); + let tri_count = (mesh.indices.len() / 3) as u64; + + group.throughput(Throughput::Elements(tri_count)); + group.bench_with_input( + format!( + "{} triangles ({} positions, {} indices)", + underscore_separate_number(tri_count), + underscore_separate_number(mesh.positions.len()), + underscore_separate_number(mesh.indices.len()) + ), + &(ray, mesh_to_world, mesh), + |b, (ray, mesh_to_world, mesh)| { + b.iter(|| { + let intersection = ray_cast::ray_mesh_intersection( + *ray, + mesh_to_world, + &mesh.positions, + Some(&mesh.normals), + Some(&mesh.indices), + Backfaces::Include, + ); + + #[cfg(test)] + assert_eq!(intersection.is_some(), should_intersect); + + intersection + }); + }, + ); + } +} + +fn cull_no_intersect(mut group: BenchmarkGroup<'_, WallTime>, should_intersect: bool) { + for vertices_per_side in [10_u32, 100, 1000] { + let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::X); + let mesh_to_world = Mat4::IDENTITY; + let mesh = create_mesh(vertices_per_side); + let tri_count = (mesh.indices.len() / 3) as u64; + + group.throughput(Throughput::Elements(tri_count)); + group.bench_with_input( + format!( + "{} triangles ({} positions, {} indices)", + underscore_separate_number(tri_count), + underscore_separate_number(mesh.positions.len()), + underscore_separate_number(mesh.indices.len()) + ), + &(ray, mesh_to_world, mesh), + |b, (ray, mesh_to_world, mesh)| { + b.iter(|| { + let intersection = ray_cast::ray_mesh_intersection( + *ray, + mesh_to_world, + &mesh.positions, + Some(&mesh.normals), + Some(&mesh.indices), + Backfaces::Cull, + ); + + #[cfg(test)] + assert_eq!(intersection.is_some(), should_intersect); + + intersection + }); + }, + ); + } +} + +enum IntersectIndices { + Include, + Skip, +} + +fn single_plane( + mut group: BenchmarkGroup<'_, WallTime>, + should_intersect: bool, + use_indices: IntersectIndices, +) { + for subdivisions in [10_u32, 100, 1000] { + let ray = Ray3d::new(Vec3::new(0.01, 1.0, 0.0), Dir3::NEG_Y); + let mesh_to_world = Mat4::IDENTITY; + let mut mesh = PlaneMeshBuilder::new(Dir3::Y, Vec2::ONE) + .subdivisions(subdivisions) + .build(); + + if matches!(use_indices, IntersectIndices::Skip) { + // duplicate_mesh consumes the indices + mesh.duplicate_vertices(); + } + + let positions = mesh + .attribute(Mesh::ATTRIBUTE_POSITION) + .unwrap() + .as_float3() + .unwrap(); + + let normals = mesh + .attribute(Mesh::ATTRIBUTE_NORMAL) + .unwrap() + .as_float3() + .unwrap(); + + let indices = mesh.indices(); + + let tri_count = indices + .map(|i| i.len() as u64 / 3) + .unwrap_or(positions.len() as u64 / 3); + + group.throughput(Throughput::Elements(tri_count)); + group.bench_with_input( + format!( + "{} triangles ({} positions, {} indices)", + underscore_separate_number(tri_count), + underscore_separate_number(positions.len()), + underscore_separate_number(indices.map(Indices::len).unwrap_or_default()) + ), + &(ray, mesh_to_world, positions, normals, indices), + |b, (ray, mesh_to_world, positions, normals, indices)| { + b.iter(|| { + let intersection = ray_cast::ray_mesh_intersection( + *ray, + mesh_to_world, + positions, + Some(normals), + match indices { + Some(Indices::U32(indices)) => Some(indices), + _ => None, + }, + Backfaces::Cull, + ); + + #[cfg(test)] + assert_eq!(intersection.is_some(), should_intersect); + + intersection + }); + }, + ); + } +} + +enum OverlappingPlaneOrdering { + BackToFront, + FrontToBack, +} + +fn overlapping_planes( + mut group: BenchmarkGroup<'_, WallTime>, + should_intersect: bool, + plane_ordering: OverlappingPlaneOrdering, +) { + let pos_mul = match plane_ordering { + OverlappingPlaneOrdering::BackToFront => 1.0, + OverlappingPlaneOrdering::FrontToBack => -1.0, + }; + + for planes in [10_u32, 1000, 100_000] { + let ray = Ray3d::new(Vec3::new(0.1, 1.0, 0.0), Dir3::NEG_Y); + let mesh_to_world = Mat4::IDENTITY; + + // let mut mesh = PlaneMeshBuilder::new(Dir3::Y, Vec2::ONE).build(); + + // let copy_mesh = mesh.clone(); + + // Generate a mesh of many planes with subsequent plane being further from ray origin + // or closer depending on plane_ordering + let mesh = (0..planes) + .map(|i| { + let mut plane_mesh = PlaneMeshBuilder::new(Dir3::Y, Vec2::ONE).build(); + + if let VertexAttributeValues::Float32x3(positions) = + plane_mesh.attribute_mut(Mesh::ATTRIBUTE_POSITION).unwrap() + { + positions.iter_mut().for_each(|vert| { + let plane_count = planes as f32; + let plane_height = -plane_count + pos_mul * i as f32; + + vert[1] = plane_height; + }); + + // positions.iter_mut().enumerate().for_each(|(i, pos)| { + // pos[1] -= (planes as usize + planes as usize * pos_mul - i / 4) as f32; + // }); + } else { + panic!("No positions"); + } + + plane_mesh + }) + .reduce(|mut acc, next_mesh| { + acc.merge(&next_mesh); + acc + }) + .unwrap(); + + // for i in 1..(planes) { + // let mut next_plane = PlaneMeshBuilder::new(Dir3::Y, Vec2::ONE).build(); + + // if let VertexAttributeValues::Float32x3(positions) = + // next_plane.attribute_mut(Mesh::ATTRIBUTE_POSITION).unwrap() + // { + // positions.iter_mut().for_each(|vert| { + // let plane_count = planes as f32; + // let plane_height = -plane_count + pos_mul * i as f32; + + // vert[1] = plane_height; + // }); + + // // positions.iter_mut().enumerate().for_each(|(i, pos)| { + // // pos[1] -= (planes as usize + planes as usize * pos_mul - i / 4) as f32; + // // }); + // } else { + // panic!("No positions"); + // } + + // mesh.merge(&next_plane); + // } + + // for _ in 0..(planes - 1) { + // mesh.merge(©_mesh); + // } + + // if let VertexAttributeValues::Float32x3(positions) = + // mesh.attribute_mut(Mesh::ATTRIBUTE_POSITION).unwrap() + // { + // println!("{}", positions.len()); + // // place each subsequent plane closer to the ray origin + + // const CHUNK_SIZE: usize = 4; // the number of vertices of a plane with 0 subdivisions + // positions + // .chunks_exact_mut(CHUNK_SIZE) + // .enumerate() + // // .take(1) + // .for_each(|(i, vertices)| { + // let plane_count = planes as f32; + // let plane_height = -plane_count + pos_mul * i as f32; + // // println!("{i} {plane_height}"); + + // // println!("{:?}", vertices); + + // for v in vertices.iter_mut() { + // v[1] = 0.1; + // } + + // // println!("{:?}", vertices); + // }); + + // // positions.iter_mut().enumerate().for_each(|(i, pos)| { + // // pos[1] -= (planes as usize + planes as usize * pos_mul - i / 4) as f32; + // // }); + // } else { + // panic!("No positions"); + // } + + let positions = mesh + .attribute(Mesh::ATTRIBUTE_POSITION) + .unwrap() + .as_float3() + .unwrap(); + + let normals = mesh + .attribute(Mesh::ATTRIBUTE_NORMAL) + .unwrap() + .as_float3() + .unwrap(); + + let indices = mesh.indices(); + + let tri_count = indices + .map(|i| i.len() as u64 / 3) + .unwrap_or(positions.len() as u64 / 3); + + group.throughput(Throughput::Elements(tri_count)); + group.bench_with_input( + format!( + "{} triangles ({} positions, {} indices)", + underscore_separate_number(tri_count), + underscore_separate_number(positions.len()), + underscore_separate_number(indices.unwrap().len()) + ), + &(ray, mesh_to_world, positions, normals, indices), + |b, (ray, mesh_to_world, positions, normals, indices)| { + b.iter(|| { + let intersection = ray_cast::ray_mesh_intersection( + *ray, + mesh_to_world, + positions, + Some(normals), + match indices { + Some(Indices::U32(indices)) => Some(indices), + _ => None, + }, + Backfaces::Cull, + ); + + #[cfg(test)] + assert_eq!(intersection.is_some(), should_intersect); + + intersection + }); + }, + ); + } +} + +// formats numbers with separators, eg 1_000_000 +fn underscore_separate_number(n: impl ToString) -> String { + n.to_string() + .as_bytes() + .rchunks(3) + .rev() + .map(std::str::from_utf8) + .collect::, _>>() + .unwrap() + .join("_") +} + +// criterion_group!( +// benches, +// ray_mesh_intersection, +// ray_mesh_intersection_no_cull, +// ray_mesh_intersection_no_intersection, +// ray_mesh_intersection_single_plane, +// ray_mesh_intersection_overlapping_planes, +// ); + /// An enum that represents the configuration for all variations of the ray mesh intersection /// benchmarks. enum Benchmarks { @@ -129,7 +502,7 @@ impl Benchmarks { /// There are multiple different scenarios that are tracked, which are described by the /// [`Benchmarks`] enum. Each scenario has its own benchmark group, where individual benchmarks /// track a ray intersecting a square mesh of an increasing amount of vertices. -fn bench(c: &mut Criterion) { +fn bench_orig(c: &mut Criterion) { for benchmark in Benchmarks::iter() { let mut group = c.benchmark_group(benchmark.name()); @@ -168,3 +541,46 @@ fn bench(c: &mut Criterion) { } } } + +macro_rules! bench_group { + ($c: expr, $name:literal) => {{ + let mut group = $c.benchmark_group(bench!($name)); + + group + .warm_up_time(Benchmarks::WARM_UP_TIME) + // Make the scale logarithmic, to match `VERTICES_PER_SIDE`. + .plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + + group + }}; +} + +fn bench(c: &mut Criterion) { + cull_intersect(bench_group!(c, "cull_intersect"), true); + + no_cull_intersect(bench_group!(c, "no_cull_intersect"), true); + + cull_no_intersect(bench_group!(c, "cull_no_intersect"), false); + + single_plane( + bench_group!(c, "single_plane_indices"), + true, + IntersectIndices::Include, + ); + single_plane( + bench_group!(c, "single_plane_no_indices"), + true, + IntersectIndices::Skip, + ); + + overlapping_planes( + bench_group!(c, "overlapping_planes_back_to_front"), + true, + OverlappingPlaneOrdering::BackToFront, + ); + overlapping_planes( + bench_group!(c, "overlapping_planes_front_to_back"), + true, + OverlappingPlaneOrdering::FrontToBack, + ); +} 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()); + } }