Files
snow_trail/src/picking.rs
2026-03-05 15:05:19 +01:00

251 lines
6.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use glam::{Mat4, Vec3, Vec4};
use crate::components::TreeInstancesComponent;
use crate::entity::EntityHandle;
use crate::loaders::mesh::Mesh;
use crate::world::World;
pub struct Ray
{
pub origin: Vec3,
pub direction: Vec3,
}
impl Ray
{
pub fn from_screen_position(
screen_x: f32,
screen_y: f32,
screen_width: u32,
screen_height: u32,
view: &Mat4,
projection: &Mat4,
) -> Self
{
let ndc_x = (2.0 * screen_x) / screen_width as f32 - 1.0;
let ndc_y = 1.0 - (2.0 * screen_y) / screen_height as f32;
let clip_coords = Vec4::new(ndc_x, ndc_y, -1.0, 1.0);
let inv_projection = projection.inverse();
let inv_view = view.inverse();
let eye_coords = inv_projection * clip_coords;
let eye_coords = Vec4::new(eye_coords.x, eye_coords.y, -1.0, 0.0);
let world_coords = inv_view * eye_coords;
let direction = Vec3::new(world_coords.x, world_coords.y, world_coords.z).normalize();
let origin = inv_view.col(3).truncate();
Ray { origin, direction }
}
pub fn intersects_aabb(&self, min: Vec3, max: Vec3) -> Option<f32>
{
let inv_dir = Vec3::new(
1.0 / self.direction.x,
1.0 / self.direction.y,
1.0 / self.direction.z,
);
let t1 = (min - self.origin) * inv_dir;
let t2 = (max - self.origin) * inv_dir;
let tmin = t1.min(t2);
let tmax = t1.max(t2);
let tenter = tmin.x.max(tmin.y).max(tmin.z);
let texit = tmax.x.min(tmax.y).min(tmax.z);
if tenter <= texit && texit >= 0.0
{
Some(tenter.max(0.0))
}
else
{
None
}
}
}
fn transform_aabb(aabb_min: Vec3, aabb_max: Vec3, model: &Mat4) -> (Vec3, Vec3)
{
let corners = [
Vec3::new(aabb_min.x, aabb_min.y, aabb_min.z),
Vec3::new(aabb_max.x, aabb_min.y, aabb_min.z),
Vec3::new(aabb_min.x, aabb_max.y, aabb_min.z),
Vec3::new(aabb_max.x, aabb_max.y, aabb_min.z),
Vec3::new(aabb_min.x, aabb_min.y, aabb_max.z),
Vec3::new(aabb_max.x, aabb_min.y, aabb_max.z),
Vec3::new(aabb_min.x, aabb_max.y, aabb_max.z),
Vec3::new(aabb_max.x, aabb_max.y, aabb_max.z),
];
let mut world_min = Vec3::splat(f32::MAX);
let mut world_max = Vec3::splat(f32::MIN);
for corner in &corners
{
let world_corner = model.transform_point3(*corner);
world_min = world_min.min(world_corner);
world_max = world_max.max(world_corner);
}
(world_min, world_max)
}
fn intersect_triangle(origin: Vec3, dir: Vec3, v0: Vec3, v1: Vec3, v2: Vec3) -> Option<f32>
{
let edge1 = v1 - v0;
let edge2 = v2 - v0;
let h = dir.cross(edge2);
let a = edge1.dot(h);
if a.abs() < 1e-7
{
return None;
}
let f = 1.0 / a;
let s = origin - v0;
let u = f * s.dot(h);
if !(0.0..=1.0).contains(&u)
{
return None;
}
let q = s.cross(edge1);
let v = f * dir.dot(q);
if v < 0.0 || u + v > 1.0
{
return None;
}
let t = f * edge2.dot(q);
if t > 1e-7
{
Some(t)
}
else
{
None
}
}
/// Raymesh triangle intersection in local space, returning world-space distance.
///
/// Transforms the ray into local mesh space, tests every triangle with
/// MöllerTrumbore, then maps the closest local hit back to a world-space
/// distance so results can be compared across entities.
fn intersect_mesh_triangles(ray: &Ray, mesh: &Mesh, model: &Mat4) -> Option<f32>
{
if mesh.cpu_indices.is_empty()
{
return None;
}
let inv_model = model.inverse();
let local_origin = inv_model.transform_point3(ray.origin);
let local_dir = inv_model.transform_vector3(ray.direction);
let mut closest_t = f32::MAX;
let mut hit = false;
for tri in mesh.cpu_indices.chunks_exact(3)
{
let v0 = Vec3::from(mesh.cpu_positions[tri[0] as usize]);
let v1 = Vec3::from(mesh.cpu_positions[tri[1] as usize]);
let v2 = Vec3::from(mesh.cpu_positions[tri[2] as usize]);
if let Some(t) = intersect_triangle(local_origin, local_dir, v0, v1, v2)
{
if t < closest_t
{
closest_t = t;
hit = true;
}
}
}
if hit
{
let local_hit = local_origin + local_dir * closest_t;
let world_hit = model.transform_point3(local_hit);
Some((world_hit - ray.origin).length())
}
else
{
None
}
}
fn intersect_instances(
ray: &Ray,
mesh: &Mesh,
tree_instances: &TreeInstancesComponent,
) -> Option<f32>
{
let mut closest: Option<f32> = None;
for instance in &tree_instances.instances
{
let instance_model = Mat4::from_scale_rotation_translation(
instance.scale,
instance.rotation,
instance.position,
);
let (world_min, world_max) = transform_aabb(mesh.aabb_min, mesh.aabb_max, &instance_model);
if let Some(d) = ray.intersects_aabb(world_min, world_max)
{
if closest.map_or(true, |c| d < c)
{
closest = Some(d);
}
}
}
closest
}
pub fn pick_entity(ray: &Ray, world: &World) -> Option<EntityHandle>
{
let mut closest_entity = None;
let mut closest_distance = f32::MAX;
for (&entity, mesh_component) in &world.meshes.components
{
let transform = match world.transforms.get(entity)
{
Some(t) => t,
None => continue,
};
let model = transform.to_matrix();
let distance = if let Some(tree_instances) = world.tree_instances.get(entity)
{
intersect_instances(ray, &mesh_component.mesh, tree_instances)
}
else
{
let (world_min, world_max) = transform_aabb(
mesh_component.mesh.aabb_min,
mesh_component.mesh.aabb_max,
&model,
);
if ray.intersects_aabb(world_min, world_max).is_none()
{
continue;
}
intersect_mesh_triangles(ray, &mesh_component.mesh, &model)
};
if let Some(d) = distance
{
if d < closest_distance
{
closest_distance = d;
closest_entity = Some(entity);
}
}
}
closest_entity
}