Skip to content

Commit

Permalink
Mesh picking fixes (bevyengine#16110)
Browse files Browse the repository at this point in the history
# Objective

- Mesh picking is noisy when a non triangle list is used
- Mesh picking runs even when users don't need it
- Resolve bevyengine#16065 

## Solution

- Don't add the mesh picking plugin by default
- Remove error spam
  • Loading branch information
aevyrie authored Oct 27, 2024
1 parent a644ac7 commit 54b323e
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 157 deletions.
2 changes: 1 addition & 1 deletion benches/benches/bevy_picking/ray_mesh_intersection.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use bevy_math::{Dir3, Mat4, Ray3d, Vec3};
use bevy_picking::{mesh_picking::ray_cast, prelude::*};
use bevy_picking::mesh_picking::ray_cast;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn ptoxznorm(p: u32, size: u32) -> (f32, f32) {
Expand Down
18 changes: 2 additions & 16 deletions crates/bevy_picking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,24 +277,10 @@ pub struct DefaultPickingPlugins;

impl PluginGroup for DefaultPickingPlugins {
fn build(self) -> PluginGroupBuilder {
#[cfg_attr(
not(feature = "bevy_mesh"),
expect(
unused_mut,
reason = "Group is not mutated when `bevy_mesh` is not enabled."
)
)]
let mut group = PluginGroupBuilder::start::<Self>()
PluginGroupBuilder::start::<Self>()
.add(input::PointerInputPlugin::default())
.add(PickingPlugin::default())
.add(InteractionPlugin);

#[cfg(feature = "bevy_mesh")]
{
group = group.add(mesh_picking::MeshPickingPlugin);
};

group
.add(InteractionPlugin)
}
}

Expand Down
174 changes: 63 additions & 111 deletions crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use bevy_math::{bounding::Aabb3d, Dir3, Mat4, Ray3d, Vec3, Vec3A};
use bevy_reflect::Reflect;
use bevy_render::mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues};
use bevy_utils::tracing::{error, warn};
use bevy_render::mesh::{Indices, Mesh, PrimitiveTopology};

use super::Backfaces;

