bevy_mod_skinned_aabb/
lib.rs

1use bevy_app::{App, Plugin, PostUpdate, Update};
2use bevy_asset::{Asset, AssetApp, AssetId, Assets, Handle};
3use bevy_camera::{primitives::Aabb, visibility::VisibilitySystems};
4use bevy_ecs::{
5    change_detection::{Res, ResMut},
6    component::Component,
7    entity::Entity,
8    query::Without,
9    resource::Resource,
10    schedule::IntoScheduleConfigs,
11    system::{Commands, Query},
12    world::Mut,
13};
14#[cfg(feature = "trace")]
15use bevy_log::info_span;
16use bevy_math::{
17    Affine3A, Vec3, Vec3A,
18    bounding::{Aabb3d, BoundingVolume},
19};
20use bevy_mesh::Mesh3d;
21use bevy_mesh::{
22    Mesh, VertexAttributeValues,
23    skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
24};
25use bevy_reflect::{Reflect, TypePath};
26use bevy_transform::{TransformSystems, components::GlobalTransform};
27
28pub mod debug;
29
30pub mod prelude {
31    pub use crate::SkinnedAabbPlugin;
32    pub use crate::debug::prelude::*;
33}
34
35#[derive(Default)]
36pub struct SkinnedAabbPlugin;
37
38impl Plugin for SkinnedAabbPlugin {
39    fn build(&self, app: &mut App) {
40        app.init_asset::<SkinnedAabbAsset>()
41            .insert_resource(SkinnedAabbPluginSettings { parallel: true })
42            .add_systems(Update, create_skinned_aabbs)
43            .add_systems(
44                PostUpdate,
45                update_skinned_aabbs
46                    .after(TransformSystems::Propagate)
47                    .before(VisibilitySystems::CheckVisibility),
48            );
49    }
50}
51
52#[derive(Resource, Copy, Clone)]
53pub struct SkinnedAabbPluginSettings {
54    // If true, the skinned AABB update will run on multiple threads. Defaults to true.
55    pub parallel: bool,
56}
57
58impl Default for SkinnedAabbPluginSettings {
59    fn default() -> Self {
60        SkinnedAabbPluginSettings { parallel: true }
61    }
62}
63
64// Match the Mesh limits on joint indices (ATTRIBUTE_JOINT_INDEX = VertexFormat::Uint16x4)
65pub type JointIndex = u16;
66
67// TODO: Bit janky hard-coding this here. Could petition for it to be added to
68// bevy_pbr alongside MAX_JOINTS?
69pub const MAX_INFLUENCES: usize = 4;
70
71// An `Aabb3d` without padding.
72#[derive(Copy, Clone, Debug, Reflect)]
73pub struct PackedAabb3d {
74    pub min: Vec3,
75    pub max: Vec3,
76}
77
78impl From<PackedAabb3d> for Aabb3d {
79    fn from(value: PackedAabb3d) -> Self {
80        Self {
81            min: value.min.into(),
82            max: value.max.into(),
83        }
84    }
85}
86
87impl From<Aabb3d> for PackedAabb3d {
88    fn from(value: Aabb3d) -> Self {
89        Self {
90            min: value.min.into(),
91            max: value.max.into(),
92        }
93    }
94}
95
96// The assets that are used to create a `SkinnedAabbAsset`.
97#[derive(PartialEq, Eq, Debug)]
98pub struct SkinnedAabbSourceAssets {
99    pub mesh: AssetId<Mesh>,
100    pub inverse_bindposes: AssetId<SkinnedMeshInverseBindposes>,
101}
102
103#[derive(Asset, Debug, TypePath)]
104pub struct SkinnedAabbAsset {
105    // The source assets. We keep these so that entities can reuse existing
106    // SkinnedAabbAssets by searching for matching source assets.
107    pub source: SkinnedAabbSourceAssets,
108
109    // Joint-space AABB of each skinned joint.
110    pub aabbs: Box<[PackedAabb3d]>,
111
112    // Mapping from `SkinnedAabbAsset::aabbs` index to `SkinnedMesh::joints` index.
113    pub aabb_index_to_joint_index: Box<[JointIndex]>,
114}
115
116impl SkinnedAabbAsset {
117    pub fn aabb(&self, aabb_index: usize) -> PackedAabb3d {
118        self.aabbs[aabb_index]
119    }
120
121    pub fn num_aabbs(&self) -> usize {
122        self.aabbs.len()
123    }
124
125    pub fn world_from_joint(
126        &self,
127        aabb_index: usize,
128        skinned_mesh: &SkinnedMesh,
129        joints: &Query<&GlobalTransform>,
130    ) -> Option<Affine3A> {
131        // TODO: Should return an error instead of silently failing?
132        let joint_index = *self.aabb_index_to_joint_index.get(aabb_index)? as usize;
133        let joint_entity = *skinned_mesh.joints.get(joint_index)?;
134
135        Some(joints.get(joint_entity).ok()?.affine())
136    }
137}
138
139// TODO: Is this name misleading? Could be interpreted as the actual AABB.
140#[derive(Component, Debug, Default)]
141pub struct SkinnedAabb {
142    pub asset: Handle<SkinnedAabbAsset>,
143}
144
145// Return `aabb` extended to include `point`. If `aabb` is none, return the
146// AABB of `point`.
147fn merge(aabb: Option<Aabb3d>, point: Vec3A) -> Aabb3d {
148    match aabb {
149        Some(aabb) => Aabb3d {
150            min: point.min(aabb.min),
151            max: point.max(aabb.max),
152        },
153        None => Aabb3d {
154            min: point,
155            max: point,
156        },
157    }
158}
159
160struct Influence {
161    position: Vec3,
162    joint_index: usize,
163}
164
165/// Iterator over all vertex influences with non-zero weight.
166#[derive(Default)]
167struct InfluenceIterator<'a> {
168    vertex_index: usize,
169    influence_index: usize,
170    positions: &'a [[f32; 3]],
171    joint_indices: &'a [[u16; 4]],
172    joint_weights: &'a [[f32; 4]],
173}
174
175impl<'a> InfluenceIterator<'a> {
176    fn new(mesh: &'a Mesh) -> Self {
177        if let (
178            Some(VertexAttributeValues::Float32x3(positions)),
179            Some(VertexAttributeValues::Uint16x4(joint_indices)),
180            Some(VertexAttributeValues::Float32x4(joint_weights)),
181        ) = (
182            mesh.attribute(Mesh::ATTRIBUTE_POSITION),
183            mesh.attribute(Mesh::ATTRIBUTE_JOINT_INDEX),
184            mesh.attribute(Mesh::ATTRIBUTE_JOINT_WEIGHT),
185        ) {
186            if (joint_indices.len() != positions.len()) | (joint_weights.len() != positions.len()) {
187                // TODO: Should be an error?
188                return InfluenceIterator::default();
189            }
190
191            return InfluenceIterator {
192                vertex_index: 0,
193                influence_index: 0,
194                positions,
195                joint_indices,
196                joint_weights,
197            };
198        }
199
200        InfluenceIterator::default()
201    }
202}
203
204impl Iterator for InfluenceIterator<'_> {
205    type Item = Influence;
206
207    fn next(&mut self) -> Option<Influence> {
208        loop {
209            assert!(self.influence_index <= MAX_INFLUENCES);
210            assert!(self.vertex_index <= self.positions.len());
211
212            if self.influence_index >= MAX_INFLUENCES {
213                self.influence_index = 0;
214                self.vertex_index += 1;
215            }
216
217            if self.vertex_index >= self.positions.len() {
218                break None;
219            }
220
221            let position = Vec3::from_array(self.positions[self.vertex_index]);
222            let joint_index = self.joint_indices[self.vertex_index][self.influence_index];
223            let joint_weight = self.joint_weights[self.vertex_index][self.influence_index];
224
225            self.influence_index += 1;
226
227            if joint_weight > 0.0 {
228                break Some(Influence {
229                    position,
230                    joint_index: joint_index as usize,
231                });
232            }
233        }
234    }
235}
236
237fn create_skinned_aabb_asset(
238    mesh: &Mesh,
239    mesh_handle: AssetId<Mesh>,
240    inverse_bindposes: &SkinnedMeshInverseBindposes,
241    inverse_bindposes_handle: AssetId<SkinnedMeshInverseBindposes>,
242) -> SkinnedAabbAsset {
243    let num_joints = inverse_bindposes.len();
244
245    // TODO: Error if num_joints exceeds JointIndex limits?
246
247    // Allocate an optional AABB for each joint.
248
249    let mut optional_aabbs: Box<[Option<Aabb3d>]> = vec![None; num_joints].into_boxed_slice();
250
251    // Iterate over all influences and add the vertex position to the joint's AABB.
252
253    for Influence {
254        position,
255        joint_index,
256    } in InfluenceIterator::new(mesh)
257    {
258        // TODO: Replace assert with error?
259
260        assert!(
261            joint_index < num_joints,
262            "Joint index out of range. Joint index = {joint_index}, number of joints = {num_joints}.",
263        );
264
265        let jointspace_position = inverse_bindposes[joint_index].transform_point3(position);
266
267        optional_aabbs[joint_index] = Some(merge(
268            optional_aabbs[joint_index],
269            Vec3A::from(jointspace_position),
270        ));
271    }
272
273    // Create the final list of AABBs. This will only contain joints that had
274    // vertices skinned to them.
275
276    let num_aabbs = optional_aabbs.iter().filter(|o| o.is_some()).count();
277
278    let mut aabbs = Vec::<PackedAabb3d>::with_capacity(num_aabbs);
279    let mut aabb_index_to_joint_index = Vec::<JointIndex>::with_capacity(num_aabbs);
280
281    for (joint_index, _) in optional_aabbs.iter().enumerate() {
282        if let Some(aabb) = optional_aabbs[joint_index] {
283            aabbs.push(aabb.into());
284            aabb_index_to_joint_index.push(joint_index as JointIndex);
285        }
286    }
287
288    assert!(aabbs.len() == num_aabbs);
289    assert!(aabb_index_to_joint_index.len() == num_aabbs);
290
291    SkinnedAabbAsset {
292        source: SkinnedAabbSourceAssets {
293            mesh: mesh_handle,
294            inverse_bindposes: inverse_bindposes_handle,
295        },
296        aabbs: aabbs.into(),
297        aabb_index_to_joint_index: aabb_index_to_joint_index.into(),
298    }
299}
300
301#[cfg(feature = "trace")]
302fn asset_handle_to_string<A: Asset>(h: &Handle<A>) -> &str {
303    h.path().and_then(|p| p.path().to_str()).unwrap_or("")
304}
305
306fn create_skinned_aabb_component(
307    skinned_aabb_assets: &mut ResMut<Assets<SkinnedAabbAsset>>,
308    mesh_assets: &Assets<Mesh>,
309    mesh_handle: &Handle<Mesh>,
310    inverse_bindposes_assets: &Assets<SkinnedMeshInverseBindposes>,
311    inverse_bindposes_handle: &Handle<SkinnedMeshInverseBindposes>,
312) -> Option<SkinnedAabb> {
313    // If the source assets are invalid then return None.
314    //
315    // TODO: I think this is needed to handle assets that are temporarily
316    // invalid then get added later. But if the assets are never valid then
317    // we're awkwardly checking them every frame. Would be nice to improve, but
318    // hopefully this whole thing moves into the asset pipeline and the issue
319    // becomes moot.
320
321    let (Some(mesh), Some(inverse_bindposes)) = (
322        mesh_assets.get(mesh_handle),
323        inverse_bindposes_assets.get(inverse_bindposes_handle),
324    ) else {
325        return None;
326    };
327
328    let source = SkinnedAabbSourceAssets {
329        mesh: mesh_handle.id(),
330        inverse_bindposes: inverse_bindposes_handle.id(),
331    };
332
333    // Check for an existing asset that matches the source assets.
334    //
335    // TODO: Linear search is not great if there's many assets. But in the
336    // long run this should all move to the asset pipeline.
337
338    let existing_asset_id = skinned_aabb_assets
339        .iter()
340        .find(|(_, candidate_asset)| candidate_asset.source == source)
341        .map(|(id, _)| id);
342
343    if let Some(existing_asset_id) = existing_asset_id
344        && let Some(existing_asset_handle) =
345            skinned_aabb_assets.get_strong_handle(existing_asset_id)
346    {
347        return Some(SkinnedAabb {
348            asset: existing_asset_handle,
349        });
350    }
351
352    // No existing asset found so create a new one.
353
354    #[cfg(feature = "trace")]
355    let _span = info_span!(
356        "bevy_mod_skinned_aabb::create_skinned_aabb_asset",
357        asset = asset_handle_to_string(mesh_handle)
358    )
359    .entered();
360
361    let asset = skinned_aabb_assets.add(create_skinned_aabb_asset(
362        mesh,
363        mesh_handle.id(),
364        inverse_bindposes,
365        inverse_bindposes_handle.id(),
366    ));
367
368    Some(SkinnedAabb { asset })
369}
370
371// If any entities have `Mesh3d` and `SkinnedMesh` components but no
372// `SkinnedAabb` component, try to create one.
373pub fn create_skinned_aabbs(
374    mut commands: Commands,
375    mut skinned_aabb_assets: ResMut<Assets<SkinnedAabbAsset>>,
376    mesh_assets: Res<Assets<Mesh>>,
377    inverse_bindposes_assets: Res<Assets<SkinnedMeshInverseBindposes>>,
378    query: Query<(Entity, &Mesh3d, &SkinnedMesh), Without<SkinnedAabb>>,
379) {
380    for (entity, mesh, skinned_mesh) in &query {
381        if let Some(skinned_aabb) = create_skinned_aabb_component(
382            &mut skinned_aabb_assets,
383            &mesh_assets,
384            &mesh.0,
385            &inverse_bindposes_assets,
386            &skinned_mesh.inverse_bindposes,
387        ) {
388            commands.entity(entity).insert(skinned_aabb);
389        }
390    }
391}
392
393// Scalar version of aabb_transformed_by, kept here for reference. Takes roughly
394// 1.4x - 1.6x the time of the simd version.
395#[cfg(any())]
396fn aabb_transformed_by(input: Aabb3d, transform: Affine3A) -> Aabb3d {
397    let rs = transform.matrix3.to_cols_array_2d();
398    let t = transform.translation;
399
400    let mut min = t;
401    let mut max = t;
402
403    for i in 0..3 {
404        for j in 0..3 {
405            let e = rs[j][i] * input.min[j];
406            let f = rs[j][i] * input.max[j];
407
408            min[i] += e.min(f);
409            max[i] += e.max(f);
410        }
411    }
412
413    return Aabb3d { min, max };
414}
415
416// Return an AABB that contains the transformed input AABB.
417//
418// Algorithm from "Transforming Axis-Aligned Bounding Boxes", James Arvo, Graphics Gems (1990).
419#[inline]
420pub fn aabb_transformed_by(input: PackedAabb3d, transform: Affine3A) -> Aabb3d {
421    let rs = transform.matrix3;
422    let t = transform.translation;
423
424    let e_x = rs.x_axis * Vec3A::splat(input.min.x);
425    let e_y = rs.y_axis * Vec3A::splat(input.min.y);
426    let e_z = rs.z_axis * Vec3A::splat(input.min.z);
427
428    let f_x = rs.x_axis * Vec3A::splat(input.max.x);
429    let f_y = rs.y_axis * Vec3A::splat(input.max.y);
430    let f_z = rs.z_axis * Vec3A::splat(input.max.z);
431
432    let min_x = e_x.min(f_x);
433    let min_y = e_y.min(f_y);
434    let min_z = e_z.min(f_z);
435
436    let max_x = e_x.max(f_x);
437    let max_y = e_y.max(f_y);
438    let max_z = e_z.max(f_z);
439
440    let min = t + min_x + min_y + min_z;
441    let max = t + max_x + max_y + max_z;
442
443    Aabb3d { min, max }
444}
445
446// Given a skinned mesh and world-space joints, return the entity-space AABB.
447// Returns None if no joints were found or the asset was not found.
448fn get_skinned_aabb(
449    component: &SkinnedAabb,
450    joints: &Query<&GlobalTransform>,
451    assets: &Assets<SkinnedAabbAsset>,
452    skinned_mesh: &SkinnedMesh,
453    world_from_entity: &GlobalTransform,
454) -> Option<Aabb> {
455    let asset = assets.get(&component.asset)?;
456    let world_from_entity = world_from_entity.affine();
457    let num_aabbs = asset.num_aabbs();
458
459    if num_aabbs == 0 {
460        return None;
461    }
462
463    let entity_from_world = world_from_entity.inverse();
464
465    let mut entity_aabb = Aabb3d {
466        min: Vec3A::MAX,
467        max: Vec3A::MIN,
468    };
469
470    for aabb_index in 0..num_aabbs {
471        if let Some(world_from_joint) = asset.world_from_joint(aabb_index, skinned_mesh, joints) {
472            let entity_from_joint = entity_from_world * world_from_joint;
473            let joint_aabb = aabb_transformed_by(asset.aabb(aabb_index), entity_from_joint);
474
475            entity_aabb = entity_aabb.merge(&joint_aabb);
476        }
477    }
478
479    // If min > max then no joints were found.
480    if entity_aabb.min.x > entity_aabb.max.x {
481        return None;
482    }
483
484    Some(Aabb::from_min_max(
485        Vec3::from(entity_aabb.min),
486        Vec3::from(entity_aabb.max),
487    ))
488}
489
490pub fn update_skinned_aabbs(
491    mut query: Query<(&mut Aabb, &SkinnedAabb, &SkinnedMesh, &GlobalTransform)>,
492    joints: Query<&GlobalTransform>,
493    assets: Res<Assets<SkinnedAabbAsset>>,
494    settings: Res<SkinnedAabbPluginSettings>,
495) {
496    // Awkward closure so we don't have to duplicate the parallel/non-parallel paths.
497    // TODO: Urgh. Alternatives?
498    let update =
499        |(mut entity_aabb, skinned_aabb, skinned_mesh, world_from_entity): (Mut<Aabb>, _, _, _)| {
500            if let Some(updated) = get_skinned_aabb(
501                skinned_aabb,
502                &joints,
503                &assets,
504                skinned_mesh,
505                world_from_entity,
506            ) {
507                *entity_aabb = updated;
508            }
509        };
510
511    if settings.parallel {
512        query.par_iter_mut().for_each(update);
513    } else {
514        query.iter_mut().for_each(update);
515    }
516}