Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_math/src/bounding/raycast3d.rs
6596 views
1
use super::{Aabb3d, BoundingSphere, IntersectsVolume};
2
use crate::{
3
ops::{self, FloatPow},
4
Dir3A, Ray3d, Vec3A,
5
};
6
7
#[cfg(feature = "bevy_reflect")]
8
use bevy_reflect::Reflect;
9
10
/// A raycast intersection test for 3D bounding volumes
11
#[derive(Clone, Debug)]
12
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
13
pub struct RayCast3d {
14
/// The origin of the ray.
15
pub origin: Vec3A,
16
/// The direction of the ray.
17
pub direction: Dir3A,
18
/// The maximum distance for the ray
19
pub max: f32,
20
/// The multiplicative inverse direction of the ray
21
direction_recip: Vec3A,
22
}
23
24
impl RayCast3d {
25
/// Construct a [`RayCast3d`] from an origin, [direction], and max distance.
26
///
27
/// [direction]: crate::direction::Dir3
28
pub fn new(origin: impl Into<Vec3A>, direction: impl Into<Dir3A>, max: f32) -> Self {
29
let direction = direction.into();
30
Self {
31
origin: origin.into(),
32
direction,
33
direction_recip: direction.recip(),
34
max,
35
}
36
}
37
38
/// Construct a [`RayCast3d`] from a [`Ray3d`] and max distance.
39
pub fn from_ray(ray: Ray3d, max: f32) -> Self {
40
Self::new(ray.origin, ray.direction, max)
41
}
42
43
/// Get the cached multiplicative inverse of the direction of the ray.
44
pub fn direction_recip(&self) -> Vec3A {
45
self.direction_recip
46
}
47
48
/// Get the distance of an intersection with an [`Aabb3d`], if any.
49
pub fn aabb_intersection_at(&self, aabb: &Aabb3d) -> Option<f32> {
50
let positive = self.direction.signum().cmpgt(Vec3A::ZERO);
51
let min = Vec3A::select(positive, aabb.min, aabb.max);
52
let max = Vec3A::select(positive, aabb.max, aabb.min);
53
54
// Calculate the minimum/maximum time for each axis based on how much the direction goes that
55
// way. These values can get arbitrarily large, or even become NaN, which is handled by the
56
// min/max operations below
57
let tmin = (min - self.origin) * self.direction_recip;
58
let tmax = (max - self.origin) * self.direction_recip;
59
60
// An axis that is not relevant to the ray direction will be NaN. When one of the arguments
61
// to min/max is NaN, the other argument is used.
62
// An axis for which the direction is the wrong way will return an arbitrarily large
63
// negative value.
64
let tmin = tmin.max_element().max(0.);
65
let tmax = tmax.min_element().min(self.max);
66
67
if tmin <= tmax {
68
Some(tmin)
69
} else {
70
None
71
}
72
}
73
74
/// Get the distance of an intersection with a [`BoundingSphere`], if any.
75
pub fn sphere_intersection_at(&self, sphere: &BoundingSphere) -> Option<f32> {
76
let offset = self.origin - sphere.center;
77
let projected = offset.dot(*self.direction);
78
let closest_point = offset - projected * *self.direction;
79
let distance_squared = sphere.radius().squared() - closest_point.length_squared();
80
if distance_squared < 0.
81
|| ops::copysign(projected.squared(), -projected) < -distance_squared
82
{
83
None
84
} else {
85
let toi = -projected - ops::sqrt(distance_squared);
86
if toi > self.max {
87
None
88
} else {
89
Some(toi.max(0.))
90
}
91
}
92
}
93
}
94
95
impl IntersectsVolume<Aabb3d> for RayCast3d {
96
fn intersects(&self, volume: &Aabb3d) -> bool {
97
self.aabb_intersection_at(volume).is_some()
98
}
99
}
100
101
impl IntersectsVolume<BoundingSphere> for RayCast3d {
102
fn intersects(&self, volume: &BoundingSphere) -> bool {
103
self.sphere_intersection_at(volume).is_some()
104
}
105
}
106
107
/// An intersection test that casts an [`Aabb3d`] along a ray.
108
#[derive(Clone, Debug)]
109
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
110
pub struct AabbCast3d {
111
/// The ray along which to cast the bounding volume
112
pub ray: RayCast3d,
113
/// The aabb that is being cast
114
pub aabb: Aabb3d,
115
}
116
117
impl AabbCast3d {
118
/// Construct an [`AabbCast3d`] from an [`Aabb3d`], origin, [direction], and max distance.
119
///
120
/// [direction]: crate::direction::Dir3
121
pub fn new(
122
aabb: Aabb3d,
123
origin: impl Into<Vec3A>,
124
direction: impl Into<Dir3A>,
125
max: f32,
126
) -> Self {
127
Self {
128
ray: RayCast3d::new(origin, direction, max),
129
aabb,
130
}
131
}
132
133
/// Construct an [`AabbCast3d`] from an [`Aabb3d`], [`Ray3d`], and max distance.
134
pub fn from_ray(aabb: Aabb3d, ray: Ray3d, max: f32) -> Self {
135
Self::new(aabb, ray.origin, ray.direction, max)
136
}
137
138
/// Get the distance at which the [`Aabb3d`]s collide, if at all.
139
pub fn aabb_collision_at(&self, mut aabb: Aabb3d) -> Option<f32> {
140
aabb.min -= self.aabb.max;
141
aabb.max -= self.aabb.min;
142
self.ray.aabb_intersection_at(&aabb)
143
}
144
}
145
146
impl IntersectsVolume<Aabb3d> for AabbCast3d {
147
fn intersects(&self, volume: &Aabb3d) -> bool {
148
self.aabb_collision_at(*volume).is_some()
149
}
150
}
151
152
/// An intersection test that casts a [`BoundingSphere`] along a ray.
153
#[derive(Clone, Debug)]
154
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
155
pub struct BoundingSphereCast {
156
/// The ray along which to cast the bounding volume
157
pub ray: RayCast3d,
158
/// The sphere that is being cast
159
pub sphere: BoundingSphere,
160
}
161
162
impl BoundingSphereCast {
163
/// Construct a [`BoundingSphereCast`] from a [`BoundingSphere`], origin, [direction], and max distance.
164
///
165
/// [direction]: crate::direction::Dir3
166
pub fn new(
167
sphere: BoundingSphere,
168
origin: impl Into<Vec3A>,
169
direction: impl Into<Dir3A>,
170
max: f32,
171
) -> Self {
172
Self {
173
ray: RayCast3d::new(origin, direction, max),
174
sphere,
175
}
176
}
177
178
/// Construct a [`BoundingSphereCast`] from a [`BoundingSphere`], [`Ray3d`], and max distance.
179
pub fn from_ray(sphere: BoundingSphere, ray: Ray3d, max: f32) -> Self {
180
Self::new(sphere, ray.origin, ray.direction, max)
181
}
182
183
/// Get the distance at which the [`BoundingSphere`]s collide, if at all.
184
pub fn sphere_collision_at(&self, mut sphere: BoundingSphere) -> Option<f32> {
185
sphere.center -= self.sphere.center;
186
sphere.sphere.radius += self.sphere.radius();
187
self.ray.sphere_intersection_at(&sphere)
188
}
189
}
190
191
impl IntersectsVolume<BoundingSphere> for BoundingSphereCast {
192
fn intersects(&self, volume: &BoundingSphere) -> bool {
193
self.sphere_collision_at(*volume).is_some()
194
}
195
}
196
197
#[cfg(test)]
198
mod tests {
199
use super::*;
200
use crate::{Dir3, Vec3};
201
202
const EPSILON: f32 = 0.001;
203
204
#[test]
205
fn test_ray_intersection_sphere_hits() {
206
for (test, volume, expected_distance) in &[
207
(
208
// Hit the center of a centered bounding sphere
209
RayCast3d::new(Vec3::Y * -5., Dir3::Y, 90.),
210
BoundingSphere::new(Vec3::ZERO, 1.),
211
4.,
212
),
213
(
214
// Hit the center of a centered bounding sphere, but from the other side
215
RayCast3d::new(Vec3::Y * 5., -Dir3::Y, 90.),
216
BoundingSphere::new(Vec3::ZERO, 1.),
217
4.,
218
),
219
(
220
// Hit the center of an offset sphere
221
RayCast3d::new(Vec3::ZERO, Dir3::Y, 90.),
222
BoundingSphere::new(Vec3::Y * 3., 2.),
223
1.,
224
),
225
(
226
// Just barely hit the sphere before the max distance
227
RayCast3d::new(Vec3::X, Dir3::Y, 1.),
228
BoundingSphere::new(Vec3::new(1., 1., 0.), 0.01),
229
0.99,
230
),
231
(
232
// Hit a sphere off-center
233
RayCast3d::new(Vec3::X, Dir3::Y, 90.),
234
BoundingSphere::new(Vec3::Y * 5., 2.),
235
3.268,
236
),
237
(
238
// Barely hit a sphere on the side
239
RayCast3d::new(Vec3::X * 0.99999, Dir3::Y, 90.),
240
BoundingSphere::new(Vec3::Y * 5., 1.),
241
4.996,
242
),
243
] {
244
assert!(
245
test.intersects(volume),
246
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
247
);
248
let actual_distance = test.sphere_intersection_at(volume).unwrap();
249
assert!(
250
ops::abs(actual_distance - expected_distance) < EPSILON,
251
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
252
);
253
254
let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max);
255
assert!(
256
!inverted_ray.intersects(volume),
257
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
258
);
259
}
260
}
261
262
#[test]
263
fn test_ray_intersection_sphere_misses() {
264
for (test, volume) in &[
265
(
266
// The ray doesn't go in the right direction
267
RayCast3d::new(Vec3::ZERO, Dir3::X, 90.),
268
BoundingSphere::new(Vec3::Y * 2., 1.),
269
),
270
(
271
// Ray's alignment isn't enough to hit the sphere
272
RayCast3d::new(Vec3::ZERO, Dir3::from_xyz(1., 1., 1.).unwrap(), 90.),
273
BoundingSphere::new(Vec3::Y * 2., 1.),
274
),
275
(
276
// The ray's maximum distance isn't high enough
277
RayCast3d::new(Vec3::ZERO, Dir3::Y, 0.5),
278
BoundingSphere::new(Vec3::Y * 2., 1.),
279
),
280
] {
281
assert!(
282
!test.intersects(volume),
283
"Case:\n Test: {test:?}\n Volume: {volume:?}",
284
);
285
}
286
}
287
288
#[test]
289
fn test_ray_intersection_sphere_inside() {
290
let volume = BoundingSphere::new(Vec3::splat(0.5), 1.);
291
for origin in &[Vec3::X, Vec3::Y, Vec3::ONE, Vec3::ZERO] {
292
for direction in &[Dir3::X, Dir3::Y, Dir3::Z, -Dir3::X, -Dir3::Y, -Dir3::Z] {
293
for max in &[0., 1., 900.] {
294
let test = RayCast3d::new(*origin, *direction, *max);
295
296
assert!(
297
test.intersects(&volume),
298
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
299
);
300
301
let actual_distance = test.sphere_intersection_at(&volume);
302
assert_eq!(
303
actual_distance,
304
Some(0.),
305
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
306
);
307
}
308
}
309
}
310
}
311
312
#[test]
313
fn test_ray_intersection_aabb_hits() {
314
for (test, volume, expected_distance) in &[
315
(
316
// Hit the center of a centered aabb
317
RayCast3d::new(Vec3::Y * -5., Dir3::Y, 90.),
318
Aabb3d::new(Vec3::ZERO, Vec3::ONE),
319
4.,
320
),
321
(
322
// Hit the center of a centered aabb, but from the other side
323
RayCast3d::new(Vec3::Y * 5., -Dir3::Y, 90.),
324
Aabb3d::new(Vec3::ZERO, Vec3::ONE),
325
4.,
326
),
327
(
328
// Hit the center of an offset aabb
329
RayCast3d::new(Vec3::ZERO, Dir3::Y, 90.),
330
Aabb3d::new(Vec3::Y * 3., Vec3::splat(2.)),
331
1.,
332
),
333
(
334
// Just barely hit the aabb before the max distance
335
RayCast3d::new(Vec3::X, Dir3::Y, 1.),
336
Aabb3d::new(Vec3::new(1., 1., 0.), Vec3::splat(0.01)),
337
0.99,
338
),
339
(
340
// Hit an aabb off-center
341
RayCast3d::new(Vec3::X, Dir3::Y, 90.),
342
Aabb3d::new(Vec3::Y * 5., Vec3::splat(2.)),
343
3.,
344
),
345
(
346
// Barely hit an aabb on corner
347
RayCast3d::new(Vec3::X * -0.001, Dir3::from_xyz(1., 1., 1.).unwrap(), 90.),
348
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
349
1.732,
350
),
351
] {
352
assert!(
353
test.intersects(volume),
354
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
355
);
356
let actual_distance = test.aabb_intersection_at(volume).unwrap();
357
assert!(
358
ops::abs(actual_distance - expected_distance) < EPSILON,
359
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
360
);
361
362
let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max);
363
assert!(
364
!inverted_ray.intersects(volume),
365
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
366
);
367
}
368
}
369
370
#[test]
371
fn test_ray_intersection_aabb_misses() {
372
for (test, volume) in &[
373
(
374
// The ray doesn't go in the right direction
375
RayCast3d::new(Vec3::ZERO, Dir3::X, 90.),
376
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
377
),
378
(
379
// Ray's alignment isn't enough to hit the aabb
380
RayCast3d::new(Vec3::ZERO, Dir3::from_xyz(1., 0.99, 1.).unwrap(), 90.),
381
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
382
),
383
(
384
// The ray's maximum distance isn't high enough
385
RayCast3d::new(Vec3::ZERO, Dir3::Y, 0.5),
386
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
387
),
388
] {
389
assert!(
390
!test.intersects(volume),
391
"Case:\n Test: {test:?}\n Volume: {volume:?}",
392
);
393
}
394
}
395
396
#[test]
397
fn test_ray_intersection_aabb_inside() {
398
let volume = Aabb3d::new(Vec3::splat(0.5), Vec3::ONE);
399
for origin in &[Vec3::X, Vec3::Y, Vec3::ONE, Vec3::ZERO] {
400
for direction in &[Dir3::X, Dir3::Y, Dir3::Z, -Dir3::X, -Dir3::Y, -Dir3::Z] {
401
for max in &[0., 1., 900.] {
402
let test = RayCast3d::new(*origin, *direction, *max);
403
404
assert!(
405
test.intersects(&volume),
406
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
407
);
408
409
let actual_distance = test.aabb_intersection_at(&volume);
410
assert_eq!(
411
actual_distance,
412
Some(0.),
413
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
414
);
415
}
416
}
417
}
418
}
419
420
#[test]
421
fn test_aabb_cast_hits() {
422
for (test, volume, expected_distance) in &[
423
(
424
// Hit the center of the aabb, that a ray would've also hit
425
AabbCast3d::new(Aabb3d::new(Vec3::ZERO, Vec3::ONE), Vec3::ZERO, Dir3::Y, 90.),
426
Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
427
3.,
428
),
429
(
430
// Hit the center of the aabb, but from the other side
431
AabbCast3d::new(
432
Aabb3d::new(Vec3::ZERO, Vec3::ONE),
433
Vec3::Y * 10.,
434
-Dir3::Y,
435
90.,
436
),
437
Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
438
3.,
439
),
440
(
441
// Hit the edge of the aabb, that a ray would've missed
442
AabbCast3d::new(
443
Aabb3d::new(Vec3::ZERO, Vec3::ONE),
444
Vec3::X * 1.5,
445
Dir3::Y,
446
90.,
447
),
448
Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
449
3.,
450
),
451
(
452
// Hit the edge of the aabb, by casting an off-center AABB
453
AabbCast3d::new(
454
Aabb3d::new(Vec3::X * -2., Vec3::ONE),
455
Vec3::X * 3.,
456
Dir3::Y,
457
90.,
458
),
459
Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
460
3.,
461
),
462
] {
463
assert!(
464
test.intersects(volume),
465
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
466
);
467
let actual_distance = test.aabb_collision_at(*volume).unwrap();
468
assert!(
469
ops::abs(actual_distance - expected_distance) < EPSILON,
470
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
471
);
472
473
let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max);
474
assert!(
475
!inverted_ray.intersects(volume),
476
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
477
);
478
}
479
}
480
481
#[test]
482
fn test_sphere_cast_hits() {
483
for (test, volume, expected_distance) in &[
484
(
485
// Hit the center of the bounding sphere, that a ray would've also hit
486
BoundingSphereCast::new(
487
BoundingSphere::new(Vec3::ZERO, 1.),
488
Vec3::ZERO,
489
Dir3::Y,
490
90.,
491
),
492
BoundingSphere::new(Vec3::Y * 5., 1.),
493
3.,
494
),
495
(
496
// Hit the center of the bounding sphere, but from the other side
497
BoundingSphereCast::new(
498
BoundingSphere::new(Vec3::ZERO, 1.),
499
Vec3::Y * 10.,
500
-Dir3::Y,
501
90.,
502
),
503
BoundingSphere::new(Vec3::Y * 5., 1.),
504
3.,
505
),
506
(
507
// Hit the bounding sphere off-center, that a ray would've missed
508
BoundingSphereCast::new(
509
BoundingSphere::new(Vec3::ZERO, 1.),
510
Vec3::X * 1.5,
511
Dir3::Y,
512
90.,
513
),
514
BoundingSphere::new(Vec3::Y * 5., 1.),
515
3.677,
516
),
517
(
518
// Hit the bounding sphere off-center, by casting a sphere that is off-center
519
BoundingSphereCast::new(
520
BoundingSphere::new(Vec3::X * -1.5, 1.),
521
Vec3::X * 3.,
522
Dir3::Y,
523
90.,
524
),
525
BoundingSphere::new(Vec3::Y * 5., 1.),
526
3.677,
527
),
528
] {
529
assert!(
530
test.intersects(volume),
531
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
532
);
533
let actual_distance = test.sphere_collision_at(*volume).unwrap();
534
assert!(
535
ops::abs(actual_distance - expected_distance) < EPSILON,
536
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
537
);
538
539
let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max);
540
assert!(
541
!inverted_ray.intersects(volume),
542
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
543
);
544
}
545
}
546
}
547
548