Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
109eadd
Added `many_morph_targets` stress test.
greeble-dev Mar 20, 2025
3dacafe
First pass at replacing `inherit_weights` with extraction.
greeble-dev Mar 20, 2025
a2d982d
Fixed previous commit regressing https://github.com/bevyengine/bevy/i…
greeble-dev Mar 21, 2025
afda57b
Added methods so that the `MorphWeights` interface matches the old `M…
greeble-dev Mar 21, 2025
0cef429
Added TODO comments.
greeble-dev Mar 21, 2025
93c05d8
Merge branch 'main' into extract-morph-targets
greeble-dev Mar 21, 2025
e61bcde
Fixed clippy after merging main.
greeble-dev Mar 21, 2025
c8fdc2a
Fixed example docs.
greeble-dev Mar 21, 2025
c7778ff
Simplified code.
greeble-dev Mar 22, 2025
bae0b12
Merge branch 'main' into extract-morph-targets
greeble-dev Mar 25, 2025
4706572
Merge branch 'main' into extract-morph-targets
greeble-dev Mar 26, 2025
f207386
Removed `bevy_animation` dependency on `bevy_render`.
greeble-dev Mar 26, 2025
7f5d4e9
First pass at documentation.
greeble-dev Mar 28, 2025
8fedc15
Simplified documentation code.
greeble-dev Mar 28, 2025
00401eb
Removed redundant uses.
greeble-dev Mar 28, 2025
fbb11c9
Removed `many_morph_targets` stress test - this will be covered by #1…
greeble-dev Mar 28, 2025
1ea9d8c
Oops, missed this.
greeble-dev Mar 28, 2025
bb11563
Comment tweak.
greeble-dev Mar 28, 2025
7b8c3e7
Merge branch 'main' into extract-morph-targets
greeble-dev Apr 11, 2025
9693d0c
Better documentation.
greeble-dev Apr 11, 2025
4ac99f5
Merge branch 'main' into extract-morph-targets
greeble-dev May 2, 2025
dc64b9f
Added migration guide.
greeble-dev May 2, 2025
37762ac
Merge branch 'main' into extract-morph-targets
greeble-dev May 5, 2025
81a43d4
Merge branch 'main' into extract-morph-targets
greeble-dev Aug 10, 2025
ed46c1d
Removed manual type registrations. These were removed in #20435 but a…
greeble-dev Aug 10, 2025
d9a4b2f
Updated migration guide.
greeble-dev Aug 10, 2025
383d93c
Removed now redundant `InheritWeights` system set.
greeble-dev Aug 10, 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
4 changes: 1 addition & 3 deletions crates/bevy_animation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1244,9 +1244,7 @@ impl Plugin for AnimationPlugin {
// it to its own system set after `Update` but before
// `PostUpdate`. For now, we just disable ambiguity testing
// for this system.
animate_targets
.before(bevy_mesh::InheritWeights)
.ambiguous_with_all(),
animate_targets.ambiguous_with_all(),
trigger_untargeted_animation_events,
expire_completed_transitions,
)
Expand Down
53 changes: 28 additions & 25 deletions crates/bevy_gltf/src/loader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1509,7 +1509,7 @@ fn load_node(
// Map node index to entity
node_index_to_entity_map.insert(gltf_node.index(), node.id());

let mut morph_weights = None;
let mut max_morph_target_count = 0;

node.with_children(|parent| {
// Only include meshes in the output if they're set to be retained in the MAIN_WORLD and/or RENDER_WORLD by the load_meshes flag
Expand All @@ -1536,6 +1536,7 @@ fn load_node(
primitive: primitive.index(),
};
let bounds = primitive.bounding_box();
let parent_entity = parent.target_entity();

let mut mesh_entity = parent.spawn((
// TODO: handle missing label handle errors here?
Expand All @@ -1547,22 +1548,8 @@ fn load_node(

let target_count = primitive.morph_targets().len();
if target_count != 0 {
let weights = match mesh.weights() {
Some(weights) => weights.to_vec(),
None => vec![0.0; target_count],
};

if morph_weights.is_none() {
morph_weights = Some(weights.clone());
}

// unwrap: the parent's call to `MeshMorphWeights::new`
// means this code doesn't run if it returns an `Err`.
// According to https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#morph-targets
// they should all have the same length.
// > All morph target accessors MUST have the same count as
// > the accessors of the original primitive.
mesh_entity.insert(MeshMorphWeights::new(weights).unwrap());
max_morph_target_count = max_morph_target_count.max(target_count);
mesh_entity.insert(MeshMorphWeights(parent_entity));
}
mesh_entity.insert(Aabb::from_min_max(
Vec3::from_slice(&bounds.min),
Expand Down Expand Up @@ -1700,15 +1687,31 @@ fn load_node(

// Only include meshes in the output if they're set to be retained in the MAIN_WORLD and/or RENDER_WORLD by the load_meshes flag
if !settings.load_meshes.is_empty()
&& let (Some(mesh), Some(weights)) = (gltf_node.mesh(), morph_weights)
&& let Some(mesh) = gltf_node.mesh()
{
let primitive_label = mesh.primitives().next().map(|p| GltfAssetLabel::Primitive {
mesh: mesh.index(),
primitive: p.index(),
});
let first_mesh =
primitive_label.map(|label| load_context.get_label_handle(label.to_string()));
node.insert(MorphWeights::new(weights, first_mesh)?);
// Create the `MorphWeights` component. The weights will be copied
// from `mesh.weights()` if present. If not then the weights are
// zero.
//
// The glTF spec says that all primitives within a mesh must have
// the same number of morph targets, and `mesh.weights()` should be
// equal to that number if present. We're more forgiving and take
// whichever is largest, leaving any unspecified weights at zero.
if (max_morph_target_count > 0) || mesh.weights().is_some() {
let mut weights = Vec::from(mesh.weights().unwrap_or(&[]));

if max_morph_target_count > weights.len() {
weights.resize(max_morph_target_count, 0.0);
}

let primitive_label = mesh.primitives().next().map(|p| GltfAssetLabel::Primitive {
mesh: mesh.index(),
primitive: p.index(),
});
let first_mesh =
primitive_label.map(|label| load_context.get_label_handle(label.to_string()));
node.insert(MorphWeights::new(weights, first_mesh)?);
}
}

if let Some(err) = gltf_error {
Expand Down
5 changes: 0 additions & 5 deletions crates/bevy_mesh/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ pub mod morph;
pub mod primitives;
pub mod skinning;
mod vertex;
use bevy_ecs::schedule::SystemSet;
use bitflags::bitflags;
pub use components::*;
pub use index::*;
Expand Down Expand Up @@ -68,7 +67,3 @@ impl BaseMeshPipelineKey {
}
}
}

/// `bevy_render::mesh::inherit_weights` runs in this `SystemSet`
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub struct InheritWeights;
108 changes: 66 additions & 42 deletions crates/bevy_mesh/src/morph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,64 @@ impl MorphTargetImage {
}
}

/// Controls the [morph targets] for all child `Mesh3d` entities. In most cases, [`MorphWeights`] should be considered
/// the "source of truth" when writing morph targets for meshes. However you can choose to write child [`MeshMorphWeights`]
/// if your situation requires more granularity. Just note that if you set [`MorphWeights`], it will overwrite child
/// [`MeshMorphWeights`] values.
/// A component that controls the [morph targets] of one or more `Mesh3d`
/// components.
///
/// This exists because Bevy's [`Mesh`] corresponds to a _single_ surface / material, whereas morph targets
/// as defined in the GLTF spec exist on "multi-primitive meshes" (where each primitive is its own surface with its own material).
/// Therefore in Bevy [`MorphWeights`] an a parent entity are the "canonical weights" from a GLTF perspective, which then
/// synchronized to child `Mesh3d` / [`MeshMorphWeights`] (which correspond to "primitives" / "surfaces" from a GLTF perspective).
/// To find the weights of its morph targets, a `Mesh3d` component looks for a
/// [`MeshMorphWeights`] component in the same entity. This points to another
/// entity, which is expected to contain a `MorphWeights` component.
///
/// Add this to the parent of one or more [`Entities`](`Entity`) with a `Mesh3d` with a [`MeshMorphWeights`].
/// The intermediate `MeshMorphWeights` component allows multiple `Mesh3d`
/// components to share one `MorphWeights` component.
///
/// The example shows a single mesh entity with a separate weights entity:
///
/// ```
/// # use bevy_asset::prelude::*;
/// # use bevy_ecs::prelude::*;
/// # use bevy_mesh::Mesh;
/// # use bevy_mesh::morph::*;
/// # #[derive(Component)]
/// # struct Mesh3d(Handle<Mesh>);
/// fn setup(mut commands: Commands, mesh_handle: Handle<Mesh>) {
/// // Create the `MorphWeights` component.
/// let weights_component = MorphWeights::new(
/// vec![0.0, 0.5, 1.0],
/// None,
/// ).unwrap();
///
/// // Spawn an entity to contain the weights.
/// let weights_entity = commands.spawn(weights_component).id();
///
/// // Spawn an entity with a mesh and a `MeshMorphWeights` component that
/// // points to `weights_entity`.
/// let mesh_entity = commands.spawn((
/// Mesh3d(mesh_handle.clone()),
/// MeshMorphWeights(weights_entity),
/// ));
/// }
/// ```
///
/// In the simplest case, all the components can be in one entity:
///
/// ```
/// # use bevy_asset::prelude::*;
/// # use bevy_ecs::prelude::*;
/// # use bevy_mesh::Mesh;
/// # use bevy_mesh::morph::*;
/// # #[derive(Component)]
/// # struct Mesh3d(Handle<Mesh>);
/// # fn setup(mut commands: Commands, mesh_entity: Entity) {
/// # let weights_component = MorphWeights::new(vec![0.0, 0.5, 1.0], None).unwrap();
/// # let mesh_handle = Handle::<Mesh>::default();
/// let weights_entity = commands.spawn(weights_component).id();
///
/// commands.entity(weights_entity).insert((
/// Mesh3d(mesh_handle.clone()),
/// MeshMorphWeights(weights_entity),
/// ));
/// # }
/// ```
///
/// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation
#[derive(Reflect, Default, Debug, Clone, Component)]
Expand Down Expand Up @@ -143,39 +190,6 @@ impl MorphWeights {
pub fn weights_mut(&mut self) -> &mut [f32] {
&mut self.weights
}
}

