1use phys_geom::math::*;
16use phys_geom::shape::Cylinder;
17
18use crate::{Raycast, RaycastHitResult};
19
20impl Raycast for Cylinder {
21 #[allow(clippy::many_single_char_names)]
22 fn raycast(
23 &self,
24 local_ray: phys_geom::Ray,
25 max_distance: Real,
26 discard_inside_hit: bool,
27 ) -> Option<RaycastHitResult> {
28 let half_height = self.half_height();
29 let radius = self.radius();
30
31 let offset = (-local_ray
32 .origin
33 .coords
34 .dot(&local_ray.direction.into_inner())
35 - (half_height + radius))
36 .max(0.0);
37 let translated_origin = local_ray.origin + local_ray.direction.into_inner() * offset;
38
39 let origin_xz = Vec3::new(translated_origin.x, 0.0, translated_origin.z);
43 let direction_xz = Vec3::new(local_ray.direction.x, 0.0, local_ray.direction.z);
44
45 let circle_center_y: Real;
47
48 let discr_half_b = origin_xz.dot(&direction_xz);
53 let discr_c = origin_xz.norm_squared() - radius * radius;
54
55 if discr_half_b > 0.0 && discr_c > 0.0 {
56 return None;
59 }
60
61 let discr_a = direction_xz.norm_squared();
62 if discr_a > 1e-8 {
63 let delta = discr_half_b * discr_half_b - discr_a * discr_c;
64 if delta < 0.0 {
65 return None;
66 }
67 let mut t = (-discr_half_b - delta.sqrt()) / discr_a;
69 let inside = if t < -offset {
70 t = -offset;
71 true
72 } else {
73 false
74 };
75
76 let hit_position = translated_origin + local_ray.direction.into_inner() * t;
79 if hit_position.y > half_height {
80 circle_center_y = half_height;
81 } else if hit_position.y < -half_height {
82 circle_center_y = -half_height;
83 } else {
84 if inside {
85 if discard_inside_hit {
86 return None;
87 }
88 return Some(RaycastHitResult {
89 distance: 0.0,
90 normal: -local_ray.direction,
91 });
92 }
93 let normal =
95 UnitVec3::new_normalize(Vec3::new(hit_position.x, 0.0, hit_position.z));
96 let distance = t + offset;
97 if distance <= max_distance {
98 return Some(RaycastHitResult { distance, normal });
99 }
100 return None;
101 }
102 } else {
103 circle_center_y = if local_ray.direction.y > 0.0 {
105 -half_height
106 } else {
107 half_height
108 }
109 }
110
111 if translated_origin.y.abs() > half_height
113 && translated_origin.y * local_ray.direction.y >= 0.0
114 {
115 return None;
117 }
118
119 let t = (circle_center_y - translated_origin.y) / local_ray.direction.y;
122 let hit_position = translated_origin + local_ray.direction.into_inner() * t;
123
124 if hit_position.x * hit_position.x + hit_position.z * hit_position.z > radius * radius {
126 return None;
127 }
128
129 let distance = t + offset;
130 if distance <= max_distance {
131 Some(RaycastHitResult {
132 distance,
133 normal: UnitVec3::new_normalize(Vec3::new(
134 0.0,
135 if local_ray.direction.y < 0.0 {
136 1.0
137 } else {
138 -1.0
139 },
140 0.0,
141 )),
142 })
143 } else {
144 None
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use approx::assert_relative_eq;
152
153 use super::*;
154
155 #[test]
156 fn test_raycast_cylinder_side() {
157 let cylinder = Cylinder::new(2.0, 1.0); let ray = phys_geom::Ray::new(
161 Point3::new(-3.0, 0.0, 0.0),
162 UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
163 );
164
165 let hit = cylinder.raycast(ray, 10.0, false).unwrap();
166 assert_relative_eq!(hit.distance, 2.0);
167 assert_relative_eq!(
168 hit.normal,
169 UnitVec3::new_normalize(Vec3::new(-1.0, 0.0, 0.0))
170 );
171 }
172
173 #[test]
174 fn test_raycast_cylinder_top() {
175 let cylinder = Cylinder::new(2.0, 1.0);
176
177 let ray = phys_geom::Ray::new(
179 Point3::new(0.0, 4.0, 0.0),
180 UnitVec3::new_normalize(Vec3::new(0.0, -1.0, 0.0)),
181 );
182
183 let hit = cylinder.raycast(ray, 10.0, false).unwrap();
184 assert_relative_eq!(hit.distance, 2.0);
185 assert_relative_eq!(
186 hit.normal,
187 UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0))
188 );
189 }
190
191 #[test]
192 fn test_raycast_cylinder_bottom() {
193 let cylinder = Cylinder::new(2.0, 1.0);
194
195 let ray = phys_geom::Ray::new(
197 Point3::new(0.0, -4.0, 0.0),
198 UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0)),
199 );
200
201 let hit = cylinder.raycast(ray, 10.0, false).unwrap();
202 assert_relative_eq!(hit.distance, 2.0);
203 assert_relative_eq!(
204 hit.normal,
205 UnitVec3::new_normalize(Vec3::new(0.0, -1.0, 0.0))
206 );
207 }
208
209 #[test]
210 fn test_raycast_cylinder_inside() {
211 let cylinder = Cylinder::new(2.0, 1.0);
212
213 let ray = phys_geom::Ray::new(
215 Point3::new(0.0, 0.0, 0.0),
216 UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
217 );
218
219 let hit = cylinder.raycast(ray, 10.0, false).unwrap();
220 assert_relative_eq!(hit.distance, 0.0);
221 assert_relative_eq!(
222 hit.normal,
223 UnitVec3::new_normalize(Vec3::new(-1.0, 0.0, 0.0))
224 );
225 }
226
227 #[test]
228 fn test_raycast_cylinder_inside_discarded() {
229 let cylinder = Cylinder::new(2.0, 1.0);
230
231 let ray = phys_geom::Ray::new(
233 Point3::new(0.0, 0.0, 0.0),
234 UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
235 );
236
237 assert_eq!(cylinder.raycast(ray, 10.0, true), None);
238 }
239
240 #[test]
241 fn test_raycast_cylinder_miss() {
242 let cylinder = Cylinder::new(2.0, 1.0);
243
244 let ray = phys_geom::Ray::new(
246 Point3::new(-3.0, 5.0, 0.0),
247 UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
248 );
249
250 assert_eq!(cylinder.raycast(ray, 10.0, false), None);
251 }
252
253 #[test]
254 fn test_raycast_cylinder_parallel() {
255 let cylinder = Cylinder::new(2.0, 1.0);
256
257 let ray = phys_geom::Ray::new(
259 Point3::new(0.5, 4.0, 0.0),
260 UnitVec3::new_normalize(Vec3::new(0.0, -1.0, 0.0)),
261 );
262
263 let hit = cylinder.raycast(ray, 10.0, false).unwrap();
264 assert_relative_eq!(hit.distance, 2.0);
265 assert_relative_eq!(
266 hit.normal,
267 UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0))
268 );
269 }
270
271 #[test]
272 fn test_raycast_cylinder_max_distance() {
273 let cylinder = Cylinder::new(2.0, 1.0);
274
275 let ray = phys_geom::Ray::new(
276 Point3::new(-5.0, 0.0, 0.0),
277 UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
278 );
279
280 assert_eq!(cylinder.raycast(ray, 1.0, false), None);
282
283 let hit = cylinder.raycast(ray, 5.0, false).unwrap();
285 assert_relative_eq!(hit.distance, 4.0);
286 }
287}