Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
11b3a8d
First pass. Enough for Fox.glb to work in the scene viewer, but not p…
greeble-dev Nov 10, 2025
fb303fb
Made `InfluenceIterator` more general purpose and added a test.
greeble-dev Nov 11, 2025
f77a496
Changed `update_skinned_mesh_bounds` to make the entity transform opt…
greeble-dev Nov 11, 2025
e4d6dac
Moved the skinned mesh bounds out of `Mesh` and into a separate compo…
greeble-dev Nov 11, 2025
fa8f542
Added skinned mesh bounds to the `custom_skinned_mesh` example. Also …
greeble-dev Nov 11, 2025
a0c793e
Changed `SkinnedMeshBoundsAsset` to use a single array of bounds and …
greeble-dev Nov 11, 2025
aba2acb
Added `GltfSkinnedMeshBoundsPolicy`, currently defaulting to skinned …
greeble-dev Nov 12, 2025
551c534
Changed my mind and split `SkinnedMeshBoundsAsset` back into two arrays.
greeble-dev Nov 12, 2025
b254a01
Commenting and renaming pass. Also added better `AabbAccumulator` tests.
greeble-dev Nov 12, 2025
c21b453
Use parallel iterator.
greeble-dev Nov 12, 2025
ed38893
Tidying and comments. Changed `create_skinned_mesh_bounds_asset` to b…
greeble-dev Nov 12, 2025
fce7288
Added comment.
greeble-dev Nov 12, 2025
48a7665
Tidy ups and commenting.
greeble-dev Nov 13, 2025
c68c350
Added joint bounds gizmos to the `scene_viewer` example.
greeble-dev Nov 13, 2025
0cab132
Refactored iteration into `SkinnedMeshBounds::iter`.
greeble-dev Nov 13, 2025
2567a29
Tidy-ups and commenting.
greeble-dev Nov 14, 2025
d0a252b
Stubbed out release notes.
greeble-dev Nov 14, 2025
79a3da2
Added PR number to release notes.
greeble-dev Nov 14, 2025
d6fd861
Typos.
greeble-dev Nov 14, 2025
655e6eb
Markdown lint.
greeble-dev Nov 14, 2025
c1f30ed
Added gizmos for skinned mesh bounds, replacing the previous `scene_v…
greeble-dev Nov 14, 2025
4294b63
Fixed import.
greeble-dev Nov 14, 2025
4bf5041
Merge branch 'main' into skinned-mesh-bounds-v2
greeble-dev Nov 28, 2025
781c20e
Changed back to putting `SkinnedMeshBounds` on `Mesh`. This assumes t…
greeble-dev Nov 28, 2025
d71feaa
Fixed system ordering ambiguity.
greeble-dev Nov 28, 2025
c05cfb8
Added error handling.
greeble-dev Nov 28, 2025
9bd7bf9
Fixed docs.
greeble-dev Nov 28, 2025
aa6d005
Replaced errors with a more generic mesh attribute error.
greeble-dev Nov 28, 2025
6f2831d
Made `JointIndex` private for now.
greeble-dev Nov 28, 2025
6f3daa4
Changed the joint AABBs from min/max to center/size. In a microbenchm…
greeble-dev Nov 29, 2025
5a19ef3
Removed FMA. Don't think it's worth the risk for a +10% optimization.
greeble-dev Nov 29, 2025
0d4a416
Tidy ups and commenting.
greeble-dev Nov 29, 2025
2e8448a
Changed `JointIndex` to be a newtype. Not sure if this is a good idea.
greeble-dev Nov 29, 2025
a20f637
Improved errors with `thiserror`. Added `JointAabb::min/max`. Added c…
greeble-dev Nov 29, 2025
e3a5178
Improved documentation. Changed `Mesh::with_generated_skinned_mesh_bo…
greeble-dev Nov 29, 2025
9c7d0db
Clippy.
greeble-dev Nov 29, 2025
fd01bae
Added release note and finished off last documentation TODOs.
greeble-dev Dec 1, 2025
b704509
Tidy ups and commenting.
greeble-dev Dec 1, 2025
196e4c9
Removed bounds debugging from `custom_skinned_mesh`.
greeble-dev Dec 1, 2025
eee85ab
Fixed docs.
greeble-dev Dec 1, 2025
1483483
Added `bevy_mod_skinned_aabb` upgrade instructions.
greeble-dev Dec 1, 2025
3ceaf8d
Typo.
greeble-dev Dec 1, 2025
729f8ae
Markdown lint.
greeble-dev Dec 1, 2025
e5d4b32
Release note tweak.
greeble-dev Dec 1, 2025
3003e29
Markdown lint.
greeble-dev Dec 1, 2025
ee7031a
Release note tweaks.
greeble-dev Dec 2, 2025
4be9dc7
Added `test_skinned_mesh_bounds` example.
greeble-dev Dec 2, 2025
2573626
Merge branch 'main' into skinned-mesh-bounds-v2
greeble-dev Dec 2, 2025
9ae5972
Markdown lint.
greeble-dev Dec 2, 2025
639ea27
Markdown lint is my nemesis.
greeble-dev Dec 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4607,6 +4607,14 @@ doc-scrape-examples = true
[package.metadata.example.test_invalid_skinned_mesh]
hidden = true

