use glam::{Mat4, Vec2, Vec3};
pub fn screen_to_ray(mouse_pos: Vec2, window_size: Vec2, view_proj: &Mat4) -> (Vec3, Vec3) {
let ndc_x = 2.0 * mouse_pos.x / window_size.x - 1.0;
let ndc_y = 1.0 - 2.0 * mouse_pos.y / window_size.y;
let inv_vp = view_proj.inverse();
let near_clip = inv_vp * glam::Vec4::new(ndc_x, ndc_y, 0.0, 1.0);
let far_clip = inv_vp * glam::Vec4::new(ndc_x, ndc_y, 1.0, 1.0);
let near_world = Vec3::new(
near_clip.x / near_clip.w,
near_clip.y / near_clip.w,
near_clip.z / near_clip.w,
);
let far_world = Vec3::new(
far_clip.x / far_clip.w,
far_clip.y / far_clip.w,
far_clip.z / far_clip.w,
);
let direction = (far_world - near_world).normalize();
(near_world, direction)
}
pub fn ray_plane_intersection(origin: Vec3, direction: Vec3, plane_y: f32) -> Option<Vec3> {
if direction.y.abs() < 1e-7 {
return None;
}
let t = (plane_y - origin.y) / direction.y;
if t < 0.0 {
return None;
}
Some(origin + direction * t)
}
pub fn ray_sphere_intersection(
origin: Vec3,
direction: Vec3,
center: Vec3,
radius: f32,
) -> Option<f32> {
let oc = origin - center;
let a = direction.dot(direction);
let b = 2.0 * oc.dot(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 inv_2a = 1.0 / (2.0 * a);
let t1 = (-b - sqrt_disc) * inv_2a;
if t1 >= 0.0 {
return Some(t1);
}
let t2 = (-b + sqrt_disc) * inv_2a;
if t2 >= 0.0 {
return Some(t2);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ray_plane_straight_down() {
let hit = ray_plane_intersection(
Vec3::new(0.0, 10.0, 0.0),
Vec3::new(0.0, -1.0, 0.0),
0.0,
);
assert!(hit.is_some());
let p = hit.unwrap();
assert!((p.x).abs() < 1e-5);
assert!((p.y).abs() < 1e-5);
assert!((p.z).abs() < 1e-5);
}
#[test]
fn test_ray_plane_diagonal() {
let hit = ray_plane_intersection(
Vec3::new(0.0, 10.0, 0.0),
Vec3::new(1.0, -1.0, 0.0).normalize(),
0.0,
);
assert!(hit.is_some());
let p = hit.unwrap();
assert!((p.x - 10.0).abs() < 1e-4);
assert!((p.y).abs() < 1e-4);
}
#[test]
fn test_ray_plane_parallel() {
let hit = ray_plane_intersection(
Vec3::new(0.0, 5.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
0.0,
);
assert!(hit.is_none());
}
#[test]
fn test_ray_plane_behind() {
let hit = ray_plane_intersection(
Vec3::new(0.0, 5.0, 0.0),
Vec3::new(0.0, -1.0, 0.0),
10.0,
);
assert!(hit.is_none());
}
#[test]
fn test_ray_sphere_hit() {
let t = ray_sphere_intersection(
Vec3::new(0.0, 0.0, -5.0),
Vec3::new(0.0, 0.0, 1.0),
Vec3::ZERO,
1.0,
);
assert!(t.is_some());
assert!((t.unwrap() - 4.0).abs() < 1e-5);
}
#[test]
fn test_ray_sphere_miss() {
let t = ray_sphere_intersection(
Vec3::new(0.0, 5.0, -5.0),
Vec3::new(0.0, 0.0, 1.0),
Vec3::ZERO,
1.0,
);
assert!(t.is_none());
}
#[test]
fn test_ray_sphere_inside() {
let t = ray_sphere_intersection(
Vec3::ZERO,
Vec3::new(0.0, 0.0, 1.0),
Vec3::ZERO,
2.0,
);
assert!(t.is_some());
assert!((t.unwrap() - 2.0).abs() < 1e-5);
}
#[test]
fn test_ray_sphere_tangent() {
let t = ray_sphere_intersection(
Vec3::new(-5.0, 1.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::ZERO,
1.0,
);
assert!(t.is_some());
assert!((t.unwrap() - 5.0).abs() < 1e-4);
}
#[test]
fn test_screen_to_ray_center() {
let vp = Mat4::IDENTITY;
let (origin, dir) = screen_to_ray(
Vec2::new(640.0, 360.0),
Vec2::new(1280.0, 720.0),
&vp,
);
assert!((origin.x).abs() < 1e-3);
assert!((origin.y).abs() < 1e-3);
assert!(dir.z > 0.9);
}
#[test]
fn test_screen_to_ray_with_perspective() {
let view = Mat4::look_at_lh(
Vec3::new(0.0, 10.0, 0.0),
Vec3::ZERO,
Vec3::Z,
);
let proj = Mat4::perspective_lh(60f32.to_radians(), 1.0, 0.1, 100.0);
let vp = proj * view;
let (origin, dir) = screen_to_ray(
Vec2::new(400.0, 400.0), Vec2::new(800.0, 800.0),
&vp,
);
assert!(dir.y < 0.0, "Expected downward ray, got dir.y={}", dir.y);
let hit = ray_plane_intersection(origin, dir, 0.0);
assert!(hit.is_some(), "Expected ray to hit y=0 plane");
}
}