anvilkit_render/renderer/
raycast.rs1use glam::{Mat4, Vec2, Vec3};
29
30pub fn screen_to_ray(mouse_pos: Vec2, window_size: Vec2, view_proj: &Mat4) -> (Vec3, Vec3) {
54 let ndc_x = 2.0 * mouse_pos.x / window_size.x - 1.0;
56 let ndc_y = 1.0 - 2.0 * mouse_pos.y / window_size.y;
57
58 let inv_vp = view_proj.inverse();
59
60 let near_clip = inv_vp * glam::Vec4::new(ndc_x, ndc_y, 0.0, 1.0);
62 let far_clip = inv_vp * glam::Vec4::new(ndc_x, ndc_y, 1.0, 1.0);
63
64 let near_world = Vec3::new(
66 near_clip.x / near_clip.w,
67 near_clip.y / near_clip.w,
68 near_clip.z / near_clip.w,
69 );
70 let far_world = Vec3::new(
71 far_clip.x / far_clip.w,
72 far_clip.y / far_clip.w,
73 far_clip.z / far_clip.w,
74 );
75
76 let direction = (far_world - near_world).normalize();
77 (near_world, direction)
78}
79
80pub fn ray_plane_intersection(origin: Vec3, direction: Vec3, plane_y: f32) -> Option<Vec3> {
108 if direction.y.abs() < 1e-7 {
110 return None;
111 }
112
113 let t = (plane_y - origin.y) / direction.y;
114 if t < 0.0 {
115 return None;
116 }
117
118 Some(origin + direction * t)
119}
120
121pub fn ray_sphere_intersection(
152 origin: Vec3,
153 direction: Vec3,
154 center: Vec3,
155 radius: f32,
156) -> Option<f32> {
157 let oc = origin - center;
158 let a = direction.dot(direction);
159 let b = 2.0 * oc.dot(direction);
160 let c = oc.dot(oc) - radius * radius;
161 let discriminant = b * b - 4.0 * a * c;
162
163 if discriminant < 0.0 {
164 return None;
165 }
166
167 let sqrt_disc = discriminant.sqrt();
168 let inv_2a = 1.0 / (2.0 * a);
169
170 let t1 = (-b - sqrt_disc) * inv_2a;
172 if t1 >= 0.0 {
173 return Some(t1);
174 }
175
176 let t2 = (-b + sqrt_disc) * inv_2a;
178 if t2 >= 0.0 {
179 return Some(t2);
180 }
181
182 None
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_ray_plane_straight_down() {
191 let hit = ray_plane_intersection(
192 Vec3::new(0.0, 10.0, 0.0),
193 Vec3::new(0.0, -1.0, 0.0),
194 0.0,
195 );
196 assert!(hit.is_some());
197 let p = hit.unwrap();
198 assert!((p.x).abs() < 1e-5);
199 assert!((p.y).abs() < 1e-5);
200 assert!((p.z).abs() < 1e-5);
201 }
202
203 #[test]
204 fn test_ray_plane_diagonal() {
205 let hit = ray_plane_intersection(
206 Vec3::new(0.0, 10.0, 0.0),
207 Vec3::new(1.0, -1.0, 0.0).normalize(),
208 0.0,
209 );
210 assert!(hit.is_some());
211 let p = hit.unwrap();
212 assert!((p.x - 10.0).abs() < 1e-4);
213 assert!((p.y).abs() < 1e-4);
214 }
215
216 #[test]
217 fn test_ray_plane_parallel() {
218 let hit = ray_plane_intersection(
219 Vec3::new(0.0, 5.0, 0.0),
220 Vec3::new(1.0, 0.0, 0.0),
221 0.0,
222 );
223 assert!(hit.is_none());
224 }
225
226 #[test]
227 fn test_ray_plane_behind() {
228 let hit = ray_plane_intersection(
230 Vec3::new(0.0, 5.0, 0.0),
231 Vec3::new(0.0, -1.0, 0.0),
232 10.0,
233 );
234 assert!(hit.is_none());
235 }
236
237 #[test]
238 fn test_ray_sphere_hit() {
239 let t = ray_sphere_intersection(
240 Vec3::new(0.0, 0.0, -5.0),
241 Vec3::new(0.0, 0.0, 1.0),
242 Vec3::ZERO,
243 1.0,
244 );
245 assert!(t.is_some());
246 assert!((t.unwrap() - 4.0).abs() < 1e-5);
247 }
248
249 #[test]
250 fn test_ray_sphere_miss() {
251 let t = ray_sphere_intersection(
252 Vec3::new(0.0, 5.0, -5.0),
253 Vec3::new(0.0, 0.0, 1.0),
254 Vec3::ZERO,
255 1.0,
256 );
257 assert!(t.is_none());
258 }
259
260 #[test]
261 fn test_ray_sphere_inside() {
262 let t = ray_sphere_intersection(
264 Vec3::ZERO,
265 Vec3::new(0.0, 0.0, 1.0),
266 Vec3::ZERO,
267 2.0,
268 );
269 assert!(t.is_some());
270 assert!((t.unwrap() - 2.0).abs() < 1e-5);
271 }
272
273 #[test]
274 fn test_ray_sphere_tangent() {
275 let t = ray_sphere_intersection(
277 Vec3::new(-5.0, 1.0, 0.0),
278 Vec3::new(1.0, 0.0, 0.0),
279 Vec3::ZERO,
280 1.0,
281 );
282 assert!(t.is_some());
284 assert!((t.unwrap() - 5.0).abs() < 1e-4);
285 }
286
287 #[test]
288 fn test_screen_to_ray_center() {
289 let vp = Mat4::IDENTITY;
291 let (origin, dir) = screen_to_ray(
292 Vec2::new(640.0, 360.0),
293 Vec2::new(1280.0, 720.0),
294 &vp,
295 );
296 assert!((origin.x).abs() < 1e-3);
298 assert!((origin.y).abs() < 1e-3);
299 assert!(dir.z > 0.9);
301 }
302
303 #[test]
304 fn test_screen_to_ray_with_perspective() {
305 let view = Mat4::look_at_lh(
306 Vec3::new(0.0, 10.0, 0.0),
307 Vec3::ZERO,
308 Vec3::Z,
309 );
310 let proj = Mat4::perspective_lh(60f32.to_radians(), 1.0, 0.1, 100.0);
311 let vp = proj * view;
312
313 let (origin, dir) = screen_to_ray(
314 Vec2::new(400.0, 400.0), Vec2::new(800.0, 800.0),
316 &vp,
317 );
318
319 assert!(dir.y < 0.0, "Expected downward ray, got dir.y={}", dir.y);
321
322 let hit = ray_plane_intersection(origin, dir, 0.0);
324 assert!(hit.is_some(), "Expected ray to hit y=0 plane");
325 }
326}