From 0699af7fa7ccaf4a0b34cd8bded18d10e345c7f0 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Sat, 8 Nov 2025 15:39:35 -0800 Subject: [PATCH] Don't automatically compute `Aabb` components for skinned meshes. Currently, Bevy automatically computes AABBs for all meshes, even those that have skins or morph targets. This is incorrect, as each skin and/or morph target can deform the mesh arbitrarily. This is not a theoretical problem, as Maya relies on rest poses to position and rotate meshes that are children of skins. Right now, the only way to disable this Bevy feature is to add the `NoFrustumCulling` component, or to overwrite the generated `Aabb` with a different one. Neither are a particularly good experience: 1. Adding `NoFrustumCulling` is impossible when loading a glTF scene, and it's also not obvious that `NoFrustumCulling` is needed to correctly render skinned meshes sometimes. 2. Overwriting the `Aabb` is cumbersome. This commit changes Bevy's behavior to not generate `Aabb` components for skinned meshes, fixing the issue. Note that, to keep this patch small, morph targets aren't handled yet. The documentation has been updated to inform developers that they may wish to add `Aabb` components manually to skinned meshes in order to opt in to frustum culling. --- crates/bevy_camera/src/primitives.rs | 12 ++++++- crates/bevy_camera/src/visibility/mod.rs | 22 +++++++++++-- crates/bevy_gltf/src/loader/mod.rs | 40 ++++++++++++++++-------- 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/crates/bevy_camera/src/primitives.rs b/crates/bevy_camera/src/primitives.rs index 1f3abe97ed6f3..6d30cf0e74fae 100644 --- a/crates/bevy_camera/src/primitives.rs +++ b/crates/bevy_camera/src/primitives.rs @@ -43,13 +43,23 @@ impl MeshAabb for Mesh { /// It will be added automatically by the systems in [`CalculateBounds`] to entities that: /// - could be subject to frustum culling, for example with a [`Mesh3d`] /// or `Sprite` component, -/// - don't have the [`NoFrustumCulling`] component. +/// - don't have the [`NoFrustumCulling`] component, +/// - and don't have the [`SkinnedMesh`] component. /// /// It won't be updated automatically if the space occupied by the entity changes, /// for example if the vertex positions of a [`Mesh3d`] are updated. /// +/// Bevy doesn't add the [`Aabb`] component to skinned meshes automatically, +/// because skins can deform meshes arbitrarily and therefore there's no +/// [`Aabb`] that can be automatically determined for them. You can, however, +/// add an [`Aabb`] component yourself if you can guarantee to Bevy that no +/// vertex in your skinned mesh will ever be positioned outside the boundaries +/// of that AABB. This will allow your skinned meshes to participate in frustum +/// and occlusion culling. +/// /// [`Camera`]: crate::Camera /// [`NoFrustumCulling`]: crate::visibility::NoFrustumCulling +/// [`SkinnedMesh`]: bevy_mesh::skinning::SkinnedMesh /// [`CalculateBounds`]: crate::visibility::VisibilitySystems::CalculateBounds /// [`Mesh3d`]: bevy_mesh::Mesh #[derive(Component, Clone, Copy, Debug, Default, Reflect, PartialEq)] diff --git a/crates/bevy_camera/src/visibility/mod.rs b/crates/bevy_camera/src/visibility/mod.rs index 76d3f3db02122..9062f4f40497a 100644 --- a/crates/bevy_camera/src/visibility/mod.rs +++ b/crates/bevy_camera/src/visibility/mod.rs @@ -13,6 +13,7 @@ pub use render_layers::*; use bevy_app::{Plugin, PostUpdate}; use bevy_asset::Assets; use bevy_ecs::{hierarchy::validate_parent_has_component, prelude::*}; +use bevy_mesh::skinning::SkinnedMesh; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_transform::{components::GlobalTransform, TransformSystems}; use bevy_utils::{Parallel, TypeIdMap}; @@ -384,14 +385,29 @@ impl Plugin for VisibilityPlugin { } } -/// Computes and adds an [`Aabb`] component to entities with a -/// [`Mesh3d`] component and without a [`NoFrustumCulling`] component. +/// Computes and adds an [`Aabb`] component to entities with a [`Mesh3d`] +/// component and that have neither a [`NoFrustumCulling`] component nor a +/// [`SkinnedMesh`] component. +/// +/// Bevy doesn't automatically calculate bounding boxes for meshes that are +/// skinned because, in general, the skin may deform the mesh arbitrarily. If +/// you want Bevy to frustum cull skinned meshes, you can manually add an +/// [`Aabb`] component to those meshes. If you do so, you're promising to Bevy +/// that the deformed vertices of that mesh will never go outside the bounds of +/// that AABB. /// /// This system is used in system set [`VisibilitySystems::CalculateBounds`]. pub fn calculate_bounds( mut commands: Commands, meshes: Res>, - without_aabb: Query<(Entity, &Mesh3d), (Without, Without)>, + without_aabb: Query< + (Entity, &Mesh3d), + ( + Without, + Without, + Without, + ), + >, ) { for (entity, mesh_handle) in &without_aabb { if let Some(mesh) = meshes.get(mesh_handle) diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 7c6c0eaa56ca9..734dd167c71e9 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -861,6 +861,11 @@ impl GltfLoader { // Then check for cycles. check_for_cycles(&gltf)?; + // Record which meshes are skinned. We need to do this so that we don't + // generate AABBs for them, which might cause them to be incorrectly + // culled. + let mut skinned_meshes = HashSet::new(); + // Now populate the nodes. for node in gltf.nodes() { let skin = node.skin().map(|skin| { @@ -907,10 +912,11 @@ impl GltfLoader { .map(|child| nodes.get(&child.index()).unwrap().clone()) .collect(); - let mesh = node - .mesh() - .map(|mesh| mesh.index()) - .and_then(|i| meshes.get(i).cloned()); + let maybe_mesh_index = node.mesh().map(|mesh| mesh.index()); + let mesh = maybe_mesh_index.and_then(|i| meshes.get(i).cloned()); + if let Some(mesh_index) = maybe_mesh_index { + skinned_meshes.insert(mesh_index); + } let gltf_node = GltfNode::new( &node, @@ -968,6 +974,7 @@ impl GltfLoader { #[cfg(feature = "bevy_animation")] None, &gltf.document, + &skinned_meshes, convert_coordinates, ); if result.is_err() { @@ -1412,6 +1419,7 @@ fn load_node( #[cfg(feature = "bevy_animation")] animation_roots: &HashSet, #[cfg(feature = "bevy_animation")] mut animation_context: Option, document: &Document, + skinned_meshes: &HashSet, convert_coordinates: bool, ) -> Result<(), GltfError> { let mut gltf_error = None; @@ -1559,18 +1567,23 @@ fn load_node( mesh_entity.insert(MeshMorphWeights::new(weights).unwrap()); } - let mut bounds_min = Vec3::from_slice(&bounds.min); - let mut bounds_max = Vec3::from_slice(&bounds.max); + // Add an AABB if this mesh isn't skinned. (If it is skinned, we + // don't add the AABB because skinned meshes can be deformed + // arbitrarily.) + if !skinned_meshes.contains(&mesh.index()) { + let mut bounds_min = Vec3::from_slice(&bounds.min); + let mut bounds_max = Vec3::from_slice(&bounds.max); - if convert_coordinates { - let converted_min = bounds_min.convert_coordinates(); - let converted_max = bounds_max.convert_coordinates(); + if convert_coordinates { + let converted_min = bounds_min.convert_coordinates(); + let converted_max = bounds_max.convert_coordinates(); - bounds_min = converted_min.min(converted_max); - bounds_max = converted_min.max(converted_max); - } + bounds_min = converted_min.min(converted_max); + bounds_max = converted_min.max(converted_max); + } - mesh_entity.insert(Aabb::from_min_max(bounds_min, bounds_max)); + mesh_entity.insert(Aabb::from_min_max(bounds_min, bounds_max)); + } if let Some(extras) = primitive.extras() { mesh_entity.insert(GltfExtras { @@ -1693,6 +1706,7 @@ fn load_node( #[cfg(feature = "bevy_animation")] animation_context.clone(), document, + skinned_meshes, convert_coordinates, ) { gltf_error = Some(err);