/// Control a specific [`Mesh`] instance's [morph targets]. These control the weights of
/// specific "mesh primitives" in scene formats like GLTF. They can be set manually, but
/// in most cases they should "automatically" synced by setting the [`MorphWeights`] component
/// on a parent entity.
///
/// See [`MorphWeights`] for more details on Bevy's morph target implementation.
///
/// Add this to an [`Entity`] with a `Mesh3d` with a [`MorphAttributes`] set
/// to control individual weights of each morph target.
///
/// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation
#[derive(Reflect, Default, Debug, Clone, Component)]
#[reflect(Debug, Component, Default, Clone)]
pub struct MeshMorphWeights {
weights: Vec<f32>,
}

impl MeshMorphWeights {
pub fn new(weights: Vec<f32>) -> Result<Self, MorphBuildError> {
if weights.len() > MAX_MORPH_WEIGHTS {
let target_count = weights.len();
return Err(MorphBuildError::TooManyTargets { target_count });
}
Ok(MeshMorphWeights { weights })
}
pub fn weights(&self) -> &[f32] {
&self.weights
}
pub fn weights_mut(&mut self) -> &mut [f32] {
&mut self.weights
}
pub fn clear_weights(&mut self) {
self.weights.clear();
}
Expand All @@ -184,6 +198,16 @@ impl MeshMorphWeights {
}
}

