Upstream raycasting UVs (#19791)

# Objective

- Upstream mesh raycast UV support used in #19199

## Solution

- Compute UVs, debug a bunch of math issues with barycentric coordinates
and add docs.

## Testing

- Tested in diagetic UI in the linked PR.
This commit is contained in:
Aevyrie 2025-06-24 11:10:59 -07:00 committed by GitHub
parent bd4258bf5c
commit a3d406dd49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 63 additions and 20 deletions

View File

@ -155,6 +155,7 @@ fn bench(c: &mut Criterion) {
&mesh.positions,
Some(&mesh.normals),
Some(&mesh.indices),
None,
backface_culling,
);

View File

@ -1,5 +1,5 @@
use bevy_math::{bounding::Aabb3d, Dir3, Mat4, Ray3d, Vec3, Vec3A};
use bevy_mesh::{Indices, Mesh, PrimitiveTopology};
use bevy_math::{bounding::Aabb3d, Dir3, Mat4, Ray3d, Vec2, Vec3, Vec3A};
use bevy_mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues};
use bevy_reflect::Reflect;
use super::Backfaces;
@ -18,6 +18,8 @@ pub struct RayMeshHit {
pub distance: f32,
/// The vertices of the triangle that was hit.
pub triangle: Option<[Vec3; 3]>,
/// UV coordinate of the hit, if the mesh has UV attributes.
pub uv: Option<Vec2>,
/// The index of the triangle that was hit.
pub triangle_index: Option<usize>,
}
@ -26,6 +28,10 @@ pub struct RayMeshHit {
#[derive(Default, Debug)]
pub struct RayTriangleHit {
pub distance: f32,
/// Note this uses the convention from the Moller-Trumbore algorithm:
/// P = (1 - u - v)A + uB + vC
/// This is different from the more common convention of
/// P = uA + vB + (1 - u - v)C
pub barycentric_coords: (f32, f32),
}
@ -34,7 +40,7 @@ pub(super) fn ray_intersection_over_mesh(
mesh: &Mesh,
transform: &Mat4,
ray: Ray3d,
culling: Backfaces,
cull: Backfaces,
) -> Option<RayMeshHit> {
if mesh.primitive_topology() != PrimitiveTopology::TriangleList {
return None; // ray_mesh_intersection assumes vertices are laid out in a triangle list
@ -47,26 +53,37 @@ pub(super) fn ray_intersection_over_mesh(
.attribute(Mesh::ATTRIBUTE_NORMAL)
.and_then(|normal_values| normal_values.as_float3());
let uvs = mesh
.attribute(Mesh::ATTRIBUTE_UV_0)
.and_then(|uvs| match uvs {
VertexAttributeValues::Float32x2(uvs) => Some(uvs.as_slice()),
_ => None,
});
match mesh.indices() {
Some(Indices::U16(indices)) => {
ray_mesh_intersection(ray, transform, positions, normals, Some(indices), culling)
ray_mesh_intersection(ray, transform, positions, normals, Some(indices), uvs, cull)
}
Some(Indices::U32(indices)) => {
ray_mesh_intersection(ray, transform, positions, normals, Some(indices), culling)
ray_mesh_intersection(ray, transform, positions, normals, Some(indices), uvs, cull)
}
None => ray_mesh_intersection::<usize>(ray, transform, positions, normals, None, culling),
None => ray_mesh_intersection::<usize>(ray, transform, positions, normals, None, uvs, cull),
}
}
/// Checks if a ray intersects a mesh, and returns the nearest intersection if one exists.
pub fn ray_mesh_intersection<I: TryInto<usize> + Clone + Copy>(
pub fn ray_mesh_intersection<I>(
ray: Ray3d,
mesh_transform: &Mat4,
positions: &[[f32; 3]],
vertex_normals: Option<&[[f32; 3]]>,
indices: Option<&[I]>,
uvs: Option<&[[f32; 2]]>,
backface_culling: Backfaces,
) -> Option<RayMeshHit> {
) -> Option<RayMeshHit>
where
I: TryInto<usize> + Clone + Copy,
{
let world_to_mesh = mesh_transform.inverse();
let ray = Ray3d::new(
@ -139,17 +156,12 @@ pub fn ray_mesh_intersection<I: TryInto<usize> + Clone + Copy>(
closest_hit.and_then(|(tri_idx, hit)| {
let [a, b, c] = match indices {
Some(indices) => {
let triangle = indices.get((tri_idx * 3)..(tri_idx * 3 + 3))?;
let [Ok(a), Ok(b), Ok(c)] = [
triangle[0].try_into(),
triangle[1].try_into(),
triangle[2].try_into(),
] else {
return None;
};
[a, b, c]
let [i, j, k] = [tri_idx * 3, tri_idx * 3 + 1, tri_idx * 3 + 2];
[
indices.get(i).copied()?.try_into().ok()?,
indices.get(j).copied()?.try_into().ok()?,
indices.get(k).copied()?.try_into().ok()?,
]
}
None => [tri_idx * 3, tri_idx * 3 + 1, tri_idx * 3 + 2],
};
@ -168,10 +180,12 @@ pub fn ray_mesh_intersection<I: TryInto<usize> + Clone + Copy>(
});
let point = ray.get_point(hit.distance);
// Note that we need to convert from the Möller-Trumbore convention to the more common
// P = uA + vB + (1 - u - v)C convention.
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 barycentric = Vec3::new(w, u, v);
let normal = if let Some(normals) = tri_normals {
normals[1] * u + normals[2] * v + normals[0] * w
@ -181,9 +195,29 @@ pub fn ray_mesh_intersection<I: TryInto<usize> + Clone + Copy>(
.normalize()
};
let uv = uvs.and_then(|uvs| {
let tri_uvs = if let Some(indices) = indices {
let i = tri_idx * 3;
[
uvs[indices[i].try_into().ok()?],
uvs[indices[i + 1].try_into().ok()?],
uvs[indices[i + 2].try_into().ok()?],
]
} else {
let i = tri_idx * 3;
[uvs[i], uvs[i + 1], uvs[i + 2]]
};
Some(
barycentric.x * Vec2::from(tri_uvs[0])
+ barycentric.y * Vec2::from(tri_uvs[1])
+ barycentric.z * Vec2::from(tri_uvs[2]),
)
});
Some(RayMeshHit {
point: mesh_transform.transform_point3(point),
normal: mesh_transform.transform_vector3(normal),
uv,
barycentric_coords: barycentric,
distance: mesh_transform
.transform_vector3(ray.direction * hit.distance)
@ -329,6 +363,7 @@ mod tests {
positions,
vertex_normals,
indices,
None,
backface_culling,
);
@ -350,6 +385,7 @@ mod tests {
positions,
vertex_normals,
indices,
None,
backface_culling,
);
@ -372,6 +408,7 @@ mod tests {
positions,
vertex_normals,
indices,
None,
backface_culling,
);
@ -394,6 +431,7 @@ mod tests {
positions,
vertex_normals,
indices,
None,
backface_culling,
);
@ -415,6 +453,7 @@ mod tests {
positions,
vertex_normals,
indices,
None,
backface_culling,
);
@ -436,6 +475,7 @@ mod tests {
positions,
vertex_normals,
indices,
None,
backface_culling,
);
@ -457,6 +497,7 @@ mod tests {
positions,
vertex_normals,
indices,
None,
backface_culling,
);
@ -478,6 +519,7 @@ mod tests {
positions,
vertex_normals,
indices,
None,
backface_culling,
);