Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_camera/src/primitives.rs
9353 views
1
use core::borrow::Borrow;
2
3
use bevy_derive::{Deref, DerefMut};
4
use bevy_ecs::{component::Component, entity::EntityHashMap, reflect::ReflectComponent};
5
use bevy_math::{
6
bounding::{Aabb3d, BoundingVolume},
7
primitives::{HalfSpace, ViewFrustum},
8
Affine3A, Mat3A, Vec3, Vec3A,
9
};
10
use bevy_mesh::{Mesh, VertexAttributeValues};
11
use bevy_reflect::prelude::*;
12
13
pub trait MeshAabb {
14
/// Compute the Axis-Aligned Bounding Box of the mesh vertices in model space
15
///
16
/// Returns `None` if `self` doesn't have [`Mesh::ATTRIBUTE_POSITION`] of
17
/// type [`VertexAttributeValues::Float32x3`], or if `self` doesn't have any vertices.
18
fn compute_aabb(&self) -> Option<Aabb>;
19
}
20
21
impl MeshAabb for Mesh {
22
fn compute_aabb(&self) -> Option<Aabb> {
23
if let Some(aabb) = self.final_aabb {
24
// use precomputed extents
25
return Some(aabb.into());
26
}
27
28
let Ok(VertexAttributeValues::Float32x3(values)) =
29
self.try_attribute(Mesh::ATTRIBUTE_POSITION)
30
else {
31
return None;
32
};
33
34
Aabb::enclosing(values.iter().map(|p| Vec3::from_slice(p)))
35
}
36
}
37
38
/// An axis-aligned bounding box, defined by:
39
/// - a center,
40
/// - the distances from the center to each faces along the axis,
41
/// the faces are orthogonal to the axis.
42
///
43
/// It is typically used as a component on an entity to represent the local space
44
/// occupied by this entity, with faces orthogonal to its local axis.
45
///
46
/// This component is notably used during "frustum culling", a process to determine
47
/// if an entity should be rendered by a [`Camera`] if its bounding box intersects
48
/// with the camera's [`Frustum`].
49
///
50
/// It will be added automatically by the systems in [`CalculateBounds`] to entities that:
51
/// - could be subject to frustum culling, for example with a [`Mesh3d`]
52
/// or `Sprite` component,
53
/// - don't have the [`NoFrustumCulling`] component.
54
///
55
/// It won't be updated automatically if the space occupied by the entity changes,
56
/// for example if the vertex positions of a [`Mesh3d`] are updated.
57
///
58
/// [`Camera`]: crate::Camera
59
/// [`NoFrustumCulling`]: crate::visibility::NoFrustumCulling
60
/// [`CalculateBounds`]: crate::visibility::VisibilitySystems::CalculateBounds
61
/// [`Mesh3d`]: bevy_mesh::Mesh
62
#[derive(Component, Clone, Copy, Debug, Default, Reflect, PartialEq)]
63
#[reflect(Component, Default, Debug, PartialEq, Clone)]
64
pub struct Aabb {
65
pub center: Vec3A,
66
pub half_extents: Vec3A,
67
}
68
69
impl Aabb {
70
#[inline]
71
pub fn from_min_max(minimum: Vec3, maximum: Vec3) -> Self {
72
let minimum = Vec3A::from(minimum);
73
let maximum = Vec3A::from(maximum);
74
let center = 0.5 * (maximum + minimum);
75
let half_extents = 0.5 * (maximum - minimum);
76
Self {
77
center,
78
half_extents,
79
}
80
}
81
82
/// Returns a bounding box enclosing the specified set of points.
83
///
84
/// Returns `None` if the iterator is empty.
85
///
86
/// # Examples
87
///
88
/// ```
89
/// # use bevy_math::{Vec3, Vec3A};
90
/// # use bevy_camera::primitives::Aabb;
91
/// let bb = Aabb::enclosing([Vec3::X, Vec3::Z * 2.0, Vec3::Y * -0.5]).unwrap();
92
/// assert_eq!(bb.min(), Vec3A::new(0.0, -0.5, 0.0));
93
/// assert_eq!(bb.max(), Vec3A::new(1.0, 0.0, 2.0));
94
/// ```
95
pub fn enclosing<T: Borrow<Vec3>>(iter: impl IntoIterator<Item = T>) -> Option<Self> {
96
let mut iter = iter.into_iter().map(|p| *p.borrow());
97
let mut min = iter.next()?;
98
let mut max = min;
99
for v in iter {
100
min = Vec3::min(min, v);
101
max = Vec3::max(max, v);
102
}
103
Some(Self::from_min_max(min, max))
104
}
105
106
/// Calculate the relative radius of the AABB with respect to a plane
107
#[inline]
108
pub fn relative_radius(&self, p_normal: &Vec3A, world_from_local: &Mat3A) -> f32 {
109
// NOTE: dot products on Vec3A use SIMD and even with the overhead of conversion are net faster than Vec3
110
let half_extents = self.half_extents;
111
Vec3A::new(
112
p_normal.dot(world_from_local.x_axis),
113
p_normal.dot(world_from_local.y_axis),
114
p_normal.dot(world_from_local.z_axis),
115
)
116
.abs()
117
.dot(half_extents)
118
}
119
120
#[inline]
121
pub fn min(&self) -> Vec3A {
122
self.center - self.half_extents
123
}
124
125
#[inline]
126
pub fn max(&self) -> Vec3A {
127
self.center + self.half_extents
128
}
129
130
/// Check if the AABB is at the front side of the bisecting plane.
131
/// Referenced from: [AABB Plane intersection](https://gdbooks.gitbooks.io/3dcollisions/content/Chapter2/static_aabb_plane.html)
132
#[inline]
133
pub fn is_in_half_space(&self, half_space: &HalfSpace, world_from_local: &Affine3A) -> bool {
134
// transform the half-extents into world space.
135
let half_extents_world = world_from_local.matrix3.abs() * self.half_extents.abs();
136
// collapse the half-extents onto the plane normal.
137
let p_normal = half_space.normal();
138
let r = half_extents_world.dot(p_normal.abs());
139
let aabb_center_world = world_from_local.transform_point3a(self.center);
140
let signed_distance = p_normal.dot(aabb_center_world) + half_space.d();
141
signed_distance > r
142
}
143
144
/// Optimized version of [`Self::is_in_half_space`] when the AABB is already in world space.
145
/// Use this when `world_from_local` would be the identity transform.
146
#[inline]
147
pub fn is_in_half_space_identity(&self, half_space: &HalfSpace) -> bool {
148
let p_normal = half_space.normal();
149
let r = self.half_extents.abs().dot(p_normal.abs());
150
let signed_distance = p_normal.dot(self.center) + half_space.d();
151
signed_distance > r
152
}
153
}
154
155
impl From<Aabb3d> for Aabb {
156
fn from(aabb: Aabb3d) -> Self {
157
Self {
158
center: aabb.center(),
159
half_extents: aabb.half_size(),
160
}
161
}
162
}
163
164
impl From<Aabb> for Aabb3d {
165
fn from(aabb: Aabb) -> Self {
166
Self {
167
min: aabb.min(),
168
max: aabb.max(),
169
}
170
}
171
}
172
173
impl From<Sphere> for Aabb {
174
#[inline]
175
fn from(sphere: Sphere) -> Self {
176
Self {
177
center: sphere.center,
178
half_extents: Vec3A::splat(sphere.radius),
179
}
180
}
181
}
182
183
/// A sphere, defined by a center and a radius.
184
///
185
/// This is typically used as a component on an entity to represent the local
186
/// space occupied by this entity, as an alternative to [`Aabb`]. The *frustum
187
/// culling* process uses this component to determine whether an entity is in
188
/// the view of a [`crate::Camera`].
189
///
190
/// Bevy will automatically add this component to point and spot lights, as
191
/// their ranges are most easily approximated by a sphere. The engine will keep
192
/// this entity updated as the range and/or transform of such lights changes.
193
///
194
/// If both [`Aabb`] and [`Sphere`] are present on an entity, [`Aabb`] takes
195
/// precedence.
196
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
197
#[reflect(Component, Clone, Debug, Default)]
198
pub struct Sphere {
199
/// The center of the sphere.
200
///
201
/// If this is used as a component, [`Self::center`] is in local space. That
202
/// is, it doesn't take the world-space position of the object into account.
203
pub center: Vec3A,
204
205
/// The radius of the sphere.
206
///
207
/// If this is used as a component, [`Self::radius`] is in local space. That
208
/// is, it doesn't take the world-space scale of the object into account.
209
pub radius: f32,
210
}
211
212
impl Sphere {
213
/// Returns true if this sphere intersects the given oriented bounding box.
214
///
215
/// The oriented bounding box (OBB) to test against is produced by
216
/// transforming the given AABB according to the supplied matrix.
217
#[inline]
218
pub fn intersects_obb(&self, aabb: &Aabb, world_from_local: &Affine3A) -> bool {
219
let aabb_center_world = world_from_local.transform_point3a(aabb.center);
220
let v = aabb_center_world - self.center;
221
let d = v.length();
222
let relative_radius = aabb.relative_radius(&(v / d), &world_from_local.matrix3);
223
d < self.radius + relative_radius
224
}
225
}
226
227
/// A frustum component is used on an entity with a [`Camera`] component to
228
/// determine which entities will be considered for rendering by this camera.
229
/// All entities with an [`Aabb`] component that are not contained by (or crossing
230
/// the boundary of) the frustum will not be rendered, and not be used in rendering computations.
231
///
232
/// This process is called frustum culling, and entities can opt out of it using
233
/// the [`NoFrustumCulling`] component.
234
///
235
/// The frustum component is typically added automatically for cameras, either [`Camera2d`] or [`Camera3d`].
236
/// It is usually updated automatically by [`update_frusta`] from the
237
/// [`CameraProjection`] component and [`GlobalTransform`] of the camera entity.
238
///
239
/// [`Camera`]: crate::Camera
240
/// [`NoFrustumCulling`]: crate::visibility::NoFrustumCulling
241
/// [`update_frusta`]: crate::visibility::update_frusta
242
/// [`CameraProjection`]: crate::CameraProjection
243
/// [`GlobalTransform`]: bevy_transform::components::GlobalTransform
244
/// [`Camera2d`]: crate::Camera2d
245
/// [`Camera3d`]: crate::Camera3d
246
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
247
#[reflect(Component, Default, Debug, Clone)]
248
pub struct Frustum(pub ViewFrustum);
249
250
impl Frustum {
251
/// Checks if a sphere intersects the frustum.
252
#[inline]
253
pub fn intersects_sphere(&self, sphere: &Sphere, intersect_far: bool) -> bool {
254
let sphere_center = sphere.center.extend(1.0);
255
let max = if intersect_far {
256
ViewFrustum::FAR_PLANE_IDX
257
} else {
258
ViewFrustum::NEAR_PLANE_IDX
259
};
260
for half_space in &self.half_spaces[..=max] {
261
if half_space.normal_d().dot(sphere_center) + sphere.radius <= 0.0 {
262
return false;
263
}
264
}
265
true
266
}
267
268
/// Checks if an Oriented Bounding Box (obb) intersects the frustum.
269
#[inline]
270
pub fn intersects_obb(
271
&self,
272
aabb: &Aabb,
273
world_from_local: &Affine3A,
274
intersect_near: bool,
275
intersect_far: bool,
276
) -> bool {
277
let aabb_center_world = world_from_local.transform_point3a(aabb.center).extend(1.0);
278
279
for (idx, half_space) in self.half_spaces.into_iter().enumerate() {
280
if (idx == ViewFrustum::NEAR_PLANE_IDX && !intersect_near)
281
|| (idx == ViewFrustum::FAR_PLANE_IDX && !intersect_far)
282
{
283
continue;
284
}
285
let p_normal = half_space.normal();
286
let relative_radius = aabb.relative_radius(&p_normal, &world_from_local.matrix3);
287
if half_space.normal_d().dot(aabb_center_world) + relative_radius <= 0.0 {
288
return false;
289
}
290
}
291
true
292
}
293
294
/// Optimized version of [`Frustum::intersects_obb`]
295
/// where the transform is [`Affine3A::IDENTITY`] and both `intersect_near` and `intersect_far` are `true`.
296
#[inline]
297
pub fn intersects_obb_identity(&self, aabb: &Aabb) -> bool {
298
let aabb_center_world = aabb.center.extend(1.0);
299
for half_space in self.half_spaces.iter() {
300
let p_normal = half_space.normal();
301
let relative_radius = aabb.half_extents.abs().dot(p_normal.abs());
302
if half_space.normal_d().dot(aabb_center_world) + relative_radius <= 0.0 {
303
return false;
304
}
305
}
306
true
307
}
308
309
/// Check if the frustum contains the entire Axis-Aligned Bounding Box (AABB).
310
/// Referenced from: [Frustum Culling](https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling)
311
#[inline]
312
pub fn contains_aabb(&self, aabb: &Aabb, world_from_local: &Affine3A) -> bool {
313
for half_space in &self.half_spaces {
314
if !aabb.is_in_half_space(half_space, world_from_local) {
315
return false;
316
}
317
}
318
true
319
}
320
321
/// Optimized version of [`Self::contains_aabb`] when the AABB is already in world space.
322
/// Use this when `world_from_local` would be [`Affine3A::IDENTITY`].
323
#[inline]
324
pub fn contains_aabb_identity(&self, aabb: &Aabb) -> bool {
325
for half_space in &self.half_spaces {
326
if !aabb.is_in_half_space_identity(half_space) {
327
return false;
328
}
329
}
330
true
331
}
332
}
333
334
pub struct CubeMapFace {
335
pub target: Vec3,
336
pub up: Vec3,
337
}
338
339
// Cubemap faces are [+X, -X, +Y, -Y, +Z, -Z], per https://www.w3.org/TR/webgpu/#texture-view-creation
340
// Note: Cubemap coordinates are left-handed y-up, unlike the rest of Bevy.
341
// See https://registry.khronos.org/vulkan/specs/1.2/html/chap16.html#_cube_map_face_selection
342
//
343
// For each cubemap face, we take care to specify the appropriate target/up axis such that the rendered
344
// texture using Bevy's right-handed y-up coordinate space matches the expected cubemap face in
345
// left-handed y-up cubemap coordinates.
346
pub const CUBE_MAP_FACES: [CubeMapFace; 6] = [
347
// +X
348
CubeMapFace {
349
target: Vec3::X,
350
up: Vec3::Y,
351
},
352
// -X
353
CubeMapFace {
354
target: Vec3::NEG_X,
355
up: Vec3::Y,
356
},
357
// +Y
358
CubeMapFace {
359
target: Vec3::Y,
360
up: Vec3::Z,
361
},
362
// -Y
363
CubeMapFace {
364
target: Vec3::NEG_Y,
365
up: Vec3::NEG_Z,
366
},
367
// +Z (with left-handed conventions, pointing forwards)
368
CubeMapFace {
369
target: Vec3::NEG_Z,
370
up: Vec3::Y,
371
},
372
// -Z (with left-handed conventions, pointing backwards)
373
CubeMapFace {
374
target: Vec3::Z,
375
up: Vec3::Y,
376
},
377
];
378
379
pub fn face_index_to_name(face_index: usize) -> &'static str {
380
match face_index {
381
0 => "+x",
382
1 => "-x",
383
2 => "+y",
384
3 => "-y",
385
4 => "+z",
386
5 => "-z",
387
_ => "invalid",
388
}
389
}
390
391
#[derive(Component, Clone, Debug, Default, Reflect)]
392
#[reflect(Component, Default, Debug, Clone)]
393
pub struct CubemapFrusta {
394
pub frusta: [Frustum; 6],
395
}
396
397
impl CubemapFrusta {
398
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Frustum> {
399
self.frusta.iter()
400
}
401
pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut Frustum> {
402
self.frusta.iter_mut()
403
}
404
}
405
406
/// Cubemap layout defines the order of images in a packed cubemap image.
407
#[derive(Default, Reflect, Debug, Clone, Copy)]
408
pub enum CubemapLayout {
409
/// layout in a vertical cross format
410
/// ```text
411
/// +y
412
/// -x -z +x
413
/// -y
414
/// +z
415
/// ```
416
#[default]
417
CrossVertical = 0,
418
/// layout in a horizontal cross format
419
/// ```text
420
/// +y
421
/// -x -z +x +z
422
/// -y
423
/// ```
424
CrossHorizontal = 1,
425
/// layout in a vertical sequence
426
/// ```text
427
/// +x
428
/// -x
429
/// +y
430
/// -y
431
/// -z
432
/// +z
433
/// ```
434
SequenceVertical = 2,
435
/// layout in a horizontal sequence
436
/// ```text
437
/// +x -x +y -y -z +z
438
/// ```
439
SequenceHorizontal = 3,
440
}
441
442
#[derive(Component, Debug, Default, Reflect, Clone)]
443
#[reflect(Component, Default, Debug, Clone)]
444
pub struct CascadesFrusta {
445
pub frusta: EntityHashMap<Vec<Frustum>>,
446
}
447
448
#[cfg(test)]
449
mod tests {
450
use core::f32::consts::PI;
451
452
use bevy_math::{ops, Quat, Vec4};
453
use bevy_transform::components::GlobalTransform;
454
455
use crate::{CameraProjection, PerspectiveProjection};
456
457
use super::*;
458
459
// A big, offset frustum
460
fn big_frustum() -> Frustum {
461
Frustum(ViewFrustum {
462
half_spaces: [
463
HalfSpace::new(Vec4::new(-0.9701, -0.2425, -0.0000, 7.7611)),
464
HalfSpace::new(Vec4::new(-0.0000, 1.0000, -0.0000, 4.0000)),
465
HalfSpace::new(Vec4::new(-0.0000, -0.2425, -0.9701, 2.9104)),
466
HalfSpace::new(Vec4::new(-0.0000, -1.0000, -0.0000, 4.0000)),
467
HalfSpace::new(Vec4::new(-0.0000, -0.2425, 0.9701, 2.9104)),
468
HalfSpace::new(Vec4::new(0.9701, -0.2425, -0.0000, -1.9403)),
469
],
470
})
471
}
472
473
#[test]
474
fn intersects_sphere_big_frustum_outside() {
475
// Sphere outside frustum
476
let frustum = big_frustum();
477
let sphere = Sphere {
478
center: Vec3A::new(0.9167, 0.0000, 0.0000),
479
radius: 0.7500,
480
};
481
assert!(!frustum.intersects_sphere(&sphere, true));
482
}
483
484
#[test]
485
fn intersects_sphere_big_frustum_intersect() {
486
// Sphere intersects frustum boundary
487
let frustum = big_frustum();
488
let sphere = Sphere {
489
center: Vec3A::new(7.9288, 0.0000, 2.9728),
490
radius: 2.0000,
491
};
492
assert!(frustum.intersects_sphere(&sphere, true));
493
}
494
495
// A frustum
496
fn frustum() -> Frustum {
497
Frustum(ViewFrustum {
498
half_spaces: [
499
HalfSpace::new(Vec4::new(-0.9701, -0.2425, -0.0000, 0.7276)),
500
HalfSpace::new(Vec4::new(-0.0000, 1.0000, -0.0000, 1.0000)),
501
HalfSpace::new(Vec4::new(-0.0000, -0.2425, -0.9701, 0.7276)),
502
HalfSpace::new(Vec4::new(-0.0000, -1.0000, -0.0000, 1.0000)),
503
HalfSpace::new(Vec4::new(-0.0000, -0.2425, 0.9701, 0.7276)),
504
HalfSpace::new(Vec4::new(0.9701, -0.2425, -0.0000, 0.7276)),
505
],
506
})
507
}
508
509
#[test]
510
fn intersects_sphere_frustum_surrounding() {
511
// Sphere surrounds frustum
512
let frustum = frustum();
513
let sphere = Sphere {
514
center: Vec3A::new(0.0000, 0.0000, 0.0000),
515
radius: 3.0000,
516
};
517
assert!(frustum.intersects_sphere(&sphere, true));
518
}
519
520
#[test]
521
fn intersects_sphere_frustum_contained() {
522
// Sphere is contained in frustum
523
let frustum = frustum();
524
let sphere = Sphere {
525
center: Vec3A::new(0.0000, 0.0000, 0.0000),
526
radius: 0.7000,
527
};
528
assert!(frustum.intersects_sphere(&sphere, true));
529
}
530
531
#[test]
532
fn intersects_sphere_frustum_intersects_plane() {
533
// Sphere intersects a plane
534
let frustum = frustum();
535
let sphere = Sphere {
536
center: Vec3A::new(0.0000, 0.0000, 0.9695),
537
radius: 0.7000,
538
};
539
assert!(frustum.intersects_sphere(&sphere, true));
540
}
541
542
#[test]
543
fn intersects_sphere_frustum_intersects_2_planes() {
544
// Sphere intersects 2 planes
545
let frustum = frustum();
546
let sphere = Sphere {
547
center: Vec3A::new(1.2037, 0.0000, 0.9695),
548
radius: 0.7000,
549
};
550
assert!(frustum.intersects_sphere(&sphere, true));
551
}
552
553
#[test]
554
fn intersects_sphere_frustum_intersects_3_planes() {
555
// Sphere intersects 3 planes
556
let frustum = frustum();
557
let sphere = Sphere {
558
center: Vec3A::new(1.2037, -1.0988, 0.9695),
559
radius: 0.7000,
560
};
561
assert!(frustum.intersects_sphere(&sphere, true));
562
}
563
564
#[test]
565
fn intersects_sphere_frustum_dodges_1_plane() {
566
// Sphere avoids intersecting the frustum by 1 plane
567
let frustum = frustum();
568
let sphere = Sphere {
569
center: Vec3A::new(-1.7020, 0.0000, 0.0000),
570
radius: 0.7000,
571
};
572
assert!(!frustum.intersects_sphere(&sphere, true));
573
}
574
575
// A long frustum.
576
fn long_frustum() -> Frustum {
577
Frustum(ViewFrustum {
578
half_spaces: [
579
HalfSpace::new(Vec4::new(-0.9998, -0.0222, -0.0000, -1.9543)),
580
HalfSpace::new(Vec4::new(-0.0000, 1.0000, -0.0000, 45.1249)),
581
HalfSpace::new(Vec4::new(-0.0000, -0.0168, -0.9999, 2.2718)),
582
HalfSpace::new(Vec4::new(-0.0000, -1.0000, -0.0000, 45.1249)),
583
HalfSpace::new(Vec4::new(-0.0000, -0.0168, 0.9999, 2.2718)),
584
HalfSpace::new(Vec4::new(0.9998, -0.0222, -0.0000, 7.9528)),
585
],
586
})
587
}
588
589
#[test]
590
fn intersects_sphere_long_frustum_outside() {
591
// Sphere outside frustum
592
let frustum = long_frustum();
593
let sphere = Sphere {
594
center: Vec3A::new(-4.4889, 46.9021, 0.0000),
595
radius: 0.7500,
596
};
597
assert!(!frustum.intersects_sphere(&sphere, true));
598
}
599
600
#[test]
601
fn intersects_sphere_long_frustum_intersect() {
602
// Sphere intersects frustum boundary
603
let frustum = long_frustum();
604
let sphere = Sphere {
605
center: Vec3A::new(-4.9957, 0.0000, -0.7396),
606
radius: 4.4094,
607
};
608
assert!(frustum.intersects_sphere(&sphere, true));
609
}
610
611
#[test]
612
fn aabb_enclosing() {
613
assert_eq!(Aabb::enclosing([] as [Vec3; 0]), None);
614
assert_eq!(
615
Aabb::enclosing(vec![Vec3::ONE]).unwrap(),
616
Aabb::from_min_max(Vec3::ONE, Vec3::ONE)
617
);
618
assert_eq!(
619
Aabb::enclosing(&[Vec3::Y, Vec3::X, Vec3::Z][..]).unwrap(),
620
Aabb::from_min_max(Vec3::ZERO, Vec3::ONE)
621
);
622
assert_eq!(
623
Aabb::enclosing([
624
Vec3::NEG_X,
625
Vec3::X * 2.0,
626
Vec3::NEG_Y * 5.0,
627
Vec3::Z,
628
Vec3::ZERO
629
])
630
.unwrap(),
631
Aabb::from_min_max(Vec3::new(-1.0, -5.0, 0.0), Vec3::new(2.0, 0.0, 1.0))
632
);
633
}
634
635
// A frustum with an offset for testing the [`Frustum::contains_aabb`] algorithm.
636
fn contains_aabb_test_frustum() -> Frustum {
637
let proj = PerspectiveProjection {
638
fov: 90.0_f32.to_radians(),
639
aspect_ratio: 1.0,
640
near: 1.0,
641
far: 100.0,
642
..PerspectiveProjection::default()
643
};
644
proj.compute_frustum(&GlobalTransform::from_translation(Vec3::new(2.0, 2.0, 0.0)))
645
}
646
647
fn contains_aabb_test_frustum_with_rotation() -> Frustum {
648
let half_extent_world = (((49.5 * 49.5) * 0.5) as f32).sqrt() + 0.5f32.sqrt();
649
let near = 50.5 - half_extent_world;
650
let far = near + 2.0 * half_extent_world;
651
let fov = 2.0 * ops::atan(half_extent_world / near);
652
let proj = PerspectiveProjection {
653
aspect_ratio: 1.0,
654
near,
655
far,
656
fov,
657
..PerspectiveProjection::default()
658
};
659
proj.compute_frustum(&GlobalTransform::IDENTITY)
660
}
661
662
#[test]
663
fn aabb_inside_frustum() {
664
let frustum = contains_aabb_test_frustum();
665
let aabb = Aabb {
666
center: Vec3A::ZERO,
667
half_extents: Vec3A::new(0.99, 0.99, 49.49),
668
};
669
let model = Affine3A::from_translation(Vec3::new(2.0, 2.0, -50.5));
670
assert!(frustum.contains_aabb(&aabb, &model));
671
}
672
673
#[test]
674
fn aabb_intersect_frustum() {
675
let frustum = contains_aabb_test_frustum();
676
let aabb = Aabb {
677
center: Vec3A::ZERO,
678
half_extents: Vec3A::new(0.99, 0.99, 49.6),
679
};
680
let model = Affine3A::from_translation(Vec3::new(2.0, 2.0, -50.5));
681
assert!(!frustum.contains_aabb(&aabb, &model));
682
}
683
684
#[test]
685
fn aabb_outside_frustum() {
686
let frustum = contains_aabb_test_frustum();
687
let aabb = Aabb {
688
center: Vec3A::ZERO,
689
half_extents: Vec3A::new(0.99, 0.99, 0.99),
690
};
691
let model = Affine3A::from_translation(Vec3::new(0.0, 0.0, 49.6));
692
assert!(!frustum.contains_aabb(&aabb, &model));
693
}
694
695
#[test]
696
fn aabb_inside_frustum_rotation() {
697
let frustum = contains_aabb_test_frustum_with_rotation();
698
let aabb = Aabb {
699
center: Vec3A::new(0.0, 0.0, 0.0),
700
half_extents: Vec3A::new(0.99, 0.99, 49.49),
701
};
702
703
let model = Affine3A::from_rotation_translation(
704
Quat::from_rotation_x(PI / 4.0),
705
Vec3::new(0.0, 0.0, -50.5),
706
);
707
assert!(frustum.contains_aabb(&aabb, &model));
708
}
709
710
#[test]
711
fn aabb_intersect_frustum_rotation() {
712
let frustum = contains_aabb_test_frustum_with_rotation();
713
let aabb = Aabb {
714
center: Vec3A::new(0.0, 0.0, 0.0),
715
half_extents: Vec3A::new(0.99, 0.99, 49.6),
716
};
717
718
let model = Affine3A::from_rotation_translation(
719
Quat::from_rotation_x(PI / 4.0),
720
Vec3::new(0.0, 0.0, -50.5),
721
);
722
assert!(!frustum.contains_aabb(&aabb, &model));
723
}
724
725
#[test]
726
fn test_identity_optimized_equivalence() {
727
let cases = vec![
728
(
729
Aabb {
730
center: Vec3A::ZERO,
731
half_extents: Vec3A::splat(1.0),
732
},
733
HalfSpace::new(Vec4::new(1.0, 0.0, 0.0, -0.5)),
734
),
735
(
736
Aabb {
737
center: Vec3A::new(2.0, -1.0, 0.5),
738
half_extents: Vec3A::new(1.0, 2.0, 0.5),
739
},
740
HalfSpace::new(Vec4::new(1.0, 1.0, 1.0, -1.0).normalize()),
741
),
742
(
743
Aabb {
744
center: Vec3A::new(1.0, 1.0, 1.0),
745
half_extents: Vec3A::ZERO,
746
},
747
HalfSpace::new(Vec4::new(0.0, 0.0, 1.0, -2.0)),
748
),
749
];
750
for (aabb, half_space) in cases {
751
let general = aabb.is_in_half_space(&half_space, &Affine3A::IDENTITY);
752
let identity = aabb.is_in_half_space_identity(&half_space);
753
assert_eq!(general, identity,);
754
}
755
}
756
757
#[test]
758
fn intersects_obb_identity_matches_standard_true_true() {
759
let frusta = [frustum(), long_frustum(), big_frustum()];
760
let aabbs = [
761
Aabb {
762
center: Vec3A::ZERO,
763
half_extents: Vec3A::new(0.5, 0.5, 0.5),
764
},
765
Aabb {
766
center: Vec3A::new(1.0, 0.0, 0.5),
767
half_extents: Vec3A::new(0.9, 0.9, 0.9),
768
},
769
Aabb {
770
center: Vec3A::new(100.0, 100.0, 100.0),
771
half_extents: Vec3A::new(1.0, 1.0, 1.0),
772
},
773
];
774
for fr in &frusta {
775
for aabb in &aabbs {
776
let standard = fr.intersects_obb(aabb, &Affine3A::IDENTITY, true, true);
777
let optimized = fr.intersects_obb_identity(aabb);
778
assert_eq!(standard, optimized);
779
}
780
}
781
}
782
}
783
784