/// Controls the [morph targets] of a `Mesh3d` component by referencing an
/// entity with a `MorphWeights` component.
///
/// See [`MorphWeights`] for examples.
///
/// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation
#[derive(Reflect, Debug, Clone, Component)]
#[reflect(Debug, Component, Clone)]
pub struct MeshMorphWeights(#[entities] pub Entity);

/// Attributes **differences** used for morph targets.
///
/// See [`MorphTargetImage`] for more information.
Expand Down
9 changes: 6 additions & 3 deletions crates/bevy_pbr/src/render/morph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use core::{iter, mem};

use bevy_camera::visibility::ViewVisibility;
use bevy_ecs::prelude::*;
use bevy_mesh::morph::{MeshMorphWeights, MAX_MORPH_WEIGHTS};
use bevy_mesh::morph::{MeshMorphWeights, MorphWeights, MAX_MORPH_WEIGHTS};
use bevy_render::sync_world::MainEntityHashMap;
use bevy_render::{
batching::NoAutomaticBatching,
Expand Down Expand Up @@ -110,6 +110,7 @@ pub fn extract_morphs(
morph_indices: ResMut<MorphIndices>,
uniform: ResMut<MorphUniforms>,
query: Extract<Query<(Entity, &ViewVisibility, &MeshMorphWeights)>>,
weights_query: Extract<Query<&MorphWeights>>,
) {
// Borrow check workaround.
let (morph_indices, uniform) = (morph_indices.into_inner(), uniform.into_inner());
Expand All @@ -125,9 +126,11 @@ pub fn extract_morphs(
if !view_visibility.get() {
continue;
}
let Ok(weights) = weights_query.get(morph_weights.0) else {
continue;
};
let start = uniform.current_buffer.len();
let weights = morph_weights.weights();
let legal_weights = weights.iter().take(MAX_MORPH_WEIGHTS).copied();
let legal_weights = weights.weights().iter().take(MAX_MORPH_WEIGHTS).copied();
uniform.current_buffer.extend(legal_weights);
add_to_alignment::<f32>(&mut uniform.current_buffer);

Expand Down
3 changes: 1 addition & 2 deletions crates/bevy_render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ pub use extract_param::Extract;
use crate::{
camera::CameraPlugin,
gpu_readback::GpuReadbackPlugin,
mesh::{MeshPlugin, MorphPlugin, RenderMesh},
mesh::{MeshPlugin, RenderMesh},
render_asset::prepare_assets,
render_resource::{init_empty_bind_group_layout, PipelineCache, Shader, ShaderLoader},
renderer::{render_system, RenderInstance},
Expand Down Expand Up @@ -457,7 +457,6 @@ impl Plugin for RenderPlugin {
ViewPlugin,
MeshPlugin,
GlobalsPlugin,
MorphPlugin,
BatchingPlugin {
debug_flags: self.debug_flags,
},
Expand Down
27 changes: 0 additions & 27 deletions crates/bevy_render/src/mesh/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ use bevy_ecs::{
SystemParamItem,
},
};
use bevy_mesh::morph::{MeshMorphWeights, MorphWeights};
pub use bevy_mesh::*;
use wgpu::IndexFormat;

Expand Down Expand Up @@ -46,32 +45,6 @@ impl Plugin for MeshPlugin {
}
}

/// [Inherit weights](inherit_weights) from glTF mesh parent entity to direct
/// bevy mesh child entities (ie: glTF primitive).
pub struct MorphPlugin;
impl Plugin for MorphPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PostUpdate, inherit_weights.in_set(InheritWeights));
}
}

/// Bevy meshes are gltf primitives, [`MorphWeights`] on the bevy node entity
/// should be inherited by children meshes.
///
/// Only direct children are updated, to fulfill the expectations of glTF spec.
pub fn inherit_weights(
morph_nodes: Query<(&Children, &MorphWeights), (Without<Mesh3d>, Changed<MorphWeights>)>,
mut morph_primitives: Query<&mut MeshMorphWeights, With<Mesh3d>>,
) {
for (children, parent_weights) in &morph_nodes {
let mut iter = morph_primitives.iter_many_mut(children);
while let Some(mut child_weight) = iter.fetch_next() {
child_weight.clear_weights();
child_weight.extend_weights(parent_weights.weights());
}
}
}

/// The render world representation of a [`Mesh`].
#[derive(Debug, Clone)]
pub struct RenderMesh {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
title: `MeshMorphWeights` is now a reference, and `MorphPlugin` has been removed
pull_requests: [18465]
---

Some morph target components have been changed to improve performance. Users who
let the glTF loader set up their components do not need to make any changes.
Users who set up their morph target components manually or modify the glTF
loader's components may need to make changes.

`MeshMorphWeights` is now a reference to an entity with a `MorphWeights`
component. Previously it contained a copy of the weights.

```diff
- struct MeshMorphWeights { weights: Vec<f32> }
+ struct MeshMorphWeights(Entity);
```

See the `MorphWeights` documentation for examples of how to set up morph targets
with the new structure.

`MorphPlugin` has been removed as it was no longer necessary. Users who added
this plugin manually can remove it.

```diff
- app.add_plugins(MorphPlugin);
```
Loading