1use glam::{DVec2, DVec3, DVec4, UVec2};
4
5use crate::camera::Camera;
6use crate::math::Aabb;
7use crate::render_params::VolumeRenderParams;
8use crate::volume::{DynVolume, VolumeInfo};
9
10#[derive(Debug, Clone, Copy)]
12pub struct Ray {
13 pub origin: DVec3,
15 pub direction: DVec3,
17}
18
19impl Ray {
20 #[must_use]
22 pub fn new(origin: DVec3, direction: DVec3) -> Self {
23 Self {
24 origin,
25 direction: direction.normalize(),
26 }
27 }
28
29 #[must_use]
31 pub fn at(&self, t: f64) -> DVec3 {
32 self.origin + self.direction * t
33 }
34
35 #[must_use]
40 pub fn intersect_aabb(&self, aabb: &Aabb) -> Option<(f64, f64)> {
41 let inv_dir = DVec3::new(
42 1.0 / self.direction.x,
43 1.0 / self.direction.y,
44 1.0 / self.direction.z,
45 );
46
47 let t1 = (aabb.min - self.origin) * inv_dir;
48 let t2 = (aabb.max - self.origin) * inv_dir;
49
50 let t_min = t1.min(t2);
51 let t_max = t1.max(t2);
52
53 let t_enter = t_min.x.max(t_min.y).max(t_min.z);
54 let t_exit = t_max.x.min(t_max.y).min(t_max.z);
55
56 if t_enter <= t_exit && t_exit >= 0.0 {
57 Some((t_enter, t_exit))
58 } else {
59 None
60 }
61 }
62}
63
64#[must_use]
69pub fn unproject_ray(screen_pos: DVec2, camera: &Camera, viewport_size: UVec2) -> Ray {
70 let aspect = f64::from(viewport_size.x) / f64::from(viewport_size.y);
71 let view = camera.view_matrix();
72 let proj = camera.projection_matrix(aspect);
73 let inv_vp = (proj * view).inverse();
74
75 let ndc_x = (screen_pos.x / f64::from(viewport_size.x)) * 2.0 - 1.0;
77 let ndc_y = 1.0 - (screen_pos.y / f64::from(viewport_size.y)) * 2.0;
78
79 let near_clip = inv_vp * DVec4::new(ndc_x, ndc_y, -1.0, 1.0);
80 let far_clip = inv_vp * DVec4::new(ndc_x, ndc_y, 1.0, 1.0);
81
82 let near_world = near_clip.truncate() / near_clip.w;
83 let far_world = far_clip.truncate() / far_clip.w;
84
85 Ray::new(near_world, far_world - near_world)
86}
87
88#[derive(Debug, Clone)]
90pub struct PickResult {
91 pub world_position: DVec3,
93 pub voxel_index: DVec3,
95 pub voxel_value: f64,
97}
98
99#[must_use]
107pub fn pick_volume(
108 screen_pos: DVec2,
109 camera: &Camera,
110 viewport_size: UVec2,
111 volume: &DynVolume,
112 params: &VolumeRenderParams,
113) -> Option<PickResult> {
114 let ray = unproject_ray(screen_pos, camera, viewport_size);
115 let bounds = volume.world_bounds();
116
117 let (t_enter, t_exit) = ray.intersect_aabb(&bounds)?;
118 let t_start = t_enter.max(0.0);
119
120 let spacing = volume.spacing();
122 let min_spacing = spacing.x.min(spacing.y).min(spacing.z);
123 let step = min_spacing * f64::from(params.step_size_factor);
124
125 let mut t = t_start;
126 while t <= t_exit {
127 let world_pos = ray.at(t);
128 let voxel_idx = volume.world_to_index(world_pos);
129
130 if let Some(value) = volume.sample_linear(voxel_idx) {
131 let opacity = params.opacity_tf.evaluate(value);
132
133 if opacity > 0.01 {
134 return Some(PickResult {
135 world_position: world_pos,
136 voxel_index: voxel_idx,
137 voxel_value: value,
138 });
139 }
140 }
141
142 t += step;
143 }
144
145 None
146}
147
148#[cfg(test)]
151mod tests {
152 use super::*;
153 use approx::assert_abs_diff_eq;
154
155 #[test]
156 fn ray_intersect_aabb_hit() {
157 let ray = Ray::new(DVec3::new(-5.0, 0.5, 0.5), DVec3::X);
158 let aabb = Aabb::new(DVec3::ZERO, DVec3::ONE);
159 let (t_enter, t_exit) = ray.intersect_aabb(&aabb).expect("should hit");
160 assert_abs_diff_eq!(t_enter, 5.0, epsilon = 1e-10);
161 assert_abs_diff_eq!(t_exit, 6.0, epsilon = 1e-10);
162 }
163
164 #[test]
165 fn ray_intersect_aabb_miss() {
166 let ray = Ray::new(DVec3::new(-5.0, 5.0, 0.5), DVec3::X);
167 let aabb = Aabb::new(DVec3::ZERO, DVec3::ONE);
168 assert!(ray.intersect_aabb(&aabb).is_none());
169 }
170
171 #[test]
172 fn ray_intersect_aabb_inside() {
173 let ray = Ray::new(DVec3::new(0.5, 0.5, 0.5), DVec3::X);
174 let aabb = Aabb::new(DVec3::ZERO, DVec3::ONE);
175 let (t_enter, t_exit) = ray.intersect_aabb(&aabb).expect("should hit");
176 assert!(
177 t_enter < 0.0,
178 "origin is inside, t_enter should be negative"
179 );
180 assert!(t_exit > 0.0);
181 }
182
183 #[test]
184 fn ray_at() {
185 let ray = Ray::new(DVec3::ZERO, DVec3::X);
186 let p = ray.at(3.0);
187 assert_abs_diff_eq!(p.x, 3.0, epsilon = 1e-10);
188 assert_abs_diff_eq!(p.y, 0.0, epsilon = 1e-10);
189 }
190
191 #[test]
192 fn unproject_center_looks_forward() {
193 let cam = Camera::new(DVec3::new(0.0, 0.0, 5.0), DVec3::ZERO, DVec3::Y);
194 let viewport = UVec2::new(800, 600);
195 let ray = unproject_ray(DVec2::new(400.0, 300.0), &cam, viewport);
196 assert!(ray.direction.z < 0.0, "ray should point toward -Z");
198 assert_abs_diff_eq!(ray.direction.x, 0.0, epsilon = 0.01);
199 assert_abs_diff_eq!(ray.direction.y, 0.0, epsilon = 0.01);
200 }
201}