diff --git a/Cargo.toml b/Cargo.toml index bffdaf531a4e2..2c0113a0e62e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,6 +111,7 @@ default = [ "bevy_gilrs", "bevy_gizmos", "bevy_gltf", + "bevy_mesh_picking_backend", "bevy_pbr", "bevy_picking", "bevy_remote", @@ -136,6 +137,9 @@ default = [ "x11", ] +# Provides an implementation for picking meshes +bevy_mesh_picking_backend = ["bevy_picking"] + # Provides an implementation for picking sprites bevy_sprite_picking_backend = ["bevy_picking"] @@ -1213,6 +1217,17 @@ setup = [ ], ] +[[example]] +name = "mesh_ray_cast" +path = "examples/3d/mesh_ray_cast.rs" +doc-scrape-examples = true + +[package.metadata.example.mesh_ray_cast] +name = "Mesh Ray Cast" +description = "Demonstrates ray casting with the `MeshRayCast` system parameter" +category = "3D Rendering" +wasm = true + [[example]] name = "lightmaps" path = "examples/3d/lightmaps.rs" @@ -3695,6 +3710,18 @@ description = "Demonstrates how to rotate the skybox and the environment map sim category = "3D Rendering" wasm = false +[[example]] +name = "mesh_picking" +path = "examples/picking/mesh_picking.rs" +doc-scrape-examples = true +required-features = ["bevy_mesh_picking_backend"] + +[package.metadata.example.mesh_picking] +name = "Mesh Picking" +description = "Demonstrates picking meshes" +category = "Picking" +wasm = true + [[example]] name = "simple_picking" path = "examples/picking/simple_picking.rs" diff --git a/benches/Cargo.toml b/benches/Cargo.toml index cc586a3e66241..25fa79256d914 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -14,6 +14,7 @@ bevy_app = { path = "../crates/bevy_app" } bevy_ecs = { path = "../crates/bevy_ecs", features = ["multi_threaded"] } bevy_hierarchy = { path = "../crates/bevy_hierarchy" } bevy_math = { path = "../crates/bevy_math" } +bevy_picking = { path = "../crates/bevy_picking", features = ["bevy_mesh"] } bevy_reflect = { path = "../crates/bevy_reflect", features = ["functions"] } bevy_render = { path = "../crates/bevy_render" } bevy_tasks = { path = "../crates/bevy_tasks" } @@ -37,6 +38,11 @@ name = "ecs" path = "benches/bevy_ecs/benches.rs" harness = false +[[bench]] +name = "ray_mesh_intersection" +path = "benches/bevy_picking/ray_mesh_intersection.rs" +harness = false + [[bench]] name = "reflect_function" path = "benches/bevy_reflect/function.rs" diff --git a/benches/benches/bevy_picking/ray_mesh_intersection.rs b/benches/benches/bevy_picking/ray_mesh_intersection.rs new file mode 100644 index 0000000000000..6be84a649dd7b --- /dev/null +++ b/benches/benches/bevy_picking/ray_mesh_intersection.rs @@ -0,0 +1,120 @@ +use bevy_math::{Dir3, Mat4, Ray3d, Vec3}; +use bevy_picking::{mesh_picking::ray_cast, prelude::*}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +fn ptoxznorm(p: u32, size: u32) -> (f32, f32) { + let ij = (p / (size), p % (size)); + (ij.0 as f32 / size as f32, ij.1 as f32 / size as f32) +} + +struct SimpleMesh { + positions: Vec<[f32; 3]>, + normals: Vec<[f32; 3]>, + indices: Vec, +} + +fn mesh_creation(vertices_per_side: u32) -> SimpleMesh { + let mut positions = Vec::new(); + let mut normals = Vec::new(); + for p in 0..vertices_per_side.pow(2) { + let xz = ptoxznorm(p, vertices_per_side); + positions.push([xz.0 - 0.5, 0.0, xz.1 - 0.5]); + normals.push([0.0, 1.0, 0.0]); + } + + let mut indices = vec![]; + for p in 0..vertices_per_side.pow(2) { + if p % (vertices_per_side) != vertices_per_side - 1 + && p / (vertices_per_side) != vertices_per_side - 1 + { + indices.extend_from_slice(&[p, p + 1, p + vertices_per_side]); + indices.extend_from_slice(&[p + vertices_per_side, p + 1, p + vertices_per_side + 1]); + } + } + + SimpleMesh { + positions, + normals, + indices, + } +} + +fn ray_mesh_intersection(c: &mut Criterion) { + let mut group = c.benchmark_group("ray_mesh_intersection"); + group.warm_up_time(std::time::Duration::from_millis(500)); + + for vertices_per_side in [10_u32, 100, 1000] { + group.bench_function(format!("{}_vertices", vertices_per_side.pow(2)), |b| { + let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y); + let mesh_to_world = Mat4::IDENTITY; + let mesh = mesh_creation(vertices_per_side); + + b.iter(|| { + black_box(ray_cast::ray_mesh_intersection( + ray, + &mesh_to_world, + &mesh.positions, + Some(&mesh.normals), + Some(&mesh.indices), + ray_cast::Backfaces::Cull, + )); + }); + }); + } +} + +fn ray_mesh_intersection_no_cull(c: &mut Criterion) { + let mut group = c.benchmark_group("ray_mesh_intersection_no_cull"); + group.warm_up_time(std::time::Duration::from_millis(500)); + + for vertices_per_side in [10_u32, 100, 1000] { + group.bench_function(format!("{}_vertices", vertices_per_side.pow(2)), |b| { + let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y); + let mesh_to_world = Mat4::IDENTITY; + let mesh = mesh_creation(vertices_per_side); + + b.iter(|| { + black_box(ray_cast::ray_mesh_intersection( + ray, + &mesh_to_world, + &mesh.positions, + Some(&mesh.normals), + Some(&mesh.indices), + ray_cast::Backfaces::Include, + )); + }); + }); + } +} + +fn ray_mesh_intersection_no_intersection(c: &mut Criterion) { + let mut group = c.benchmark_group("ray_mesh_intersection_no_intersection"); + group.warm_up_time(std::time::Duration::from_millis(500)); + + for vertices_per_side in [10_u32, 100, 1000] { + group.bench_function(format!("{}_vertices", (vertices_per_side).pow(2)), |b| { + let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::X); + let mesh_to_world = Mat4::IDENTITY; + let mesh = mesh_creation(vertices_per_side); + + b.iter(|| { + black_box(ray_cast::ray_mesh_intersection( + ray, + &mesh_to_world, + &mesh.positions, + Some(&mesh.normals), + Some(&mesh.indices), + ray_cast::Backfaces::Cull, + )); + }); + }); + } +} + +criterion_group!( + benches, + ray_mesh_intersection, + ray_mesh_intersection_no_cull, + ray_mesh_intersection_no_intersection +); +criterion_main!(benches); diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 2ea147a826a52..2f077379ed970 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -214,9 +214,10 @@ bevy_dev_tools = ["dep:bevy_dev_tools"] # Enable support for the Bevy Remote Protocol bevy_remote = ["dep:bevy_remote"] -# Provides a picking functionality +# Provides picking functionality bevy_picking = [ "dep:bevy_picking", + "bevy_picking/bevy_mesh", "bevy_ui?/bevy_picking", "bevy_sprite?/bevy_picking", ] diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml index 64eada2dbb04e..d4731e001b0cb 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -7,6 +7,10 @@ homepage = "https://bevyengine.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" +[features] +# Provides a mesh picking backend +bevy_mesh = ["dep:bevy_mesh", "dep:crossbeam-channel"] + [dependencies] bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } @@ -15,6 +19,7 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } bevy_input = { path = "../bevy_input", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.15.0-dev", optional = true } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" } bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } @@ -22,6 +27,7 @@ bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } +crossbeam-channel = { version = "0.5", optional = true } uuid = { version = "1.1", features = ["v4"] } [lints] diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index df6d37ca004ff..d93f887a8a157 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -156,6 +156,8 @@ pub mod backend; pub mod events; pub mod focus; pub mod input; +#[cfg(feature = "bevy_mesh")] +pub mod mesh_picking; pub mod pointer; use bevy_app::prelude::*; @@ -166,6 +168,12 @@ use bevy_reflect::prelude::*; /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { + #[cfg(feature = "bevy_mesh")] + #[doc(hidden)] + pub use crate::mesh_picking::{ + ray_cast::{MeshRayCast, RayCastBackfaces, RayCastSettings, RayCastVisibility}, + MeshPickingBackend, MeshPickingBackendSettings, RayCastPickable, + }; #[doc(hidden)] pub use crate::{ events::*, input::PointerInputPlugin, pointer::PointerButton, DefaultPickingPlugins, @@ -274,6 +282,8 @@ impl Plugin for DefaultPickingPlugins { PickingPlugin::default(), InteractionPlugin, )); + #[cfg(feature = "bevy_mesh")] + app.add_plugins(mesh_picking::MeshPickingBackend); } } diff --git a/crates/bevy_picking/src/mesh_picking/mod.rs b/crates/bevy_picking/src/mesh_picking/mod.rs new file mode 100644 index 0000000000000..ce9129c54d536 --- /dev/null +++ b/crates/bevy_picking/src/mesh_picking/mod.rs @@ -0,0 +1,133 @@ +//! A [mesh ray casting](ray_cast) backend for [`bevy_picking`](crate). +//! +//! By default, all meshes are pickable. Picking can be disabled for individual entities +//! by adding [`PickingBehavior::IGNORE`]. +//! +//! To make mesh picking entirely opt-in, set [`MeshPickingBackendSettings::require_markers`] +//! to `true` and add a [`RayCastPickable`] component to the desired camera and target entities. +//! +//! To manually perform mesh ray casts independent of picking, use the [`MeshRayCast`] system parameter. + +pub mod ray_cast; + +use crate::{ + backend::{ray::RayMap, HitData, PointerHits}, + prelude::*, + PickSet, +}; +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use bevy_reflect::prelude::*; +use bevy_render::{prelude::*, view::RenderLayers}; +use ray_cast::{MeshRayCast, RayCastSettings, RayCastVisibility, SimplifiedMesh}; + +/// Runtime settings for the [`MeshPickingBackend`]. +#[derive(Resource, Reflect)] +#[reflect(Resource, Default)] +pub struct MeshPickingBackendSettings { + /// When set to `true` ray casting will only happen between cameras and entities marked with + /// [`RayCastPickable`]. `false` by default. + /// + /// This setting is provided to give you fine-grained control over which cameras and entities + /// should be used by the mesh picking backend at runtime. + pub require_markers: bool, + + /// Determines how mesh picking should consider [`Visibility`]. When set to [`RayCastVisibility::Any`], + /// ray casts can be performed against both visible and hidden entities. + /// + /// Defaults to [`RayCastVisibility::VisibleInView`], only performing picking against visible entities + /// that are in the view of a camera. + pub ray_cast_visibility: RayCastVisibility, +} + +impl Default for MeshPickingBackendSettings { + fn default() -> Self { + Self { + require_markers: false, + ray_cast_visibility: RayCastVisibility::VisibleInView, + } + } +} + +/// An optional component that marks cameras and target entities that should be used in the [`MeshPickingBackend`]. +/// Only needed if [`MeshPickingBackendSettings::require_markers`] is set to `true`, and ignored otherwise. +#[derive(Debug, Clone, Default, Component, Reflect)] +#[reflect(Component, Default)] +pub struct RayCastPickable; + +/// Adds the mesh picking backend to your app. +#[derive(Clone, Default)] +pub struct MeshPickingBackend; + +impl Plugin for MeshPickingBackend { + fn build(&self, app: &mut App) { + app.init_resource::() + .register_type::<(RayCastPickable, MeshPickingBackendSettings, SimplifiedMesh)>() + .add_systems(PreUpdate, update_hits.in_set(PickSet::Backend)); + } +} + +/// Casts rays into the scene using [`MeshPickingBackendSettings`] and sends [`PointerHits`] events. +#[allow(clippy::too_many_arguments)] +pub fn update_hits( + backend_settings: Res, + ray_map: Res, + picking_cameras: Query<(&Camera, Option<&RayCastPickable>, Option<&RenderLayers>)>, + pickables: Query<&PickingBehavior>, + marked_targets: Query<&RayCastPickable>, + layers: Query<&RenderLayers>, + mut ray_cast: MeshRayCast, + mut output: EventWriter, +) { + for (&ray_id, &ray) in ray_map.map().iter() { + let Ok((camera, cam_pickable, cam_layers)) = picking_cameras.get(ray_id.camera) else { + continue; + }; + if backend_settings.require_markers && cam_pickable.is_none() { + continue; + } + + let cam_layers = cam_layers.to_owned().unwrap_or_default(); + + let settings = RayCastSettings { + visibility: backend_settings.ray_cast_visibility, + filter: &|entity| { + let marker_requirement = + !backend_settings.require_markers || marked_targets.get(entity).is_ok(); + + // Other entities missing render layers are on the default layer 0 + let entity_layers = layers.get(entity).cloned().unwrap_or_default(); + let render_layers_match = cam_layers.intersects(&entity_layers); + + let is_pickable = pickables + .get(entity) + .map(|p| p.is_hoverable) + .unwrap_or(true); + + marker_requirement && render_layers_match && is_pickable + }, + early_exit_test: &|entity_hit| { + pickables + .get(entity_hit) + .is_ok_and(|pickable| pickable.should_block_lower) + }, + }; + let picks = ray_cast + .cast_ray(ray, &settings) + .iter() + .map(|(entity, hit)| { + let hit_data = HitData::new( + ray_id.camera, + hit.distance, + Some(hit.point), + Some(hit.normal), + ); + (*entity, hit_data) + }) + .collect::>(); + let order = camera.order as f32; + if !picks.is_empty() { + output.send(PointerHits::new(ray_id.pointer, picks, order)); + } + } +} diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs new file mode 100644 index 0000000000000..0cea98e566fc3 --- /dev/null +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs @@ -0,0 +1,387 @@ +use bevy_math::{bounding::Aabb3d, Dir3, Mat4, Ray3d, Vec3, Vec3A}; +use bevy_reflect::Reflect; +use bevy_render::mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues}; +use bevy_utils::tracing::{error, warn}; + +use super::Backfaces; + +/// Hit data for an intersection between a ray and a mesh. +#[derive(Debug, Clone, Reflect)] +pub struct RayMeshHit { + /// The point of intersection in world space. + pub point: Vec3, + /// The normal vector of the triangle at the point of intersection. Not guaranteed to be normalized for scaled meshes. + pub normal: Vec3, + /// The barycentric coordinates of the intersection. + pub barycentric_coords: Vec3, + /// The distance from the ray origin to the intersection point. + pub distance: f32, + /// The vertices of the triangle that was hit. + pub triangle: Option<[Vec3A; 3]>, + /// The index of the triangle that was hit. + pub triangle_index: Option, +} + +/// Hit data for an intersection between a ray and a triangle. +#[derive(Default, Debug)] +pub struct RayTriangleHit { + pub distance: f32, + pub barycentric_coords: (f32, f32), +} + +/// Casts a ray on a mesh, and returns the intersection. +pub(super) fn ray_intersection_over_mesh( + mesh: &Mesh, + mesh_transform: &Mat4, + ray: Ray3d, + backface_culling: Backfaces, +) -> Option { + if mesh.primitive_topology() != PrimitiveTopology::TriangleList { + error!( + "Invalid intersection check: `TriangleList` is the only supported `PrimitiveTopology`" + ); + return None; + } + + // Get the vertex positions and normals from the mesh. + let vertex_positions: &Vec<[f32; 3]> = match mesh.attribute(Mesh::ATTRIBUTE_POSITION) { + None => { + error!("Mesh does not contain vertex positions"); + return None; + } + Some(vertex_values) => match &vertex_values { + VertexAttributeValues::Float32x3(positions) => positions, + _ => { + error!("Unexpected types in {:?}", Mesh::ATTRIBUTE_POSITION); + return None; + } + }, + }; + let vertex_normals: Option<&[[f32; 3]]> = + if let Some(normal_values) = mesh.attribute(Mesh::ATTRIBUTE_NORMAL) { + match &normal_values { + VertexAttributeValues::Float32x3(normals) => Some(normals), + _ => None, + } + } else { + None + }; + + if let Some(indices) = &mesh.indices() { + match indices { + Indices::U16(vertex_indices) => ray_mesh_intersection( + ray, + mesh_transform, + vertex_positions, + vertex_normals, + Some(vertex_indices), + backface_culling, + ), + Indices::U32(vertex_indices) => ray_mesh_intersection( + ray, + mesh_transform, + vertex_positions, + vertex_normals, + Some(vertex_indices), + backface_culling, + ), + } + } else { + ray_mesh_intersection( + ray, + mesh_transform, + vertex_positions, + vertex_normals, + None::<&[usize]>, + backface_culling, + ) + } +} + +/// Checks if a ray intersects a mesh, and returns the nearest intersection if one exists. +pub fn ray_mesh_intersection( + ray: Ray3d, + mesh_transform: &Mat4, + vertex_positions: &[[f32; 3]], + vertex_normals: Option<&[[f32; 3]]>, + indices: Option<&[Index]>, + backface_culling: Backfaces, +) -> Option +where + usize: TryFrom, +{ + // 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( + world_to_mesh.transform_point3(ray.origin), + Dir3::new(world_to_mesh.transform_vector3(*ray.direction)).ok()?, + ); + + if let Some(indices) = indices { + // Make sure this chunk has 3 vertices to avoid a panic. + if indices.len() % 3 != 0 { + warn!("Index list not a multiple of 3"); + return None; + } + + // Now that we're in the vector of vertex indices, we want to look at the vertex + // positions for each triangle, so we'll take indices in chunks of three, where each + // chunk of three indices are references to the three vertices of a triangle. + for index_chunk in indices.chunks_exact(3) { + let [index1, index2, index3] = [ + usize::try_from(index_chunk[0]).ok()?, + usize::try_from(index_chunk[1]).ok()?, + usize::try_from(index_chunk[2]).ok()?, + ]; + let triangle_index = Some(index1); + let tri_vertex_positions = [ + Vec3A::from(vertex_positions[index1]), + Vec3A::from(vertex_positions[index2]), + Vec3A::from(vertex_positions[index3]), + ]; + let tri_normals = vertex_normals.map(|normals| { + [ + Vec3A::from(normals[index1]), + Vec3A::from(normals[index2]), + Vec3A::from(normals[index3]), + ] + }); + + let Some(hit) = triangle_intersection( + tri_vertex_positions, + tri_normals, + 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_point3a(tri[0]), + mesh_transform.transform_point3a(tri[1]), + mesh_transform.transform_point3a(tri[2]), + ] + }), + triangle_index, + }); + closest_hit_distance = hit.distance; + } + } else { + for (i, chunk) in vertex_positions.chunks_exact(3).enumerate() { + let &[a, b, c] = chunk else { + continue; + }; + let triangle_index = Some(i); + let tri_vertex_positions = [Vec3A::from(a), Vec3A::from(b), Vec3A::from(c)]; + let tri_normals = vertex_normals.map(|normals| { + [ + Vec3A::from(normals[i]), + Vec3A::from(normals[i + 1]), + Vec3A::from(normals[i + 2]), + ] + }); + + let Some(hit) = triangle_intersection( + tri_vertex_positions, + tri_normals, + 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_point3a(tri[0]), + mesh_transform.transform_point3a(tri[1]), + mesh_transform.transform_point3a(tri[2]), + ] + }), + triangle_index, + }); + closest_hit_distance = hit.distance; + } + } + + closest_hit +} + +#[inline(always)] +fn triangle_intersection( + tri_vertices: [Vec3A; 3], + tri_normals: Option<[Vec3A; 3]>, + max_distance: f32, + ray: &Ray3d, + backface_culling: Backfaces, +) -> Option { + let hit = ray_triangle_intersection(ray, &tri_vertices, backface_culling)?; + + if hit.distance < 0.0 || hit.distance > max_distance { + return None; + }; + + 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, + normal: normal.into(), + barycentric_coords: barycentric, + distance: hit.distance, + triangle: Some(tri_vertices), + triangle_index: None, + }) +} + +/// Takes a ray and triangle and computes the intersection. +#[inline(always)] +fn ray_triangle_intersection( + ray: &Ray3d, + triangle: &[Vec3A; 3], + backface_culling: Backfaces, +) -> Option { + // Source: https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-rendering-a-triangle/moller-trumbore-ray-triangle-intersection + let vector_v0_to_v1: Vec3A = triangle[1] - triangle[0]; + let vector_v0_to_v2: Vec3A = triangle[2] - triangle[0]; + let p_vec: Vec3A = (Vec3A::from(*ray.direction)).cross(vector_v0_to_v2); + let determinant: f32 = vector_v0_to_v1.dot(p_vec); + + match backface_culling { + Backfaces::Cull => { + // if the determinant is negative the triangle is back facing + // if the determinant is close to 0, the ray misses the triangle + // This test checks both cases + if determinant < f32::EPSILON { + return None; + } + } + Backfaces::Include => { + // ray and triangle are parallel if det is close to 0 + if determinant.abs() < f32::EPSILON { + return None; + } + } + } + + let determinant_inverse = 1.0 / determinant; + + let t_vec = Vec3A::from(ray.origin) - triangle[0]; + let u = t_vec.dot(p_vec) * determinant_inverse; + if !(0.0..=1.0).contains(&u) { + return None; + } + + let q_vec = t_vec.cross(vector_v0_to_v1); + let v = Vec3A::from(*ray.direction).dot(q_vec) * determinant_inverse; + if v < 0.0 || u + v > 1.0 { + return None; + } + + // The distance between ray origin and intersection is t. + let t: f32 = vector_v0_to_v2.dot(q_vec) * determinant_inverse; + + Some(RayTriangleHit { + distance: t, + barycentric_coords: (u, v), + }) +} + +// TODO: It'd be nice to reuse `RayCast3d::aabb_intersection_at`, but it assumes a normalized ray. +// In our case, the ray is transformed to model space, which could involve scaling. +/// Checks if the ray intersects with the AABB of a mesh, returning the distance to the point of intersection. +/// The distance is zero if the ray starts inside the AABB. +pub fn ray_aabb_intersection_3d(ray: Ray3d, aabb: &Aabb3d, model_to_world: &Mat4) -> Option { + // Transform the ray to model space + let world_to_model = model_to_world.inverse(); + let ray_direction: Vec3A = world_to_model.transform_vector3a((*ray.direction).into()); + let ray_direction_recip = ray_direction.recip(); + let ray_origin: Vec3A = world_to_model.transform_point3a(ray.origin.into()); + + // Check if the ray intersects the mesh's AABB. It's useful to work in model space + // because we can do an AABB intersection test, instead of an OBB intersection test. + + // NOTE: This is largely copied from `RayCast3d::aabb_intersection_at`. + let positive = ray_direction.signum().cmpgt(Vec3A::ZERO); + let min = Vec3A::select(positive, aabb.min, aabb.max); + let max = Vec3A::select(positive, aabb.max, aabb.min); + + // Calculate the minimum/maximum time for each axis based on how much the direction goes that + // way. These values can get arbitrarily large, or even become NaN, which is handled by the + // min/max operations below + let tmin = (min - ray_origin) * ray_direction_recip; + let tmax = (max - ray_origin) * ray_direction_recip; + + // An axis that is not relevant to the ray direction will be NaN. When one of the arguments + // to min/max is NaN, the other argument is used. + // An axis for which the direction is the wrong way will return an arbitrarily large + // negative value. + let tmin = tmin.max_element().max(0.0); + let tmax = tmax.min_element(); + + if tmin <= tmax { + Some(tmin) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use bevy_math::Vec3; + + use super::*; + + // Triangle vertices to be used in a left-hand coordinate system + const V0: [f32; 3] = [1.0, -1.0, 2.0]; + const V1: [f32; 3] = [1.0, 2.0, -1.0]; + const V2: [f32; 3] = [1.0, -1.0, -1.0]; + + #[test] + fn ray_cast_triangle_mt() { + let triangle = [V0.into(), V1.into(), V2.into()]; + let ray = Ray3d::new(Vec3::ZERO, Dir3::X); + let result = ray_triangle_intersection(&ray, &triangle, Backfaces::Include); + assert!(result.unwrap().distance - 1.0 <= f32::EPSILON); + } + + #[test] + fn ray_cast_triangle_mt_culling() { + let triangle = [V2.into(), V1.into(), V0.into()]; + let ray = Ray3d::new(Vec3::ZERO, Dir3::X); + let result = ray_triangle_intersection(&ray, &triangle, Backfaces::Cull); + assert!(result.is_none()); + } +} diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs new file mode 100644 index 0000000000000..3afebb870aacc --- /dev/null +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs @@ -0,0 +1,307 @@ +//! Ray casting for meshes. +//! +//! See the [`MeshRayCast`] system parameter for more information. + +mod intersections; + +use bevy_derive::{Deref, DerefMut}; + +use bevy_math::{bounding::Aabb3d, Ray3d}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::mesh::Mesh; + +use intersections::*; +pub use intersections::{ray_aabb_intersection_3d, ray_mesh_intersection, RayMeshHit}; + +use bevy_asset::{Assets, Handle}; +use bevy_ecs::{prelude::*, system::lifetimeless::Read, system::SystemParam}; +use bevy_math::FloatOrd; +use bevy_render::{prelude::*, primitives::Aabb}; +use bevy_transform::components::GlobalTransform; +use bevy_utils::tracing::*; + +/// How a ray cast should handle [`Visibility`]. +#[derive(Clone, Copy, Reflect)] +pub enum RayCastVisibility { + /// Completely ignore visibility checks. Hidden items can still be ray casted against. + Any, + /// Only cast rays against entities that are visible in the hierarchy. See [`Visibility`]. + Visible, + /// Only cast rays against entities that are visible in the hierarchy and visible to a camera or + /// light. See [`Visibility`]. + VisibleInView, +} + +/// Settings for a ray cast. +#[derive(Clone)] +pub struct RayCastSettings<'a> { + /// Determines how ray casting should consider [`Visibility`]. + pub visibility: RayCastVisibility, + /// A predicate that is applied for every entity that ray casts are performed against. + /// Only entities that return `true` will be considered. + pub filter: &'a dyn Fn(Entity) -> bool, + /// A function that is run every time a hit is found. Ray casting will continue to check for hits + /// along the ray as long as this returns `false`. + pub early_exit_test: &'a dyn Fn(Entity) -> bool, +} + +impl<'a> RayCastSettings<'a> { + /// Set the filter to apply to the ray cast. + pub fn with_filter(mut self, filter: &'a impl Fn(Entity) -> bool) -> Self { + self.filter = filter; + self + } + + /// Set the early exit test to apply to the ray cast. + pub fn with_early_exit_test(mut self, early_exit_test: &'a impl Fn(Entity) -> bool) -> Self { + self.early_exit_test = early_exit_test; + self + } + + /// Set the [`RayCastVisibility`] setting to apply to the ray cast. + pub fn with_visibility(mut self, visibility: RayCastVisibility) -> Self { + self.visibility = visibility; + self + } + + /// This ray cast should exit as soon as the nearest hit is found. + pub fn always_early_exit(self) -> Self { + self.with_early_exit_test(&|_| true) + } + + /// This ray cast should check all entities whose AABB intersects the ray and return all hits. + pub fn never_early_exit(self) -> Self { + self.with_early_exit_test(&|_| false) + } +} + +impl<'a> Default for RayCastSettings<'a> { + fn default() -> Self { + Self { + visibility: RayCastVisibility::VisibleInView, + filter: &|_| true, + early_exit_test: &|_| true, + } + } +} + +/// Determines whether backfaces should be culled or included in ray intersection tests. +/// +/// By default, backfaces are culled. +#[derive(Copy, Clone, Default, Reflect)] +#[reflect(Default)] +pub enum Backfaces { + /// Cull backfaces. + #[default] + Cull, + /// Include backfaces. + Include, +} + +/// Disables backface culling for [ray casts](MeshRayCast) on this entity. +#[derive(Component, Copy, Clone, Default, Reflect)] +#[reflect(Component, Default)] +pub struct RayCastBackfaces; + +/// A simplified mesh component that can be used for [ray casting](super::MeshRayCast). +/// +/// Consider using this component for complex meshes that don't need perfectly accurate ray casting. +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect)] +#[reflect(Component, Debug)] +pub struct SimplifiedMesh(pub Handle); + +type MeshFilter = Or<(With, With, With)>; + +/// Add this ray casting [`SystemParam`] to your system to cast rays into the world with an +/// immediate-mode API. Call `cast_ray` to immediately perform a ray cast and get a result. +/// +/// Under the hood, this is a collection of regular bevy queries, resources, and local parameters +/// that are added to your system. +/// +/// ## Usage +/// +/// The following system casts a ray into the world with the ray positioned at the origin, pointing in +/// the X-direction, and returns a list of intersections: +/// +/// ``` +/// # use bevy_math::prelude::*; +/// # use bevy_picking::prelude::*; +/// fn ray_cast_system(mut ray_cast: MeshRayCast) { +/// let ray = Ray3d::new(Vec3::ZERO, Dir3::X); +/// let hits = ray_cast.cast_ray(ray, &RayCastSettings::default()); +/// } +/// ``` +/// +/// ## Configuration +/// +/// You can specify the behavior of the ray cast using [`RayCastSettings`]. This allows you to filter out +/// entities, configure early-out behavior, and set whether the [`Visibility`] of an entity should be +/// considered. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_math::prelude::*; +/// # use bevy_picking::prelude::*; +/// # #[derive(Component)] +/// # struct Foo; +/// fn ray_cast_system(mut ray_cast: MeshRayCast, foo_query: Query<(), With>) { +/// let ray = Ray3d::new(Vec3::ZERO, Dir3::X); +/// +/// // Only ray cast against entities with the `Foo` component. +/// let filter = |entity| foo_query.contains(entity); +/// +/// // Never early-exit. Note that you can change behavior per-entity. +/// let early_exit_test = |_entity| false; +/// +/// // Ignore the visibility of entities. This allows ray casting hidden entities. +/// let visibility = RayCastVisibility::Any; +/// +/// let settings = RayCastSettings::default() +/// .with_filter(&filter) +/// .with_early_exit_test(&early_exit_test) +/// .with_visibility(visibility); +/// +/// // Cast the ray with the settings, returning a list of intersections. +/// let hits = ray_cast.cast_ray(ray, &settings); +/// } +/// ``` +#[derive(SystemParam)] +pub struct MeshRayCast<'w, 's> { + #[doc(hidden)] + pub meshes: Res<'w, Assets>, + #[doc(hidden)] + pub hits: Local<'s, Vec<(FloatOrd, (Entity, RayMeshHit))>>, + #[doc(hidden)] + pub output: Local<'s, Vec<(Entity, RayMeshHit)>>, + #[doc(hidden)] + pub culled_list: Local<'s, Vec<(FloatOrd, Entity)>>, + #[doc(hidden)] + pub culling_query: Query< + 'w, + 's, + ( + Read, + Read, + Read, + Read, + Entity, + ), + MeshFilter, + >, + #[doc(hidden)] + pub mesh_query: Query< + 'w, + 's, + ( + Option>, + Option>, + Option>, + Has, + Read, + ), + MeshFilter, + >, +} + +impl<'w, 's> MeshRayCast<'w, 's> { + /// Casts the `ray` into the world and returns a sorted list of intersections, nearest first. + pub fn cast_ray(&mut self, ray: Ray3d, settings: &RayCastSettings) -> &[(Entity, RayMeshHit)] { + let ray_cull = info_span!("ray culling"); + let ray_cull_guard = ray_cull.enter(); + + self.hits.clear(); + self.culled_list.clear(); + self.output.clear(); + + // Check all entities to see if the ray intersects the AABB. Use this to build a short list + // of entities that are in the path of the ray. + let (aabb_hits_tx, aabb_hits_rx) = crossbeam_channel::unbounded::<(FloatOrd, Entity)>(); + let visibility_setting = settings.visibility; + self.culling_query.par_iter().for_each( + |(inherited_visibility, view_visibility, aabb, transform, entity)| { + let should_ray_cast = match visibility_setting { + RayCastVisibility::Any => true, + RayCastVisibility::Visible => inherited_visibility.get(), + RayCastVisibility::VisibleInView => view_visibility.get(), + }; + if should_ray_cast { + if let Some(distance) = ray_aabb_intersection_3d( + ray, + &Aabb3d::new(aabb.center, aabb.half_extents), + &transform.compute_matrix(), + ) { + aabb_hits_tx.send((FloatOrd(distance), entity)).ok(); + } + } + }, + ); + *self.culled_list = aabb_hits_rx.try_iter().collect(); + + // Sort by the distance along the ray. + self.culled_list.sort_by_key(|(aabb_near, _)| *aabb_near); + + drop(ray_cull_guard); + + // Perform ray casts against the culled entities. + let mut nearest_blocking_hit = FloatOrd(f32::INFINITY); + let ray_cast_guard = debug_span!("ray_cast"); + self.culled_list + .iter() + .filter(|(_, entity)| (settings.filter)(*entity)) + .for_each(|(aabb_near, entity)| { + // Get the mesh components and transform. + let Ok((mesh2d, mesh3d, simplified_mesh, has_backfaces, transform)) = + self.mesh_query.get(*entity) + else { + return; + }; + + // Get the underlying mesh handle. One of these will always be `Some` because of the query filters. + let Some(mesh_handle) = simplified_mesh + .map(|m| &m.0) + .or(mesh3d.map(|m| &m.0).or(mesh2d.map(|m| &m.0))) + else { + return; + }; + + // Is it even possible the mesh could be closer than the current best? + if *aabb_near > nearest_blocking_hit { + return; + } + + // Does the mesh handle resolve? + let Some(mesh) = self.meshes.get(mesh_handle) else { + return; + }; + + let backfaces = match has_backfaces { + true => Backfaces::Include, + false => Backfaces::Cull, + }; + + // Perform the actual ray cast. + let _ray_cast_guard = ray_cast_guard.enter(); + let transform = transform.compute_matrix(); + let intersection = ray_intersection_over_mesh(mesh, &transform, ray, backfaces); + + if let Some(intersection) = intersection { + let distance = FloatOrd(intersection.distance); + if (settings.early_exit_test)(*entity) && distance < nearest_blocking_hit { + // The reason we don't just return here is because right now we are + // going through the AABBs in order, but that doesn't mean that an + // AABB that starts further away can't end up with a closer hit than + // an AABB that starts closer. We need to keep checking AABBs that + // could possibly contain a nearer hit. + nearest_blocking_hit = distance.min(nearest_blocking_hit); + } + self.hits.push((distance, (*entity, intersection))); + }; + }); + + self.hits.retain(|(dist, _)| *dist <= nearest_blocking_hit); + self.hits.sort_by_key(|(k, _)| *k); + let hits = self.hits.iter().map(|(_, (e, i))| (*e, i.to_owned())); + self.output.extend(hits); + self.output.as_ref() + } +} diff --git a/docs/cargo_features.md b/docs/cargo_features.md index e45c379955740..527ef205f0ee7 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -22,6 +22,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_gilrs|Adds gamepad support| |bevy_gizmos|Adds support for rendering gizmos| |bevy_gltf|[glTF](https://www.khronos.org/gltf/) support| +|bevy_mesh_picking_backend|Provides an implementation for picking meshes| |bevy_pbr|Adds PBR rendering| |bevy_picking|Provides picking functionality| |bevy_remote|Enable the Bevy Remote Protocol| diff --git a/examples/3d/mesh_ray_cast.rs b/examples/3d/mesh_ray_cast.rs new file mode 100644 index 0000000000000..9be7a35505dbd --- /dev/null +++ b/examples/3d/mesh_ray_cast.rs @@ -0,0 +1,112 @@ +//! Demonstrates how to use the [`MeshRayCast`] system parameter to chain multiple ray casts +//! and bounce off of surfaces. + +use std::f32::consts::{FRAC_PI_2, PI}; + +use bevy::{ + color::palettes::css, + core_pipeline::{bloom::Bloom, tonemapping::Tonemapping}, + math::vec3, + picking::backend::ray::RayMap, + prelude::*, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, bouncing_raycast) + .insert_resource(ClearColor(Color::BLACK)) + .run(); +} + +const MAX_BOUNCES: usize = 64; +const LASER_SPEED: f32 = 0.03; + +fn bouncing_raycast( + mut ray_cast: MeshRayCast, + mut gizmos: Gizmos, + time: Res