Skip to content

Commit 9d2db88

Browse files
Improve frustum culling of skinned meshes through per-joint bounds (#21837)
## Objective Mostly fix #4971 by adding a new option for updating skinned mesh `Aabb` components from joint transforms. https://github.com/user-attachments/assets/c25b31fa-142d-462b-9a1d-012ea928f839 This fixes cases where vertex positions are only modified through skinning. It doesn't fix other cases like morph targets and vertex shaders. The PR kind of upstreams [`bevy_mod_skinned_aabb`](https://github.com/greeble-dev/bevy_mod_skinned_aabb), but with some changes to make it simpler and more reliable. ### Dependencies - (MERGED) #21732 (or something similar) is desirable to make the new option work with `RenderAssetUsages::RENDER_WORLD`-only meshes. - This PR is authored as if 21732 has landed. But if that doesn't happen then I can adjust this PR to note the limitation. - (Optional) #21845 adds an option related to skinned mesh bounds. - Either PR can land first - the second will need to be updated. ## Background If a main world entity has a `Mesh3d` component then it's automatically assigned an `Aabb` component. This is done by `bevy_camera` or `bevy_gltf`. The `Aabb` is used by `bevy_camera` for frustum culling. It can also be used by `bevy_picking` as an optimization, and by third party crates. But there's a problem - the `Aabb` can be wrong if something changes the mesh's vertex positions after the `Aabb` is calculated. This can be done by vertex shaders - notably skinning and morph targets - or by mutating the `Mesh` asset (#4294). For the skinning case, the most common solution has been to disable frustum culling via the `NoFrustumCulling` component. This is simple, and might even be the most efficient approach for apps where meshes tend to stay on-screen. But it's annoying to implement, bad for apps where meshes are often off-screen, and it only fixes frustum culling - it doesn't help other systems that use the `Aabb`. ## Solution This PR adds a reliable and reasonably efficient method of updating the `Aabb` of a skinned mesh from its animated joint transforms. See the "How does it work" section for more detail. The glTF loader can add skinned bounds automatically if a new `GltfSkinnedMeshBoundsPolicy` option is enabled in `GltfPlugin` or `GltfLoaderSettings`: ```rust app.add_plugins(DefaultPlugins.set(GltfPlugin { skinned_mesh_bounds_policy: GltfSkinnedMeshBoundsPolicy::Dynamic, ..default() })) ``` _The new glTF loader option is enabled by default_. I think this is the right choice for several reasons: - Bugs caused by skinned mesh culling have been a regular pain for both new and experienced users. Now the most common case Just Works(tm). - The CPU cost is modest (see later section), and sophisticated users can opt-out. - GPU limited apps might see a performance increase if the user was previously disabling culling. Non-glTF cases require some manual steps. The user must ask `Mesh` to generate the skinned bounds, and then add the `DynamicSkinnedMeshBounds` marker component to their mesh entity. ```rust mesh.generate_skinned_mesh_bounds()?; let mesh_asset = mesh_assets.add(mesh); entity.insert((Mesh3d(mesh_asset), DynamicSkinnedMeshBounds)); ``` See the `custom_skinned_mesh` example for real code. ## Bonus Features ### `GltfSkinnedMeshBoundsPolicy::NoFrustumCulling` This is a convenience for users who prefer the `NoFrustumCulling` workaround, but want to avoid the hassle of adding it after a glTF scene has been spawned. ```rust app.add_plugins(DefaultPlugins.set(GltfPlugin { skinned_mesh_bounds_policy: GltfSkinnedMeshBoundsPolicy::NoFrustumCulling, ..default() })) ``` PR #21845 is also adding an option related to skinned mesh bounds. I'm fine if that PR lands first - I'll update this PR to include the option. ### Gizmos `bevy_gizmos::SkinnedMeshBoundsGizmoPlugin` can draw the per-joint AABBs. ```rust fn toggle_skinned_mesh_bounds(mut config: ResMut<GizmoConfigStore>) { config.config_mut::<SkinnedMeshBoundsGizmoConfigGroup>().1.draw_all ^= true; } ``` The name is debatable. It's not technically drawing the bounds of the skinned mesh - it's drawing the per-joint bounds that contribute to the bounds of the skinned mesh. ## Testing ```sh cargo run --example test_skinned_mesh_bounds # Press `B` to show mesh bounds, 'J' to show joint bounds. cargo run --example scene_viewer --features "free_camera" -- "assets/models/animated/Fox.glb" cargo run --example scene_viewer --features "free_camera" -- "assets/models/SimpleSkin/SimpleSkin.gltf" # More complicated mesh downloaded from https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main/Models/RecursiveSkeletons cargo run --example scene_viewer --features "free_camera" -- "RecursiveSkeletons.glb" cargo run --example custom_skinned_mesh ``` I also hacked `custom_skinned_mesh` to simulate awkward cases like rotated and off-screen entities. ## How Does It Work? <details><summary>Click to expand</summary> ### Summary `Mesh::generated_skinned_mesh_bounds` calculates an AABB for each joint in the mesh - the AABB encloses all the vertices skinned to that joint. Then every frame, `bevy_camera::update_skinned_mesh_bounds` uses the current joint transforms to calculate an `Aabb` that encloses all the joint AABBs. This approach is reliable, in that the final `Aabb` will always enclose the skinned vertices. But it can be larger than necessary. In practice it's tight enough to be useful, and rarely more than 50% bigger. This approach works even with non-rigid transforms and soft skinning. If there's any doubt then I can add more detail. ### Awkward Bits The solution is not as simple and efficient as it could be. #### Problem 1: Joint transforms are world-space, `Aabb` is entity-space. - Ideally we'd use the world-space joint transforms to calculate a world-space `Aabb`, but that's not possible. - The obvious solution is to transform the joints to entity-space, so the `Aabb` is directly calculated in entity-space. - But that means an extra matrix multiply per joint. - This PR calculates the `Aabb` in world-space and then transforms it to entity-space. - That avoids a matrix multiply per-joint, but can increase the size of the `Aabb`. #### Problem 2: Joint AABBs are in a surprising(?) space. - When creating joint AABBs from a mesh, the intuitive solution would be to calculate them in joint-space. - Then the update just has to transform them by the world-space joint transform. - But to calculate them in joint-space we need both the bind pose vertex positions and the bind pose joint transforms. - These two parts are in separate assets - `Mesh` and `SkinnedMeshInverseBindposes` - and those assets can be mixed and matched. - So we'd need to calculate a `SkinnedMeshBoundsAsset` for each combination of `Mesh` and `SkinnedMeshInverseBindposes`. - (`bevy_mod_skinned_aabb` uses this approach - it's slow and fragile.) - This PR calculates joint AABBs in *mesh-space* (or more strictly speaking: bind pose space). - That can be done with just the `Mesh` asset. - One downside is that the update needs an extra matrix multiply so we can go from mesh-space to world-space. - However, this might become a performance advantage if frustum culling changes - see the "Future Options" section. - Another minor downside is that mesh-space AABBs (red in the screenshot below) tend to be bigger than joint-space AABBs (green), since joints with one long axis might be at an awkward angle in mesh-space. <img width="1085" height="759" alt="image" src="https://github.com/user-attachments/assets/a02a28c3-8882-412c-9be1-64109b767da7" /> ### Future Options For frustum culling there's a cheeky way to optimize and simplify skinned bounds - put frustum culling in the renderer and calculate a world-space AABB during `extract_skins`. The joint transform will be already loaded and in the right space, so we can avoid an entity lookup and matrix multiply. I estimate this would make skinned bounds 3x faster. Another option is to change main world frustum culling to use a world-space AABB. So there would be a new `GlobalAabb` component that gets updated each frame from `Aabb` and the entity transform (which is basically the same as transform propagation and the relationship between `Transform` and `GlobalTransform`). This has some advantages and disadvantages but I won't get into them here - I think putting frustum culling into the renderer is a better option. (Note that putting frustum culling into the renderer doesn't mean removing the current main world visibility system - it just means the main world system would be separate opt-in system) </details> ## Performance <details><summary>Click to expand</summary> ### Initialization Creating the skinned bounds asset for `Fox.glb` (576 verts, 22 skinned joints) takes **0.03ms**. Loading the whole glTF takes 8.7ms, so this is a **<1% increase**. ### Per-Frame The `many_foxes` example has 1000 skinned meshes, each with 22 skinned joints. Updating the skinned bounds takes **0.086ms**. This is a throughput of roughly 250,000 joints per millisecond, using two threads. <img width="2404" height="861" alt="image" src="https://github.com/user-attachments/assets/c27165ae-dc6c-4f6b-bbfb-4e211ab0263c" /> The whole animation update takes 3.67ms (where "animation update" = advancing players + graph evaluation + transform propagation). So we can kinda sorta claim that this PR increases the cost of skinned animation by roughly **3%**. But that's very hand-wavey and situation dependent. This was tested on an AMD Ryzen 7900 but with `TaskPoolOptions::with_num_threads(6)` to simulate a lower spec CPU. Comparing against a few other threading options: - Non-parallel: **0.141ms**. - 6 threads (2 compute threads): **0.086ms**. - 24 threads (15 compute threads): **0.051ms**. So the parallel iterator is better but quickly hits diminishing returns as the number of threads increases. ### Future Options The "How Does It Work" section mentions moving skinned mesh bounds into the renderer's skin extraction. Based on some microbenchmarks, I estimate this would reduce non-parallel `many_foxes` from 0.141ms to 0.049ms, so roughly 3x faster. Requiring AVX2 (to enable broadcast loads) or pre-splatting (to fake broadcast loads for SSE) would knock off another 25%. And fancier SIMD approaches could do better again. There's also approaches that trade reliability for performance. For character rigs, an effective optimization is to fold face and finger joints into a single bound on the head and hand joints. This can reduce the number of joints required by 50-80%. </details> ## FAQ <details><summary>Click to expand</summary> #### Why can't it be automatically added to any mesh? Then the glTF importer and custom mesh generators wouldn't need special logic. `bevy_mod_skinned_aabb` took the automatic approach, and I don't think the outcome was good. It needs some surprisingly fiddly and fragile logic to decide when an entity has the right combination of assets in the right loaded state. And it can never work with `RenderAssetUsages::RENDER_WORLD`. So this PR takes a more modest and manual approach. I think there's plenty of scope to generalise and automate as the asset pipeline matures. If the glTF importer becomes a purer glTF -> BSN transform, then adding skinned bounds could be a general scene/asset transform that's shared with other importers and custom mesh generators. #### Why is the data in `Mesh`? Shouldn't it go in `SkinnedMesh` or `SkinnedMeshInverseBindposes`? That might seem intuitive, but it wouldn't work in practice - the data is derived from `Mesh` alone. `SkinnedMesh` doesn't work because it's per mesh instance, so the data would be duplicated. `SkinnedMeshInverseBindposes` doesn't work because it can be shared between multiple meshes. The names are a bit misleading - `Mesh` does contain some skinning data, while `SkinnedMesh` and `SkinnedMeshInverseBindposes` are more like joint bindings one step removed from the vertex data. #### Why not put the bounds on the joint entities? This is surprisingly tricky in practice because multiple meshes can be bound to the same joint entity. So there would need to be logic that tracks the bindings and updates the bounds as meshes are added and removed. #### Why is the `DynamicSkinnedMeshBounds` component required? It's an optimisation for users who want to opt out. It might also be useful for future expansion, like adding options to approximate the bounds with an AABB attached to a single joint. #### Why are the update system and `DynamicSkinnedMeshBounds` component in `bevy_camera`? Shouldn't they be in `bevy_mesh`? `bevy_camera` is the owner and main user of `Aabb`, and already has some mesh related logic (`calculate_bounds` automatically adds an `Aabb` to mesh entities). So putting it in `bevy_camera` is consistent with the current structure. I'd agree that it's a little awkward though and could change in future. </details> ## What Do Other Engines Do? <details><summary>Click to expand</summary> - **Unreal**: Automatically uses [collision shapes](https://dev.epicgames.com/documentation/en-us/unreal-engine/physics-asset-editor-in-unreal-engine) attached to joints, which is similar to this PR in practice but fragile and inefficient. Also supports various fixed bounds options. - **Unity**: Fixed bounds attached to the root bone. Automatically calculated from animation poses or specified manually ([documentation](https://docs.unity3d.com/6000.4/Documentation/Manual/troubleshooting-skinned-mesh-renderer-visibility.html)). - **Godot**: Appears to use roughly the same method as this PR, although I didn't 100% confirm. See [`MeshStorage::mesh_get_aabb`](https://github.com/godotengine/godot/blob/fafc07335bdecacd96b548c4119fbe1f47ee5866/servers/rendering/renderer_rd/storage_rd/mesh_storage.cpp#L650) and [`RendererSceneCull::_update_instance_aabb`](https://github.com/godotengine/godot/blob/235a32ad11f40ecba26d6d9ceea8ab245c13adb0/servers/rendering/renderer_scene_cull.cpp#L1991). - **O3DE**: Fixed bounds attached to root bone, plus option to approximate the AABB from joint origins and a fudge factor. - **Northlight** (Remedy, Alan Wake 2): Specifically for vegetation, calculates bounds from joint extents on GPU ([source](https://gdcvault.com/play/1034310/Large-Scale-GPU-Based-Skinning), slide 48) An approach that's been proposed several times for Bevy is copying Unity's "fixed AABB from animation poses". I think this is more complicated and less reliable than many people expect. More complicated because linking animations to meshes can often be difficult. Less reliable because it doesn't account for ragdolls and procedural animation. But it could still be viable for for simple cases like a single self-contained glTF with basic animation. </details> --------- Co-authored-by: Alice Cecile <[email protected]>
1 parent 304265b commit 9d2db88

File tree

15 files changed

+1399
-10
lines changed

15 files changed

+1399
-10
lines changed

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5092,6 +5092,14 @@ doc-scrape-examples = false
50925092
[package.metadata.example.test_invalid_skinned_mesh]
50935093
hidden = true
50945094

5095+
[[example]]
5096+
name = "test_skinned_mesh_bounds"
5097+
path = "tests/3d/test_skinned_mesh_bounds.rs"
5098+
doc-scrape-examples = true
5099+
5100+
[package.metadata.example.test_skinned_mesh_bounds]
5101+
hidden = true
5102+
50955103
[profile.wasm-release]
50965104
inherits = "release"
50975105
opt-level = "z"

crates/bevy_camera/src/visibility/mod.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ use core::any::TypeId;
66
use bevy_ecs::entity::EntityHashMap;
77
use bevy_ecs::lifecycle::HookContext;
88
use bevy_ecs::world::DeferredWorld;
9+
use bevy_mesh::skinning::{
10+
entity_aabb_from_skinned_mesh_bounds, SkinnedMesh, SkinnedMeshInverseBindposes,
11+
};
912
use derive_more::derive::{Deref, DerefMut};
1013
pub use range::*;
1114
pub use render_layers::*;
@@ -265,6 +268,19 @@ impl<'a> SetViewVisibility for Mut<'a, ViewVisibility> {
265268
#[reflect(Component, Default, Debug)]
266269
pub struct NoFrustumCulling;
267270

271+
/// Use this component to enable dynamic skinned mesh bounds. The [`Aabb`]
272+
/// component of the skinned mesh will be automatically updated each frame based
273+
/// on the current joint transforms.
274+
///
275+
/// `DynamicSkinnedMeshBounds` depends on data from `Mesh::skinned_mesh_bounds`
276+
/// and `SkinnedMesh`. The resulting `Aabb` will reliably enclose meshes where
277+
/// vertex positions are only affected by skinning. But the `Aabb` may be larger
278+
/// than is optimal, and doesn't account for morph targets, vertex shaders, and
279+
/// anything else that modifies vertex positions.
280+
#[derive(Debug, Component, Default, Reflect)]
281+
#[reflect(Component, Default, Debug)]
282+
pub struct DynamicSkinnedMeshBounds;
283+
268284
/// Collection of entities visible from the current view.
269285
///
270286
/// This component contains all entities which are visible from the currently
@@ -420,7 +436,9 @@ impl Plugin for VisibilityPlugin {
420436
.add_systems(
421437
PostUpdate,
422438
(
423-
calculate_bounds.in_set(CalculateBounds),
439+
(calculate_bounds, update_skinned_mesh_bounds)
440+
.chain()
441+
.in_set(CalculateBounds),
424442
(visibility_propagate_system, reset_view_visibility)
425443
.in_set(VisibilityPropagate),
426444
check_visibility.in_set(CheckVisibility),
@@ -485,6 +503,36 @@ pub fn calculate_bounds(
485503
});
486504
}
487505

506+
// Update the `Aabb` component of all skinned mesh entities with a `DynamicSkinnedMeshBounds`
507+
// component.
508+
fn update_skinned_mesh_bounds(
509+
inverse_bindposes_assets: Res<Assets<SkinnedMeshInverseBindposes>>,
510+
mesh_assets: Res<Assets<Mesh>>,
511+
mut mesh_entities: Query<
512+
(&mut Aabb, &Mesh3d, &SkinnedMesh, Option<&GlobalTransform>),
513+
With<DynamicSkinnedMeshBounds>,
514+
>,
515+
joint_entities: Query<&GlobalTransform>,
516+
) {
517+
mesh_entities
518+
.par_iter_mut()
519+
.for_each(|(mut aabb, mesh, skinned_mesh, world_from_entity)| {
520+
if let Some(inverse_bindposes_asset) =
521+
inverse_bindposes_assets.get(&skinned_mesh.inverse_bindposes)
522+
&& let Some(mesh_asset) = mesh_assets.get(mesh)
523+
&& let Ok(skinned_aabb) = entity_aabb_from_skinned_mesh_bounds(
524+
&joint_entities,
525+
mesh_asset,
526+
skinned_mesh,
527+
inverse_bindposes_asset,
528+
world_from_entity,
529+
)
530+
{
531+
*aabb = skinned_aabb.into();
532+
}
533+
});
534+
}
535+
488536
/// Updates [`Frustum`].
489537
///
490538
/// This system is used in [`CameraProjectionPlugin`](crate::CameraProjectionPlugin).

crates/bevy_gizmos/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.19.0-dev" }
2121
bevy_transform = { path = "../bevy_transform", version = "0.19.0-dev" }
2222
bevy_gizmos_macros = { path = "macros", version = "0.19.0-dev" }
2323
bevy_time = { path = "../bevy_time", version = "0.19.0-dev" }
24+
bevy_mesh = { path = "../bevy_mesh", version = "0.19.0-dev", optional = true }
2425

2526
[lints]
2627
workspace = true

crates/bevy_gizmos/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,22 @@ pub mod primitives;
3636
pub mod retained;
3737
pub mod rounded_box;
3838

39+
#[cfg(feature = "bevy_mesh")]
40+
pub mod skinned_mesh_bounds;
41+
3942
/// The gizmos prelude.
4043
///
4144
/// This includes the most common types in this crate, re-exported for your convenience.
4245
pub mod prelude {
4346
#[doc(hidden)]
4447
pub use crate::aabb::{AabbGizmoConfigGroup, ShowAabbGizmo};
4548

49+
#[doc(hidden)]
50+
#[cfg(feature = "bevy_mesh")]
51+
pub use crate::skinned_mesh_bounds::{
52+
ShowSkinnedMeshBoundsGizmo, SkinnedMeshBoundsGizmoConfigGroup,
53+
};
54+
4655
#[doc(hidden)]
4756
pub use crate::{
4857
config::{
@@ -74,6 +83,9 @@ use config::{DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfig
7483
use core::{any::TypeId, marker::PhantomData, mem};
7584
use gizmos::{GizmoStorage, Swap};
7685

86+
#[cfg(feature = "bevy_mesh")]
87+
use crate::skinned_mesh_bounds::SkinnedMeshBoundsGizmoPlugin;
88+
7789
/// A [`Plugin`] that provides an immediate mode drawing api for visual debugging.
7890
#[derive(Default)]
7991
pub struct GizmoPlugin;
@@ -86,6 +98,9 @@ impl Plugin for GizmoPlugin {
8698
.init_gizmo_group::<DefaultGizmoConfigGroup>();
8799

88100
app.add_plugins((aabb::AabbGizmoPlugin, global::GlobalGizmosPlugin));
101+
102+
#[cfg(feature = "bevy_mesh")]
103+
app.add_plugins(SkinnedMeshBoundsGizmoPlugin);
89104
}
90105
}
91106

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
//! A module adding debug visualization of [`DynamicSkinnedMeshBounds`].
2+
3+
use bevy_app::{Plugin, PostUpdate};
4+
use bevy_asset::Assets;
5+
use bevy_camera::visibility::DynamicSkinnedMeshBounds;
6+
use bevy_color::Color;
7+
use bevy_ecs::{
8+
component::Component,
9+
query::{With, Without},
10+
reflect::ReflectComponent,
11+
schedule::IntoScheduleConfigs,
12+
system::{Query, Res},
13+
};
14+
use bevy_math::Affine3A;
15+
use bevy_mesh::{
16+
mark_3d_meshes_as_changed_if_their_assets_changed,
17+
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
18+
Mesh, Mesh3d,
19+
};
20+
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
21+
use bevy_transform::{components::GlobalTransform, TransformSystems};
22+
23+
use crate::{
24+
config::{GizmoConfigGroup, GizmoConfigStore},
25+
gizmos::Gizmos,
26+
AppGizmoBuilder,
27+
};
28+
29+
/// A [`Plugin`] that provides visualization of entities with [`DynamicSkinnedMeshBounds`].
30+
pub struct SkinnedMeshBoundsGizmoPlugin;
31+
32+
impl Plugin for SkinnedMeshBoundsGizmoPlugin {
33+
fn build(&self, app: &mut bevy_app::App) {
34+
app.init_gizmo_group::<SkinnedMeshBoundsGizmoConfigGroup>()
35+
.add_systems(
36+
PostUpdate,
37+
(
38+
draw_skinned_mesh_bounds,
39+
draw_all_skinned_mesh_bounds.run_if(|config: Res<GizmoConfigStore>| {
40+
config
41+
.config::<SkinnedMeshBoundsGizmoConfigGroup>()
42+
.1
43+
.draw_all
44+
}),
45+
)
46+
.after(TransformSystems::Propagate)
47+
.ambiguous_with(mark_3d_meshes_as_changed_if_their_assets_changed),
48+
);
49+
}
50+
}
51+
/// The [`GizmoConfigGroup`] used for debug visualizations of entities with [`DynamicSkinnedMeshBounds`]
52+
#[derive(Clone, Reflect, GizmoConfigGroup)]
53+
#[reflect(Clone, Default)]
54+
pub struct SkinnedMeshBoundsGizmoConfigGroup {
55+
/// When set to `true`, draws all the bounds that contribute to skinned mesh
56+
/// bounds.
57+
///
58+
/// To draw a specific entity's skinned mesh bounds, you can add the [`ShowSkinnedMeshBoundsGizmo`] component.
59+
///
60+
/// Defaults to `false`.
61+
pub draw_all: bool,
62+
/// The default color for skinned mesh bounds gizmos.
63+
pub default_color: Color,
64+
}
65+
66+
impl Default for SkinnedMeshBoundsGizmoConfigGroup {
67+
fn default() -> Self {
68+
Self {
69+
draw_all: false,
70+
default_color: Color::WHITE,
71+
}
72+
}
73+
}
74+
75+
/// Add this [`Component`] to an entity to draw its [`DynamicSkinnedMeshBounds`] component.
76+
#[derive(Component, Reflect, Default, Debug)]
77+
#[reflect(Component, Default, Debug)]
78+
pub struct ShowSkinnedMeshBoundsGizmo {
79+
/// The color of the bounds.
80+
///
81+
/// The default color from the [`SkinnedMeshBoundsGizmoConfigGroup`] config is used if `None`,
82+
pub color: Option<Color>,
83+
}
84+
85+
fn draw(
86+
color: Color,
87+
mesh: &Mesh3d,
88+
mesh_assets: &Res<Assets<Mesh>>,
89+
skinned_mesh: &SkinnedMesh,
90+
joint_entities: &Query<&GlobalTransform>,
91+
inverse_bindposes_assets: &Res<Assets<SkinnedMeshInverseBindposes>>,
92+
gizmos: &mut Gizmos<SkinnedMeshBoundsGizmoConfigGroup>,
93+
) {
94+
if let Some(mesh_asset) = mesh_assets.get(mesh)
95+
&& let Some(bounds) = mesh_asset.skinned_mesh_bounds()
96+
&& let Some(inverse_bindposes_asset) =
97+
inverse_bindposes_assets.get(&skinned_mesh.inverse_bindposes)
98+
{
99+
for (&joint_index, &joint_aabb) in bounds.iter() {
100+
let joint_index = joint_index.0 as usize;
101+
102+
if let Some(&joint_entity) = skinned_mesh.joints.get(joint_index)
103+
&& let Ok(&world_from_joint) = joint_entities.get(joint_entity)
104+
&& let Some(&joint_from_mesh) = inverse_bindposes_asset.get(joint_index)
105+
{
106+
let world_from_mesh =
107+
world_from_joint.affine() * Affine3A::from_mat4(joint_from_mesh);
108+
109+
gizmos.aabb_3d(joint_aabb, world_from_mesh, color);
110+
}
111+
}
112+
}
113+
}
114+
115+
fn draw_skinned_mesh_bounds(
116+
mesh_entities: Query<
117+
(&Mesh3d, &SkinnedMesh, &ShowSkinnedMeshBoundsGizmo),
118+
With<DynamicSkinnedMeshBounds>,
119+
>,
120+
joint_entities: Query<&GlobalTransform>,
121+
mesh_assets: Res<Assets<Mesh>>,
122+
inverse_bindposes_assets: Res<Assets<SkinnedMeshInverseBindposes>>,
123+
mut gizmos: Gizmos<SkinnedMeshBoundsGizmoConfigGroup>,
124+
) {
125+
for (mesh, skinned_mesh, gizmo) in mesh_entities {
126+
let color = gizmo.color.unwrap_or(gizmos.config_ext.default_color);
127+
128+
draw(
129+
color,
130+
mesh,
131+
&mesh_assets,
132+
skinned_mesh,
133+
&joint_entities,
134+
&inverse_bindposes_assets,
135+
&mut gizmos,
136+
);
137+
}
138+
}
139+
140+
fn draw_all_skinned_mesh_bounds(
141+
mesh_entities: Query<
142+
(&Mesh3d, &SkinnedMesh),
143+
(
144+
With<DynamicSkinnedMeshBounds>,
145+
Without<ShowSkinnedMeshBoundsGizmo>,
146+
),
147+
>,
148+
joint_entities: Query<&GlobalTransform>,
149+
mesh_assets: Res<Assets<Mesh>>,
150+
inverse_bindposes_assets: Res<Assets<SkinnedMeshInverseBindposes>>,
151+
mut gizmos: Gizmos<SkinnedMeshBoundsGizmoConfigGroup>,
152+
) {
153+
for (mesh, skinned_mesh) in mesh_entities {
154+
draw(
155+
gizmos.config_ext.default_color,
156+
mesh,
157+
&mesh_assets,
158+
skinned_mesh,
159+
&joint_entities,
160+
&inverse_bindposes_assets,
161+
&mut gizmos,
162+
);
163+
}
164+
}

crates/bevy_gltf/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ mod vertex_attributes;
136136
extern crate alloc;
137137

138138
use alloc::sync::Arc;
139+
use serde::{Deserialize, Serialize};
139140
use std::sync::Mutex;
140141
use tracing::warn;
141142

@@ -191,6 +192,24 @@ impl DefaultGltfImageSampler {
191192
}
192193
}
193194

195+
/// Controls the bounds related components that are assigned to skinned mesh
196+
/// entities. These components are used by systems like frustum culling.
197+
#[derive(Default, Copy, Clone, PartialEq, Serialize, Deserialize)]
198+
pub enum GltfSkinnedMeshBoundsPolicy {
199+
/// Skinned meshes are assigned an `Aabb` component calculated from the bind
200+
/// pose `Mesh`.
201+
BindPose,
202+
/// Skinned meshes are created with [`SkinnedMeshBounds`](bevy_mesh::skinning::SkinnedMeshBounds)
203+
/// and assigned a [`DynamicSkinnedMeshBounds`](bevy_camera::visibility::DynamicSkinnedMeshBounds)
204+
/// component. See `DynamicSkinnedMeshBounds` for details.
205+
#[default]
206+
Dynamic,
207+
/// Same as `BindPose`, but also assign a `NoFrustumCulling` component. That
208+
/// component tells the `bevy_camera` plugin to avoid frustum culling the
209+
/// skinned mesh.
210+
NoFrustumCulling,
211+
}
212+
194213
/// Adds support for glTF file loading to the app.
195214
pub struct GltfPlugin {
196215
/// The default image sampler to lay glTF sampler data on top of.
@@ -206,6 +225,10 @@ pub struct GltfPlugin {
206225
///
207226
/// To specify, use [`GltfPlugin::add_custom_vertex_attribute`].
208227
pub custom_vertex_attributes: HashMap<Box<str>, MeshVertexAttribute>,
228+
229+
/// The default policy for skinned mesh bounds. Can be overridden by
230+
/// [`GltfLoaderSettings::skinned_mesh_bounds_policy`].
231+
pub skinned_mesh_bounds_policy: GltfSkinnedMeshBoundsPolicy,
209232
}
210233

211234
impl Default for GltfPlugin {
@@ -214,6 +237,7 @@ impl Default for GltfPlugin {
214237
default_sampler: ImageSamplerDescriptor::linear(),
215238
custom_vertex_attributes: HashMap::default(),
216239
convert_coordinates: GltfConvertCoordinates::default(),
240+
skinned_mesh_bounds_policy: Default::default(),
217241
}
218242
}
219243
}
@@ -268,6 +292,7 @@ impl Plugin for GltfPlugin {
268292
default_sampler,
269293
default_convert_coordinates: self.convert_coordinates,
270294
extensions: extensions.0.clone(),
295+
default_skinned_mesh_bounds_policy: self.skinned_mesh_bounds_policy,
271296
});
272297
}
273298
}

0 commit comments

Comments
 (0)