[[example]]
name = "test_skinned_mesh_bounds"
path = "tests/3d/test_skinned_mesh_bounds.rs"
doc-scrape-examples = true

[package.metadata.example.test_skinned_mesh_bounds]
hidden = true

[profile.wasm-release]
inherits = "release"
opt-level = "z"
Expand Down
50 changes: 49 additions & 1 deletion crates/bevy_camera/src/visibility/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ use core::any::TypeId;
use bevy_ecs::entity::{EntityHashMap, EntityHashSet};
use bevy_ecs::lifecycle::HookContext;
use bevy_ecs::world::DeferredWorld;
use bevy_mesh::skinning::{
entity_aabb_from_skinned_mesh_bounds, SkinnedMesh, SkinnedMeshInverseBindposes,
};
use derive_more::derive::{Deref, DerefMut};
pub use range::*;
pub use render_layers::*;
Expand Down Expand Up @@ -216,6 +219,19 @@ impl ViewVisibility {
#[reflect(Component, Default, Debug)]
pub struct NoFrustumCulling;

/// Use this component to enable dynamic skinned mesh bounds. The [`Aabb`]
/// component of the skinned mesh will be automatically updated each frame based
/// on the current joint transforms.
///
/// `DynamicSkinnedMeshBounds` depends on data from `Mesh::skinned_mesh_bounds`
/// and `SkinnedMesh`. The resulting `Aabb` will reliably enclose meshes where
/// vertex positions are only affected by skinning. But the `Aabb` may be larger
/// than is optimal, and doesn't account for morph targets, vertex shaders, and
/// anything else that modifies vertex positions.
#[derive(Debug, Component, Default, Reflect)]
#[reflect(Component, Default, Debug)]
pub struct DynamicSkinnedMeshBounds;

/// Collection of entities visible from the current view.
///
/// This component contains all entities which are visible from the currently
Expand Down Expand Up @@ -368,7 +384,9 @@ impl Plugin for VisibilityPlugin {
.add_systems(
PostUpdate,
(
calculate_bounds.in_set(CalculateBounds),
(calculate_bounds, update_skinned_mesh_bounds)
.chain()
.in_set(CalculateBounds),
(visibility_propagate_system, reset_view_visibility)
.in_set(VisibilityPropagate),
check_visibility.in_set(CheckVisibility),
Expand Down Expand Up @@ -402,6 +420,36 @@ pub fn calculate_bounds(
}
}

// Update the `Aabb` component of all skinned mesh entities with a `DynamicSkinnedMeshBounds`
// component.
fn update_skinned_mesh_bounds(
inverse_bindposes_assets: Res<Assets<SkinnedMeshInverseBindposes>>,
mesh_assets: Res<Assets<Mesh>>,
mut mesh_entities: Query<
(&mut Aabb, &Mesh3d, &SkinnedMesh, Option<&GlobalTransform>),
With<DynamicSkinnedMeshBounds>,
>,
joint_entities: Query<&GlobalTransform>,
) {
mesh_entities
.par_iter_mut()
.for_each(|(mut aabb, mesh, skinned_mesh, world_from_entity)| {
if let Some(inverse_bindposes_asset) =
inverse_bindposes_assets.get(&skinned_mesh.inverse_bindposes)
&& let Some(mesh_asset) = mesh_assets.get(mesh)
&& let Ok(skinned_aabb) = entity_aabb_from_skinned_mesh_bounds(
&joint_entities,
mesh_asset,
skinned_mesh,
inverse_bindposes_asset,
world_from_entity,
)
{
*aabb = skinned_aabb.into();
}
});
}

/// Updates [`Frustum`].
///
/// This system is used in [`CameraProjectionPlugin`](crate::CameraProjectionPlugin).
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_gizmos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.18.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.18.0-dev" }
bevy_gizmos_macros = { path = "macros", version = "0.18.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.18.0-dev" }
bevy_mesh = { path = "../bevy_mesh", version = "0.18.0-dev", optional = true }

[lints]
workspace = true
Expand Down
16 changes: 16 additions & 0 deletions crates/bevy_gizmos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,22 @@ pub mod rounded_box;
#[cfg(feature = "bevy_light")]
pub mod light;

#[cfg(feature = "bevy_mesh")]
pub mod skinned_mesh_bounds;

/// The gizmos prelude.
///
/// This includes the most common types in this crate, re-exported for your convenience.
pub mod prelude {
#[doc(hidden)]
pub use crate::aabb::{AabbGizmoConfigGroup, ShowAabbGizmo};

#[doc(hidden)]
#[cfg(feature = "bevy_mesh")]
pub use crate::skinned_mesh_bounds::{
ShowSkinnedMeshBoundsGizmo, SkinnedMeshBoundsGizmoConfigGroup,
};

#[doc(hidden)]
pub use crate::{
config::{
Expand Down Expand Up @@ -78,9 +87,13 @@ use bevy_utils::TypeIdMap;
use config::{DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore};
use core::{any::TypeId, marker::PhantomData, mem};
use gizmos::{GizmoStorage, Swap};

#[cfg(feature = "bevy_light")]
use light::LightGizmoPlugin;

#[cfg(feature = "bevy_mesh")]
use crate::skinned_mesh_bounds::SkinnedMeshBoundsGizmoPlugin;

/// A [`Plugin`] that provides an immediate mode drawing api for visual debugging.
#[derive(Default)]
pub struct GizmoPlugin;
Expand All @@ -96,6 +109,9 @@ impl Plugin for GizmoPlugin {

#[cfg(feature = "bevy_light")]
app.add_plugins(LightGizmoPlugin);

#[cfg(feature = "bevy_mesh")]
app.add_plugins(SkinnedMeshBoundsGizmoPlugin);
}
}

Expand Down
164 changes: 164 additions & 0 deletions crates/bevy_gizmos/src/skinned_mesh_bounds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//! A module adding debug visualization of [`DynamicSkinnedMeshBounds`].

use bevy_app::{Plugin, PostUpdate};
use bevy_asset::Assets;
use bevy_camera::visibility::DynamicSkinnedMeshBounds;
use bevy_color::Color;
use bevy_ecs::{
component::Component,
query::{With, Without},
reflect::ReflectComponent,
schedule::IntoScheduleConfigs,
system::{Query, Res},
};
use bevy_math::Affine3A;
use bevy_mesh::{
mark_3d_meshes_as_changed_if_their_assets_changed,
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
Mesh, Mesh3d,
};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_transform::{components::GlobalTransform, TransformSystems};

use crate::{
config::{GizmoConfigGroup, GizmoConfigStore},
gizmos::Gizmos,
AppGizmoBuilder,
};

/// A [`Plugin`] that provides visualization of entities with [`DynamicSkinnedMeshBounds`].
pub struct SkinnedMeshBoundsGizmoPlugin;

impl Plugin for SkinnedMeshBoundsGizmoPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.init_gizmo_group::<SkinnedMeshBoundsGizmoConfigGroup>()
.add_systems(
PostUpdate,
(
draw_skinned_mesh_bounds,
draw_all_skinned_mesh_bounds.run_if(|config: Res<GizmoConfigStore>| {
config
.config::<SkinnedMeshBoundsGizmoConfigGroup>()
.1
.draw_all
}),
)
.after(TransformSystems::Propagate)
.ambiguous_with(mark_3d_meshes_as_changed_if_their_assets_changed),
);
}
}
/// The [`GizmoConfigGroup`] used for debug visualizations of entities with [`DynamicSkinnedMeshBounds`]
#[derive(Clone, Reflect, GizmoConfigGroup)]
#[reflect(Clone, Default)]
pub struct SkinnedMeshBoundsGizmoConfigGroup {
/// When set to `true`, draws all the bounds that contribute to skinned mesh
/// bounds.
///
/// To draw a specific entity's skinned mesh bounds, you can add the [`ShowSkinnedMeshBoundsGizmo`] component.
///
/// Defaults to `false`.
pub draw_all: bool,
/// The default color for skinned mesh bounds gizmos.
pub default_color: Color,
}

impl Default for SkinnedMeshBoundsGizmoConfigGroup {
fn default() -> Self {
Self {
draw_all: false,
default_color: Color::WHITE,
}
}
}

/// Add this [`Component`] to an entity to draw its [`DynamicSkinnedMeshBounds`] component.
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component, Default, Debug)]
pub struct ShowSkinnedMeshBoundsGizmo {
/// The color of the bounds.
///
/// The default color from the [`SkinnedMeshBoundsGizmoConfigGroup`] config is used if `None`,
pub color: Option<Color>,
}

fn draw(
color: Color,
mesh: &Mesh3d,
mesh_assets: &Res<Assets<Mesh>>,
skinned_mesh: &SkinnedMesh,
joint_entities: &Query<&GlobalTransform>,
inverse_bindposes_assets: &Res<Assets<SkinnedMeshInverseBindposes>>,
gizmos: &mut Gizmos<SkinnedMeshBoundsGizmoConfigGroup>,
) {
if let Some(mesh_asset) = mesh_assets.get(mesh)
&& let Some(bounds) = mesh_asset.skinned_mesh_bounds()
&& let Some(inverse_bindposes_asset) =
inverse_bindposes_assets.get(&skinned_mesh.inverse_bindposes)
{
for (&joint_index, &joint_aabb) in bounds.iter() {
let joint_index = joint_index.0 as usize;

if let Some(&joint_entity) = skinned_mesh.joints.get(joint_index)
&& let Ok(&world_from_joint) = joint_entities.get(joint_entity)
&& let Some(&joint_from_mesh) = inverse_bindposes_asset.get(joint_index)
{
let world_from_mesh =
world_from_joint.affine() * Affine3A::from_mat4(joint_from_mesh);

gizmos.aabb_3d(joint_aabb, world_from_mesh, color);
}
}
}
}

fn draw_skinned_mesh_bounds(
mesh_entities: Query<
(&Mesh3d, &SkinnedMesh, &ShowSkinnedMeshBoundsGizmo),
With<DynamicSkinnedMeshBounds>,
>,
joint_entities: Query<&GlobalTransform>,
mesh_assets: Res<Assets<Mesh>>,
inverse_bindposes_assets: Res<Assets<SkinnedMeshInverseBindposes>>,
mut gizmos: Gizmos<SkinnedMeshBoundsGizmoConfigGroup>,
) {
for (mesh, skinned_mesh, gizmo) in mesh_entities {
let color = gizmo.color.unwrap_or(gizmos.config_ext.default_color);

draw(
color,
mesh,
&mesh_assets,
skinned_mesh,
&joint_entities,
&inverse_bindposes_assets,
&mut gizmos,
);
}
}

fn draw_all_skinned_mesh_bounds(
mesh_entities: Query<
(&Mesh3d, &SkinnedMesh),
(
With<DynamicSkinnedMeshBounds>,
Without<ShowSkinnedMeshBoundsGizmo>,
),
>,
joint_entities: Query<&GlobalTransform>,
mesh_assets: Res<Assets<Mesh>>,
inverse_bindposes_assets: Res<Assets<SkinnedMeshInverseBindposes>>,
mut gizmos: Gizmos<SkinnedMeshBoundsGizmoConfigGroup>,
) {
for (mesh, skinned_mesh) in mesh_entities {
draw(
gizmos.config_ext.default_color,
mesh,
&mesh_assets,
skinned_mesh,
&joint_entities,
&inverse_bindposes_assets,
&mut gizmos,
);
}
}
24 changes: 24 additions & 0 deletions crates/bevy_gltf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ mod vertex_attributes;
extern crate alloc;

use alloc::sync::Arc;
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
use tracing::warn;

Expand Down Expand Up @@ -189,6 +190,23 @@ impl DefaultGltfImageSampler {
}
}

