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 pub parallel: bool,
56}
57
58impl Default for SkinnedAabbPluginSettings {
59 fn default() -> Self {
60 SkinnedAabbPluginSettings { parallel: true }
61 }
62}
63
64pub type JointIndex = u16;
66
67pub const MAX_INFLUENCES: usize = 4;
70
71#[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#[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 pub source: SkinnedAabbSourceAssets,
108
109 pub aabbs: Box<[PackedAabb3d]>,
111
112 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 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#[derive(Component, Debug, Default)]
141pub struct SkinnedAabb {
142 pub asset: Handle<SkinnedAabbAsset>,
143}
144
145fn 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#[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 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 let mut optional_aabbs: Box<[Option<Aabb3d>]> = vec![None; num_joints].into_boxed_slice();
250
251 for Influence {
254 position,
255 joint_index,
256 } in InfluenceIterator::new(mesh)
257 {
258 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 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 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 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 #[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
371pub 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#[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#[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
446fn 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 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 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}