use glam::{Mat4, Vec3};
#[derive(Clone, Copy, Debug)]
pub struct Ray {
pub origin: Vec3,
pub direction: Vec3,
}
impl Ray {
pub fn new(origin: Vec3, direction: Vec3) -> Self {
Self {
origin,
direction: direction.normalize_or_zero(),
}
}
pub fn from_screen(
screen_x: f32,
screen_y: f32,
screen_width: f32,
screen_height: f32,
view_matrix: Mat4,
projection_matrix: Mat4,
) -> Self {
let ndc_x = (2.0 * screen_x / screen_width) - 1.0;
let ndc_y = 1.0 - (2.0 * screen_y / screen_height);
let near_clip = glam::Vec4::new(ndc_x, ndc_y, 0.0, 1.0);
let far_clip = glam::Vec4::new(ndc_x, ndc_y, 1.0, 1.0);
let inv_view_proj = (projection_matrix * view_matrix).inverse();
let near_world = inv_view_proj * near_clip;
let far_world = inv_view_proj * far_clip;
let near_point = near_world.truncate() / near_world.w;
let far_point = far_world.truncate() / far_world.w;
let direction = (far_point - near_point).normalize_or_zero();
Self {
origin: near_point,
direction,
}
}
#[inline]
pub fn point_at(&self, t: f32) -> Vec3 {
self.origin + self.direction * t
}
pub fn intersect_aabb(&self, min: Vec3, max: Vec3) -> Option<f32> {
let mut t_min = f32::NEG_INFINITY;
let mut t_max = f32::INFINITY;
for i in 0..3 {
let origin = self.origin[i];
let dir = self.direction[i];
let box_min = min[i];
let box_max = max[i];
if dir.abs() < f32::EPSILON {
if origin < box_min || origin > box_max {
return None;
}
} else {
let inv_dir = 1.0 / dir;
let mut t1 = (box_min - origin) * inv_dir;
let mut t2 = (box_max - origin) * inv_dir;
if t1 > t2 {
std::mem::swap(&mut t1, &mut t2);
}
t_min = t_min.max(t1);
t_max = t_max.min(t2);
if t_min > t_max {
return None;
}
}
}
if t_min > 0.0 {
Some(t_min)
} else if t_max > 0.0 {
Some(t_max)
} else {
None
}
}
pub fn intersect_sphere(&self, center: Vec3, radius: f32) -> Option<f32> {
let oc = self.origin - center;
let a = self.direction.dot(self.direction);
let b = 2.0 * oc.dot(self.direction);
let c = oc.dot(oc) - radius * radius;
let discriminant = b * b - 4.0 * a * c;
if discriminant < 0.0 {
return None;
}
let sqrt_disc = discriminant.sqrt();
let t1 = (-b - sqrt_disc) / (2.0 * a);
let t2 = (-b + sqrt_disc) / (2.0 * a);
if t1 > 0.0 {
Some(t1)
} else if t2 > 0.0 {
Some(t2)
} else {
None
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum Collider {
Box {
half_extents: Vec3,
},
Sphere {
radius: f32,
},
}
impl Collider {
pub fn box_collider(size: Vec3) -> Self {
Self::Box {
half_extents: size * 0.5,
}
}
pub fn box_half_extents(half_extents: Vec3) -> Self {
Self::Box { half_extents }
}
pub fn sphere(radius: f32) -> Self {
Self::Sphere { radius }
}
pub fn unit_box() -> Self {
Self::box_collider(Vec3::ONE)
}
pub fn unit_sphere() -> Self {
Self::Sphere { radius: 0.5 }
}
pub fn intersect(&self, ray: &Ray, position: Vec3, scale: Vec3) -> Option<f32> {
match self {
Collider::Box { half_extents } => {
let scaled_half = *half_extents * scale;
let min = position - scaled_half;
let max = position + scaled_half;
ray.intersect_aabb(min, max)
}
Collider::Sphere { radius } => {
let avg_scale = (scale.x + scale.y + scale.z) / 3.0;
ray.intersect_sphere(position, radius * avg_scale)
}
}
}
}
impl Default for Collider {
fn default() -> Self {
Self::unit_box()
}
}
#[derive(Clone, Copy, Debug)]
pub struct RayHit {
pub entity: hecs::Entity,
pub distance: f32,
pub point: Vec3,
}
pub type PickResult = Option<RayHit>;
pub fn raycast_all(world: &hecs::World, ray: &Ray) -> Vec<RayHit> {
use crate::mesh::Transform;
let mut hits = Vec::new();
for (entity, (transform, collider)) in world.query::<(&Transform, &Collider)>().iter() {
if let Some(distance) = collider.intersect(ray, transform.position, transform.scale) {
hits.push(RayHit {
entity,
distance,
point: ray.point_at(distance),
});
}
}
hits.sort_by(|a, b| {
a.distance
.partial_cmp(&b.distance)
.unwrap_or(std::cmp::Ordering::Equal)
});
hits
}
pub fn raycast(world: &hecs::World, ray: &Ray) -> PickResult {
raycast_all(world, ray).into_iter().next()
}