Expand All @@ -17,7 +16,7 @@ pub struct RayMeshHit {
/// 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]>,
pub triangle: Option<[Vec3; 3]>,
/// The index of the triangle that was hit.
pub triangle_index: Option<usize>,
}
Expand All @@ -32,84 +31,41 @@ pub struct RayTriangleHit {
/// Casts a ray on a mesh, and returns the intersection.
pub(super) fn ray_intersection_over_mesh(
mesh: &Mesh,
mesh_transform: &Mat4,
transform: &Mat4,
ray: Ray3d,
backface_culling: Backfaces,
culling: Backfaces,
) -> Option<RayMeshHit> {
if mesh.primitive_topology() != PrimitiveTopology::TriangleList {
error!(
"Invalid intersection check: `TriangleList` is the only supported `PrimitiveTopology`"
);
return None;
return None; // ray_mesh_intersection assumes vertices are laid out in a triangle list
}
// Vertex positions are required
let positions = mesh.attribute(Mesh::ATTRIBUTE_POSITION)?.as_float3()?;

// Get the vertex positions and normals from the mesh.
let vertex_positions: &Vec<[f32; 3]> = match mesh.attribute(Mesh::ATTRIBUTE_POSITION) {
None => {
error!("Mesh does not contain vertex positions");
return None;
// Normals are optional
let normals = mesh
.attribute(Mesh::ATTRIBUTE_NORMAL)
.and_then(|normal_values| normal_values.as_float3());

match mesh.indices() {
Some(Indices::U16(indices)) => {
ray_mesh_intersection(ray, transform, positions, normals, Some(indices), culling)
}
Some(vertex_values) => match &vertex_values {
VertexAttributeValues::Float32x3(positions) => positions,
_ => {
error!("Unexpected types in {:?}", Mesh::ATTRIBUTE_POSITION);
return None;
}
},
};
let vertex_normals: Option<&[[f32; 3]]> =
if let Some(normal_values) = mesh.attribute(Mesh::ATTRIBUTE_NORMAL) {
match &normal_values {
VertexAttributeValues::Float32x3(normals) => Some(normals),
_ => None,
}
} else {
None
};

if let Some(indices) = &mesh.indices() {
match indices {
Indices::U16(vertex_indices) => ray_mesh_intersection(
ray,
mesh_transform,
vertex_positions,
vertex_normals,
Some(vertex_indices),
backface_culling,
),
Indices::U32(vertex_indices) => ray_mesh_intersection(
ray,
mesh_transform,
vertex_positions,
vertex_normals,
Some(vertex_indices),
backface_culling,
),
Some(Indices::U32(indices)) => {
ray_mesh_intersection(ray, transform, positions, normals, Some(indices), culling)
}
} else {
ray_mesh_intersection(
ray,
mesh_transform,
vertex_positions,
vertex_normals,
None::<&[usize]>,
backface_culling,
)
None => ray_mesh_intersection::<usize>(ray, transform, positions, normals, None, culling),
}
}

/// Checks if a ray intersects a mesh, and returns the nearest intersection if one exists.
pub fn ray_mesh_intersection<Index: Clone + Copy>(
pub fn ray_mesh_intersection<I: TryInto<usize> + Clone + Copy>(
ray: Ray3d,
mesh_transform: &Mat4,
vertex_positions: &[[f32; 3]],
positions: &[[f32; 3]],
vertex_normals: Option<&[[f32; 3]]>,
indices: Option<&[Index]>,
indices: Option<&[I]>,
backface_culling: Backfaces,
) -> Option<RayMeshHit>
where
usize: TryFrom<Index>,
{
) -> Option<RayMeshHit> {
// The ray cast can hit the same mesh many times, so we need to track which hit is
// closest to the camera, and record that.
let mut closest_hit_distance = f32::MAX;
Expand All @@ -123,38 +79,36 @@ where
);

if let Some(indices) = indices {
// Make sure this chunk has 3 vertices to avoid a panic.
// The index list must be a multiple of three. If not, the mesh is malformed and the raycast
// result might be nonsensical.
if indices.len() % 3 != 0 {
warn!("Index list not a multiple of 3");
return None;
}

// Now that we're in the vector of vertex indices, we want to look at the vertex
// positions for each triangle, so we'll take indices in chunks of three, where each
// chunk of three indices are references to the three vertices of a triangle.
for index_chunk in indices.chunks_exact(3) {
let [index1, index2, index3] = [
usize::try_from(index_chunk[0]).ok()?,
usize::try_from(index_chunk[1]).ok()?,
usize::try_from(index_chunk[2]).ok()?,
for triangle in indices.chunks_exact(3) {
let [a, b, c] = [
triangle[0].try_into().ok()?,
triangle[1].try_into().ok()?,
triangle[2].try_into().ok()?,
];
let triangle_index = Some(index1);
let tri_vertex_positions = [
Vec3A::from(vertex_positions[index1]),
Vec3A::from(vertex_positions[index2]),
Vec3A::from(vertex_positions[index3]),

let triangle_index = Some(a);
let tri_vertex_positions = &[
Vec3::from(positions[a]),
Vec3::from(positions[b]),
Vec3::from(positions[c]),
];
let tri_normals = vertex_normals.map(|normals| {
[
Vec3A::from(normals[index1]),
Vec3A::from(normals[index2]),
Vec3A::from(normals[index3]),
Vec3::from(normals[a]),
Vec3::from(normals[b]),
Vec3::from(normals[c]),
]
});

let Some(hit) = triangle_intersection(
tri_vertex_positions,
tri_normals,
tri_normals.as_ref(),
closest_hit_distance,
&mesh_space_ray,
backface_culling,
Expand All @@ -171,33 +125,33 @@ where
.length(),
triangle: hit.triangle.map(|tri| {
[
mesh_transform.transform_point3a(tri[0]),
mesh_transform.transform_point3a(tri[1]),
mesh_transform.transform_point3a(tri[2]),
mesh_transform.transform_point3(tri[0]),
mesh_transform.transform_point3(tri[1]),
mesh_transform.transform_point3(tri[2]),
]
}),
triangle_index,
});
closest_hit_distance = hit.distance;
}
} else {
for (i, chunk) in vertex_positions.chunks_exact(3).enumerate() {
let &[a, b, c] = chunk else {
for (i, triangle) in positions.chunks_exact(3).enumerate() {
let &[a, b, c] = triangle else {
continue;
};
let triangle_index = Some(i);
let tri_vertex_positions = [Vec3A::from(a), Vec3A::from(b), Vec3A::from(c)];
let tri_vertex_positions = &[Vec3::from(a), Vec3::from(b), Vec3::from(c)];
let tri_normals = vertex_normals.map(|normals| {
[
Vec3A::from(normals[i]),
Vec3A::from(normals[i + 1]),
Vec3A::from(normals[i + 2]),
Vec3::from(normals[i]),
Vec3::from(normals[i + 1]),
Vec3::from(normals[i + 2]),
]
});

let Some(hit) = triangle_intersection(
tri_vertex_positions,
tri_normals,
tri_normals.as_ref(),
closest_hit_distance,
&mesh_space_ray,
backface_culling,
Expand All @@ -214,9 +168,9 @@ where
.length(),
triangle: hit.triangle.map(|tri| {
[
mesh_transform.transform_point3a(tri[0]),
mesh_transform.transform_point3a(tri[1]),
mesh_transform.transform_point3a(tri[2]),
mesh_transform.transform_point3(tri[0]),
mesh_transform.transform_point3(tri[1]),
mesh_transform.transform_point3(tri[2]),
]
}),
triangle_index,
Expand All @@ -228,15 +182,14 @@ where
closest_hit
}

#[inline(always)]
fn triangle_intersection(
tri_vertices: [Vec3A; 3],
tri_normals: Option<[Vec3A; 3]>,
tri_vertices: &[Vec3; 3],
tri_normals: Option<&[Vec3; 3]>,
max_distance: f32,
ray: &Ray3d,
backface_culling: Backfaces,
) -> Option<RayMeshHit> {
let hit = ray_triangle_intersection(ray, &tri_vertices, backface_culling)?;
let hit = ray_triangle_intersection(ray, tri_vertices, backface_culling)?;

if hit.distance < 0.0 || hit.distance > max_distance {
return None;
Expand All @@ -258,25 +211,24 @@ fn triangle_intersection(

Some(RayMeshHit {
point,
normal: normal.into(),
normal,
barycentric_coords: barycentric,
distance: hit.distance,
triangle: Some(tri_vertices),
triangle: Some(*tri_vertices),
triangle_index: None,
})
}

/// Takes a ray and triangle and computes the intersection.
#[inline(always)]
fn ray_triangle_intersection(
ray: &Ray3d,
triangle: &[Vec3A; 3],
triangle: &[Vec3; 3],
backface_culling: Backfaces,
) -> Option<RayTriangleHit> {
// 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 vector_v0_to_v1: Vec3 = triangle[1] - triangle[0];
let vector_v0_to_v2: Vec3 = triangle[2] - triangle[0];
let p_vec: Vec3 = ray.direction.cross(vector_v0_to_v2);
let determinant: f32 = vector_v0_to_v1.dot(p_vec);

match backface_culling {
Expand All @@ -298,14 +250,14 @@ fn ray_triangle_intersection(

let determinant_inverse = 1.0 / determinant;

let t_vec = Vec3A::from(ray.origin) - triangle[0];
let t_vec = 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;
let v = (*ray.direction).dot(q_vec) * determinant_inverse;
if v < 0.0 || u + v > 1.0 {
return None;
}
Expand Down
28 changes: 23 additions & 5 deletions examples/picking/mesh_picking.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
//! 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`].
//! [`bevy::picking::backend`] provides an API for adding picking hit tests to any entity. To get
//! started with picking 3d meshes, the [`MeshPickingPlugin`] is provided as a simple starting
//! point, especially useful for debugging. For your game, you may want to use a 3d picking backend
//! provided by your physics engine, or a picking shader, depending on your specific use case.
//!
//! If you want mesh picking to be entirely opt-in, you can set [`MeshPickingSettings::require_markers`]
//! to `true` and add a [`RayCastPickable`] component to the desired camera and target entities.
//! [`bevy::picking`] allows you to compose backends together to make any entity on screen pickable
//! with pointers, regardless of how that entity is rendered. For example, `bevy_ui` and
//! `bevy_sprite` provide their own picking backends that can be enabled at the same time as this
//! mesh picking backend. This makes it painless to deal with cases like the UI or sprites blocking
//! meshes underneath them, or vice versa.
//!
//! If you want to build more complex interactions than afforded by the provided pointer events, you
//! may want to use [`MeshRayCast`] or a full physics engine with raycasting capabilities.
//!
//! By default, the mesh picking plugin will raycast against all entities, which is especially
//! useful for debugging. If you want mesh picking to be opt-in, you can set
//! [`MeshPickingSettings::require_markers`] to `true` and add a [`RayCastPickable`] component to
//! the desired camera and target entities.

use std::f32::consts::PI;

Expand All @@ -19,7 +32,12 @@ use bevy::{

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins((
DefaultPlugins,
// The mesh picking plugin is not enabled by default, because raycasting against all
// meshes has a performance cost.
MeshPickingPlugin,
))
.init_resource::<SceneMaterials>()
.add_systems(Startup, setup)
.add_systems(Update, (on_mesh_hover, rotate))
Expand Down
Loading

0 comments on commit 54b323e

Please sign in to comment.