/// Controls the bounds related components that are assigned to skinned mesh
/// entities. These components are used by systems like frustum culling.
#[derive(Copy, Clone, PartialEq, Serialize, Deserialize)]
pub enum GltfSkinnedMeshBoundsPolicy {
/// Skinned meshes are assigned an `Aabb` component calculated from the bind
/// pose `Mesh`.
BindPose,
/// Skinned meshes are created with [`SkinnedMeshBounds`](bevy_mesh::skinning::SkinnedMeshBounds)
/// and assigned a [`DynamicSkinnedMeshBounds`](bevy_camera::visibility::DynamicSkinnedMeshBounds)
/// component. See `DynamicSkinnedMeshBounds` for details.
Dynamic,
/// Same as `BindPose`, but also assign a `NoFrustumCulling` component. That
/// component tells the `bevy_camera` plugin to avoid frustum culling the
/// skinned mesh.
NoFrustumCulling,
}

/// Adds support for glTF file loading to the app.
pub struct GltfPlugin {
/// The default image sampler to lay glTF sampler data on top of.
Expand All @@ -214,6 +232,10 @@ pub struct GltfPlugin {
///
/// To specify, use [`GltfPlugin::add_custom_vertex_attribute`].
pub custom_vertex_attributes: HashMap<Box<str>, MeshVertexAttribute>,

/// The default policy for skinned mesh bounds. Can be overridden by
/// [`GltfLoaderSettings::skinned_mesh_bounds_policy`].
pub skinned_mesh_bounds_policy: GltfSkinnedMeshBoundsPolicy,
}

impl Default for GltfPlugin {
Expand All @@ -222,6 +244,7 @@ impl Default for GltfPlugin {
default_sampler: ImageSamplerDescriptor::linear(),
custom_vertex_attributes: HashMap::default(),
use_model_forward_direction: false,
skinned_mesh_bounds_policy: GltfSkinnedMeshBoundsPolicy::Dynamic,
}
}
}
Expand Down Expand Up @@ -272,6 +295,7 @@ impl Plugin for GltfPlugin {
custom_vertex_attributes: self.custom_vertex_attributes.clone(),
default_sampler,
default_use_model_forward_direction: self.use_model_forward_direction,
default_skinned_mesh_bounds_policy: self.skinned_mesh_bounds_policy,
});
}
}
Loading