Skip to content

Mesh picking fixes #16110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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