use bevy::math::{Vec2, Vec3};
use bevy::mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues};
use crate::digest::Fnv1a;
const PARALLEL_EPSILON: f32 = 1.0e-8;
pub(crate) enum Pick {
At(Vec2),
Miss,
Unusable,
}
pub(crate) fn mesh_key(mesh: &Mesh) -> u64 {
let mut f = Fnv1a::new();
let verts = mesh.count_vertices() as u64;
for k in 0..8 {
f.byte((verts >> (k * 8)) as u8);
}
let indices = match mesh.indices() {
Some(Indices::U16(v)) => v.len() as u64,
Some(Indices::U32(v)) => v.len() as u64,
None => 0,
};
for k in 0..8 {
f.byte((indices >> (k * 8)) as u8);
}
if let Some(VertexAttributeValues::Float32x3(p)) = mesh.attribute(Mesh::ATTRIBUTE_POSITION)
&& let (Some(first), Some(last)) = (p.first(), p.last())
{
for v in [first, last] {
for c in v {
for b in c.to_le_bytes() {
f.byte(b);
}
}
}
}
f.finish()
}
pub(crate) fn ray_uv(mesh: &Mesh, origin: Vec3, dir: Vec3) -> Pick {
if mesh.primitive_topology() != PrimitiveTopology::TriangleList {
return Pick::Unusable;
}
let Some(VertexAttributeValues::Float32x3(pos)) = mesh.attribute(Mesh::ATTRIBUTE_POSITION) else {
return Pick::Unusable;
};
let Some(VertexAttributeValues::Float32x2(uv)) = mesh.attribute(Mesh::ATTRIBUTE_UV_0) else {
return Pick::Unusable;
};
if uv.len() != pos.len() || pos.len() < 3 {
return Pick::Unusable;
}
if !origin.is_finite() || !dir.is_finite() || dir.length_squared() <= 0.0 {
return Pick::Miss;
}
let mut best_t = f32::INFINITY;
let mut best: Option<Vec2> = None;
let mut consider = |a: usize, b: usize, c: usize| {
if a >= pos.len() || b >= pos.len() || c >= pos.len() {
return;
}
let (p0, p1, p2) =
(Vec3::from_array(pos[a]), Vec3::from_array(pos[b]), Vec3::from_array(pos[c]));
let e1 = p1 - p0;
let e2 = p2 - p0;
let h = dir.cross(e2);
let det = e1.dot(h);
if det.abs() < PARALLEL_EPSILON {
return;
}
let inv = 1.0 / det;
let s = origin - p0;
let u = s.dot(h) * inv;
if u < 0.0 || u > 1.0 {
return;
}
let q = s.cross(e1);
let v = dir.dot(q) * inv;
if v < 0.0 || u + v > 1.0 {
return;
}
let t = e2.dot(q) * inv;
if t <= PARALLEL_EPSILON || t >= best_t {
return;
}
best_t = t;
let (t0, t1, t2) =
(Vec2::from_array(uv[a]), Vec2::from_array(uv[b]), Vec2::from_array(uv[c]));
best = Some(t0 * (1.0 - u - v) + t1 * u + t2 * v);
};
match mesh.indices() {
Some(Indices::U16(idx)) => {
for tri in idx.chunks_exact(3) {
consider(tri[0] as usize, tri[1] as usize, tri[2] as usize);
}
}
Some(Indices::U32(idx)) => {
for tri in idx.chunks_exact(3) {
consider(tri[0] as usize, tri[1] as usize, tri[2] as usize);
}
}
None => {
for t in 0..pos.len() / 3 {
consider(t * 3, t * 3 + 1, t * 3 + 2);
}
}
}
match best {
Some(found) if found.is_finite() => Pick::At(found),
_ => Pick::Miss,
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::asset::RenderAssetUsages;
fn quad_tri(with_uv: bool) -> Mesh {
let mut m = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::MAIN_WORLD);
m.insert_attribute(
Mesh::ATTRIBUTE_POSITION,
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
);
if with_uv {
m.insert_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]);
}
m.insert_indices(Indices::U32(vec![0, 1, 2]));
m
}
#[test]
fn a_hit_reads_the_barycentric_uv() {
let m = quad_tri(true);
let pick = ray_uv(&m, Vec3::new(0.25, 0.25, 1.0), Vec3::new(0.0, 0.0, -1.0));
match pick {
Pick::At(uv) => {
assert!((uv.x - 0.25).abs() < 1.0e-5, "u was {}", uv.x);
assert!((uv.y - 0.25).abs() < 1.0e-5, "v was {}", uv.y);
}
_ => panic!("expected a hit"),
}
}
#[test]
fn a_ray_past_the_triangle_misses_rather_than_refusing() {
let m = quad_tri(true);
assert!(matches!(
ray_uv(&m, Vec3::new(5.0, 5.0, 1.0), Vec3::new(0.0, 0.0, -1.0)),
Pick::Miss
));
assert!(matches!(
ray_uv(&m, Vec3::new(0.25, 0.25, 1.0), Vec3::new(0.0, 0.0, 1.0)),
Pick::Miss
));
}
#[test]
fn a_mesh_with_no_uvs_is_unusable_not_a_miss() {
let m = quad_tri(false);
assert!(matches!(
ray_uv(&m, Vec3::new(0.25, 0.25, 1.0), Vec3::new(0.0, 0.0, -1.0)),
Pick::Unusable
));
}
#[test]
fn the_nearest_forward_hit_wins() {
let mut m = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::MAIN_WORLD);
m.insert_attribute(
Mesh::ATTRIBUTE_POSITION,
vec![
[0.0, 0.0, -1.0],
[1.0, 0.0, -1.0],
[0.0, 1.0, -1.0],
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
],
);
m.insert_attribute(
Mesh::ATTRIBUTE_UV_0,
vec![[0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0]],
);
m.insert_indices(Indices::U32(vec![0, 1, 2, 3, 4, 5]));
match ray_uv(&m, Vec3::new(0.2, 0.2, 2.0), Vec3::new(0.0, 0.0, -1.0)) {
Pick::At(uv) => assert_eq!(uv, Vec2::new(1.0, 1.0)),
_ => panic!("expected a hit"),
}
}
#[test]
fn the_mesh_key_separates_two_different_meshes() {
assert_ne!(mesh_key(&quad_tri(true)), {
let mut m = quad_tri(true);
m.insert_attribute(
Mesh::ATTRIBUTE_POSITION,
vec![[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [0.0, 2.0, 0.0]],
);
mesh_key(&m)
});
}
}