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