Skip to main content

bevy_mesh/
skinning.rs

1use crate::{Mesh, MeshVertexAttribute, VertexAttributeValues, VertexFormat};
2use bevy_asset::{AsAssetId, Asset, AssetId, Handle};
3use bevy_ecs::{
4    component::Component, entity::Entity, prelude::ReflectComponent, system::Query,
5    template::FromTemplate,
6};
7use bevy_math::{
8    bounding::{Aabb3d, BoundingVolume},
9    Affine3A, Mat4, Vec3, Vec3A,
10};
11use bevy_reflect::prelude::*;
12use bevy_transform::components::GlobalTransform;
13use core::ops::Deref;
14use thiserror::Error;
15
16#[derive(Component, Debug, Default, Clone, Reflect, FromTemplate)]
17#[reflect(Component, Default, Debug, Clone)]
18pub struct SkinnedMesh {
19    pub inverse_bindposes: Handle<SkinnedMeshInverseBindposes>,
20    #[entities]
21    pub joints: Vec<Entity>,
22}
23
24impl AsAssetId for SkinnedMesh {
25    type Asset = SkinnedMeshInverseBindposes;
26
27    // We implement this so that `AssetChanged` will work to pick up any changes
28    // to `SkinnedMeshInverseBindposes`.
29    fn as_asset_id(&self) -> AssetId<Self::Asset> {
30        self.inverse_bindposes.id()
31    }
32}
33
34#[derive(Asset, TypePath, Debug)]
35pub struct SkinnedMeshInverseBindposes(Box<[Mat4]>);
36
37impl From<Vec<Mat4>> for SkinnedMeshInverseBindposes {
38    fn from(value: Vec<Mat4>) -> Self {
39        Self(value.into_boxed_slice())
40    }
41}
42
43impl Deref for SkinnedMeshInverseBindposes {
44    type Target = [Mat4];
45    fn deref(&self) -> &Self::Target {
46        &self.0
47    }
48}
49
50// The AABB of a joint. This is optimized for `transform_aabb` - center/size is
51// slightly faster than the min/max used by `bevy_math::Aabb3d`, and the vectors
52// don't benefit from alignment because they're broadcast loaded.
53#[derive(Copy, Clone, Debug, PartialEq, Reflect)]
54pub struct JointAabb {
55    pub center: Vec3,
56    pub half_size: Vec3,
57}
58
59impl JointAabb {
60    fn min(&self) -> Vec3 {
61        self.center - self.half_size
62    }
63
64    fn max(&self) -> Vec3 {
65        self.center + self.half_size
66    }
67}
68
69impl From<JointAabb> for Aabb3d {
70    fn from(value: JointAabb) -> Self {
71        Self {
72            min: value.min().into(),
73            max: value.max().into(),
74        }
75    }
76}
77
78impl From<Aabb3d> for JointAabb {
79    fn from(value: Aabb3d) -> Self {
80        Self {
81            center: value.center().into(),
82            half_size: value.half_size().into(),
83        }
84    }
85}
86
87/// Data that can be used to calculate the AABB of a skinned mesh.
88#[derive(Clone, Default, Debug, PartialEq, Reflect)]
89#[reflect(Clone)]
90pub struct SkinnedMeshBounds {
91    // Model-space AABBs that enclose the vertices skinned to a joint. Some
92    // joints may not be skinned to any vertices, so not every joint has an
93    // AABB.
94    //
95    // `aabb_index_to_joint_index` maps from an `aabbs` index to a joint index,
96    // which corresponds to `Mesh::ATTRIBUTE_JOINT_INDEX` and `SkinnedMesh::joints`.
97    //
98    // These arrays could be a single `Vec<(JointAabb, JointIndex)>`, but that
99    // would waste two bytes due to alignment.
100    //
101    // TODO: If https://github.com/bevyengine/bevy/issues/11570 is fixed, `Vec<_>`
102    // can be changed to `Box<[_]>`.
103    pub aabbs: Vec<JointAabb>,
104    pub aabb_index_to_joint_index: Vec<JointIndex>,
105}
106
107#[derive(Copy, Clone, PartialEq, Debug, Error)]
108pub enum SkinnedMeshBoundsError {
109    #[error("The mesh does not contain any joints that are skinned to vertices")]
110    NoSkinnedJoints,
111    #[error(transparent)]
112    MeshAttributeError(#[from] MeshAttributeError),
113}
114
115impl SkinnedMeshBounds {
116    /// Create a `SkinnedMeshBounds` from a [`Mesh`].
117    ///
118    /// The mesh is expected to have position, joint index and joint weight
119    /// attributes. If any are missing then a [`MeshAttributeError`] is returned.
120    pub fn from_mesh(mesh: &Mesh) -> Result<SkinnedMeshBounds, SkinnedMeshBoundsError> {
121        let vertex_positions = expect_attribute_float32x3(mesh, Mesh::ATTRIBUTE_POSITION)?;
122        let vertex_influences = InfluenceIterator::new(mesh)?;
123
124        // Find the maximum joint index.
125        let Some(max_joint_index) = vertex_influences
126            .clone()
127            .map(|i| i.joint_index.0 as usize)
128            .reduce(Ord::max)
129        else {
130            return Ok(SkinnedMeshBounds::default());
131        };
132
133        // Create an AABB accumulator for each joint.
134        let mut accumulators: Box<[AabbAccumulator]> =
135            vec![AabbAccumulator::new(); max_joint_index + 1].into();
136
137        // Iterate over all vertex influences and add the vertex position to
138        // the influencing joint's AABB.
139        for influence in vertex_influences {
140            if let Some(&vertex_position) = vertex_positions.get(influence.vertex_index) {
141                accumulators[influence.joint_index.0 as usize]
142                    .add_point(Vec3A::from_array(vertex_position));
143            }
144        }
145
146        // Filter out joints with no AABB.
147        let joint_indices_and_aabbs = accumulators
148            .iter()
149            .enumerate()
150            .filter_map(|(joint_index, &accumulator)| {
151                accumulator.finish().map(|aabb| (joint_index, aabb))
152            })
153            .collect::<Vec<_>>();
154
155        if joint_indices_and_aabbs.is_empty() {
156            return Err(SkinnedMeshBoundsError::NoSkinnedJoints);
157        }
158
159        let aabbs = joint_indices_and_aabbs
160            .iter()
161            .map(|&(_, aabb)| JointAabb::from(aabb))
162            .collect::<Vec<_>>();
163
164        let aabb_index_to_joint_index = joint_indices_and_aabbs
165            .iter()
166            .map(|&(joint_index, _)| JointIndex(joint_index as u16))
167            .collect::<Vec<_>>();
168
169        assert_eq!(aabbs.len(), aabb_index_to_joint_index.len());
170
171        Ok(SkinnedMeshBounds {
172            aabbs,
173            aabb_index_to_joint_index,
174        })
175    }
176
177    pub fn iter(&self) -> impl Iterator<Item = (&JointIndex, &JointAabb)> {
178        self.aabb_index_to_joint_index.iter().zip(self.aabbs.iter())
179    }
180}
181
182#[derive(Copy, Clone, Debug)]
183pub enum EntityAabbFromSkinnedMeshBoundsError {
184    OutOfRangeJointIndex(JointIndex),
185    MissingJointEntity,
186    MissingSkinnedMeshBounds,
187}
188
189/// Given the components of a skinned mesh entity, return an `Aabb3d` that
190/// encloses the skinned vertices of the mesh.
191pub fn entity_aabb_from_skinned_mesh_bounds(
192    joint_entities: &Query<&GlobalTransform>,
193    mesh: &Mesh,
194    skinned_mesh: &SkinnedMesh,
195    skinned_mesh_inverse_bindposes: &SkinnedMeshInverseBindposes,
196    world_from_entity: Option<&GlobalTransform>,
197) -> Result<Aabb3d, EntityAabbFromSkinnedMeshBoundsError> {
198    let Some(skinned_mesh_bounds) = mesh.skinned_mesh_bounds() else {
199        return Err(EntityAabbFromSkinnedMeshBoundsError::MissingSkinnedMeshBounds);
200    };
201
202    let mut accumulator = AabbAccumulator::new();
203
204    // For each model-space joint AABB, transform it to world-space and add it
205    // to the accumulator.
206    for (&joint_index, &modelspace_joint_aabb) in skinned_mesh_bounds.iter() {
207        let Some(joint_from_model) = skinned_mesh_inverse_bindposes
208            .get(joint_index.0 as usize)
209            .map(|&m| Affine3A::from_mat4(m))
210        else {
211            return Err(EntityAabbFromSkinnedMeshBoundsError::OutOfRangeJointIndex(
212                joint_index,
213            ));
214        };
215
216        let Some(&joint_entity) = skinned_mesh.joints.get(joint_index.0 as usize) else {
217            return Err(EntityAabbFromSkinnedMeshBoundsError::OutOfRangeJointIndex(
218                joint_index,
219            ));
220        };
221
222        let Ok(&world_from_joint) = joint_entities.get(joint_entity) else {
223            return Err(EntityAabbFromSkinnedMeshBoundsError::MissingJointEntity);
224        };
225
226        let world_from_model = world_from_joint.affine() * joint_from_model;
227        let worldspace_joint_aabb = transform_aabb(modelspace_joint_aabb, world_from_model);
228
229        accumulator.add_aabb(worldspace_joint_aabb);
230    }
231
232    let Some(worldspace_entity_aabb) = accumulator.finish() else {
233        return Err(EntityAabbFromSkinnedMeshBoundsError::MissingJointEntity);
234    };
235
236    // If the entity has a transform, move the AABB from world-space to entity-space.
237    if let Some(world_from_entity) = world_from_entity {
238        let entityspace_entity_aabb = transform_aabb(
239            worldspace_entity_aabb.into(),
240            world_from_entity.affine().inverse(),
241        );
242
243        Ok(entityspace_entity_aabb)
244    } else {
245        Ok(worldspace_entity_aabb)
246    }
247}
248
249// Return the smallest `Aabb3d` that encloses the transformed `JointAabb`.
250//
251// Algorithm from "Transforming Axis-Aligned Bounding Boxes", James Arvo, Graphics Gems (1990).
252#[inline]
253fn transform_aabb(input: JointAabb, transform: Affine3A) -> Aabb3d {
254    let mx = transform.matrix3.x_axis;
255    let my = transform.matrix3.y_axis;
256    let mz = transform.matrix3.z_axis;
257    let mt = transform.translation;
258
259    let cx = Vec3A::splat(input.center.x);
260    let cy = Vec3A::splat(input.center.y);
261    let cz = Vec3A::splat(input.center.z);
262
263    let sx = Vec3A::splat(input.half_size.x);
264    let sy = Vec3A::splat(input.half_size.y);
265    let sz = Vec3A::splat(input.half_size.z);
266
267    // Transform the center.
268    let tc = (mx * cx) + (my * cy) + (mz * cz) + mt;
269
270    // Calculate a size that encloses the transformed size.
271    let ts = (mx.abs() * sx) + (my.abs() * sy) + (mz.abs() * sz);
272
273    let min = tc - ts;
274    let max = tc + ts;
275
276    Aabb3d { min, max }
277}
278
279// Helper for efficiently accumulating an enclosing AABB from a set of points or
280// other AABBs. Intended for cases where the size of the set is not known in
281// advance and might be zero.
282//
283// ```
284// let a = AabbAccumulator::new();
285//
286// a.add_point(point); // Add a `Vec3A`.
287// a.add_aabb(aabb); // Add an `Aabb3d`.
288//
289// // Returns `Some(Aabb3d)` if at least one thing was added.
290// let result = a.finish();
291// ```
292//
293// For alternatives, see [`Aabb3d::from_point_clound`](`bevy_math::bounding::bounded3d::Aabb3d::from_point_cloud`)
294// and [`BoundingVolume::merge`](`bevy_math::bounding::BoundingVolume::merge`).
295#[derive(Copy, Clone)]
296struct AabbAccumulator {
297    min: Vec3A,
298    max: Vec3A,
299}
300
301impl AabbAccumulator {
302    fn new() -> Self {
303        // Initialize in such a way that adds can be branchless but `finish` can
304        // still detect if nothing was added. The initial state has `min > max`,
305        // but the first add will make `min <= max`.
306        Self {
307            min: Vec3A::MAX,
308            max: Vec3A::MIN,
309        }
310    }
311
312    fn add_aabb(&mut self, aabb: Aabb3d) {
313        self.min = self.min.min(aabb.min);
314        self.max = self.max.max(aabb.max);
315    }
316
317    fn add_point(&mut self, position: Vec3A) {
318        self.min = self.min.min(position);
319        self.max = self.max.max(position);
320    }
321
322    /// Returns the enclosing AABB if at least one thing was added, otherwise `None`.
323    fn finish(self) -> Option<Aabb3d> {
324        if self.min.cmpgt(self.max).any() {
325            None
326        } else {
327            Some(Aabb3d {
328                min: self.min,
329                max: self.max,
330            })
331        }
332    }
333}
334
335// An index that corresponds to `Mesh::ATTRIBUTE_JOINT_INDEX` and `SkinnedMesh::joints`.
336#[derive(Copy, Clone, PartialEq, Debug, Reflect)]
337pub struct JointIndex(pub u16);
338
339/// A single vertex influence. Used by [`InfluenceIterator`].
340#[derive(Copy, Clone, PartialEq, Debug)]
341pub struct Influence {
342    pub vertex_index: usize,
343    pub joint_index: JointIndex,
344    pub joint_weight: f32,
345}
346
347/// Iterator over all vertex influences with non-zero weight.
348#[derive(Clone, Debug)]
349pub struct InfluenceIterator<'a> {
350    vertex_count: usize,
351    joint_indices: &'a [[u16; 4]],
352    joint_weights: &'a [[f32; 4]],
353    vertex_index: usize,
354    influence_index: usize,
355}
356
357impl<'a> InfluenceIterator<'a> {
358    pub fn new(mesh: &'a Mesh) -> Result<Self, MeshAttributeError> {
359        let joint_indices = expect_attribute_uint16x4(mesh, Mesh::ATTRIBUTE_JOINT_INDEX)?;
360        let joint_weights = expect_attribute_float32x4(mesh, Mesh::ATTRIBUTE_JOINT_WEIGHT)?;
361
362        Ok(InfluenceIterator {
363            vertex_count: joint_indices.len().min(joint_weights.len()),
364            joint_indices,
365            joint_weights,
366            vertex_index: 0,
367            influence_index: 0,
368        })
369    }
370
371    // `Mesh` only supports four influences, so we can make this const for
372    // simplicity. If `Mesh` gains support for variable influences then this
373    // will become a variable.
374    const MAX_INFLUENCES: usize = 4;
375}
376
377impl Iterator for InfluenceIterator<'_> {
378    type Item = Influence;
379
380    fn next(&mut self) -> Option<Influence> {
381        loop {
382            assert!(self.influence_index <= Self::MAX_INFLUENCES);
383            assert!(self.vertex_index <= self.vertex_count);
384
385            if self.influence_index >= Self::MAX_INFLUENCES {
386                self.influence_index = 0;
387                self.vertex_index += 1;
388            }
389
390            if self.vertex_index >= self.vertex_count {
391                return None;
392            }
393
394            let joint_index = self.joint_indices[self.vertex_index][self.influence_index];
395            let joint_weight = self.joint_weights[self.vertex_index][self.influence_index];
396
397            self.influence_index += 1;
398
399            if joint_weight > 0.0 {
400                return Some(Influence {
401                    vertex_index: self.vertex_index,
402                    joint_index: JointIndex(joint_index),
403                    joint_weight,
404                });
405            }
406        }
407    }
408}
409
410/// Generic error for when a mesh was expected to have a certain attribute with
411/// a certain format.
412#[derive(Copy, Clone, PartialEq, Debug, Error)]
413pub enum MeshAttributeError {
414    #[error("Missing attribute \"{0}\"")]
415    MissingAttribute(&'static str),
416    #[error("Attribute \"{0}\" has unexpected format {1:?}")]
417    UnexpectedFormat(&'static str, VertexFormat),
418}
419
420// Implements a function that returns a mesh attribute's data or `MeshAttributeError`.
421//
422// ```
423// impl_expect_attribute!(expect_attribute_float32x3, Float32x3, [f32; 3]);
424//
425// let positions: Vec<[f32; 3]> = expect_attribute_float32x3(mesh, Mesh::ATTRIBUTE_POSITION)?;
426// ```
427macro_rules! impl_expect_attribute {
428    ($name:ident, $value_type:ident, $output_type:ty) => {
429        fn $name<'a>(
430            mesh: &'a Mesh,
431            attribute: MeshVertexAttribute,
432        ) -> Result<&'a Vec<$output_type>, MeshAttributeError> {
433            match mesh.attribute(attribute) {
434                Some(VertexAttributeValues::$value_type(v)) => Ok(v),
435                Some(v) => {
436                    return Err(MeshAttributeError::UnexpectedFormat(
437                        attribute.name,
438                        v.into(),
439                    ))
440                }
441                None => return Err(MeshAttributeError::MissingAttribute(attribute.name)),
442            }
443        }
444    };
445}
446
447impl_expect_attribute!(expect_attribute_float32x3, Float32x3, [f32; 3]);
448impl_expect_attribute!(expect_attribute_float32x4, Float32x4, [f32; 4]);
449impl_expect_attribute!(expect_attribute_uint16x4, Uint16x4, [u16; 4]);
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454    use approx::assert_abs_diff_eq;
455    use bevy_asset::RenderAssetUsages;
456    use bevy_math::{bounding::BoundingVolume, vec3, vec3a};
457
458    #[test]
459    fn aabb_accumulator() {
460        assert_eq!(AabbAccumulator::new().finish(), None);
461
462        let nice_aabbs = &[
463            Aabb3d {
464                min: vec3a(1.0, 2.0, 3.0),
465                max: vec3a(5.0, 4.0, 3.0),
466            },
467            Aabb3d {
468                min: vec3a(-99.0, 2.0, 3.0),
469                max: vec3a(5.0, 4.0, 3.0),
470            },
471            Aabb3d {
472                min: vec3a(1.0, 2.0, 3.0),
473                max: vec3a(5.0, 99.0, 3.0),
474            },
475        ];
476
477        let naughty_aabbs = &[
478            Aabb3d {
479                min: Vec3A::MIN,
480                max: Vec3A::MAX,
481            },
482            Aabb3d {
483                min: Vec3A::MIN,
484                max: Vec3A::MIN,
485            },
486            Aabb3d {
487                min: Vec3A::MAX,
488                max: Vec3A::MAX,
489            },
490        ];
491
492        for aabbs in [nice_aabbs, naughty_aabbs] {
493            for &aabb in aabbs {
494                let point = aabb.min;
495
496                let mut one_aabb = AabbAccumulator::new();
497                let mut one_point = AabbAccumulator::new();
498
499                one_aabb.add_aabb(aabb);
500                one_point.add_point(point);
501
502                assert_eq!(one_aabb.finish(), Some(aabb));
503                assert_eq!(
504                    one_point.finish(),
505                    Some(Aabb3d {
506                        min: point,
507                        max: point
508                    })
509                );
510            }
511
512            {
513                let mut multiple_aabbs = AabbAccumulator::new();
514                let mut multiple_points = AabbAccumulator::new();
515
516                for &aabb in aabbs {
517                    multiple_aabbs.add_aabb(aabb);
518                    multiple_points.add_point(aabb.min);
519                    multiple_points.add_point(aabb.max);
520                }
521
522                let expected = aabbs.iter().cloned().reduce(|l, r| l.merge(&r));
523
524                assert_eq!(multiple_aabbs.finish(), expected);
525                assert_eq!(multiple_points.finish(), expected);
526            }
527        }
528    }
529
530    #[test]
531    fn influence_iterator() {
532        let mesh = Mesh::new(
533            wgpu_types::PrimitiveTopology::TriangleList,
534            RenderAssetUsages::default(),
535        );
536
537        assert_eq!(
538            InfluenceIterator::new(&mesh).err(),
539            Some(MeshAttributeError::MissingAttribute(
540                Mesh::ATTRIBUTE_JOINT_INDEX.name
541            ))
542        );
543
544        let mesh = mesh.with_inserted_attribute(
545            Mesh::ATTRIBUTE_JOINT_INDEX,
546            VertexAttributeValues::Uint16x4(vec![
547                [1, 0, 0, 0],
548                [0, 2, 0, 0],
549                [0, 0, 3, 0],
550                [0, 0, 0, 4],
551                [1, 2, 0, 0],
552                [3, 4, 5, 0],
553                [6, 7, 8, 9],
554            ]),
555        );
556
557        assert_eq!(
558            InfluenceIterator::new(&mesh).err(),
559            Some(MeshAttributeError::MissingAttribute(
560                Mesh::ATTRIBUTE_JOINT_WEIGHT.name
561            ))
562        );
563
564        let mesh = mesh.with_inserted_attribute(
565            Mesh::ATTRIBUTE_JOINT_WEIGHT,
566            VertexAttributeValues::Float32x4(vec![
567                [1.0, 0.0, 0.0, 0.0],
568                [0.0, 1.0, 0.0, 0.0],
569                [0.0, 0.0, 1.0, 0.0],
570                [0.0, 0.0, 0.0, 1.0],
571                [0.1, 0.9, 0.0, 0.0],
572                [0.1, 0.2, 0.7, 0.0],
573                [0.1, 0.2, 0.4, 0.3],
574            ]),
575        );
576
577        let expected = &[
578            Influence {
579                vertex_index: 0,
580                joint_index: JointIndex(1),
581                joint_weight: 1.0,
582            },
583            Influence {
584                vertex_index: 1,
585                joint_index: JointIndex(2),
586                joint_weight: 1.0,
587            },
588            Influence {
589                vertex_index: 2,
590                joint_index: JointIndex(3),
591                joint_weight: 1.0,
592            },
593            Influence {
594                vertex_index: 3,
595                joint_index: JointIndex(4),
596                joint_weight: 1.0,
597            },
598            Influence {
599                vertex_index: 4,
600                joint_index: JointIndex(1),
601                joint_weight: 0.1,
602            },
603            Influence {
604                vertex_index: 4,
605                joint_index: JointIndex(2),
606                joint_weight: 0.9,
607            },
608            Influence {
609                vertex_index: 5,
610                joint_index: JointIndex(3),
611                joint_weight: 0.1,
612            },
613            Influence {
614                vertex_index: 5,
615                joint_index: JointIndex(4),
616                joint_weight: 0.2,
617            },
618            Influence {
619                vertex_index: 5,
620                joint_index: JointIndex(5),
621                joint_weight: 0.7,
622            },
623            Influence {
624                vertex_index: 6,
625                joint_index: JointIndex(6),
626                joint_weight: 0.1,
627            },
628            Influence {
629                vertex_index: 6,
630                joint_index: JointIndex(7),
631                joint_weight: 0.2,
632            },
633            Influence {
634                vertex_index: 6,
635                joint_index: JointIndex(8),
636                joint_weight: 0.4,
637            },
638            Influence {
639                vertex_index: 6,
640                joint_index: JointIndex(9),
641                joint_weight: 0.3,
642            },
643        ];
644
645        assert_eq!(
646            InfluenceIterator::new(&mesh).unwrap().collect::<Vec<_>>(),
647            expected
648        );
649    }
650
651    fn aabb_assert_eq(a: Aabb3d, b: Aabb3d) {
652        assert_abs_diff_eq!(a.min.x, b.min.x);
653        assert_abs_diff_eq!(a.min.y, b.min.y);
654        assert_abs_diff_eq!(a.min.z, b.min.z);
655        assert_abs_diff_eq!(a.max.x, b.max.x);
656        assert_abs_diff_eq!(a.max.y, b.max.y);
657        assert_abs_diff_eq!(a.max.z, b.max.z);
658    }
659
660    // Like `transform_aabb`, but uses the naive method of transforming each corner.
661    fn naive_transform_aabb(input: JointAabb, transform: Affine3A) -> Aabb3d {
662        let minmax = [input.min(), input.max()];
663
664        let mut accumulator = AabbAccumulator::new();
665
666        for i in 0..8 {
667            let corner = vec3(
668                minmax[i & 1].x,
669                minmax[(i >> 1) & 1].y,
670                minmax[(i >> 2) & 1].z,
671            );
672
673            accumulator.add_point(transform.transform_point3(corner).into());
674        }
675
676        accumulator.finish().unwrap()
677    }
678
679    #[test]
680    fn transform_aabb() {
681        let aabbs = [
682            JointAabb {
683                center: Vec3::ZERO,
684                half_size: Vec3::ZERO,
685            },
686            JointAabb {
687                center: Vec3::ZERO,
688                half_size: vec3(2.0, 3.0, 4.0),
689            },
690            JointAabb {
691                center: vec3(2.0, 3.0, 4.0),
692                half_size: Vec3::ZERO,
693            },
694            JointAabb {
695                center: vec3(20.0, -30.0, 40.0),
696                half_size: vec3(5.0, 6.0, 7.0),
697            },
698        ];
699
700        // Various transforms, including awkward ones like skews and
701        // negative/zero scales.
702        let transforms = [
703            Affine3A::IDENTITY,
704            Affine3A::from_cols(Vec3A::X, Vec3A::Z, Vec3A::Y, vec3a(1.0, 2.0, 3.0)),
705            Affine3A::from_cols(Vec3A::Y, Vec3A::X, Vec3A::Z, vec3a(1.0, 2.0, 3.0)),
706            Affine3A::from_cols(Vec3A::Z, Vec3A::Y, Vec3A::X, vec3a(1.0, 2.0, 3.0)),
707            Affine3A::from_scale(Vec3::ZERO),
708            Affine3A::from_scale(vec3(2.0, 3.0, 4.0)),
709            Affine3A::from_scale(vec3(-2.0, 3.0, -4.0)),
710            Affine3A::from_cols(
711                vec3a(1.0, 2.0, -3.0),
712                vec3a(4.0, -5.0, 6.0),
713                vec3a(-7.0, 8.0, 9.0),
714                vec3a(1.0, -2.0, 3.0),
715            ),
716        ];
717
718        for aabb in aabbs {
719            for transform in transforms {
720                aabb_assert_eq(
721                    super::transform_aabb(aabb, transform),
722                    naive_transform_aabb(aabb, transform),
723                );
724            }
725        }
726    }
727}