From 281638ef10ebd25f783bee2533509fdf756f7ab4 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Wed, 2 Oct 2024 00:35:35 +0300 Subject: [PATCH 01/27] Initial port --- crates/bevy_mesh_picking/Cargo.toml | 25 ++ crates/bevy_mesh_picking/src/lib.rs | 138 +++++++ .../src/raycast/immediate.rs | 339 +++++++++++++++++ .../bevy_mesh_picking/src/raycast/markers.rs | 10 + crates/bevy_mesh_picking/src/raycast/mod.rs | 4 + .../src/raycast/primitives.rs | 187 ++++++++++ .../bevy_mesh_picking/src/raycast/raycast.rs | 344 ++++++++++++++++++ 7 files changed, 1047 insertions(+) create mode 100644 crates/bevy_mesh_picking/Cargo.toml create mode 100644 crates/bevy_mesh_picking/src/lib.rs create mode 100644 crates/bevy_mesh_picking/src/raycast/immediate.rs create mode 100644 crates/bevy_mesh_picking/src/raycast/markers.rs create mode 100644 crates/bevy_mesh_picking/src/raycast/mod.rs create mode 100644 crates/bevy_mesh_picking/src/raycast/primitives.rs create mode 100644 crates/bevy_mesh_picking/src/raycast/raycast.rs diff --git a/crates/bevy_mesh_picking/Cargo.toml b/crates/bevy_mesh_picking/Cargo.toml new file mode 100644 index 0000000000000..acad4598a37b3 --- /dev/null +++ b/crates/bevy_mesh_picking/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "bevy_mesh_picking" +version = "0.15.0-dev" +edition = "2021" +description = "Provides a mesh picking backend for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +bevy_app = { path = "../bevy_app", version = "0.15.0-dev", default-features = false } +bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev", default-features = false } +bevy_math = { path = "../bevy_math", version = "0.15.0-dev", default-features = false } +bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", default-features = false } +bevy_render = { path = "../bevy_render", version = "0.15.0-dev", default-features = false } +bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev", default-features = false } + +crossbeam-channel = "0.5" + +[lints] +workspace = true diff --git a/crates/bevy_mesh_picking/src/lib.rs b/crates/bevy_mesh_picking/src/lib.rs new file mode 100644 index 0000000000000..84e0b820306e4 --- /dev/null +++ b/crates/bevy_mesh_picking/src/lib.rs @@ -0,0 +1,138 @@ +//! A raycasting backend for `bevy_mod_picking` that uses `bevy_mod_raycast` for raycasting. +//! +//! # Usage +//! +//! If a pointer passes through this camera's render target, it will automatically shoot rays into +//! the scene and will be able to pick things. +//! +//! To ignore an entity, you can add [`Pickable::IGNORE`] to it, and it will be ignored during +//! raycasting. +//! +//! For fine-grained control, see the [`RaycastBackendSettings::require_markers`] setting. +//! + +#![allow(clippy::too_many_arguments, clippy::type_complexity)] +#![warn(missing_docs)] + +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use bevy_picking::{ + backend::{ray::RayMap, HitData, PointerHits}, + prelude::*, + PickSet, +}; +use bevy_reflect::prelude::*; +use bevy_render::{prelude::*, view::RenderLayers}; +use raycast::immediate::{Raycast, RaycastSettings, RaycastVisibility}; + +pub mod raycast; + +/// Commonly used imports for the [`bevy_picking_raycast`](crate) crate. +pub mod prelude { + pub use crate::RaycastBackend; +} + +/// Runtime settings for the [`RaycastBackend`]. +#[derive(Resource, Reflect)] +#[reflect(Resource, Default)] +pub struct RaycastBackendSettings { + /// When set to `true` raycasting will only happen between cameras and entities marked with + /// [`RaycastPickable`]. Off by default. This setting is provided to give you fine-grained + /// control over which cameras and entities should be used by the raycast backend at runtime. + pub require_markers: bool, + /// When set to Ignore, hidden items can be raycasted against. + /// See [`RaycastSettings::visibility`] for more information. + pub raycast_visibility: RaycastVisibility, +} + +impl Default for RaycastBackendSettings { + fn default() -> Self { + Self { + require_markers: false, + raycast_visibility: RaycastVisibility::MustBeVisibleAndInView, + } + } +} + +/// Optional. Marks cameras and target entities that should be used in the raycast picking backend. +/// Only needed if [`RaycastBackendSettings::require_markers`] is set to true. +#[derive(Debug, Clone, Default, Component, Reflect)] +#[reflect(Component, Default)] +pub struct RaycastPickable; + +/// Adds the raycasting picking backend to your app. +#[derive(Clone)] +pub struct RaycastBackend; +impl Plugin for RaycastBackend { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_systems(PreUpdate, update_hits.in_set(PickSet::Backend)) + .register_type::() + .register_type::(); + } +} + +/// Raycasts into the scene using [`RaycastBackendSettings`] and [`PointerLocation`]s, then outputs +/// [`PointerHits`]. +pub fn update_hits( + backend_settings: Res, + ray_map: Res, + picking_cameras: Query<(&Camera, Option<&RaycastPickable>, Option<&RenderLayers>)>, + pickables: Query<&Pickable>, + marked_targets: Query<&RaycastPickable>, + layers: Query<&RenderLayers>, + mut raycast: Raycast, + mut output_events: 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.raycast_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 = raycast + .cast_ray(ray, &settings) + .iter() + .map(|(entity, hit)| { + let hit_data = HitData::new( + ray_id.camera, + hit.distance(), + Some(hit.position()), + Some(hit.normal()), + ); + (*entity, hit_data) + }) + .collect::>(); + let order = camera.order as f32; + if !picks.is_empty() { + output_events.send(PointerHits::new(ray_id.pointer, picks, order)); + } + } +} diff --git a/crates/bevy_mesh_picking/src/raycast/immediate.rs b/crates/bevy_mesh_picking/src/raycast/immediate.rs new file mode 100644 index 0000000000000..56cd420b33716 --- /dev/null +++ b/crates/bevy_mesh_picking/src/raycast/immediate.rs @@ -0,0 +1,339 @@ +//! # Immediate Mode Raycasting API +//! +//! See the `minimal` example for reference. +//! +//! This is the simplest way to get started. Add the [`Raycast`] [`SystemParam`] to your system, and +//! call [`Raycast::cast_ray`], to get a list of intersections. Raycasts are performed immediately +//! when you call the `cast_ray` method. See the [`Raycast`] documentation for more details. You +//! don't even need to add a plugin to your application. + +use bevy_asset::{Assets, Handle}; +use bevy_ecs::{prelude::*, system::lifetimeless::Read, system::SystemParam}; +use bevy_math::{FloatOrd, Ray3d}; +use bevy_reflect::Reflect; +use bevy_render::{prelude::*, primitives::Aabb}; +use bevy_transform::components::GlobalTransform; +use bevy_utils::tracing::*; + +use super::{ + markers::{NoBackfaceCulling, SimplifiedMesh}, + primitives::{intersects_aabb, IntersectionData}, + raycast::{ray_intersection_over_mesh, Backfaces}, +}; + +#[cfg(feature = "debug")] +use { + bevy_gizmos::gizmos::Gizmos, + bevy_math::{Quat, Vec3}, +}; + +use crate::prelude::*; + +/// How a raycast should handle visibility +#[derive(Clone, Copy, Reflect)] +pub enum RaycastVisibility { + /// Completely ignore visibility checks. Hidden items can still be raycasted against. + Ignore, + /// Only raycast against entities that are visible in the hierarchy; see [`Visibility`]. + MustBeVisible, + /// Only raycast against entities that are visible in the hierarchy and visible to a camera or + /// light; see [`Visibility`]. + MustBeVisibleAndInView, +} + +/// Settings for a raycast. +#[derive(Clone)] +pub struct RaycastSettings<'a> { + /// Determines how raycasting should consider entity visibility. + pub visibility: RaycastVisibility, + /// A filtering function that is applied to every entity that is raycasted. 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. Raycasting 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 raycast. + 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 raycast. + 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 raycast. + pub fn with_visibility(mut self, visibility: RaycastVisibility) -> Self { + self.visibility = visibility; + self + } + + /// This raycast should exit as soon as the nearest hit is found. + pub fn always_early_exit(self) -> Self { + self.with_early_exit_test(&|_| true) + } + + /// This raycast 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::MustBeVisibleAndInView, + filter: &|_| true, + early_exit_test: &|_| true, + } + } +} + +#[cfg(feature = "2d")] +type MeshFilter = Or<(With>, With)>; +#[cfg(not(feature = "2d"))] +type MeshFilter = With>; + +/// Add this raycasting [`SystemParam`] to your system to raycast into the world with an +/// immediate-mode API. Call `cast_ray` to immediately perform a raycast and get a result. Under the +/// hood, this is a collection of regular bevy queries, resources, and locals that are added to your +/// system. +/// +/// ## Usage +/// +/// The following system raycasts into the world with a ray positioned at the origin, pointing in +/// the x-direction, and returns a list of intersections: +/// +/// ``` +/// # use bevy_mod_raycast::prelude::*; +/// # use bevy::prelude::*; +/// fn raycast_system(mut raycast: Raycast) { +/// let ray = Ray3d::new(Vec3::ZERO, Vec3::X); +/// let hits = raycast.cast_ray(ray, &RaycastSettings::default()); +/// } +/// ``` +/// ## Configuration +/// +/// You can specify behavior of the raycast using [`RaycastSettings`]. This allows you to filter out +/// entities, configure early-out, and set whether the [`Visibility`] of an entity should be +/// considered. +/// +/// ``` +/// # use bevy_mod_raycast::prelude::*; +/// # use bevy::prelude::*; +/// # #[derive(Component)] +/// # struct Foo; +/// fn raycast_system(mut raycast: Raycast, foo_query: Query<(), With>) { +/// let ray = Ray3d::new(Vec3::ZERO, Vec3::X); +/// +/// // Only raycast 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 raycasting hidden entities. +/// let visibility = RaycastVisibility::Ignore; +/// +/// let settings = RaycastSettings::default() +/// .with_filter(&filter) +/// .with_early_exit_test(&early_exit_test) +/// .with_visibility(visibility); +/// +/// let hits = raycast.cast_ray(ray, &settings); +/// } +/// ``` +#[derive(SystemParam)] +pub struct Raycast<'w, 's> { + #[doc(hidden)] + pub meshes: Res<'w, Assets>, + #[doc(hidden)] + pub hits: Local<'s, Vec<(FloatOrd, (Entity, IntersectionData))>>, + #[doc(hidden)] + pub output: Local<'s, Vec<(Entity, IntersectionData)>>, + #[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, + ( + Read>, + Option>, + Option>, + Read, + ), + >, + #[cfg(feature = "2d")] + #[doc(hidden)] + pub mesh2d_query: Query< + 'w, + 's, + ( + Read, + Option>, + Read, + ), + >, +} + +impl<'w, 's> Raycast<'w, 's> { + #[cfg(feature = "debug")] + /// Like [`Raycast::cast_ray`], but debug-draws the ray and intersection. + pub fn debug_cast_ray( + &mut self, + ray: Ray3d, + settings: &RaycastSettings, + gizmos: &mut Gizmos, + ) -> &[(Entity, IntersectionData)] { + use bevy_color::palettes::css; + use bevy_math::Dir3; + + let orientation = Quat::from_rotation_arc(Vec3::NEG_Z, *ray.direction); + gizmos.ray(ray.origin, *ray.direction, css::BLUE); + gizmos.sphere(ray.origin, orientation, 0.1, css::BLUE); + + let hits = self.cast_ray(ray, settings); + + for (is_first, intersection) in hits + .iter() + .map(|i| i.1.clone()) + .enumerate() + .map(|(i, hit)| (i == 0, hit)) + { + let color = match is_first { + true => css::GREEN, + false => css::PINK, + }; + gizmos.ray(intersection.position(), intersection.normal(), color); + gizmos.circle( + intersection.position(), + Dir3::new_unchecked(intersection.normal().normalize()), + 0.1, + color, + ); + } + + if let Some(hit) = hits.first() { + debug!("{:?}", hit); + } + + hits + } + + /// 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, IntersectionData)] { + 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_raycast = match visibility_setting { + RaycastVisibility::Ignore => true, + RaycastVisibility::MustBeVisible => inherited_visibility.get(), + RaycastVisibility::MustBeVisibleAndInView => view_visibility.get(), + }; + if should_raycast { + if let Some([near, _]) = intersects_aabb(ray, aabb, &transform.compute_matrix()) + .filter(|[_, far]| *far >= 0.0) + { + aabb_hits_tx.send((FloatOrd(near), entity)).ok(); + } + } + }, + ); + *self.culled_list = aabb_hits_rx.try_iter().collect(); + self.culled_list.sort_by_key(|(aabb_near, _)| *aabb_near); + drop(ray_cull_guard); + + let mut nearest_blocking_hit = FloatOrd(f32::INFINITY); + let raycast_guard = debug_span!("raycast"); + self.culled_list + .iter() + .filter(|(_, entity)| (settings.filter)(*entity)) + .for_each(|(aabb_near, entity)| { + let mut raycast_mesh = + |mesh_handle: &Handle, + simplified_mesh: Option<&SimplifiedMesh>, + no_backface_culling: Option<&NoBackfaceCulling>, + transform: &GlobalTransform| { + // 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 mesh_handle = simplified_mesh.map(|m| &m.mesh).unwrap_or(mesh_handle); + let Some(mesh) = self.meshes.get(mesh_handle) else { + return; + }; + + let _raycast_guard = raycast_guard.enter(); + let backfaces = match no_backface_culling { + Some(_) => Backfaces::Include, + None => Backfaces::Cull, + }; + 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 cant 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))); + }; + }; + + if let Ok((mesh, simp_mesh, culling, transform)) = self.mesh_query.get(*entity) { + raycast_mesh(mesh, simp_mesh, culling, transform); + } + + #[cfg(feature = "2d")] + if let Ok((mesh, simp_mesh, transform)) = self.mesh2d_query.get(*entity) { + raycast_mesh(&mesh.0, simp_mesh, Some(&NoBackfaceCulling), transform); + } + }); + + 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 = hits.collect(); + self.output.as_ref() + } +} diff --git a/crates/bevy_mesh_picking/src/raycast/markers.rs b/crates/bevy_mesh_picking/src/raycast/markers.rs new file mode 100644 index 0000000000000..c1dc19fba8ed0 --- /dev/null +++ b/crates/bevy_mesh_picking/src/raycast/markers.rs @@ -0,0 +1,10 @@ +use bevy_asset::Handle; +use bevy_ecs::component::Component; + +#[derive(Component)] +pub struct SimplifiedMesh { + pub mesh: Handle, +} + +#[derive(Component)] +pub struct NoBackfaceCulling; diff --git a/crates/bevy_mesh_picking/src/raycast/mod.rs b/crates/bevy_mesh_picking/src/raycast/mod.rs new file mode 100644 index 0000000000000..0b99da30ba64e --- /dev/null +++ b/crates/bevy_mesh_picking/src/raycast/mod.rs @@ -0,0 +1,4 @@ +pub mod immediate; +pub mod markers; +pub mod primitives; +pub mod raycast; diff --git a/crates/bevy_mesh_picking/src/raycast/primitives.rs b/crates/bevy_mesh_picking/src/raycast/primitives.rs new file mode 100644 index 0000000000000..682c65e068084 --- /dev/null +++ b/crates/bevy_mesh_picking/src/raycast/primitives.rs @@ -0,0 +1,187 @@ +use bevy_math::{Vec3, Vec3A}; +use bevy_reflect::Reflect; + +pub use rays::*; + +#[derive(Debug, Clone, Reflect)] +pub struct IntersectionData { + position: Vec3, + normal: Vec3, + barycentric_coord: Vec3, + distance: f32, + triangle: Option<[Vec3A; 3]>, + triangle_index: Option, +} + +impl From for IntersectionData { + fn from(data: rays::PrimitiveIntersection) -> Self { + Self { + position: data.position(), + normal: data.normal(), + distance: data.distance(), + barycentric_coord: Vec3::ZERO, + triangle: None, + triangle_index: None, + } + } +} + +impl IntersectionData { + pub fn new( + position: Vec3, + normal: Vec3, + barycentric: Vec3, + distance: f32, + triangle: Option<[Vec3A; 3]>, + triangle_index: Option, + ) -> Self { + Self { + position, + normal, + barycentric_coord: barycentric, + distance, + triangle, + triangle_index, + } + } + + /// Get the intersection data's position. + #[must_use] + pub fn position(&self) -> Vec3 { + self.position + } + + /// Get the intersection data's normal. + #[must_use] + pub fn normal(&self) -> Vec3 { + self.normal + } + + /// Get the intersection data's barycentric coord. + #[must_use] + pub fn barycentric_coord(&self) -> Vec3 { + self.barycentric_coord + } + + /// Get the intersection data's distance. + #[must_use] + pub fn distance(&self) -> f32 { + self.distance + } + + /// Get the intersection data's triangle. + #[must_use] + pub fn triangle(&self) -> Option<[Vec3A; 3]> { + self.triangle + } + + /// Get the intersection data's triangle index. + #[must_use] + pub fn triangle_index(&self) -> Option { + self.triangle_index + } +} + +/// Encapsulates Ray3D, preventing use of struct literal syntax. This allows us to guarantee that +/// the `Ray3d` direction is normalized, because it can only be instantiated with the constructor. +pub mod rays { + use bevy_math::{prelude::*, Ray3d, Vec3A}; + use bevy_render::primitives::Aabb; + + pub struct PrimitiveIntersection { + position: Vec3, + normal: Vec3, + distance: f32, + } + + impl PrimitiveIntersection { + pub fn new(position: Vec3, normal: Vec3, distance: f32) -> Self { + Self { + position, + normal, + distance, + } + } + + /// Get the intersection's position + #[must_use] + pub fn position(&self) -> Vec3 { + self.position + } + + /// Get the normal vector of the primitive at the point of intersection + #[must_use] + pub fn normal(&self) -> Vec3 { + self.normal + } + + /// Get the distance between the ray origin and the intersection position + #[must_use] + pub fn distance(&self) -> f32 { + self.distance + } + } + + pub fn to_transform(ray: Ray3d) -> Mat4 { + to_aligned_transform(ray, [0., 1., 0.].into()) + } + + /// Create a transform whose origin is at the origin of the ray and + /// whose up-axis is aligned with the direction of the ray. Use `up` to + /// specify which axis of the transform should align with the ray. + pub fn to_aligned_transform(ray: Ray3d, up: Vec3) -> Mat4 { + let position = ray.origin; + let normal = ray.direction; + let new_rotation = Quat::from_rotation_arc(up, *normal); + Mat4::from_rotation_translation(new_rotation, position) + } + + pub fn ray_from_transform(transform: Mat4) -> Ray3d { + let pick_position_ndc = Vec3::from([0.0, 0.0, -1.0]); + let pick_position = transform.project_point3(pick_position_ndc); + let (_, _, source_origin) = transform.to_scale_rotation_translation(); + let ray_direction = pick_position - source_origin; + Ray3d::new(source_origin, ray_direction) + } + + /// Checks if the ray intersects with an AABB of a mesh, returning `[near, far]` if it does. + pub fn intersects_aabb(ray: Ray3d, aabb: &Aabb, model_to_world: &Mat4) -> Option<[f32; 2]> { + // Transform the ray to model space + let world_to_model = model_to_world.inverse(); + let ray_dir: Vec3A = world_to_model.transform_vector3(*ray.direction).into(); + let ray_origin: Vec3A = world_to_model.transform_point3(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. + + let t_0: Vec3A = (aabb.min() - ray_origin) / ray_dir; + let t_1: Vec3A = (aabb.max() - ray_origin) / ray_dir; + let t_min: Vec3A = t_0.min(t_1); + let t_max: Vec3A = t_0.max(t_1); + + let mut hit_near = t_min.x; + let mut hit_far = t_max.x; + + if hit_near > t_max.y || t_min.y > hit_far { + return None; + } + + if t_min.y > hit_near { + hit_near = t_min.y; + } + if t_max.y < hit_far { + hit_far = t_max.y; + } + + if (hit_near > t_max.z) || (t_min.z > hit_far) { + return None; + } + + if t_min.z > hit_near { + hit_near = t_min.z; + } + if t_max.z < hit_far { + hit_far = t_max.z; + } + Some([hit_near, hit_far]) + } +} diff --git a/crates/bevy_mesh_picking/src/raycast/raycast.rs b/crates/bevy_mesh_picking/src/raycast/raycast.rs new file mode 100644 index 0000000000000..1d2b9e7d957ff --- /dev/null +++ b/crates/bevy_mesh_picking/src/raycast/raycast.rs @@ -0,0 +1,344 @@ +use bevy_math::{Mat4, Ray3d, Vec3, Vec3A}; +use bevy_render::{ + mesh::{Indices, Mesh, VertexAttributeValues}, + render_resource::PrimitiveTopology, +}; +use bevy_utils::tracing::{error, warn}; + +use super::primitives::*; + +/// Cast a ray on a mesh, and returns the intersection +pub 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 from the mesh reference resolved from the mesh handle + let vertex_positions: &Vec<[f32; 3]> = match mesh.attribute(Mesh::ATTRIBUTE_POSITION) { + None => panic!("Mesh does not contain vertex positions"), + Some(vertex_values) => match &vertex_values { + VertexAttributeValues::Float32x3(positions) => positions, + _ => panic!("Unexpected types in {:?}", Mesh::ATTRIBUTE_POSITION), + }, + }; + 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() { + // Iterate over the list of pick rays that belong to the same group as this mesh + match indices { + Indices::U16(vertex_indices) => ray_mesh_intersection( + mesh_transform, + vertex_positions, + vertex_normals, + ray, + Some(vertex_indices), + backface_culling, + ), + Indices::U32(vertex_indices) => ray_mesh_intersection( + mesh_transform, + vertex_positions, + vertex_normals, + ray, + Some(vertex_indices), + backface_culling, + ), + } + } else { + ray_mesh_intersection( + mesh_transform, + vertex_positions, + vertex_normals, + ray, + None::<&Vec>, + backface_culling, + ) + } +} + +pub trait IntoUsize: Copy { + fn into_usize(self) -> usize; +} +impl IntoUsize for u16 { + fn into_usize(self) -> usize { + self as usize + } +} +impl IntoUsize for u32 { + fn into_usize(self) -> usize { + self as usize + } +} + +/// Checks if a ray intersects a mesh, and returns the nearest intersection if one exists. +pub fn ray_mesh_intersection( + mesh_transform: &Mat4, + vertex_positions: &[[f32; 3]], + vertex_normals: Option<&[[f32; 3]]>, + ray: Ray3d, + indices: Option<&Vec>, + 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 min_pick_distance = f32::MAX; + let mut pick_intersection = None; + + let world_to_mesh = mesh_transform.inverse(); + + let mesh_space_ray = Ray3d::new( + world_to_mesh.transform_point3(ray.origin), + world_to_mesh.transform_vector3(*ray.direction), + ); + + 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 in indices.chunks(3) { + let triangle_index = Some(index[0].into_usize()); + let tri_vertex_positions = [ + Vec3A::from(vertex_positions[index[0].into_usize()]), + Vec3A::from(vertex_positions[index[1].into_usize()]), + Vec3A::from(vertex_positions[index[2].into_usize()]), + ]; + let tri_normals = vertex_normals.map(|normals| { + [ + Vec3A::from(normals[index[0].into_usize()]), + Vec3A::from(normals[index[1].into_usize()]), + Vec3A::from(normals[index[2].into_usize()]), + ] + }); + let intersection = triangle_intersection( + tri_vertex_positions, + tri_normals, + min_pick_distance, + &mesh_space_ray, + backface_culling, + ); + if let Some(i) = intersection { + pick_intersection = Some(IntersectionData::new( + mesh_transform.transform_point3(i.position()), + mesh_transform.transform_vector3(i.normal()), + i.barycentric_coord(), + mesh_transform + .transform_vector3(mesh_space_ray.direction * i.distance()) + .length(), + i.triangle().map(|tri| { + [ + mesh_transform.transform_point3a(tri[0]), + mesh_transform.transform_point3a(tri[1]), + mesh_transform.transform_point3a(tri[2]), + ] + }), + triangle_index, + )); + min_pick_distance = i.distance(); + } + } + } else { + for i in (0..vertex_positions.len()).step_by(3) { + let triangle_index = Some(i); + let tri_vertex_positions = [ + Vec3A::from(vertex_positions[i]), + Vec3A::from(vertex_positions[i + 1]), + Vec3A::from(vertex_positions[i + 2]), + ]; + let tri_normals = vertex_normals.map(|normals| { + [ + Vec3A::from(normals[i]), + Vec3A::from(normals[i + 1]), + Vec3A::from(normals[i + 2]), + ] + }); + let intersection = triangle_intersection( + tri_vertex_positions, + tri_normals, + min_pick_distance, + &mesh_space_ray, + backface_culling, + ); + if let Some(i) = intersection { + pick_intersection = Some(IntersectionData::new( + mesh_transform.transform_point3(i.position()), + mesh_transform.transform_vector3(i.normal()), + i.barycentric_coord(), + mesh_transform + .transform_vector3(mesh_space_ray.direction * i.distance()) + .length(), + i.triangle().map(|tri| { + [ + mesh_transform.transform_point3a(tri[0]), + mesh_transform.transform_point3a(tri[1]), + mesh_transform.transform_point3a(tri[2]), + ] + }), + triangle_index, + )); + min_pick_distance = i.distance(); + } + } + } + pick_intersection +} + +#[inline(always)] +fn triangle_intersection( + tri_vertices: [Vec3A; 3], + tri_normals: Option<[Vec3A; 3]>, + max_distance: f32, + ray: &Ray3d, + backface_culling: Backfaces, +) -> Option { + // Run the raycast on the ray and triangle + let ray_hit = ray_triangle_intersection(ray, &tri_vertices, backface_culling)?; + let distance = *ray_hit.distance(); + if distance < 0.0 || distance > max_distance { + return None; + }; + let position = ray.get_point(distance); + let u = ray_hit.uv_coords().0; + let v = ray_hit.uv_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(IntersectionData::new( + position, + normal.into(), + barycentric, + distance, + Some(tri_vertices), + None, + )) +} + +#[derive(Copy, Clone, Default)] +pub enum Backfaces { + #[default] + Cull, + Include, +} + +/// Takes a ray and triangle and computes the intersection and normal +#[inline(always)] +pub 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(RayHit { + distance: t, + uv_coords: (u, v), + }) +} + +#[derive(Default, Debug)] +pub struct RayHit { + distance: f32, + uv_coords: (f32, f32), +} + +impl RayHit { + /// Get a reference to the intersection's uv coords. + pub fn uv_coords(&self) -> &(f32, f32) { + &self.uv_coords + } + + /// Get a reference to the intersection's distance. + pub fn distance(&self) -> &f32 { + &self.distance + } +} + +#[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 raycast_triangle_mt() { + let triangle = [V0.into(), V1.into(), V2.into()]; + let ray = Ray3d::new(Vec3::ZERO, Vec3::X); + let result = ray_triangle_intersection(&ray, &triangle, Backfaces::Include); + assert!(result.unwrap().distance - 1.0 <= f32::EPSILON); + } + + #[test] + fn raycast_triangle_mt_culling() { + let triangle = [V2.into(), V1.into(), V0.into()]; + let ray = Ray3d::new(Vec3::ZERO, Vec3::X); + let result = ray_triangle_intersection(&ray, &triangle, Backfaces::Cull); + assert!(result.is_none()); + } +} From 9b73b4473fea04e08807ea9104a376037a6ae202 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Wed, 2 Oct 2024 15:29:22 +0300 Subject: [PATCH 02/27] Rename to bevy_picking_mesh and add to default plugins --- Cargo.toml | 4 ++++ crates/bevy_internal/Cargo.toml | 6 +++++- crates/bevy_internal/src/default_plugins.rs | 2 ++ crates/bevy_internal/src/lib.rs | 9 ++++++++- .../{bevy_mesh_picking => bevy_picking_mesh}/Cargo.toml | 2 +- .../{bevy_mesh_picking => bevy_picking_mesh}/src/lib.rs | 2 +- .../src/raycast/immediate.rs | 0 .../src/raycast/markers.rs | 0 .../src/raycast/mod.rs | 0 .../src/raycast/primitives.rs | 0 .../src/raycast/raycast.rs | 0 11 files changed, 21 insertions(+), 4 deletions(-) rename crates/{bevy_mesh_picking => bevy_picking_mesh}/Cargo.toml (97%) rename crates/{bevy_mesh_picking => bevy_picking_mesh}/src/lib.rs (99%) rename crates/{bevy_mesh_picking => bevy_picking_mesh}/src/raycast/immediate.rs (100%) rename crates/{bevy_mesh_picking => bevy_picking_mesh}/src/raycast/markers.rs (100%) rename crates/{bevy_mesh_picking => bevy_picking_mesh}/src/raycast/mod.rs (100%) rename crates/{bevy_mesh_picking => bevy_picking_mesh}/src/raycast/primitives.rs (100%) rename crates/{bevy_mesh_picking => bevy_picking_mesh}/src/raycast/raycast.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index f7420664260fa..54ca3ecafe7a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,6 +111,7 @@ default = [ "bevy_core_pipeline", "bevy_pbr", "bevy_picking", + "bevy_picking_mesh", "bevy_sprite_picking_backend", "bevy_ui_picking_backend", "bevy_gltf", @@ -181,6 +182,9 @@ bevy_pbr = [ # Provides picking functionality bevy_picking = ["bevy_internal/bevy_picking"] +# Provides a mesh picking backend for `bevy_picking` +bevy_picking_mesh = ["bevy_picking", "bevy_internal/bevy_picking_mesh"] + # Provides rendering functionality bevy_render = ["bevy_internal/bevy_render", "bevy_color"] diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 9dba6deabd9f7..535e88d8a03b8 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -195,13 +195,16 @@ 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_ui?/bevy_picking", "bevy_sprite?/bevy_picking", ] +# Provides a mesh picking backend for `bevy_picking` +bevy_picking_mesh = ["dep:bevy_picking_mesh"] + # Enable support for the ios_simulator by downgrading some rendering capabilities ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"] @@ -252,6 +255,7 @@ bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.15.0-dev" bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.15.0-dev" } bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.15.0-dev" } bevy_picking = { path = "../bevy_picking", optional = true, version = "0.15.0-dev" } +bevy_picking_mesh = { path = "../bevy_picking_mesh", optional = true, version = "0.15.0-dev" } bevy_remote = { path = "../bevy_remote", optional = true, version = "0.15.0-dev" } bevy_render = { path = "../bevy_render", optional = true, version = "0.15.0-dev" } bevy_scene = { path = "../bevy_scene", optional = true, version = "0.15.0-dev" } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 74008dafafcc8..fbff53ac84dfb 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -58,6 +58,8 @@ plugin_group! { bevy_state::app:::StatesPlugin, #[cfg(feature = "bevy_picking")] bevy_picking:::DefaultPickingPlugins, + #[cfg(feature = "bevy_picking_mesh")] + bevy_picking_mesh:::RaycastBackend, #[cfg(feature = "bevy_dev_tools")] bevy_dev_tools:::DevToolsPlugin, #[cfg(feature = "bevy_ci_testing")] diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index bc553af972223..10daaddd2cdb2 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -43,7 +43,14 @@ pub use bevy_math as math; #[cfg(feature = "bevy_pbr")] pub use bevy_pbr as pbr; #[cfg(feature = "bevy_picking")] -pub use bevy_picking as picking; +#[allow(ambiguous_glob_reexports)] +pub mod picking { + pub use bevy_picking::*; + #[cfg(feature = "bevy_picking_mesh")] + pub mod mesh { + pub use bevy_picking_mesh::*; + } +} pub use bevy_ptr as ptr; pub use bevy_reflect as reflect; #[cfg(feature = "bevy_remote")] diff --git a/crates/bevy_mesh_picking/Cargo.toml b/crates/bevy_picking_mesh/Cargo.toml similarity index 97% rename from crates/bevy_mesh_picking/Cargo.toml rename to crates/bevy_picking_mesh/Cargo.toml index acad4598a37b3..fbd787612ddd7 100644 --- a/crates/bevy_mesh_picking/Cargo.toml +++ b/crates/bevy_picking_mesh/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bevy_mesh_picking" +name = "bevy_picking_mesh" version = "0.15.0-dev" edition = "2021" description = "Provides a mesh picking backend for Bevy Engine" diff --git a/crates/bevy_mesh_picking/src/lib.rs b/crates/bevy_picking_mesh/src/lib.rs similarity index 99% rename from crates/bevy_mesh_picking/src/lib.rs rename to crates/bevy_picking_mesh/src/lib.rs index 84e0b820306e4..285465d2fc30f 100644 --- a/crates/bevy_mesh_picking/src/lib.rs +++ b/crates/bevy_picking_mesh/src/lib.rs @@ -61,7 +61,7 @@ impl Default for RaycastBackendSettings { pub struct RaycastPickable; /// Adds the raycasting picking backend to your app. -#[derive(Clone)] +#[derive(Clone, Default)] pub struct RaycastBackend; impl Plugin for RaycastBackend { fn build(&self, app: &mut App) { diff --git a/crates/bevy_mesh_picking/src/raycast/immediate.rs b/crates/bevy_picking_mesh/src/raycast/immediate.rs similarity index 100% rename from crates/bevy_mesh_picking/src/raycast/immediate.rs rename to crates/bevy_picking_mesh/src/raycast/immediate.rs diff --git a/crates/bevy_mesh_picking/src/raycast/markers.rs b/crates/bevy_picking_mesh/src/raycast/markers.rs similarity index 100% rename from crates/bevy_mesh_picking/src/raycast/markers.rs rename to crates/bevy_picking_mesh/src/raycast/markers.rs diff --git a/crates/bevy_mesh_picking/src/raycast/mod.rs b/crates/bevy_picking_mesh/src/raycast/mod.rs similarity index 100% rename from crates/bevy_mesh_picking/src/raycast/mod.rs rename to crates/bevy_picking_mesh/src/raycast/mod.rs diff --git a/crates/bevy_mesh_picking/src/raycast/primitives.rs b/crates/bevy_picking_mesh/src/raycast/primitives.rs similarity index 100% rename from crates/bevy_mesh_picking/src/raycast/primitives.rs rename to crates/bevy_picking_mesh/src/raycast/primitives.rs diff --git a/crates/bevy_mesh_picking/src/raycast/raycast.rs b/crates/bevy_picking_mesh/src/raycast/raycast.rs similarity index 100% rename from crates/bevy_mesh_picking/src/raycast/raycast.rs rename to crates/bevy_picking_mesh/src/raycast/raycast.rs From 6ccc5397b86ff50195ff709deee38bab12aa1487 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Thu, 3 Oct 2024 16:57:03 +0300 Subject: [PATCH 03/27] Clean up and rename "raycast" to "ray cast" everywhere --- crates/bevy_internal/src/default_plugins.rs | 2 +- crates/bevy_picking_mesh/Cargo.toml | 4 + crates/bevy_picking_mesh/src/lib.rs | 70 +++--- .../src/{raycast => ray_cast}/immediate.rs | 201 +++++++----------- .../src/ray_cast/intersections.rs | 168 +++++++++++++++ .../{raycast/raycast.rs => ray_cast/mod.rs} | 51 +++-- .../src/ray_cast/simplified_mesh.rs | 10 + .../bevy_picking_mesh/src/raycast/markers.rs | 10 - crates/bevy_picking_mesh/src/raycast/mod.rs | 4 - .../src/raycast/primitives.rs | 187 ---------------- 10 files changed, 328 insertions(+), 379 deletions(-) rename crates/bevy_picking_mesh/src/{raycast => ray_cast}/immediate.rs (54%) create mode 100644 crates/bevy_picking_mesh/src/ray_cast/intersections.rs rename crates/bevy_picking_mesh/src/{raycast/raycast.rs => ray_cast/mod.rs} (91%) create mode 100644 crates/bevy_picking_mesh/src/ray_cast/simplified_mesh.rs delete mode 100644 crates/bevy_picking_mesh/src/raycast/markers.rs delete mode 100644 crates/bevy_picking_mesh/src/raycast/mod.rs delete mode 100644 crates/bevy_picking_mesh/src/raycast/primitives.rs diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index fbff53ac84dfb..b6bd0a68b3d59 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -59,7 +59,7 @@ plugin_group! { #[cfg(feature = "bevy_picking")] bevy_picking:::DefaultPickingPlugins, #[cfg(feature = "bevy_picking_mesh")] - bevy_picking_mesh:::RaycastBackend, + bevy_picking_mesh:::RayCastBackend, #[cfg(feature = "bevy_dev_tools")] bevy_dev_tools:::DevToolsPlugin, #[cfg(feature = "bevy_ci_testing")] diff --git a/crates/bevy_picking_mesh/Cargo.toml b/crates/bevy_picking_mesh/Cargo.toml index fbd787612ddd7..146ff9116f285 100644 --- a/crates/bevy_picking_mesh/Cargo.toml +++ b/crates/bevy_picking_mesh/Cargo.toml @@ -8,10 +8,14 @@ repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] +[features] +2d = [] + [dependencies] bevy_app = { path = "../bevy_app", version = "0.15.0-dev", default-features = false } bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev", default-features = false } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev", default-features = false } +bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev", default-features = false } bevy_math = { path = "../bevy_math", version = "0.15.0-dev", default-features = false } bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", default-features = false } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", default-features = false } diff --git a/crates/bevy_picking_mesh/src/lib.rs b/crates/bevy_picking_mesh/src/lib.rs index 285465d2fc30f..e5697ef5f51d1 100644 --- a/crates/bevy_picking_mesh/src/lib.rs +++ b/crates/bevy_picking_mesh/src/lib.rs @@ -1,4 +1,4 @@ -//! A raycasting backend for `bevy_mod_picking` that uses `bevy_mod_raycast` for raycasting. +//! A ray casting backend for `bevy_picking`. //! //! # Usage //! @@ -6,10 +6,9 @@ //! the scene and will be able to pick things. //! //! To ignore an entity, you can add [`Pickable::IGNORE`] to it, and it will be ignored during -//! raycasting. -//! -//! For fine-grained control, see the [`RaycastBackendSettings::require_markers`] setting. +//! ray casting. //! +//! For fine-grained control, see the [`RayCastBackendSettings::require_markers`] setting. #![allow(clippy::too_many_arguments, clippy::type_complexity)] #![warn(missing_docs)] @@ -23,65 +22,71 @@ use bevy_picking::{ }; use bevy_reflect::prelude::*; use bevy_render::{prelude::*, view::RenderLayers}; -use raycast::immediate::{Raycast, RaycastSettings, RaycastVisibility}; +use ray_cast::{Backfaces, MeshRayCast, RayCastSettings, RayCastVisibility}; -pub mod raycast; +pub mod ray_cast; /// Commonly used imports for the [`bevy_picking_raycast`](crate) crate. pub mod prelude { - pub use crate::RaycastBackend; + pub use crate::{ + ray_cast::{Backfaces, MeshRayCast, RayCastSettings, RayCastVisibility, RayTriangleHit}, + RayCastBackend, + }; } -/// Runtime settings for the [`RaycastBackend`]. +/// Runtime settings for the [`RayCastBackend`]. #[derive(Resource, Reflect)] #[reflect(Resource, Default)] -pub struct RaycastBackendSettings { - /// When set to `true` raycasting will only happen between cameras and entities marked with - /// [`RaycastPickable`]. Off by default. This setting is provided to give you fine-grained - /// control over which cameras and entities should be used by the raycast backend at runtime. +pub struct RayCastBackendSettings { + /// When set to `true` ray casting will only happen between cameras and entities marked with + /// [`RayCastPickable`]. Off by default. This setting is provided to give you fine-grained + /// control over which cameras and entities should be used by the ray cast backend at runtime. pub require_markers: bool, /// When set to Ignore, hidden items can be raycasted against. - /// See [`RaycastSettings::visibility`] for more information. - pub raycast_visibility: RaycastVisibility, + /// See [`RayCastSettings::visibility`] for more information. + pub raycast_visibility: RayCastVisibility, + /// When set to [`Backfaces::Cull`], backfaces will be ignored during ray casting. + pub backfaces: Backfaces, } -impl Default for RaycastBackendSettings { +impl Default for RayCastBackendSettings { fn default() -> Self { Self { require_markers: false, - raycast_visibility: RaycastVisibility::MustBeVisibleAndInView, + raycast_visibility: RayCastVisibility::VisibleAndInView, + backfaces: Backfaces::default(), } } } -/// Optional. Marks cameras and target entities that should be used in the raycast picking backend. -/// Only needed if [`RaycastBackendSettings::require_markers`] is set to true. +/// Optional. Marks cameras and target entities that should be used in the ray cast picking backend. +/// Only needed if [`RayCastBackendSettings::require_markers`] is set to true. #[derive(Debug, Clone, Default, Component, Reflect)] #[reflect(Component, Default)] -pub struct RaycastPickable; +pub struct RayCastPickable; -/// Adds the raycasting picking backend to your app. +/// Adds the ray casting picking backend to your app. #[derive(Clone, Default)] -pub struct RaycastBackend; -impl Plugin for RaycastBackend { +pub struct RayCastBackend; +impl Plugin for RayCastBackend { fn build(&self, app: &mut App) { - app.init_resource::() + app.init_resource::() .add_systems(PreUpdate, update_hits.in_set(PickSet::Backend)) - .register_type::() - .register_type::(); + .register_type::() + .register_type::(); } } -/// Raycasts into the scene using [`RaycastBackendSettings`] and [`PointerLocation`]s, then outputs +/// Raycasts into the scene using [`RayCastBackendSettings`] and [`PointerLocation`]s, then outputs /// [`PointerHits`]. pub fn update_hits( - backend_settings: Res, + backend_settings: Res, ray_map: Res, - picking_cameras: Query<(&Camera, Option<&RaycastPickable>, Option<&RenderLayers>)>, + picking_cameras: Query<(&Camera, Option<&RayCastPickable>, Option<&RenderLayers>)>, pickables: Query<&Pickable>, - marked_targets: Query<&RaycastPickable>, + marked_targets: Query<&RayCastPickable>, layers: Query<&RenderLayers>, - mut raycast: Raycast, + mut ray_cast: MeshRayCast, mut output_events: EventWriter, ) { for (&ray_id, &ray) in ray_map.map().iter() { @@ -94,8 +99,9 @@ pub fn update_hits( let cam_layers = cam_layers.to_owned().unwrap_or_default(); - let settings = RaycastSettings { + let settings = RayCastSettings { visibility: backend_settings.raycast_visibility, + backfaces: backend_settings.backfaces, filter: &|entity| { let marker_requirement = !backend_settings.require_markers || marked_targets.get(entity).is_ok(); @@ -117,7 +123,7 @@ pub fn update_hits( .is_ok_and(|pickable| pickable.should_block_lower) }, }; - let picks = raycast + let picks = ray_cast .cast_ray(ray, &settings) .iter() .map(|(entity, hit)| { diff --git a/crates/bevy_picking_mesh/src/raycast/immediate.rs b/crates/bevy_picking_mesh/src/ray_cast/immediate.rs similarity index 54% rename from crates/bevy_picking_mesh/src/raycast/immediate.rs rename to crates/bevy_picking_mesh/src/ray_cast/immediate.rs index 56cd420b33716..62f403b6bdbf1 100644 --- a/crates/bevy_picking_mesh/src/raycast/immediate.rs +++ b/crates/bevy_picking_mesh/src/ray_cast/immediate.rs @@ -1,10 +1,10 @@ -//! # Immediate Mode Raycasting API +//! # Immediate Mode Ray Casting API //! //! See the `minimal` example for reference. //! -//! This is the simplest way to get started. Add the [`Raycast`] [`SystemParam`] to your system, and -//! call [`Raycast::cast_ray`], to get a list of intersections. Raycasts are performed immediately -//! when you call the `cast_ray` method. See the [`Raycast`] documentation for more details. You +//! This is the simplest way to get started. Add the [`MeshRayCast`] [`SystemParam`] to your system, and +//! call [`MeshRayCast::cast_ray`], to get a list of intersections. Ray casts are performed immediately +//! when you call the `cast_ray` method. See the [`MeshRayCast`] documentation for more details. You //! don't even need to add a plugin to your application. use bevy_asset::{Assets, Handle}; @@ -16,78 +16,74 @@ use bevy_transform::components::GlobalTransform; use bevy_utils::tracing::*; use super::{ - markers::{NoBackfaceCulling, SimplifiedMesh}, - primitives::{intersects_aabb, IntersectionData}, - raycast::{ray_intersection_over_mesh, Backfaces}, + intersections::{ray_aabb_intersection_3d, RayMeshHit}, + ray_intersection_over_mesh, + simplified_mesh::SimplifiedMesh, + Backfaces, }; -#[cfg(feature = "debug")] -use { - bevy_gizmos::gizmos::Gizmos, - bevy_math::{Quat, Vec3}, -}; - -use crate::prelude::*; - -/// How a raycast should handle visibility +/// How a ray cast should handle visibility. #[derive(Clone, Copy, Reflect)] -pub enum RaycastVisibility { +pub enum RayCastVisibility { /// Completely ignore visibility checks. Hidden items can still be raycasted against. - Ignore, - /// Only raycast against entities that are visible in the hierarchy; see [`Visibility`]. - MustBeVisible, - /// Only raycast against entities that are visible in the hierarchy and visible to a camera or - /// light; see [`Visibility`]. - MustBeVisibleAndInView, + Any, + /// Only ray cast against entities that are visible in the hierarchy. See [`Visibility`]. + Visible, + /// Only ray cast against entities that are visible in the hierarchy and visible to a camera or + /// light. See [`Visibility`]. + VisibleAndInView, } -/// Settings for a raycast. +/// Settings for a ray cast. #[derive(Clone)] -pub struct RaycastSettings<'a> { - /// Determines how raycasting should consider entity visibility. - pub visibility: RaycastVisibility, +pub struct RayCastSettings<'a> { + /// Determines how ray casting should consider entity visibility. + pub visibility: RayCastVisibility, + /// Determines how ray casting should handle backfaces. + pub backfaces: Backfaces, /// A filtering function that is applied to every entity that is raycasted. 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. Raycasting will continue to check for hits + /// 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 raycast. +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 raycast. + /// 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 raycast. - pub fn with_visibility(mut self, visibility: RaycastVisibility) -> 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 raycast should exit as soon as the nearest hit is found. + /// 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 raycast should check all entities whose AABB intersects the ray and return all hits. + /// 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> { +impl<'a> Default for RayCastSettings<'a> { fn default() -> Self { Self { - visibility: RaycastVisibility::MustBeVisibleAndInView, + visibility: RayCastVisibility::VisibleAndInView, + backfaces: Backfaces::default(), filter: &|_| true, early_exit_test: &|_| true, } @@ -95,65 +91,64 @@ impl<'a> Default for RaycastSettings<'a> { } #[cfg(feature = "2d")] -type MeshFilter = Or<(With>, With)>; +type MeshFilter = Or<(With, With)>; #[cfg(not(feature = "2d"))] -type MeshFilter = With>; +type MeshFilter = With; -/// Add this raycasting [`SystemParam`] to your system to raycast into the world with an -/// immediate-mode API. Call `cast_ray` to immediately perform a raycast and get a result. Under the +/// Add this raycasting [`SystemParam`] to your system to ray cast 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 locals that are added to your /// system. /// /// ## Usage /// -/// The following system raycasts into the world with a ray positioned at the origin, pointing in +/// The following system ray casts into the world with a ray positioned at the origin, pointing in /// the x-direction, and returns a list of intersections: /// /// ``` -/// # use bevy_mod_raycast::prelude::*; /// # use bevy::prelude::*; -/// fn raycast_system(mut raycast: Raycast) { +/// fn ray_cast_system(mut raycast: MeshRayCast) { /// let ray = Ray3d::new(Vec3::ZERO, Vec3::X); -/// let hits = raycast.cast_ray(ray, &RaycastSettings::default()); +/// let hits = raycast.cast_ray(ray, &RayCastSettings::default()); /// } /// ``` +/// /// ## Configuration /// -/// You can specify behavior of the raycast using [`RaycastSettings`]. This allows you to filter out +/// You can specify behavior of the ray cast using [`RayCastSettings`]. This allows you to filter out /// entities, configure early-out, and set whether the [`Visibility`] of an entity should be /// considered. /// /// ``` -/// # use bevy_mod_raycast::prelude::*; /// # use bevy::prelude::*; /// # #[derive(Component)] /// # struct Foo; -/// fn raycast_system(mut raycast: Raycast, foo_query: Query<(), With>) { +/// fn ray_cast_system(mut ray_cast: MeshRayCast, foo_query: Query<(), With>) { /// let ray = Ray3d::new(Vec3::ZERO, Vec3::X); /// -/// // Only raycast against entities with the `Foo` component. +/// // 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 raycasting hidden entities. -/// let visibility = RaycastVisibility::Ignore; +/// // Ignore the visibility of entities. This allows ray casting hidden entities. +/// let visibility = RayCastVisibility::Ignore; /// -/// let settings = RaycastSettings::default() +/// let settings = RayCastSettings::default() /// .with_filter(&filter) /// .with_early_exit_test(&early_exit_test) /// .with_visibility(visibility); /// -/// let hits = raycast.cast_ray(ray, &settings); +/// let hits = ray_cast.cast_ray(ray, &settings); /// } /// ``` #[derive(SystemParam)] -pub struct Raycast<'w, 's> { +pub struct MeshRayCast<'w, 's> { #[doc(hidden)] pub meshes: Res<'w, Assets>, #[doc(hidden)] - pub hits: Local<'s, Vec<(FloatOrd, (Entity, IntersectionData))>>, + pub hits: Local<'s, Vec<(FloatOrd, (Entity, RayMeshHit))>>, #[doc(hidden)] - pub output: Local<'s, Vec<(Entity, IntersectionData)>>, + pub output: Local<'s, Vec<(Entity, RayMeshHit)>>, #[doc(hidden)] pub culled_list: Local<'s, Vec<(FloatOrd, Entity)>>, #[doc(hidden)] @@ -174,9 +169,8 @@ pub struct Raycast<'w, 's> { 'w, 's, ( - Read>, + Read, Option>, - Option>, Read, ), >, @@ -186,63 +180,16 @@ pub struct Raycast<'w, 's> { 'w, 's, ( - Read, + Read, Option>, Read, ), >, } -impl<'w, 's> Raycast<'w, 's> { - #[cfg(feature = "debug")] - /// Like [`Raycast::cast_ray`], but debug-draws the ray and intersection. - pub fn debug_cast_ray( - &mut self, - ray: Ray3d, - settings: &RaycastSettings, - gizmos: &mut Gizmos, - ) -> &[(Entity, IntersectionData)] { - use bevy_color::palettes::css; - use bevy_math::Dir3; - - let orientation = Quat::from_rotation_arc(Vec3::NEG_Z, *ray.direction); - gizmos.ray(ray.origin, *ray.direction, css::BLUE); - gizmos.sphere(ray.origin, orientation, 0.1, css::BLUE); - - let hits = self.cast_ray(ray, settings); - - for (is_first, intersection) in hits - .iter() - .map(|i| i.1.clone()) - .enumerate() - .map(|(i, hit)| (i == 0, hit)) - { - let color = match is_first { - true => css::GREEN, - false => css::PINK, - }; - gizmos.ray(intersection.position(), intersection.normal(), color); - gizmos.circle( - intersection.position(), - Dir3::new_unchecked(intersection.normal().normalize()), - 0.1, - color, - ); - } - - if let Some(hit) = hits.first() { - debug!("{:?}", hit); - } - - hits - } - +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, IntersectionData)] { + 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(); @@ -256,14 +203,15 @@ impl<'w, 's> Raycast<'w, 's> { let visibility_setting = settings.visibility; self.culling_query.par_iter().for_each( |(inherited_visibility, view_visibility, aabb, transform, entity)| { - let should_raycast = match visibility_setting { - RaycastVisibility::Ignore => true, - RaycastVisibility::MustBeVisible => inherited_visibility.get(), - RaycastVisibility::MustBeVisibleAndInView => view_visibility.get(), + let should_ray_cast = match visibility_setting { + RayCastVisibility::Any => true, + RayCastVisibility::Visible => inherited_visibility.get(), + RayCastVisibility::VisibleAndInView => view_visibility.get(), }; - if should_raycast { - if let Some([near, _]) = intersects_aabb(ray, aabb, &transform.compute_matrix()) - .filter(|[_, far]| *far >= 0.0) + if should_ray_cast { + if let Some([near, _]) = + ray_aabb_intersection_3d(ray, aabb, &transform.compute_matrix()) + .filter(|[_, far]| *far >= 0.0) { aabb_hits_tx.send((FloatOrd(near), entity)).ok(); } @@ -275,15 +223,14 @@ impl<'w, 's> Raycast<'w, 's> { drop(ray_cull_guard); let mut nearest_blocking_hit = FloatOrd(f32::INFINITY); - let raycast_guard = debug_span!("raycast"); + let ray_cast_guard = debug_span!("ray_cast"); self.culled_list .iter() .filter(|(_, entity)| (settings.filter)(*entity)) .for_each(|(aabb_near, entity)| { - let mut raycast_mesh = + let mut ray_cast_mesh = |mesh_handle: &Handle, simplified_mesh: Option<&SimplifiedMesh>, - no_backface_culling: Option<&NoBackfaceCulling>, transform: &GlobalTransform| { // Is it even possible the mesh could be closer than the current best? if *aabb_near > nearest_blocking_hit { @@ -291,19 +238,15 @@ impl<'w, 's> Raycast<'w, 's> { } // Does the mesh handle resolve? - let mesh_handle = simplified_mesh.map(|m| &m.mesh).unwrap_or(mesh_handle); + let mesh_handle = simplified_mesh.map(|m| &m.0).unwrap_or(mesh_handle); let Some(mesh) = self.meshes.get(mesh_handle) else { return; }; - let _raycast_guard = raycast_guard.enter(); - let backfaces = match no_backface_culling { - Some(_) => Backfaces::Include, - None => Backfaces::Cull, - }; + let _ray_cast_guard = ray_cast_guard.enter(); let transform = transform.compute_matrix(); let intersection = - ray_intersection_over_mesh(mesh, &transform, ray, backfaces); + ray_intersection_over_mesh(mesh, &transform, ray, settings.backfaces); if let Some(intersection) = intersection { let distance = FloatOrd(intersection.distance()); if (settings.early_exit_test)(*entity) @@ -320,13 +263,13 @@ impl<'w, 's> Raycast<'w, 's> { }; }; - if let Ok((mesh, simp_mesh, culling, transform)) = self.mesh_query.get(*entity) { - raycast_mesh(mesh, simp_mesh, culling, transform); + if let Ok((mesh, simplified_mesh, transform)) = self.mesh_query.get(*entity) { + ray_cast_mesh(mesh, simplified_mesh, transform); } #[cfg(feature = "2d")] - if let Ok((mesh, simp_mesh, transform)) = self.mesh2d_query.get(*entity) { - raycast_mesh(&mesh.0, simp_mesh, Some(&NoBackfaceCulling), transform); + if let Ok((mesh, simplified_mesh, transform)) = self.mesh2d_query.get(*entity) { + ray_cast_mesh(&mesh.0, simplified_mesh, transform); } }); diff --git a/crates/bevy_picking_mesh/src/ray_cast/intersections.rs b/crates/bevy_picking_mesh/src/ray_cast/intersections.rs new file mode 100644 index 0000000000000..9b4a8255b10a9 --- /dev/null +++ b/crates/bevy_picking_mesh/src/ray_cast/intersections.rs @@ -0,0 +1,168 @@ +use bevy_math::{Mat4, Ray3d, Vec3, Vec3A}; +use bevy_reflect::Reflect; +use bevy_render::primitives::Aabb; + +/// A ray intersection with a mesh. +#[derive(Debug, Clone, Reflect)] +pub struct RayMeshHit { + position: Vec3, + normal: Vec3, + barycentric_coord: Vec3, + distance: f32, + triangle: Option<[Vec3A; 3]>, + triangle_index: Option, +} + +impl From for RayMeshHit { + fn from(data: PrimitiveIntersection) -> Self { + Self { + position: data.position(), + normal: data.normal(), + distance: data.distance(), + barycentric_coord: Vec3::ZERO, + triangle: None, + triangle_index: None, + } + } +} + +impl RayMeshHit { + /// Creates a new [`RayMeshHit`] with the given data. + pub fn new( + position: Vec3, + normal: Vec3, + barycentric: Vec3, + distance: f32, + triangle: Option<[Vec3A; 3]>, + triangle_index: Option, + ) -> Self { + Self { + position, + normal, + barycentric_coord: barycentric, + distance, + triangle, + triangle_index, + } + } + + /// Get the intersection data's position. + #[must_use] + pub fn position(&self) -> Vec3 { + self.position + } + + /// Get the intersection data's normal. + #[must_use] + pub fn normal(&self) -> Vec3 { + self.normal + } + + /// Get the intersection data's barycentric coord. + #[must_use] + pub fn barycentric_coord(&self) -> Vec3 { + self.barycentric_coord + } + + /// Get the intersection data's distance. + #[must_use] + pub fn distance(&self) -> f32 { + self.distance + } + + /// Get the intersection data's triangle. + #[must_use] + pub fn triangle(&self) -> Option<[Vec3A; 3]> { + self.triangle + } + + /// Get the intersection data's triangle index. + #[must_use] + pub fn triangle_index(&self) -> Option { + self.triangle_index + } +} + +/// A ray intersection with a primitive. +pub struct PrimitiveIntersection { + position: Vec3, + normal: Vec3, + distance: f32, +} + +impl PrimitiveIntersection { + /// Creates a new [`PrimitiveIntersection`] with the given data. + pub fn new(position: Vec3, normal: Vec3, distance: f32) -> Self { + Self { + position, + normal, + distance, + } + } + + /// Get the intersection's position + #[must_use] + pub fn position(&self) -> Vec3 { + self.position + } + + /// Get the normal vector of the primitive at the point of intersection + #[must_use] + pub fn normal(&self) -> Vec3 { + self.normal + } + + /// Get the distance between the ray origin and the intersection position + #[must_use] + pub fn distance(&self) -> f32 { + self.distance + } +} + +// TODO: It'd be nice to use `RayCast3d` from `bevy_math` instead, but it only works on normalized rays. +/// Checks if the ray intersects with an AABB of a mesh, returning `[near, far]` if it does.44 +pub(crate) fn ray_aabb_intersection_3d( + ray: Ray3d, + aabb: &Aabb, + model_to_world: &Mat4, +) -> Option<[f32; 2]> { + // Transform the ray to model space + let world_to_model = model_to_world.inverse(); + let ray_dir: Vec3A = world_to_model.transform_vector3(*ray.direction).into(); + let ray_dir_recip: Vec3A = ray_dir.recip(); + let ray_origin: Vec3A = world_to_model.transform_point3(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. + + let t_0: Vec3A = (aabb.min() - ray_origin) * ray_dir_recip; + let t_1: Vec3A = (aabb.max() - ray_origin) * ray_dir_recip; + let t_min: Vec3A = t_0.min(t_1); + let t_max: Vec3A = t_0.max(t_1); + + let mut hit_near = t_min.x; + let mut hit_far = t_max.x; + + if hit_near > t_max.y || t_min.y > hit_far { + return None; + } + + if t_min.y > hit_near { + hit_near = t_min.y; + } + if t_max.y < hit_far { + hit_far = t_max.y; + } + + if (hit_near > t_max.z) || (t_min.z > hit_far) { + return None; + } + + if t_min.z > hit_near { + hit_near = t_min.z; + } + if t_max.z < hit_far { + hit_far = t_max.z; + } + Some([hit_near, hit_far]) +} diff --git a/crates/bevy_picking_mesh/src/raycast/raycast.rs b/crates/bevy_picking_mesh/src/ray_cast/mod.rs similarity index 91% rename from crates/bevy_picking_mesh/src/raycast/raycast.rs rename to crates/bevy_picking_mesh/src/ray_cast/mod.rs index 1d2b9e7d957ff..16dde670922c6 100644 --- a/crates/bevy_picking_mesh/src/raycast/raycast.rs +++ b/crates/bevy_picking_mesh/src/ray_cast/mod.rs @@ -1,19 +1,29 @@ +//! Ray casting on meshes. + +mod immediate; +mod intersections; +mod simplified_mesh; + +pub use immediate::*; +pub use simplified_mesh::*; + use bevy_math::{Mat4, Ray3d, Vec3, Vec3A}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ mesh::{Indices, Mesh, VertexAttributeValues}, render_resource::PrimitiveTopology, }; use bevy_utils::tracing::{error, warn}; -use super::primitives::*; +use intersections::*; -/// Cast a ray on a mesh, and returns the intersection +/// Casts a ray on a mesh, and returns the intersection pub fn ray_intersection_over_mesh( mesh: &Mesh, mesh_transform: &Mat4, ray: Ray3d, backface_culling: Backfaces, -) -> Option { +) -> Option { if mesh.primitive_topology() != PrimitiveTopology::TriangleList { error!( "Invalid intersection check: `TriangleList` is the only supported `PrimitiveTopology`" @@ -70,14 +80,18 @@ pub fn ray_intersection_over_mesh( } } +/// A trait for converting a value into a [`usize`]. pub trait IntoUsize: Copy { + /// Converts the value into a [`usize`]. fn into_usize(self) -> usize; } + impl IntoUsize for u16 { fn into_usize(self) -> usize { self as usize } } + impl IntoUsize for u32 { fn into_usize(self) -> usize { self as usize @@ -92,7 +106,7 @@ pub fn ray_mesh_intersection( ray: Ray3d, indices: Option<&Vec>, backface_culling: Backfaces, -) -> Option { +) -> 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 min_pick_distance = f32::MAX; @@ -136,7 +150,7 @@ pub fn ray_mesh_intersection( backface_culling, ); if let Some(i) = intersection { - pick_intersection = Some(IntersectionData::new( + pick_intersection = Some(RayMeshHit::new( mesh_transform.transform_point3(i.position()), mesh_transform.transform_vector3(i.normal()), i.barycentric_coord(), @@ -178,7 +192,7 @@ pub fn ray_mesh_intersection( backface_culling, ); if let Some(i) = intersection { - pick_intersection = Some(IntersectionData::new( + pick_intersection = Some(RayMeshHit::new( mesh_transform.transform_point3(i.position()), mesh_transform.transform_vector3(i.normal()), i.barycentric_coord(), @@ -208,8 +222,8 @@ fn triangle_intersection( max_distance: f32, ray: &Ray3d, backface_culling: Backfaces, -) -> Option { - // Run the raycast on the ray and triangle +) -> Option { + // Run the ray cast on the ray and triangle let ray_hit = ray_triangle_intersection(ray, &tri_vertices, backface_culling)?; let distance = *ray_hit.distance(); if distance < 0.0 || distance > max_distance { @@ -227,7 +241,7 @@ fn triangle_intersection( .cross(tri_vertices[2] - tri_vertices[0]) .normalize() }; - Some(IntersectionData::new( + Some(RayMeshHit::new( position, normal.into(), barycentric, @@ -237,10 +251,14 @@ fn triangle_intersection( )) } -#[derive(Copy, Clone, Default)] +/// Determines whether backfaces should be culled or included in intersection checks. +#[derive(Copy, Clone, Default, Reflect)] +#[reflect(Default)] pub enum Backfaces { + /// Cull backfaces. #[default] Cull, + /// Include backfaces. Include, } @@ -250,7 +268,7 @@ pub fn ray_triangle_intersection( ray: &Ray3d, triangle: &[Vec3A; 3], backface_culling: Backfaces, -) -> Option { +) -> 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]; @@ -291,19 +309,20 @@ pub fn ray_triangle_intersection( // The distance between ray origin and intersection is t. let t: f32 = vector_v0_to_v2.dot(q_vec) * determinant_inverse; - Some(RayHit { + Some(RayTriangleHit { distance: t, uv_coords: (u, v), }) } +/// A hit result from a ray cast on a triangle. #[derive(Default, Debug)] -pub struct RayHit { +pub struct RayTriangleHit { distance: f32, uv_coords: (f32, f32), } -impl RayHit { +impl RayTriangleHit { /// Get a reference to the intersection's uv coords. pub fn uv_coords(&self) -> &(f32, f32) { &self.uv_coords @@ -327,7 +346,7 @@ mod tests { const V2: [f32; 3] = [1.0, -1.0, -1.0]; #[test] - fn raycast_triangle_mt() { + fn ray_cast_triangle_mt() { let triangle = [V0.into(), V1.into(), V2.into()]; let ray = Ray3d::new(Vec3::ZERO, Vec3::X); let result = ray_triangle_intersection(&ray, &triangle, Backfaces::Include); @@ -335,7 +354,7 @@ mod tests { } #[test] - fn raycast_triangle_mt_culling() { + fn ray_cast_triangle_mt_culling() { let triangle = [V2.into(), V1.into(), V0.into()]; let ray = Ray3d::new(Vec3::ZERO, Vec3::X); let result = ray_triangle_intersection(&ray, &triangle, Backfaces::Cull); diff --git a/crates/bevy_picking_mesh/src/ray_cast/simplified_mesh.rs b/crates/bevy_picking_mesh/src/ray_cast/simplified_mesh.rs new file mode 100644 index 0000000000000..761cdfcd78f77 --- /dev/null +++ b/crates/bevy_picking_mesh/src/ray_cast/simplified_mesh.rs @@ -0,0 +1,10 @@ +use bevy_asset::Handle; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{component::Component, reflect::ReflectComponent}; +use bevy_reflect::Reflect; +use bevy_render::mesh::Mesh; + +/// A simplified mesh component that can be used for [ray casting](crate::MeshRayCast). +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect)] +#[reflect(Component, Debug)] +pub struct SimplifiedMesh(pub Handle); diff --git a/crates/bevy_picking_mesh/src/raycast/markers.rs b/crates/bevy_picking_mesh/src/raycast/markers.rs deleted file mode 100644 index c1dc19fba8ed0..0000000000000 --- a/crates/bevy_picking_mesh/src/raycast/markers.rs +++ /dev/null @@ -1,10 +0,0 @@ -use bevy_asset::Handle; -use bevy_ecs::component::Component; - -#[derive(Component)] -pub struct SimplifiedMesh { - pub mesh: Handle, -} - -#[derive(Component)] -pub struct NoBackfaceCulling; diff --git a/crates/bevy_picking_mesh/src/raycast/mod.rs b/crates/bevy_picking_mesh/src/raycast/mod.rs deleted file mode 100644 index 0b99da30ba64e..0000000000000 --- a/crates/bevy_picking_mesh/src/raycast/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod immediate; -pub mod markers; -pub mod primitives; -pub mod raycast; diff --git a/crates/bevy_picking_mesh/src/raycast/primitives.rs b/crates/bevy_picking_mesh/src/raycast/primitives.rs deleted file mode 100644 index 682c65e068084..0000000000000 --- a/crates/bevy_picking_mesh/src/raycast/primitives.rs +++ /dev/null @@ -1,187 +0,0 @@ -use bevy_math::{Vec3, Vec3A}; -use bevy_reflect::Reflect; - -pub use rays::*; - -#[derive(Debug, Clone, Reflect)] -pub struct IntersectionData { - position: Vec3, - normal: Vec3, - barycentric_coord: Vec3, - distance: f32, - triangle: Option<[Vec3A; 3]>, - triangle_index: Option, -} - -impl From for IntersectionData { - fn from(data: rays::PrimitiveIntersection) -> Self { - Self { - position: data.position(), - normal: data.normal(), - distance: data.distance(), - barycentric_coord: Vec3::ZERO, - triangle: None, - triangle_index: None, - } - } -} - -impl IntersectionData { - pub fn new( - position: Vec3, - normal: Vec3, - barycentric: Vec3, - distance: f32, - triangle: Option<[Vec3A; 3]>, - triangle_index: Option, - ) -> Self { - Self { - position, - normal, - barycentric_coord: barycentric, - distance, - triangle, - triangle_index, - } - } - - /// Get the intersection data's position. - #[must_use] - pub fn position(&self) -> Vec3 { - self.position - } - - /// Get the intersection data's normal. - #[must_use] - pub fn normal(&self) -> Vec3 { - self.normal - } - - /// Get the intersection data's barycentric coord. - #[must_use] - pub fn barycentric_coord(&self) -> Vec3 { - self.barycentric_coord - } - - /// Get the intersection data's distance. - #[must_use] - pub fn distance(&self) -> f32 { - self.distance - } - - /// Get the intersection data's triangle. - #[must_use] - pub fn triangle(&self) -> Option<[Vec3A; 3]> { - self.triangle - } - - /// Get the intersection data's triangle index. - #[must_use] - pub fn triangle_index(&self) -> Option { - self.triangle_index - } -} - -/// Encapsulates Ray3D, preventing use of struct literal syntax. This allows us to guarantee that -/// the `Ray3d` direction is normalized, because it can only be instantiated with the constructor. -pub mod rays { - use bevy_math::{prelude::*, Ray3d, Vec3A}; - use bevy_render::primitives::Aabb; - - pub struct PrimitiveIntersection { - position: Vec3, - normal: Vec3, - distance: f32, - } - - impl PrimitiveIntersection { - pub fn new(position: Vec3, normal: Vec3, distance: f32) -> Self { - Self { - position, - normal, - distance, - } - } - - /// Get the intersection's position - #[must_use] - pub fn position(&self) -> Vec3 { - self.position - } - - /// Get the normal vector of the primitive at the point of intersection - #[must_use] - pub fn normal(&self) -> Vec3 { - self.normal - } - - /// Get the distance between the ray origin and the intersection position - #[must_use] - pub fn distance(&self) -> f32 { - self.distance - } - } - - pub fn to_transform(ray: Ray3d) -> Mat4 { - to_aligned_transform(ray, [0., 1., 0.].into()) - } - - /// Create a transform whose origin is at the origin of the ray and - /// whose up-axis is aligned with the direction of the ray. Use `up` to - /// specify which axis of the transform should align with the ray. - pub fn to_aligned_transform(ray: Ray3d, up: Vec3) -> Mat4 { - let position = ray.origin; - let normal = ray.direction; - let new_rotation = Quat::from_rotation_arc(up, *normal); - Mat4::from_rotation_translation(new_rotation, position) - } - - pub fn ray_from_transform(transform: Mat4) -> Ray3d { - let pick_position_ndc = Vec3::from([0.0, 0.0, -1.0]); - let pick_position = transform.project_point3(pick_position_ndc); - let (_, _, source_origin) = transform.to_scale_rotation_translation(); - let ray_direction = pick_position - source_origin; - Ray3d::new(source_origin, ray_direction) - } - - /// Checks if the ray intersects with an AABB of a mesh, returning `[near, far]` if it does. - pub fn intersects_aabb(ray: Ray3d, aabb: &Aabb, model_to_world: &Mat4) -> Option<[f32; 2]> { - // Transform the ray to model space - let world_to_model = model_to_world.inverse(); - let ray_dir: Vec3A = world_to_model.transform_vector3(*ray.direction).into(); - let ray_origin: Vec3A = world_to_model.transform_point3(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. - - let t_0: Vec3A = (aabb.min() - ray_origin) / ray_dir; - let t_1: Vec3A = (aabb.max() - ray_origin) / ray_dir; - let t_min: Vec3A = t_0.min(t_1); - let t_max: Vec3A = t_0.max(t_1); - - let mut hit_near = t_min.x; - let mut hit_far = t_max.x; - - if hit_near > t_max.y || t_min.y > hit_far { - return None; - } - - if t_min.y > hit_near { - hit_near = t_min.y; - } - if t_max.y < hit_far { - hit_far = t_max.y; - } - - if (hit_near > t_max.z) || (t_min.z > hit_far) { - return None; - } - - if t_min.z > hit_near { - hit_near = t_min.z; - } - if t_max.z < hit_far { - hit_far = t_max.z; - } - Some([hit_near, hit_far]) - } -} From 74cda857b9b3ebe1834b81bae979fbb7aee3edb8 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Tue, 8 Oct 2024 23:29:02 +0300 Subject: [PATCH 04/27] Clean up, refactor module structure, and rename types --- crates/bevy_internal/src/default_plugins.rs | 2 +- crates/bevy_picking_mesh/src/lib.rs | 48 +- .../src/ray_cast/immediate.rs | 282 --------- .../src/ray_cast/intersections.rs | 411 ++++++++++--- crates/bevy_picking_mesh/src/ray_cast/mod.rs | 553 ++++++++---------- 5 files changed, 586 insertions(+), 710 deletions(-) delete mode 100644 crates/bevy_picking_mesh/src/ray_cast/immediate.rs diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index b6bd0a68b3d59..27e0af1e97a4e 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -59,7 +59,7 @@ plugin_group! { #[cfg(feature = "bevy_picking")] bevy_picking:::DefaultPickingPlugins, #[cfg(feature = "bevy_picking_mesh")] - bevy_picking_mesh:::RayCastBackend, + bevy_picking_mesh:::MeshPickingBackend, #[cfg(feature = "bevy_dev_tools")] bevy_dev_tools:::DevToolsPlugin, #[cfg(feature = "bevy_ci_testing")] diff --git a/crates/bevy_picking_mesh/src/lib.rs b/crates/bevy_picking_mesh/src/lib.rs index e5697ef5f51d1..ab449b1353ef2 100644 --- a/crates/bevy_picking_mesh/src/lib.rs +++ b/crates/bevy_picking_mesh/src/lib.rs @@ -1,4 +1,4 @@ -//! A ray casting backend for `bevy_picking`. +//! A ray casting backend for [`bevy_picking`]. //! //! # Usage //! @@ -8,7 +8,7 @@ //! To ignore an entity, you can add [`Pickable::IGNORE`] to it, and it will be ignored during //! ray casting. //! -//! For fine-grained control, see the [`RayCastBackendSettings::require_markers`] setting. +//! For fine-grained control, see the [`MeshPickingBackendSettings::require_markers`] setting. #![allow(clippy::too_many_arguments, clippy::type_complexity)] #![warn(missing_docs)] @@ -26,30 +26,35 @@ use ray_cast::{Backfaces, MeshRayCast, RayCastSettings, RayCastVisibility}; pub mod ray_cast; -/// Commonly used imports for the [`bevy_picking_raycast`](crate) crate. +/// The mesh picking prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { + #[doc(hidden)] pub use crate::{ - ray_cast::{Backfaces, MeshRayCast, RayCastSettings, RayCastVisibility, RayTriangleHit}, - RayCastBackend, + ray_cast::{Backfaces, MeshRayCast, RayCastSettings, RayCastVisibility}, + MeshPickingBackend, }; } -/// Runtime settings for the [`RayCastBackend`]. +/// Runtime settings for the [`MeshPickingBackend`]. #[derive(Resource, Reflect)] #[reflect(Resource, Default)] -pub struct RayCastBackendSettings { +pub struct MeshPickingBackendSettings { /// When set to `true` ray casting will only happen between cameras and entities marked with /// [`RayCastPickable`]. Off by default. This setting is provided to give you fine-grained /// control over which cameras and entities should be used by the ray cast backend at runtime. pub require_markers: bool, - /// When set to Ignore, hidden items can be raycasted against. + + /// When set to [`RayCastVisibility::Any`], hidden items can be ray casted against. /// See [`RayCastSettings::visibility`] for more information. pub raycast_visibility: RayCastVisibility, + /// When set to [`Backfaces::Cull`], backfaces will be ignored during ray casting. pub backfaces: Backfaces, } -impl Default for RayCastBackendSettings { +impl Default for MeshPickingBackendSettings { fn default() -> Self { Self { require_markers: false, @@ -60,34 +65,33 @@ impl Default for RayCastBackendSettings { } /// Optional. Marks cameras and target entities that should be used in the ray cast picking backend. -/// Only needed if [`RayCastBackendSettings::require_markers`] is set to true. +/// Only needed if [`MeshPickingBackendSettings::require_markers`] is set to true. #[derive(Debug, Clone, Default, Component, Reflect)] #[reflect(Component, Default)] pub struct RayCastPickable; /// Adds the ray casting picking backend to your app. #[derive(Clone, Default)] -pub struct RayCastBackend; -impl Plugin for RayCastBackend { +pub struct MeshPickingBackend; +impl Plugin for MeshPickingBackend { fn build(&self, app: &mut App) { - app.init_resource::() + app.init_resource::() .add_systems(PreUpdate, update_hits.in_set(PickSet::Backend)) .register_type::() - .register_type::(); + .register_type::(); } } -/// Raycasts into the scene using [`RayCastBackendSettings`] and [`PointerLocation`]s, then outputs -/// [`PointerHits`]. +/// Casts rays into the scene using [`MeshPickingBackendSettings`] and outputs [`PointerHits`]. pub fn update_hits( - backend_settings: Res, + backend_settings: Res, ray_map: Res, picking_cameras: Query<(&Camera, Option<&RayCastPickable>, Option<&RenderLayers>)>, pickables: Query<&Pickable>, marked_targets: Query<&RayCastPickable>, layers: Query<&RenderLayers>, mut ray_cast: MeshRayCast, - mut output_events: EventWriter, + 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 { @@ -129,16 +133,16 @@ pub fn update_hits( .map(|(entity, hit)| { let hit_data = HitData::new( ray_id.camera, - hit.distance(), - Some(hit.position()), - Some(hit.normal()), + hit.distance, + Some(hit.point), + Some(*hit.normal), ); (*entity, hit_data) }) .collect::>(); let order = camera.order as f32; if !picks.is_empty() { - output_events.send(PointerHits::new(ray_id.pointer, picks, order)); + output.send(PointerHits::new(ray_id.pointer, picks, order)); } } } diff --git a/crates/bevy_picking_mesh/src/ray_cast/immediate.rs b/crates/bevy_picking_mesh/src/ray_cast/immediate.rs deleted file mode 100644 index 62f403b6bdbf1..0000000000000 --- a/crates/bevy_picking_mesh/src/ray_cast/immediate.rs +++ /dev/null @@ -1,282 +0,0 @@ -//! # Immediate Mode Ray Casting API -//! -//! See the `minimal` example for reference. -//! -//! This is the simplest way to get started. Add the [`MeshRayCast`] [`SystemParam`] to your system, and -//! call [`MeshRayCast::cast_ray`], to get a list of intersections. Ray casts are performed immediately -//! when you call the `cast_ray` method. See the [`MeshRayCast`] documentation for more details. You -//! don't even need to add a plugin to your application. - -use bevy_asset::{Assets, Handle}; -use bevy_ecs::{prelude::*, system::lifetimeless::Read, system::SystemParam}; -use bevy_math::{FloatOrd, Ray3d}; -use bevy_reflect::Reflect; -use bevy_render::{prelude::*, primitives::Aabb}; -use bevy_transform::components::GlobalTransform; -use bevy_utils::tracing::*; - -use super::{ - intersections::{ray_aabb_intersection_3d, RayMeshHit}, - ray_intersection_over_mesh, - simplified_mesh::SimplifiedMesh, - Backfaces, -}; - -/// How a ray cast should handle visibility. -#[derive(Clone, Copy, Reflect)] -pub enum RayCastVisibility { - /// Completely ignore visibility checks. Hidden items can still be raycasted against. - Any, - /// Only ray cast against entities that are visible in the hierarchy. See [`Visibility`]. - Visible, - /// Only ray cast against entities that are visible in the hierarchy and visible to a camera or - /// light. See [`Visibility`]. - VisibleAndInView, -} - -/// Settings for a ray cast. -#[derive(Clone)] -pub struct RayCastSettings<'a> { - /// Determines how ray casting should consider entity visibility. - pub visibility: RayCastVisibility, - /// Determines how ray casting should handle backfaces. - pub backfaces: Backfaces, - /// A filtering function that is applied to every entity that is raycasted. 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::VisibleAndInView, - backfaces: Backfaces::default(), - filter: &|_| true, - early_exit_test: &|_| true, - } - } -} - -#[cfg(feature = "2d")] -type MeshFilter = Or<(With, With)>; -#[cfg(not(feature = "2d"))] -type MeshFilter = With; - -/// Add this raycasting [`SystemParam`] to your system to ray cast 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 locals that are added to your -/// system. -/// -/// ## Usage -/// -/// The following system ray casts into the world with a ray positioned at the origin, pointing in -/// the x-direction, and returns a list of intersections: -/// -/// ``` -/// # use bevy::prelude::*; -/// fn ray_cast_system(mut raycast: MeshRayCast) { -/// let ray = Ray3d::new(Vec3::ZERO, Vec3::X); -/// let hits = raycast.cast_ray(ray, &RayCastSettings::default()); -/// } -/// ``` -/// -/// ## Configuration -/// -/// You can specify behavior of the ray cast using [`RayCastSettings`]. This allows you to filter out -/// entities, configure early-out, and set whether the [`Visibility`] of an entity should be -/// considered. -/// -/// ``` -/// # use bevy::prelude::*; -/// # #[derive(Component)] -/// # struct Foo; -/// fn ray_cast_system(mut ray_cast: MeshRayCast, foo_query: Query<(), With>) { -/// let ray = Ray3d::new(Vec3::ZERO, Vec3::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::Ignore; -/// -/// let settings = RayCastSettings::default() -/// .with_filter(&filter) -/// .with_early_exit_test(&early_exit_test) -/// .with_visibility(visibility); -/// -/// 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, - ( - Read, - Option>, - Read, - ), - >, - #[cfg(feature = "2d")] - #[doc(hidden)] - pub mesh2d_query: Query< - 'w, - 's, - ( - Read, - Option>, - Read, - ), - >, -} - -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::VisibleAndInView => view_visibility.get(), - }; - if should_ray_cast { - if let Some([near, _]) = - ray_aabb_intersection_3d(ray, aabb, &transform.compute_matrix()) - .filter(|[_, far]| *far >= 0.0) - { - aabb_hits_tx.send((FloatOrd(near), entity)).ok(); - } - } - }, - ); - *self.culled_list = aabb_hits_rx.try_iter().collect(); - self.culled_list.sort_by_key(|(aabb_near, _)| *aabb_near); - drop(ray_cull_guard); - - 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)| { - let mut ray_cast_mesh = - |mesh_handle: &Handle, - simplified_mesh: Option<&SimplifiedMesh>, - transform: &GlobalTransform| { - // 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 mesh_handle = simplified_mesh.map(|m| &m.0).unwrap_or(mesh_handle); - let Some(mesh) = self.meshes.get(mesh_handle) else { - return; - }; - - let _ray_cast_guard = ray_cast_guard.enter(); - let transform = transform.compute_matrix(); - let intersection = - ray_intersection_over_mesh(mesh, &transform, ray, settings.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 cant 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))); - }; - }; - - if let Ok((mesh, simplified_mesh, transform)) = self.mesh_query.get(*entity) { - ray_cast_mesh(mesh, simplified_mesh, transform); - } - - #[cfg(feature = "2d")] - if let Ok((mesh, simplified_mesh, transform)) = self.mesh2d_query.get(*entity) { - ray_cast_mesh(&mesh.0, simplified_mesh, transform); - } - }); - - 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 = hits.collect(); - self.output.as_ref() - } -} diff --git a/crates/bevy_picking_mesh/src/ray_cast/intersections.rs b/crates/bevy_picking_mesh/src/ray_cast/intersections.rs index 9b4a8255b10a9..61ee76ff1e7dd 100644 --- a/crates/bevy_picking_mesh/src/ray_cast/intersections.rs +++ b/crates/bevy_picking_mesh/src/ray_cast/intersections.rs @@ -1,124 +1,351 @@ -use bevy_math::{Mat4, Ray3d, Vec3, Vec3A}; +use bevy_math::{Dir3, Mat4, Ray3d, Vec3, Vec3A}; use bevy_reflect::Reflect; -use bevy_render::primitives::Aabb; +use bevy_render::{ + mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues}, + primitives::Aabb, +}; +use bevy_utils::tracing::{error, warn}; -/// A ray intersection with a mesh. -#[derive(Debug, Clone, Reflect)] -pub struct RayMeshHit { - position: Vec3, - normal: Vec3, - barycentric_coord: Vec3, - distance: f32, - triangle: Option<[Vec3A; 3]>, - triangle_index: Option, -} +use super::Backfaces; -impl From for RayMeshHit { - fn from(data: PrimitiveIntersection) -> Self { - Self { - position: data.position(), - normal: data.normal(), - distance: data.distance(), - barycentric_coord: Vec3::ZERO, - triangle: None, - triangle_index: None, - } +/// Casts a ray on a mesh, and returns the intersection +pub 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 from the mesh reference resolved from the mesh handle + let vertex_positions: &Vec<[f32; 3]> = match mesh.attribute(Mesh::ATTRIBUTE_POSITION) { + None => panic!("Mesh does not contain vertex positions"), + Some(vertex_values) => match &vertex_values { + VertexAttributeValues::Float32x3(positions) => positions, + _ => panic!("Unexpected types in {:?}", Mesh::ATTRIBUTE_POSITION), + }, + }; + 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 + }; -impl RayMeshHit { - /// Creates a new [`RayMeshHit`] with the given data. - pub fn new( - position: Vec3, - normal: Vec3, - barycentric: Vec3, - distance: f32, - triangle: Option<[Vec3A; 3]>, - triangle_index: Option, - ) -> Self { - Self { - position, - normal, - barycentric_coord: barycentric, - distance, - triangle, - triangle_index, + if let Some(indices) = &mesh.indices() { + // Iterate over the list of pick rays that belong to the same group as this mesh + match indices { + Indices::U16(vertex_indices) => ray_mesh_intersection( + mesh_transform, + vertex_positions, + vertex_normals, + ray, + Some(vertex_indices), + backface_culling, + ), + Indices::U32(vertex_indices) => ray_mesh_intersection( + mesh_transform, + vertex_positions, + vertex_normals, + ray, + Some(vertex_indices), + backface_culling, + ), } + } else { + ray_mesh_intersection( + mesh_transform, + vertex_positions, + vertex_normals, + ray, + None::<&Vec>, + backface_culling, + ) } +} - /// Get the intersection data's position. - #[must_use] - pub fn position(&self) -> Vec3 { - self.position - } +/// A trait for converting a value into a [`usize`]. +pub trait IntoUsize: Copy { + /// Converts the value into a [`usize`]. + fn into_usize(self) -> usize; +} - /// Get the intersection data's normal. - #[must_use] - pub fn normal(&self) -> Vec3 { - self.normal +impl IntoUsize for u16 { + fn into_usize(self) -> usize { + self as usize } +} - /// Get the intersection data's barycentric coord. - #[must_use] - pub fn barycentric_coord(&self) -> Vec3 { - self.barycentric_coord +impl IntoUsize for u32 { + fn into_usize(self) -> usize { + self as usize } +} - /// Get the intersection data's distance. - #[must_use] - pub fn distance(&self) -> f32 { - self.distance - } +/// Checks if a ray intersects a mesh, and returns the nearest intersection if one exists. +pub fn ray_mesh_intersection( + mesh_transform: &Mat4, + vertex_positions: &[[f32; 3]], + vertex_normals: Option<&[[f32; 3]]>, + ray: Ray3d, + indices: Option<&Vec>, + 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 min_pick_distance = f32::MAX; + let mut pick_intersection = None; - /// Get the intersection data's triangle. - #[must_use] - pub fn triangle(&self) -> Option<[Vec3A; 3]> { - self.triangle - } + let world_to_mesh = mesh_transform.inverse(); - /// Get the intersection data's triangle index. - #[must_use] - pub fn triangle_index(&self) -> Option { - self.triangle_index + let mesh_space_ray = Ray3d::new( + world_to_mesh.transform_point3(ray.origin), + world_to_mesh.transform_vector3(*ray.direction), + ); + + 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 in indices.chunks(3) { + let triangle_index = Some(index[0].into_usize()); + let tri_vertex_positions = [ + Vec3A::from(vertex_positions[index[0].into_usize()]), + Vec3A::from(vertex_positions[index[1].into_usize()]), + Vec3A::from(vertex_positions[index[2].into_usize()]), + ]; + let tri_normals = vertex_normals.map(|normals| { + [ + Vec3A::from(normals[index[0].into_usize()]), + Vec3A::from(normals[index[1].into_usize()]), + Vec3A::from(normals[index[2].into_usize()]), + ] + }); + let intersection = triangle_intersection( + tri_vertex_positions, + tri_normals, + min_pick_distance, + &mesh_space_ray, + backface_culling, + ); + if let Some(i) = intersection { + pick_intersection = Some(RayMeshHit { + point: mesh_transform.transform_point3(i.point), + normal: Dir3::new_unchecked(mesh_transform.transform_vector3(*i.normal)), + barycentric_coord: i.barycentric_coord, + distance: mesh_transform + .transform_vector3(mesh_space_ray.direction * i.distance) + .length(), + triangle: i.triangle.map(|tri| { + [ + mesh_transform.transform_point3a(tri[0]), + mesh_transform.transform_point3a(tri[1]), + mesh_transform.transform_point3a(tri[2]), + ] + }), + triangle_index, + }); + min_pick_distance = i.distance; + } + } + } else { + for i in (0..vertex_positions.len()).step_by(3) { + let triangle_index = Some(i); + let tri_vertex_positions = [ + Vec3A::from(vertex_positions[i]), + Vec3A::from(vertex_positions[i + 1]), + Vec3A::from(vertex_positions[i + 2]), + ]; + let tri_normals = vertex_normals.map(|normals| { + [ + Vec3A::from(normals[i]), + Vec3A::from(normals[i + 1]), + Vec3A::from(normals[i + 2]), + ] + }); + let intersection = triangle_intersection( + tri_vertex_positions, + tri_normals, + min_pick_distance, + &mesh_space_ray, + backface_culling, + ); + if let Some(i) = intersection { + pick_intersection = Some(RayMeshHit { + point: mesh_transform.transform_point3(i.point), + normal: Dir3::new_unchecked(mesh_transform.transform_vector3(*i.normal)), + barycentric_coord: i.barycentric_coord, + distance: mesh_transform + .transform_vector3(mesh_space_ray.direction * i.distance) + .length(), + triangle: i.triangle.map(|tri| { + [ + mesh_transform.transform_point3a(tri[0]), + mesh_transform.transform_point3a(tri[1]), + mesh_transform.transform_point3a(tri[2]), + ] + }), + triangle_index, + }); + min_pick_distance = i.distance; + } + } } + + pick_intersection } -/// A ray intersection with a primitive. -pub struct PrimitiveIntersection { - position: Vec3, - normal: Vec3, - distance: f32, +#[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 position = 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: position, + normal: Dir3::new_unchecked(normal.into()), + barycentric_coord: barycentric, + distance: hit.distance, + triangle: Some(tri_vertices), + triangle_index: None, + }) } -impl PrimitiveIntersection { - /// Creates a new [`PrimitiveIntersection`] with the given data. - pub fn new(position: Vec3, normal: Vec3, distance: f32) -> Self { - Self { - position, - normal, - distance, +/// Takes a ray and triangle and computes the intersection and normal +#[inline(always)] +pub 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; + } } } - /// Get the intersection's position - #[must_use] - pub fn position(&self) -> Vec3 { - self.position + 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; } - /// Get the normal vector of the primitive at the point of intersection - #[must_use] - pub fn normal(&self) -> Vec3 { - self.normal + 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; } - /// Get the distance between the ray origin and the intersection position - #[must_use] - pub fn distance(&self) -> f32 { - self.distance + // 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), + }) +} + +/// A hit result from a ray cast on a triangle. +#[derive(Default, Debug)] +pub struct RayTriangleHit { + pub distance: f32, + pub barycentric_coords: (f32, f32), +} + +#[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, Vec3::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, Vec3::X); + let result = ray_triangle_intersection(&ray, &triangle, Backfaces::Cull); + assert!(result.is_none()); } } +/// A ray intersection with a mesh. +#[derive(Debug, Clone, Reflect)] +pub struct RayMeshHit { + /// The point of intersection in world space. + pub point: Vec3, + /// The normal of the triangle at the point of intersection. + pub normal: Dir3, + /// The barycentric coordinates of the intersection. + pub barycentric_coord: 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, +} + // TODO: It'd be nice to use `RayCast3d` from `bevy_math` instead, but it only works on normalized rays. /// Checks if the ray intersects with an AABB of a mesh, returning `[near, far]` if it does.44 pub(crate) fn ray_aabb_intersection_3d( @@ -129,7 +356,7 @@ pub(crate) fn ray_aabb_intersection_3d( // Transform the ray to model space let world_to_model = model_to_world.inverse(); let ray_dir: Vec3A = world_to_model.transform_vector3(*ray.direction).into(); - let ray_dir_recip: Vec3A = ray_dir.recip(); + let ray_dir_recip = ray_dir.recip(); let ray_origin: Vec3A = world_to_model.transform_point3(ray.origin).into(); // Check if the ray intersects the mesh's AABB. It's useful to work in model space diff --git a/crates/bevy_picking_mesh/src/ray_cast/mod.rs b/crates/bevy_picking_mesh/src/ray_cast/mod.rs index 16dde670922c6..37a31cddc6004 100644 --- a/crates/bevy_picking_mesh/src/ray_cast/mod.rs +++ b/crates/bevy_picking_mesh/src/ray_cast/mod.rs @@ -1,254 +1,90 @@ //! Ray casting on meshes. -mod immediate; mod intersections; mod simplified_mesh; -pub use immediate::*; pub use simplified_mesh::*; -use bevy_math::{Mat4, Ray3d, Vec3, Vec3A}; +use bevy_math::Ray3d; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::{ - mesh::{Indices, Mesh, VertexAttributeValues}, - render_resource::PrimitiveTopology, -}; -use bevy_utils::tracing::{error, warn}; +use bevy_render::mesh::Mesh; +pub use intersections::RayMeshHit; use intersections::*; -/// Casts a ray on a mesh, and returns the intersection -pub 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 from the mesh reference resolved from the mesh handle - let vertex_positions: &Vec<[f32; 3]> = match mesh.attribute(Mesh::ATTRIBUTE_POSITION) { - None => panic!("Mesh does not contain vertex positions"), - Some(vertex_values) => match &vertex_values { - VertexAttributeValues::Float32x3(positions) => positions, - _ => panic!("Unexpected types in {:?}", Mesh::ATTRIBUTE_POSITION), - }, - }; - 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 - }; +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::*; - if let Some(indices) = &mesh.indices() { - // Iterate over the list of pick rays that belong to the same group as this mesh - match indices { - Indices::U16(vertex_indices) => ray_mesh_intersection( - mesh_transform, - vertex_positions, - vertex_normals, - ray, - Some(vertex_indices), - backface_culling, - ), - Indices::U32(vertex_indices) => ray_mesh_intersection( - mesh_transform, - vertex_positions, - vertex_normals, - ray, - Some(vertex_indices), - backface_culling, - ), - } - } else { - ray_mesh_intersection( - mesh_transform, - vertex_positions, - vertex_normals, - ray, - None::<&Vec>, - backface_culling, - ) - } +/// 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 ray cast against entities that are visible in the hierarchy. See [`Visibility`]. + Visible, + /// Only ray cast against entities that are visible in the hierarchy and visible to a camera or + /// light. See [`Visibility`]. + VisibleAndInView, } -/// A trait for converting a value into a [`usize`]. -pub trait IntoUsize: Copy { - /// Converts the value into a [`usize`]. - fn into_usize(self) -> usize; +/// Settings for a ray cast. +#[derive(Clone)] +pub struct RayCastSettings<'a> { + /// Determines how ray casting should consider entity visibility. + pub visibility: RayCastVisibility, + /// Determines how ray casting should handle backfaces. + pub backfaces: Backfaces, + /// A filtering function that is applied to every entity that is ray casted. 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 IntoUsize for u16 { - fn into_usize(self) -> usize { - self as usize +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 } -} -impl IntoUsize for u32 { - fn into_usize(self) -> usize { - self as usize + /// 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 } -} - -/// Checks if a ray intersects a mesh, and returns the nearest intersection if one exists. -pub fn ray_mesh_intersection( - mesh_transform: &Mat4, - vertex_positions: &[[f32; 3]], - vertex_normals: Option<&[[f32; 3]]>, - ray: Ray3d, - indices: Option<&Vec>, - 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 min_pick_distance = f32::MAX; - let mut pick_intersection = None; - let world_to_mesh = mesh_transform.inverse(); + /// Set the [`RayCastVisibility`] setting to apply to the ray cast. + pub fn with_visibility(mut self, visibility: RayCastVisibility) -> Self { + self.visibility = visibility; + self + } - let mesh_space_ray = Ray3d::new( - world_to_mesh.transform_point3(ray.origin), - world_to_mesh.transform_vector3(*ray.direction), - ); + /// 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) + } - 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 in indices.chunks(3) { - let triangle_index = Some(index[0].into_usize()); - let tri_vertex_positions = [ - Vec3A::from(vertex_positions[index[0].into_usize()]), - Vec3A::from(vertex_positions[index[1].into_usize()]), - Vec3A::from(vertex_positions[index[2].into_usize()]), - ]; - let tri_normals = vertex_normals.map(|normals| { - [ - Vec3A::from(normals[index[0].into_usize()]), - Vec3A::from(normals[index[1].into_usize()]), - Vec3A::from(normals[index[2].into_usize()]), - ] - }); - let intersection = triangle_intersection( - tri_vertex_positions, - tri_normals, - min_pick_distance, - &mesh_space_ray, - backface_culling, - ); - if let Some(i) = intersection { - pick_intersection = Some(RayMeshHit::new( - mesh_transform.transform_point3(i.position()), - mesh_transform.transform_vector3(i.normal()), - i.barycentric_coord(), - mesh_transform - .transform_vector3(mesh_space_ray.direction * i.distance()) - .length(), - i.triangle().map(|tri| { - [ - mesh_transform.transform_point3a(tri[0]), - mesh_transform.transform_point3a(tri[1]), - mesh_transform.transform_point3a(tri[2]), - ] - }), - triangle_index, - )); - min_pick_distance = i.distance(); - } - } - } else { - for i in (0..vertex_positions.len()).step_by(3) { - let triangle_index = Some(i); - let tri_vertex_positions = [ - Vec3A::from(vertex_positions[i]), - Vec3A::from(vertex_positions[i + 1]), - Vec3A::from(vertex_positions[i + 2]), - ]; - let tri_normals = vertex_normals.map(|normals| { - [ - Vec3A::from(normals[i]), - Vec3A::from(normals[i + 1]), - Vec3A::from(normals[i + 2]), - ] - }); - let intersection = triangle_intersection( - tri_vertex_positions, - tri_normals, - min_pick_distance, - &mesh_space_ray, - backface_culling, - ); - if let Some(i) = intersection { - pick_intersection = Some(RayMeshHit::new( - mesh_transform.transform_point3(i.position()), - mesh_transform.transform_vector3(i.normal()), - i.barycentric_coord(), - mesh_transform - .transform_vector3(mesh_space_ray.direction * i.distance()) - .length(), - i.triangle().map(|tri| { - [ - mesh_transform.transform_point3a(tri[0]), - mesh_transform.transform_point3a(tri[1]), - mesh_transform.transform_point3a(tri[2]), - ] - }), - triangle_index, - )); - min_pick_distance = i.distance(); - } - } + /// 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) } - pick_intersection } -#[inline(always)] -fn triangle_intersection( - tri_vertices: [Vec3A; 3], - tri_normals: Option<[Vec3A; 3]>, - max_distance: f32, - ray: &Ray3d, - backface_culling: Backfaces, -) -> Option { - // Run the ray cast on the ray and triangle - let ray_hit = ray_triangle_intersection(ray, &tri_vertices, backface_culling)?; - let distance = *ray_hit.distance(); - if distance < 0.0 || distance > max_distance { - return None; - }; - let position = ray.get_point(distance); - let u = ray_hit.uv_coords().0; - let v = ray_hit.uv_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::new( - position, - normal.into(), - barycentric, - distance, - Some(tri_vertices), - None, - )) +impl<'a> Default for RayCastSettings<'a> { + fn default() -> Self { + Self { + visibility: RayCastVisibility::VisibleAndInView, + backfaces: Backfaces::default(), + filter: &|_| true, + early_exit_test: &|_| true, + } + } } /// Determines whether backfaces should be culled or included in intersection checks. @@ -262,102 +98,193 @@ pub enum Backfaces { Include, } -/// Takes a ray and triangle and computes the intersection and normal -#[inline(always)] -pub 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; +#[cfg(feature = "2d")] +type MeshFilter = Or<(With, With)>; +#[cfg(not(feature = "2d"))] +type MeshFilter = With; - Some(RayTriangleHit { - distance: t, - uv_coords: (u, v), - }) +/// Add this raycasting [`SystemParam`] to your system to ray cast 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 locals that are added to your +/// system. +/// +/// ## Usage +/// +/// The following system ray casts into the world with a ray positioned at the origin, pointing in +/// the x-direction, and returns a list of intersections: +/// +/// ``` +/// # use bevy::prelude::*; +/// fn ray_cast_system(mut raycast: MeshRayCast) { +/// let ray = Ray3d::new(Vec3::ZERO, Vec3::X); +/// let hits = raycast.cast_ray(ray, &RayCastSettings::default()); +/// } +/// ``` +/// +/// ## Configuration +/// +/// You can specify behavior of the ray cast using [`RayCastSettings`]. This allows you to filter out +/// entities, configure early-out, and set whether the [`Visibility`] of an entity should be +/// considered. +/// +/// ``` +/// # use bevy::prelude::*; +/// # #[derive(Component)] +/// # struct Foo; +/// fn ray_cast_system(mut ray_cast: MeshRayCast, foo_query: Query<(), With>) { +/// let ray = Ray3d::new(Vec3::ZERO, Vec3::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::Ignore; +/// +/// let settings = RayCastSettings::default() +/// .with_filter(&filter) +/// .with_early_exit_test(&early_exit_test) +/// .with_visibility(visibility); +/// +/// 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, + ( + Read, + Option>, + Read, + ), + >, + #[cfg(feature = "2d")] + #[doc(hidden)] + pub mesh2d_query: Query< + 'w, + 's, + ( + Read, + Option>, + Read, + ), + >, } -/// A hit result from a ray cast on a triangle. -#[derive(Default, Debug)] -pub struct RayTriangleHit { - distance: f32, - uv_coords: (f32, f32), -} +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(); -impl RayTriangleHit { - /// Get a reference to the intersection's uv coords. - pub fn uv_coords(&self) -> &(f32, f32) { - &self.uv_coords - } + self.hits.clear(); + self.culled_list.clear(); + self.output.clear(); - /// Get a reference to the intersection's distance. - pub fn distance(&self) -> &f32 { - &self.distance - } -} + // 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::VisibleAndInView => view_visibility.get(), + }; + if should_ray_cast { + if let Some([near, _]) = + ray_aabb_intersection_3d(ray, aabb, &transform.compute_matrix()) + .filter(|[_, far]| *far >= 0.0) + { + aabb_hits_tx.send((FloatOrd(near), entity)).ok(); + } + } + }, + ); + *self.culled_list = aabb_hits_rx.try_iter().collect(); + self.culled_list.sort_by_key(|(aabb_near, _)| *aabb_near); + drop(ray_cull_guard); -#[cfg(test)] -mod tests { - use bevy_math::Vec3; + 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)| { + let mut ray_cast_mesh = + |mesh_handle: &Handle, + simplified_mesh: Option<&SimplifiedMesh>, + transform: &GlobalTransform| { + // Is it even possible the mesh could be closer than the current best? + if *aabb_near > nearest_blocking_hit { + return; + } - use super::*; + // Does the mesh handle resolve? + let mesh_handle = simplified_mesh.map(|m| &m.0).unwrap_or(mesh_handle); + let Some(mesh) = self.meshes.get(mesh_handle) else { + return; + }; - // 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]; + let _ray_cast_guard = ray_cast_guard.enter(); + let transform = transform.compute_matrix(); + let intersection = + ray_intersection_over_mesh(mesh, &transform, ray, settings.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 cant 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))); + }; + }; - #[test] - fn ray_cast_triangle_mt() { - let triangle = [V0.into(), V1.into(), V2.into()]; - let ray = Ray3d::new(Vec3::ZERO, Vec3::X); - let result = ray_triangle_intersection(&ray, &triangle, Backfaces::Include); - assert!(result.unwrap().distance - 1.0 <= f32::EPSILON); - } + if let Ok((mesh, simplified_mesh, transform)) = self.mesh_query.get(*entity) { + ray_cast_mesh(mesh, simplified_mesh, transform); + } + + #[cfg(feature = "2d")] + if let Ok((mesh, simplified_mesh, transform)) = self.mesh2d_query.get(*entity) { + ray_cast_mesh(&mesh.0, simplified_mesh, transform); + } + }); - #[test] - fn ray_cast_triangle_mt_culling() { - let triangle = [V2.into(), V1.into(), V0.into()]; - let ray = Ray3d::new(Vec3::ZERO, Vec3::X); - let result = ray_triangle_intersection(&ray, &triangle, Backfaces::Cull); - assert!(result.is_none()); + 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 = hits.collect(); + self.output.as_ref() } } From 171f032d83010cddc9cdfe94ccfb3c88f1eddeb1 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Tue, 8 Oct 2024 23:35:00 +0300 Subject: [PATCH 05/27] Add basic mesh picking example --- Cargo.toml | 12 ++++ examples/picking/mesh_picking.rs | 114 +++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 examples/picking/mesh_picking.rs diff --git a/Cargo.toml b/Cargo.toml index cfac8908dea1f..30dca1a253501 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3609,6 +3609,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_picking_mesh"] + +[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/examples/picking/mesh_picking.rs b/examples/picking/mesh_picking.rs new file mode 100644 index 0000000000000..325ad2537941a --- /dev/null +++ b/examples/picking/mesh_picking.rs @@ -0,0 +1,114 @@ +//! A simple 3D scene to demonstrate mesh picking. + +use bevy::{ + color::palettes::{ + css::{GREEN, PINK, RED}, + tailwind::CYAN_400, + }, + picking::backend::PointerHits, + prelude::*, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .init_resource::() + .add_systems(Startup, setup) + .add_systems(Update, on_mesh_hover) + .run(); +} + +#[derive(Resource, Default)] +struct SceneMaterials { + pub white: Handle, + pub hover: Handle, + pub pressed: Handle, +} + +/// Set up a simple 3D scene. +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut scene_materials: ResMut, +) { + scene_materials.white = materials.add(Color::WHITE); + scene_materials.hover = materials.add(Color::from(CYAN_400)); + scene_materials.pressed = materials.add(Color::from(GREEN)); + + // Circular base + commands + .spawn(( + Mesh3d(meshes.add(Circle::new(4.0))), + MeshMaterial3d(scene_materials.white.clone()), + Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + )) + .observe(on_pointer_over) + .observe(on_pointer_out); + + // Cube + commands + .spawn(( + Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), + MeshMaterial3d(scene_materials.white.clone()), + Transform::from_xyz(0.0, 0.5, 0.0), + )) + .observe(on_pointer_over) + .observe(on_pointer_out); + + // Light + commands.spawn(( + PointLight { + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(4.0, 8.0, 4.0), + )); + + // Camera + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); +} + +fn on_pointer_over( + trigger: Trigger>, + scene_materials: Res, + mut query: Query<&mut MeshMaterial3d>, +) { + if let Ok(mut material) = query.get_mut(trigger.entity()) { + material.0 = scene_materials.hover.clone(); + } +} + +fn on_pointer_out( + trigger: Trigger>, + scene_materials: Res, + mut query: Query<&mut MeshMaterial3d>, +) { + if let Ok(mut material) = query.get_mut(trigger.entity()) { + material.0 = scene_materials.white.clone(); + } +} + +fn on_mesh_hover( + mut pointer_hits: EventReader, + meshes: Query>, + mut gizmos: Gizmos, +) { + for hit in pointer_hits.read() { + let mesh_hits = hit + .picks + .iter() + .filter_map(|(entity, hit)| meshes.get(*entity).map(|_| hit).ok()); + + for hit in mesh_hits { + let (Some(point), Some(normal)) = (hit.position, hit.normal) else { + return; + }; + gizmos.sphere(Isometry3d::from_translation(point), 0.05, RED); + gizmos.arrow(point, point + normal * 0.5, PINK); + } + } +} From 4b728dca1f2cbed08885eef2cf65beded1072394 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Wed, 9 Oct 2024 02:22:05 +0300 Subject: [PATCH 06/27] Move to `bevy_picking` crate --- Cargo.toml | 10 +++---- crates/bevy_internal/Cargo.toml | 5 +--- crates/bevy_internal/src/default_plugins.rs | 2 -- crates/bevy_internal/src/lib.rs | 9 +----- crates/bevy_picking/Cargo.toml | 6 ++++ crates/bevy_picking/src/lib.rs | 9 ++++++ .../src/mesh_picking/mod.rs} | 10 +++---- .../mesh_picking}/ray_cast/intersections.rs | 0 .../src/mesh_picking}/ray_cast/mod.rs | 5 ---- .../mesh_picking}/ray_cast/simplified_mesh.rs | 0 crates/bevy_picking_mesh/Cargo.toml | 29 ------------------- examples/picking/mesh_picking.rs | 8 ++--- 12 files changed, 31 insertions(+), 62 deletions(-) rename crates/{bevy_picking_mesh/src/lib.rs => bevy_picking/src/mesh_picking/mod.rs} (98%) rename crates/{bevy_picking_mesh/src => bevy_picking/src/mesh_picking}/ray_cast/intersections.rs (100%) rename crates/{bevy_picking_mesh/src => bevy_picking/src/mesh_picking}/ray_cast/mod.rs (98%) rename crates/{bevy_picking_mesh/src => bevy_picking/src/mesh_picking}/ray_cast/simplified_mesh.rs (100%) delete mode 100644 crates/bevy_picking_mesh/Cargo.toml diff --git a/Cargo.toml b/Cargo.toml index 8a58289afcdc5..5dfb228087897 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,9 +111,9 @@ default = [ "bevy_gilrs", "bevy_gizmos", "bevy_gltf", + "bevy_mesh_picking_backend", "bevy_pbr", "bevy_picking", - "bevy_picking_mesh", "bevy_remote", "bevy_render", "bevy_scene", @@ -137,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"] @@ -185,9 +188,6 @@ bevy_pbr = [ # Provides picking functionality bevy_picking = ["bevy_internal/bevy_picking"] -# Provides a mesh picking backend for `bevy_picking` -bevy_picking_mesh = ["bevy_picking", "bevy_internal/bevy_picking_mesh"] - # Provides rendering functionality bevy_render = ["bevy_internal/bevy_render", "bevy_color"] @@ -3703,7 +3703,7 @@ wasm = false name = "mesh_picking" path = "examples/picking/mesh_picking.rs" doc-scrape-examples = true -required-features = ["bevy_picking_mesh"] +required-features = ["bevy_mesh_picking_backend"] [package.metadata.example.mesh_picking] name = "Mesh Picking" diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 1e33b5bf4587b..2f077379ed970 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -217,13 +217,11 @@ bevy_remote = ["dep:bevy_remote"] # Provides picking functionality bevy_picking = [ "dep:bevy_picking", + "bevy_picking/bevy_mesh", "bevy_ui?/bevy_picking", "bevy_sprite?/bevy_picking", ] -# Provides a mesh picking backend for `bevy_picking` -bevy_picking_mesh = ["dep:bevy_picking_mesh"] - # Enable support for the ios_simulator by downgrading some rendering capabilities ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"] @@ -278,7 +276,6 @@ bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.15.0-dev" } bevy_image = { path = "../bevy_image", optional = true, version = "0.15.0-dev" } bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.15.0-dev" } bevy_picking = { path = "../bevy_picking", optional = true, version = "0.15.0-dev" } -bevy_picking_mesh = { path = "../bevy_picking_mesh", optional = true, version = "0.15.0-dev" } bevy_remote = { path = "../bevy_remote", optional = true, version = "0.15.0-dev" } bevy_render = { path = "../bevy_render", optional = true, version = "0.15.0-dev" } bevy_scene = { path = "../bevy_scene", optional = true, version = "0.15.0-dev" } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 27e0af1e97a4e..74008dafafcc8 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -58,8 +58,6 @@ plugin_group! { bevy_state::app:::StatesPlugin, #[cfg(feature = "bevy_picking")] bevy_picking:::DefaultPickingPlugins, - #[cfg(feature = "bevy_picking_mesh")] - bevy_picking_mesh:::MeshPickingBackend, #[cfg(feature = "bevy_dev_tools")] bevy_dev_tools:::DevToolsPlugin, #[cfg(feature = "bevy_ci_testing")] diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 10daaddd2cdb2..bc553af972223 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -43,14 +43,7 @@ pub use bevy_math as math; #[cfg(feature = "bevy_pbr")] pub use bevy_pbr as pbr; #[cfg(feature = "bevy_picking")] -#[allow(ambiguous_glob_reexports)] -pub mod picking { - pub use bevy_picking::*; - #[cfg(feature = "bevy_picking_mesh")] - pub mod mesh { - pub use bevy_picking_mesh::*; - } -} +pub use bevy_picking as picking; pub use bevy_ptr as ptr; pub use bevy_reflect as reflect; #[cfg(feature = "bevy_remote")] 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 c1ec7a49e9d96..4bc2b4dd34cf4 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,11 @@ 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")] + pub use crate::mesh_picking::{ + ray_cast::{Backfaces, MeshRayCast, RayCastSettings, RayCastVisibility}, + MeshPickingBackend, + }; #[doc(hidden)] pub use crate::{ events::*, input::PointerInputPlugin, pointer::PointerButton, DefaultPickingPlugins, @@ -274,6 +281,8 @@ impl Plugin for DefaultPickingPlugins { PickingPlugin::default(), InteractionPlugin, )); + #[cfg(feature = "bevy_mesh")] + app.add_plugins(mesh_picking::MeshPickingBackend); } } diff --git a/crates/bevy_picking_mesh/src/lib.rs b/crates/bevy_picking/src/mesh_picking/mod.rs similarity index 98% rename from crates/bevy_picking_mesh/src/lib.rs rename to crates/bevy_picking/src/mesh_picking/mod.rs index 9196b26b3f8b3..fb02ff4732245 100644 --- a/crates/bevy_picking_mesh/src/lib.rs +++ b/crates/bevy_picking/src/mesh_picking/mod.rs @@ -1,4 +1,4 @@ -//! A ray casting backend for [`bevy_picking`]. +//! A ray casting backend for [`bevy_picking`](crate). //! //! # Usage //! @@ -13,13 +13,13 @@ #![allow(clippy::too_many_arguments, clippy::type_complexity)] #![warn(missing_docs)] -use bevy_app::prelude::*; -use bevy_ecs::prelude::*; -use bevy_picking::{ +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::{Backfaces, MeshRayCast, RayCastSettings, RayCastVisibility}; @@ -31,7 +31,7 @@ pub mod ray_cast; /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] - pub use crate::{ + pub use super::{ ray_cast::{Backfaces, MeshRayCast, RayCastSettings, RayCastVisibility}, MeshPickingBackend, }; diff --git a/crates/bevy_picking_mesh/src/ray_cast/intersections.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs similarity index 100% rename from crates/bevy_picking_mesh/src/ray_cast/intersections.rs rename to crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs diff --git a/crates/bevy_picking_mesh/src/ray_cast/mod.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs similarity index 98% rename from crates/bevy_picking_mesh/src/ray_cast/mod.rs rename to crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs index 37a31cddc6004..5f8bf5a6ef809 100644 --- a/crates/bevy_picking_mesh/src/ray_cast/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs @@ -98,10 +98,7 @@ pub enum Backfaces { Include, } -#[cfg(feature = "2d")] type MeshFilter = Or<(With, With)>; -#[cfg(not(feature = "2d"))] -type MeshFilter = With; /// Add this raycasting [`SystemParam`] to your system to ray cast into the world with an /// immediate-mode API. Call `cast_ray` to immediately perform a ray cast and get a result. Under the @@ -182,7 +179,6 @@ pub struct MeshRayCast<'w, 's> { Read, ), >, - #[cfg(feature = "2d")] #[doc(hidden)] pub mesh2d_query: Query< 'w, @@ -275,7 +271,6 @@ impl<'w, 's> MeshRayCast<'w, 's> { ray_cast_mesh(mesh, simplified_mesh, transform); } - #[cfg(feature = "2d")] if let Ok((mesh, simplified_mesh, transform)) = self.mesh2d_query.get(*entity) { ray_cast_mesh(&mesh.0, simplified_mesh, transform); } diff --git a/crates/bevy_picking_mesh/src/ray_cast/simplified_mesh.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/simplified_mesh.rs similarity index 100% rename from crates/bevy_picking_mesh/src/ray_cast/simplified_mesh.rs rename to crates/bevy_picking/src/mesh_picking/ray_cast/simplified_mesh.rs diff --git a/crates/bevy_picking_mesh/Cargo.toml b/crates/bevy_picking_mesh/Cargo.toml deleted file mode 100644 index 146ff9116f285..0000000000000 --- a/crates/bevy_picking_mesh/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "bevy_picking_mesh" -version = "0.15.0-dev" -edition = "2021" -description = "Provides a mesh picking backend for Bevy Engine" -homepage = "https://bevyengine.org" -repository = "https://github.com/bevyengine/bevy" -license = "MIT OR Apache-2.0" -keywords = ["bevy"] - -[features] -2d = [] - -[dependencies] -bevy_app = { path = "../bevy_app", version = "0.15.0-dev", default-features = false } -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev", default-features = false } -bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev", default-features = false } -bevy_math = { path = "../bevy_math", version = "0.15.0-dev", default-features = false } -bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", default-features = false } -bevy_render = { path = "../bevy_render", version = "0.15.0-dev", default-features = false } -bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev", default-features = false } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev", default-features = false } - -crossbeam-channel = "0.5" - -[lints] -workspace = true diff --git a/examples/picking/mesh_picking.rs b/examples/picking/mesh_picking.rs index 325ad2537941a..c4c81dc011dee 100644 --- a/examples/picking/mesh_picking.rs +++ b/examples/picking/mesh_picking.rs @@ -66,10 +66,10 @@ fn setup( )); // Camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), + )); } fn on_pointer_over( From 7b4a2e771242f599cde1d050cf49ccd3b59c3928 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Wed, 9 Oct 2024 02:26:18 +0300 Subject: [PATCH 07/27] Fix doc things and name --- crates/bevy_picking/src/lib.rs | 1 + crates/bevy_picking/src/mesh_picking/mod.rs | 2 +- .../bevy_picking/src/mesh_picking/ray_cast/intersections.rs | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 4bc2b4dd34cf4..2ad6cedf5dd16 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -169,6 +169,7 @@ 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::{Backfaces, MeshRayCast, RayCastSettings, RayCastVisibility}, MeshPickingBackend, diff --git a/crates/bevy_picking/src/mesh_picking/mod.rs b/crates/bevy_picking/src/mesh_picking/mod.rs index fb02ff4732245..de4951b834aae 100644 --- a/crates/bevy_picking/src/mesh_picking/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/mod.rs @@ -5,7 +5,7 @@ //! If a pointer passes through this camera's render target, it will automatically shoot rays into //! the scene and will be able to pick things. //! -//! To ignore an entity, you can add [`Pickable::IGNORE`] to it, and it will be ignored during +//! To ignore an entity, you can add [`PickingBehavior::IGNORE`] to it, and it will be ignored during //! ray casting. //! //! For fine-grained control, see the [`MeshPickingBackendSettings::require_markers`] setting. 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 81f7451f11e73..a5d472d677824 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs @@ -219,7 +219,7 @@ fn triangle_intersection( if hit.distance < 0.0 || hit.distance > max_distance { return None; }; - let position = ray.get_point(hit.distance); + 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; @@ -232,7 +232,7 @@ fn triangle_intersection( .normalize() }; Some(RayMeshHit { - point: position, + point, normal: Dir3::new_unchecked(normal.into()), barycentric_coord: barycentric, distance: hit.distance, From 3526cfa63b3573644548e84554854dd43bd1f9a8 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Wed, 9 Oct 2024 17:00:57 +0300 Subject: [PATCH 08/27] Clean up and improve docs --- crates/bevy_picking/src/mesh_picking/mod.rs | 59 +++---- .../src/mesh_picking/ray_cast/mod.rs | 165 +++++++++--------- .../mesh_picking/ray_cast/simplified_mesh.rs | 10 -- 3 files changed, 110 insertions(+), 124 deletions(-) delete mode 100644 crates/bevy_picking/src/mesh_picking/ray_cast/simplified_mesh.rs diff --git a/crates/bevy_picking/src/mesh_picking/mod.rs b/crates/bevy_picking/src/mesh_picking/mod.rs index de4951b834aae..4bf037bd5110c 100644 --- a/crates/bevy_picking/src/mesh_picking/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/mod.rs @@ -1,17 +1,15 @@ -//! A ray casting backend for [`bevy_picking`](crate). -//! -//! # Usage +//! A [mesh ray casting](ray_cast) backend for [`bevy_picking`](crate). //! //! If a pointer passes through this camera's render target, it will automatically shoot rays into -//! the scene and will be able to pick things. +//! the scene and be able to pick meshes. //! -//! To ignore an entity, you can add [`PickingBehavior::IGNORE`] to it, and it will be ignored during -//! ray casting. +//! To ignore an entity for picking, you can add [`PickingBehavior::IGNORE`] to it. You can also configure +//! [`MeshPickingBackendSettings::require_markers`] to only perform ray casts between cameras and meshes +//! marked with the [`RayCastPickable`] component. //! -//! For fine-grained control, see the [`MeshPickingBackendSettings::require_markers`] setting. +//! To manually perform mesh ray casts independent of picking, use the [`MeshRayCast`] system parameter. -#![allow(clippy::too_many_arguments, clippy::type_complexity)] -#![warn(missing_docs)] +pub mod ray_cast; use crate::{ backend::{ray::RayMap, HitData, PointerHits}, @@ -22,35 +20,25 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_reflect::prelude::*; use bevy_render::{prelude::*, view::RenderLayers}; -use ray_cast::{Backfaces, MeshRayCast, RayCastSettings, RayCastVisibility}; - -pub mod ray_cast; - -/// The mesh picking prelude. -/// -/// This includes the most common types in this crate, re-exported for your convenience. -pub mod prelude { - #[doc(hidden)] - pub use super::{ - ray_cast::{Backfaces, MeshRayCast, RayCastSettings, RayCastVisibility}, - MeshPickingBackend, - }; -} +use ray_cast::{Backfaces, 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`]. Off by default. This setting is provided to give you fine-grained - /// control over which cameras and entities should be used by the ray cast backend at runtime. + /// [`RayCastPickable`]. Off by default. + /// + /// This setting is provided to give you fine-grained control over which cameras and entities + /// should be used by the ray cast backend at runtime. pub require_markers: bool, - /// When set to [`RayCastVisibility::Any`], hidden items can be ray casted against. + /// When set to [`RayCastVisibility::Any`], hidden meshes can be ray casted against. + /// /// See [`RayCastSettings::visibility`] for more information. pub raycast_visibility: RayCastVisibility, - /// When set to [`Backfaces::Cull`], backfaces will be ignored during ray casting. + /// When set to [`Backfaces::Cull`], backfaces of meshes will be ignored during ray casting. pub backfaces: Backfaces, } @@ -58,31 +46,32 @@ impl Default for MeshPickingBackendSettings { fn default() -> Self { Self { require_markers: false, - raycast_visibility: RayCastVisibility::VisibleAndInView, + raycast_visibility: RayCastVisibility::VisibleInView, backfaces: Backfaces::default(), } } } -/// Optional. Marks cameras and target entities that should be used in the ray cast picking backend. -/// Only needed if [`MeshPickingBackendSettings::require_markers`] is set to true. +/// An optional component that marks cameras and target entities that should be used in the ray cast picking backend. +/// Only needed if [`MeshPickingBackendSettings::require_markers`] is set to `true`. #[derive(Debug, Clone, Default, Component, Reflect)] #[reflect(Component, Default)] pub struct RayCastPickable; -/// Adds the ray casting picking backend to your app. +/// 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::() - .add_systems(PreUpdate, update_hits.in_set(PickSet::Backend)) - .register_type::() - .register_type::(); + .register_type::<(RayCastPickable, MeshPickingBackendSettings, SimplifiedMesh)>() + .add_systems(PreUpdate, update_hits.in_set(PickSet::Backend)); } } -/// Casts rays into the scene using [`MeshPickingBackendSettings`] and outputs [`PointerHits`]. +/// 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, diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs index 5f8bf5a6ef809..050fe262b76ab 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs @@ -1,9 +1,10 @@ -//! Ray casting on meshes. +//! Ray casting for meshes. +//! +//! See the [`MeshRayCast`] system parameter for more information. mod intersections; -mod simplified_mesh; -pub use simplified_mesh::*; +use bevy_derive::{Deref, DerefMut}; use bevy_math::Ray3d; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -19,30 +20,30 @@ use bevy_render::{prelude::*, primitives::Aabb}; use bevy_transform::components::GlobalTransform; use bevy_utils::tracing::*; -/// How a ray cast should handle visibility. +/// 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 ray cast against entities that are visible in the hierarchy. See [`Visibility`]. + /// Only cast rays against entities that are visible in the hierarchy. See [`Visibility`]. Visible, - /// Only ray cast against entities that are visible in the hierarchy and visible to a camera or + /// Only cast rays against entities that are visible in the hierarchy and visible to a camera or /// light. See [`Visibility`]. - VisibleAndInView, + VisibleInView, } /// Settings for a ray cast. #[derive(Clone)] pub struct RayCastSettings<'a> { - /// Determines how ray casting should consider entity visibility. + /// Determines how ray casting should consider [`Visibility`]. pub visibility: RayCastVisibility, - /// Determines how ray casting should handle backfaces. + /// Determines if ray casting should include backfaces or cull them. pub backfaces: Backfaces, - /// A filtering function that is applied to every entity that is ray casted. Only entities that - /// return `true` will be considered. + /// 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. + /// along the ray as long as this returns `false`. pub early_exit_test: &'a dyn Fn(Entity) -> bool, } @@ -79,7 +80,7 @@ impl<'a> RayCastSettings<'a> { impl<'a> Default for RayCastSettings<'a> { fn default() -> Self { Self { - visibility: RayCastVisibility::VisibleAndInView, + visibility: RayCastVisibility::VisibleInView, backfaces: Backfaces::default(), filter: &|_| true, early_exit_test: &|_| true, @@ -87,7 +88,7 @@ impl<'a> Default for RayCastSettings<'a> { } } -/// Determines whether backfaces should be culled or included in intersection checks. +/// Determines whether backfaces should be culled or included in ray intersection tests. #[derive(Copy, Clone, Default, Reflect)] #[reflect(Default)] pub enum Backfaces { @@ -97,31 +98,38 @@ pub enum Backfaces { /// Include backfaces. Include, } +/// 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)>; +type MeshFilter = Or<(With, With, With)>; -/// Add this raycasting [`SystemParam`] to your system to ray cast 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 locals that are added to your -/// system. +/// 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 ray casts into the world with a ray positioned at the origin, pointing in -/// the x-direction, and returns a list of intersections: +/// 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::prelude::*; -/// fn ray_cast_system(mut raycast: MeshRayCast) { -/// let ray = Ray3d::new(Vec3::ZERO, Vec3::X); -/// let hits = raycast.cast_ray(ray, &RayCastSettings::default()); +/// 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 behavior of the ray cast using [`RayCastSettings`]. This allows you to filter out -/// entities, configure early-out, and set whether the [`Visibility`] of an entity should be +/// 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. /// /// ``` @@ -129,20 +137,23 @@ type MeshFilter = Or<(With, With)>; /// # #[derive(Component)] /// # struct Foo; /// fn ray_cast_system(mut ray_cast: MeshRayCast, foo_query: Query<(), With>) { -/// let ray = Ray3d::new(Vec3::ZERO, Vec3::X); +/// 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::Ignore; +/// 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); /// } /// ``` @@ -174,20 +185,12 @@ pub struct MeshRayCast<'w, 's> { 'w, 's, ( - Read, - Option>, - Read, - ), - >, - #[doc(hidden)] - pub mesh2d_query: Query< - 'w, - 's, - ( - Read, + Option>, + Option>, Option>, Read, ), + MeshFilter, >, } @@ -201,7 +204,7 @@ impl<'w, 's> MeshRayCast<'w, 's> { 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 + // 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; @@ -210,7 +213,7 @@ impl<'w, 's> MeshRayCast<'w, 's> { let should_ray_cast = match visibility_setting { RayCastVisibility::Any => true, RayCastVisibility::Visible => inherited_visibility.get(), - RayCastVisibility::VisibleAndInView => view_visibility.get(), + RayCastVisibility::VisibleInView => view_visibility.get(), }; if should_ray_cast { if let Some([near, _]) = @@ -223,57 +226,61 @@ impl<'w, 's> MeshRayCast<'w, 's> { }, ); *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)| { - let mut ray_cast_mesh = - |mesh_handle: &Handle, - simplified_mesh: Option<&SimplifiedMesh>, - transform: &GlobalTransform| { - // 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 mesh_handle = simplified_mesh.map(|m| &m.0).unwrap_or(mesh_handle); - let Some(mesh) = self.meshes.get(mesh_handle) else { - return; - }; + // Get the mesh components and transform. + let Ok((mesh2d, mesh3d, simplified_mesh, transform)) = self.mesh_query.get(*entity) + else { + return; + }; - let _ray_cast_guard = ray_cast_guard.enter(); - let transform = transform.compute_matrix(); - let intersection = - ray_intersection_over_mesh(mesh, &transform, ray, settings.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 cant 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))); - }; - }; + // 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; + }; - if let Ok((mesh, simplified_mesh, transform)) = self.mesh_query.get(*entity) { - ray_cast_mesh(mesh, simplified_mesh, transform); + // Is it even possible the mesh could be closer than the current best? + if *aabb_near > nearest_blocking_hit { + return; } - if let Ok((mesh, simplified_mesh, transform)) = self.mesh2d_query.get(*entity) { - ray_cast_mesh(&mesh.0, simplified_mesh, transform); - } + // Does the mesh handle resolve? + let Some(mesh) = self.meshes.get(mesh_handle) else { + return; + }; + + // 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, settings.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); diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/simplified_mesh.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/simplified_mesh.rs deleted file mode 100644 index 761cdfcd78f77..0000000000000 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/simplified_mesh.rs +++ /dev/null @@ -1,10 +0,0 @@ -use bevy_asset::Handle; -use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{component::Component, reflect::ReflectComponent}; -use bevy_reflect::Reflect; -use bevy_render::mesh::Mesh; - -/// A simplified mesh component that can be used for [ray casting](crate::MeshRayCast). -#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect)] -#[reflect(Component, Debug)] -pub struct SimplifiedMesh(pub Handle); From 3af0565a1609064743b7eac119741a241d4a9c1f Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Wed, 9 Oct 2024 18:47:30 +0300 Subject: [PATCH 09/27] Fix scaling issue and clean up --- crates/bevy_picking/src/mesh_picking/mod.rs | 2 +- .../mesh_picking/ray_cast/intersections.rs | 135 +++++++++--------- 2 files changed, 71 insertions(+), 66 deletions(-) diff --git a/crates/bevy_picking/src/mesh_picking/mod.rs b/crates/bevy_picking/src/mesh_picking/mod.rs index 4bf037bd5110c..5e87683298fb5 100644 --- a/crates/bevy_picking/src/mesh_picking/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/mod.rs @@ -124,7 +124,7 @@ pub fn update_hits( ray_id.camera, hit.distance, Some(hit.point), - Some(*hit.normal), + Some(hit.normal), ); (*entity, hit_data) }) 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 a5d472d677824..1cdf9210358e5 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs @@ -8,8 +8,8 @@ use bevy_utils::tracing::{error, warn}; use super::Backfaces; -/// Casts a ray on a mesh, and returns the intersection -pub fn ray_intersection_over_mesh( +/// Casts a ray on a mesh, and returns the intersection. +pub(super) fn ray_intersection_over_mesh( mesh: &Mesh, mesh_transform: &Mat4, ray: Ray3d, @@ -21,7 +21,8 @@ pub fn ray_intersection_over_mesh( ); return None; } - // Get the vertex positions from the mesh reference resolved from the mesh handle + + // Get the vertex positions and normals from the mesh. let vertex_positions: &Vec<[f32; 3]> = match mesh.attribute(Mesh::ATTRIBUTE_POSITION) { None => panic!("Mesh does not contain vertex positions"), Some(vertex_values) => match &vertex_values { @@ -40,31 +41,30 @@ pub fn ray_intersection_over_mesh( }; if let Some(indices) = &mesh.indices() { - // Iterate over the list of pick rays that belong to the same group as this mesh match indices { Indices::U16(vertex_indices) => ray_mesh_intersection( + ray, mesh_transform, vertex_positions, vertex_normals, - ray, Some(vertex_indices), backface_culling, ), Indices::U32(vertex_indices) => ray_mesh_intersection( + ray, mesh_transform, vertex_positions, vertex_normals, - ray, Some(vertex_indices), backface_culling, ), } } else { ray_mesh_intersection( + ray, mesh_transform, vertex_positions, vertex_normals, - ray, None::<&Vec>, backface_culling, ) @@ -72,7 +72,7 @@ pub fn ray_intersection_over_mesh( } /// A trait for converting a value into a [`usize`]. -pub trait IntoUsize: Copy { +trait IntoUsize: Copy { /// Converts the value into a [`usize`]. fn into_usize(self) -> usize; } @@ -90,18 +90,18 @@ impl IntoUsize for u32 { } /// Checks if a ray intersects a mesh, and returns the nearest intersection if one exists. -pub fn ray_mesh_intersection( +fn ray_mesh_intersection( + ray: Ray3d, mesh_transform: &Mat4, vertex_positions: &[[f32; 3]], vertex_normals: Option<&[[f32; 3]]>, - ray: Ray3d, indices: Option<&Vec>, 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 min_pick_distance = f32::MAX; - let mut pick_intersection = None; + let mut closest_hit_distance = f32::MAX; + let mut closest_hit = None; let world_to_mesh = mesh_transform.inverse(); @@ -116,6 +116,7 @@ pub fn ray_mesh_intersection( 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. @@ -133,32 +134,34 @@ pub fn ray_mesh_intersection( Vec3A::from(normals[index[2].into_usize()]), ] }); - let intersection = triangle_intersection( + + let Some(hit) = triangle_intersection( tri_vertex_positions, tri_normals, - min_pick_distance, + closest_hit_distance, &mesh_space_ray, backface_culling, - ); - if let Some(i) = intersection { - pick_intersection = Some(RayMeshHit { - point: mesh_transform.transform_point3(i.point), - normal: Dir3::new_unchecked(mesh_transform.transform_vector3(*i.normal)), - barycentric_coord: i.barycentric_coord, - distance: mesh_transform - .transform_vector3(mesh_space_ray.direction * i.distance) - .length(), - triangle: i.triangle.map(|tri| { - [ - mesh_transform.transform_point3a(tri[0]), - mesh_transform.transform_point3a(tri[1]), - mesh_transform.transform_point3a(tri[2]), - ] - }), - triangle_index, - }); - min_pick_distance = i.distance; - } + ) 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 in (0..vertex_positions.len()).step_by(3) { @@ -175,36 +178,38 @@ pub fn ray_mesh_intersection( Vec3A::from(normals[i + 2]), ] }); - let intersection = triangle_intersection( + + let Some(hit) = triangle_intersection( tri_vertex_positions, tri_normals, - min_pick_distance, + closest_hit_distance, &mesh_space_ray, backface_culling, - ); - if let Some(i) = intersection { - pick_intersection = Some(RayMeshHit { - point: mesh_transform.transform_point3(i.point), - normal: Dir3::new_unchecked(mesh_transform.transform_vector3(*i.normal)), - barycentric_coord: i.barycentric_coord, - distance: mesh_transform - .transform_vector3(mesh_space_ray.direction * i.distance) - .length(), - triangle: i.triangle.map(|tri| { - [ - mesh_transform.transform_point3a(tri[0]), - mesh_transform.transform_point3a(tri[1]), - mesh_transform.transform_point3a(tri[2]), - ] - }), - triangle_index, - }); - min_pick_distance = i.distance; - } + ) 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; } } - pick_intersection + closest_hit } #[inline(always)] @@ -233,17 +238,17 @@ fn triangle_intersection( }; Some(RayMeshHit { point, - normal: Dir3::new_unchecked(normal.into()), - barycentric_coord: barycentric, + 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 and normal +/// Takes a ray and triangle and computes the intersection. #[inline(always)] -pub fn ray_triangle_intersection( +fn ray_triangle_intersection( ray: &Ray3d, triangle: &[Vec3A; 3], backface_culling: Backfaces, @@ -334,10 +339,10 @@ mod tests { pub struct RayMeshHit { /// The point of intersection in world space. pub point: Vec3, - /// The normal of the triangle at the point of intersection. - pub normal: Dir3, + /// 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_coord: Vec3, + 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. @@ -347,7 +352,7 @@ pub struct RayMeshHit { } // TODO: It'd be nice to use `RayCast3d` from `bevy_math` instead, but it only works on normalized rays. -/// Checks if the ray intersects with an AABB of a mesh, returning `[near, far]` if it does.44 +/// Checks if the ray intersects with an AABB of a mesh, returning `[near, far]` if it does. pub(crate) fn ray_aabb_intersection_3d( ray: Ray3d, aabb: &Aabb, From 0ce79a57f29d8b6529367144e19314d6a189b0de Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Wed, 9 Oct 2024 22:16:48 +0300 Subject: [PATCH 10/27] Add back backface marker component `RayCastBackfaces` --- crates/bevy_picking/src/lib.rs | 2 +- crates/bevy_picking/src/mesh_picking/mod.rs | 13 +- .../mesh_picking/ray_cast/intersections.rs | 115 +++++++++--------- .../src/mesh_picking/ray_cast/mod.rs | 25 ++-- 4 files changed, 83 insertions(+), 72 deletions(-) diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 2ad6cedf5dd16..9b2040a5e0d29 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -171,7 +171,7 @@ pub mod prelude { #[cfg(feature = "bevy_mesh")] #[doc(hidden)] pub use crate::mesh_picking::{ - ray_cast::{Backfaces, MeshRayCast, RayCastSettings, RayCastVisibility}, + ray_cast::{MeshRayCast, RayCastBackfaces, RayCastSettings, RayCastVisibility}, MeshPickingBackend, }; #[doc(hidden)] diff --git a/crates/bevy_picking/src/mesh_picking/mod.rs b/crates/bevy_picking/src/mesh_picking/mod.rs index 5e87683298fb5..a4101c4f76ce1 100644 --- a/crates/bevy_picking/src/mesh_picking/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/mod.rs @@ -20,7 +20,7 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_reflect::prelude::*; use bevy_render::{prelude::*, view::RenderLayers}; -use ray_cast::{Backfaces, MeshRayCast, RayCastSettings, RayCastVisibility, SimplifiedMesh}; +use ray_cast::{MeshRayCast, RayCastSettings, RayCastVisibility, SimplifiedMesh}; /// Runtime settings for the [`MeshPickingBackend`]. #[derive(Resource, Reflect)] @@ -36,18 +36,14 @@ pub struct MeshPickingBackendSettings { /// When set to [`RayCastVisibility::Any`], hidden meshes can be ray casted against. /// /// See [`RayCastSettings::visibility`] for more information. - pub raycast_visibility: RayCastVisibility, - - /// When set to [`Backfaces::Cull`], backfaces of meshes will be ignored during ray casting. - pub backfaces: Backfaces, + pub ray_cast_visibility: RayCastVisibility, } impl Default for MeshPickingBackendSettings { fn default() -> Self { Self { require_markers: false, - raycast_visibility: RayCastVisibility::VisibleInView, - backfaces: Backfaces::default(), + ray_cast_visibility: RayCastVisibility::VisibleInView, } } } @@ -93,8 +89,7 @@ pub fn update_hits( let cam_layers = cam_layers.to_owned().unwrap_or_default(); let settings = RayCastSettings { - visibility: backend_settings.raycast_visibility, - backfaces: backend_settings.backfaces, + visibility: backend_settings.ray_cast_visibility, filter: &|entity| { let marker_requirement = !backend_settings.require_markers || marked_targets.get(entity).is_ok(); 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 1cdf9210358e5..dc5c42589d057 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs @@ -8,6 +8,30 @@ use bevy_utils::tracing::{error, warn}; use super::Backfaces; +/// A ray intersection with 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, +} + +/// A hit result from a ray cast on 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, @@ -72,7 +96,7 @@ pub(super) fn ray_intersection_over_mesh( } /// A trait for converting a value into a [`usize`]. -trait IntoUsize: Copy { +pub trait IntoUsize: Copy { /// Converts the value into a [`usize`]. fn into_usize(self) -> usize; } @@ -90,7 +114,7 @@ impl IntoUsize for u32 { } /// Checks if a ray intersects a mesh, and returns the nearest intersection if one exists. -fn ray_mesh_intersection( +pub fn ray_mesh_intersection( ray: Ray3d, mesh_transform: &Mat4, vertex_positions: &[[f32; 3]], @@ -221,14 +245,17 @@ fn triangle_intersection( 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 { @@ -236,6 +263,7 @@ fn triangle_intersection( .cross(tri_vertices[2] - tri_vertices[0]) .normalize() }; + Some(RayMeshHit { point, normal: normal.into(), @@ -299,59 +327,7 @@ fn ray_triangle_intersection( }) } -/// A hit result from a ray cast on a triangle. -#[derive(Default, Debug)] -pub struct RayTriangleHit { - pub distance: f32, - pub barycentric_coords: (f32, f32), -} - -#[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()); - } -} - -/// A ray intersection with 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, -} - -// TODO: It'd be nice to use `RayCast3d` from `bevy_math` instead, but it only works on normalized rays. +// TODO: It'd be nice to use `RayCast3d` from `bevy_math` instead. It caches the direction reciprocal. /// Checks if the ray intersects with an AABB of a mesh, returning `[near, far]` if it does. pub(crate) fn ray_aabb_intersection_3d( ray: Ray3d, @@ -396,5 +372,34 @@ pub(crate) fn ray_aabb_intersection_3d( if t_max.z < hit_far { hit_far = t_max.z; } + Some([hit_near, hit_far]) } + +#[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 index 050fe262b76ab..ecc2c9603e5f9 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs @@ -10,8 +10,8 @@ use bevy_math::Ray3d; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::mesh::Mesh; -pub use intersections::RayMeshHit; use intersections::*; +pub use intersections::{ray_mesh_intersection, RayMeshHit}; use bevy_asset::{Assets, Handle}; use bevy_ecs::{prelude::*, system::lifetimeless::Read, system::SystemParam}; @@ -37,8 +37,6 @@ pub enum RayCastVisibility { pub struct RayCastSettings<'a> { /// Determines how ray casting should consider [`Visibility`]. pub visibility: RayCastVisibility, - /// Determines if ray casting should include backfaces or cull them. - pub backfaces: Backfaces, /// 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, @@ -81,7 +79,6 @@ impl<'a> Default for RayCastSettings<'a> { fn default() -> Self { Self { visibility: RayCastVisibility::VisibleInView, - backfaces: Backfaces::default(), filter: &|_| true, early_exit_test: &|_| true, } @@ -89,6 +86,8 @@ impl<'a> Default for RayCastSettings<'a> { } /// 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 { @@ -98,6 +97,12 @@ pub enum Backfaces { /// 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. @@ -188,6 +193,7 @@ pub struct MeshRayCast<'w, 's> { Option>, Option>, Option>, + Has, Read, ), MeshFilter, @@ -240,7 +246,8 @@ impl<'w, 's> MeshRayCast<'w, 's> { .filter(|(_, entity)| (settings.filter)(*entity)) .for_each(|(aabb_near, entity)| { // Get the mesh components and transform. - let Ok((mesh2d, mesh3d, simplified_mesh, transform)) = self.mesh_query.get(*entity) + let Ok((mesh2d, mesh3d, simplified_mesh, has_backfaces, transform)) = + self.mesh_query.get(*entity) else { return; }; @@ -263,11 +270,15 @@ impl<'w, 's> MeshRayCast<'w, 's> { 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, settings.backfaces); + let intersection = ray_intersection_over_mesh(mesh, &transform, ray, backfaces); if let Some(intersection) = intersection { let distance = FloatOrd(intersection.distance); From 3d544c6bf156f04b91f36c8b3220982564cf2ca1 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Wed, 9 Oct 2024 22:17:03 +0300 Subject: [PATCH 11/27] Add benchmark --- benches/Cargo.toml | 6 + .../bevy_picking/ray_mesh_intersection.rs | 120 ++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 benches/benches/bevy_picking/ray_mesh_intersection.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..177c128aff2e1 --- /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::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(bevy_picking::mesh_picking::ray_cast::ray_mesh_intersection( + ray, + &mesh_to_world, + &mesh.positions, + Some(&mesh.normals), + Some(&mesh.indices), + 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(bevy_picking::mesh_picking::ray_cast::ray_mesh_intersection( + ray, + &mesh_to_world, + &mesh.positions, + Some(&mesh.normals), + Some(&mesh.indices), + 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(bevy_picking::mesh_picking::ray_cast::ray_mesh_intersection( + ray, + &mesh_to_world, + &mesh.positions, + Some(&mesh.normals), + Some(&mesh.indices), + Backfaces::Cull, + )); + }); + }); + } +} + +criterion_group!( + benches, + ray_mesh_intersection, + ray_mesh_intersection_no_cull, + ray_mesh_intersection_no_intersection +); +criterion_main!(benches); From 673d75fa42e1331b66232e28ce27a3be13661d50 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Wed, 9 Oct 2024 23:01:09 +0300 Subject: [PATCH 12/27] Improve example --- crates/bevy_picking/src/lib.rs | 2 +- examples/picking/mesh_picking.rs | 199 +++++++++++++++++++++++++------ 2 files changed, 162 insertions(+), 39 deletions(-) diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 9b2040a5e0d29..2a5c84ca6d086 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -172,7 +172,7 @@ pub mod prelude { #[doc(hidden)] pub use crate::mesh_picking::{ ray_cast::{MeshRayCast, RayCastBackfaces, RayCastSettings, RayCastVisibility}, - MeshPickingBackend, + MeshPickingBackend, MeshPickingBackendSettings, RayCastPickable, }; #[doc(hidden)] pub use crate::{ diff --git a/examples/picking/mesh_picking.rs b/examples/picking/mesh_picking.rs index c4c81dc011dee..4d6240f5b27c9 100644 --- a/examples/picking/mesh_picking.rs +++ b/examples/picking/mesh_picking.rs @@ -1,9 +1,17 @@ //! A simple 3D scene to demonstrate mesh picking. +//! +//! By default, all meshes are pickable. Picking can be disabled for individual entities +//! by adding [`PickingBehavior::IGNORE`]. +//! +//! If you want mesh picking to be entirely opt-in, you can set [`MeshPickingBackendSettings::require_markers`] +//! to `true` and add a [`RayCastPickable`] component to the desired camera and target entities. + +use std::f32::consts::PI; use bevy::{ color::palettes::{ - css::{GREEN, PINK, RED}, - tailwind::CYAN_400, + css::{PINK, RED, SILVER}, + tailwind::{CYAN_300, YELLOW_300}, }, picking::backend::PointerHits, prelude::*, @@ -14,64 +22,143 @@ fn main() { .add_plugins(DefaultPlugins) .init_resource::() .add_systems(Startup, setup) - .add_systems(Update, on_mesh_hover) + .add_systems(Update, (on_mesh_hover, rotate)) .run(); } +/// Materials for the scene #[derive(Resource, Default)] struct SceneMaterials { pub white: Handle, + pub ground: Handle, pub hover: Handle, pub pressed: Handle, } -/// Set up a simple 3D scene. +/// A marker component for our shapes so we can query them separately from the ground plane. +#[derive(Component)] +struct Shape; + +const SHAPES_X_EXTENT: f32 = 14.0; +const EXTRUSION_X_EXTENT: f32 = 16.0; +const Z_EXTENT: f32 = 5.0; + fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, mut scene_materials: ResMut, ) { + // Set up the materials. scene_materials.white = materials.add(Color::WHITE); - scene_materials.hover = materials.add(Color::from(CYAN_400)); - scene_materials.pressed = materials.add(Color::from(GREEN)); - - // Circular base - commands - .spawn(( - Mesh3d(meshes.add(Circle::new(4.0))), - MeshMaterial3d(scene_materials.white.clone()), - Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), - )) - .observe(on_pointer_over) - .observe(on_pointer_out); - - // Cube - commands - .spawn(( - Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), - MeshMaterial3d(scene_materials.white.clone()), - Transform::from_xyz(0.0, 0.5, 0.0), - )) - .observe(on_pointer_over) - .observe(on_pointer_out); + scene_materials.ground = materials.add(Color::from(SILVER)); + scene_materials.hover = materials.add(Color::from(CYAN_300)); + scene_materials.pressed = materials.add(Color::from(YELLOW_300)); + + let shapes = [ + meshes.add(Cuboid::default()), + meshes.add(Tetrahedron::default()), + meshes.add(Capsule3d::default()), + meshes.add(Torus::default()), + meshes.add(Cylinder::default()), + meshes.add(Cone::default()), + meshes.add(ConicalFrustum::default()), + meshes.add(Sphere::default().mesh().ico(5).unwrap()), + meshes.add(Sphere::default().mesh().uv(32, 18)), + ]; + + let extrusions = [ + meshes.add(Extrusion::new(Rectangle::default(), 1.)), + meshes.add(Extrusion::new(Capsule2d::default(), 1.)), + meshes.add(Extrusion::new(Annulus::default(), 1.)), + meshes.add(Extrusion::new(Circle::default(), 1.)), + meshes.add(Extrusion::new(Ellipse::default(), 1.)), + meshes.add(Extrusion::new(RegularPolygon::default(), 1.)), + meshes.add(Extrusion::new(Triangle2d::default(), 1.)), + ]; + + let num_shapes = shapes.len(); + + // Spawn the shapes. The meshes will be pickable by default. + for (i, shape) in shapes.into_iter().enumerate() { + commands + .spawn(( + Mesh3d(shape), + MeshMaterial3d(scene_materials.white.clone()), + Transform::from_xyz( + -SHAPES_X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * SHAPES_X_EXTENT, + 2.0, + Z_EXTENT / 2., + ) + .with_rotation(Quat::from_rotation_x(-PI / 4.)), + Shape, + )) + .observe(on_pointer_over) + .observe(on_pointer_out) + .observe(on_pointer_down) + .observe(on_pointer_up); + } + + let num_extrusions = extrusions.len(); + + for (i, shape) in extrusions.into_iter().enumerate() { + commands + .spawn(( + Mesh3d(shape), + MeshMaterial3d(scene_materials.white.clone()), + Transform::from_xyz( + -EXTRUSION_X_EXTENT / 2. + + i as f32 / (num_extrusions - 1) as f32 * EXTRUSION_X_EXTENT, + 2.0, + -Z_EXTENT / 2., + ) + .with_rotation(Quat::from_rotation_x(-PI / 4.)), + Shape, + )) + .observe(on_pointer_over) + .observe(on_pointer_out) + .observe(on_pointer_down) + .observe(on_pointer_up); + } + + // Disable picking for the ground plane. + commands.spawn(( + Mesh3d(meshes.add(Plane3d::default().mesh().size(50.0, 50.0).subdivisions(10))), + MeshMaterial3d(scene_materials.ground.clone()), + PickingBehavior::IGNORE, + )); // Light commands.spawn(( PointLight { shadows_enabled: true, + intensity: 10_000_000., + range: 100.0, + shadow_depth_bias: 0.2, ..default() }, - Transform::from_xyz(4.0, 8.0, 4.0), + Transform::from_xyz(8.0, 16.0, 8.0), )); // Camera commands.spawn(( Camera3d::default(), - Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), + Transform::from_xyz(0.0, 7., 14.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y), )); + + // Instructions + commands.spawn( + TextBundle::from_section("Hover over the shapes to pick them", TextStyle::default()) + .with_style(Style { + position_type: PositionType::Absolute, + top: Val::Px(12.0), + left: Val::Px(12.0), + ..default() + }), + ); } +/// Changes the material when the pointer is over the mesh. fn on_pointer_over( trigger: Trigger>, scene_materials: Res, @@ -82,6 +169,7 @@ fn on_pointer_over( } } +/// Resets the material when the pointer leaves the mesh. fn on_pointer_out( trigger: Trigger>, scene_materials: Res, @@ -92,23 +180,58 @@ fn on_pointer_out( } } +/// Changes the material when the pointer is pressed. +fn on_pointer_down( + trigger: Trigger>, + scene_materials: Res, + mut query: Query<&mut MeshMaterial3d>, +) { + if let Ok(mut material) = query.get_mut(trigger.entity()) { + material.0 = scene_materials.pressed.clone(); + } +} + +/// Resets the material when the pointer is released. +fn on_pointer_up( + trigger: Trigger>, + scene_materials: Res, + mut query: Query<&mut MeshMaterial3d>, +) { + if let Ok(mut material) = query.get_mut(trigger.entity()) { + material.0 = scene_materials.hover.clone(); + } +} + +/// Draws the closest point of intersection for pointer hits. fn on_mesh_hover( mut pointer_hits: EventReader, meshes: Query>, mut gizmos: Gizmos, ) { for hit in pointer_hits.read() { - let mesh_hits = hit + // Get the first mesh hit. + // The hits are sorted by distance from the camera, so this is the closest hit. + let Some(closest_hit) = hit .picks .iter() - .filter_map(|(entity, hit)| meshes.get(*entity).map(|_| hit).ok()); - - for hit in mesh_hits { - let (Some(point), Some(normal)) = (hit.position, hit.normal) else { - return; - }; - gizmos.sphere(Isometry3d::from_translation(point), 0.05, RED); - gizmos.arrow(point, point + normal * 0.5, PINK); - } + .filter_map(|(entity, hit)| meshes.get(*entity).map(|_| hit).ok()) + .next() + else { + continue; + }; + + let (Some(point), Some(normal)) = (closest_hit.position, closest_hit.normal) else { + return; + }; + + gizmos.sphere(point, 0.05, RED); + gizmos.arrow(point, point + normal * 0.5, PINK); + } +} + +/// Rotates the shapes. +fn rotate(mut query: Query<&mut Transform, With>, time: Res