Skip to content

Restructure morph target pipeline to reduce crate dependencies #18465

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 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
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
1 change: 0 additions & 1 deletion crates/bevy_animation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [
"petgraph",
] }
bevy_render = { path = "../bevy_render", version = "0.16.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.16.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" }
Expand Down
4 changes: 1 addition & 3 deletions crates/bevy_animation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1253,9 +1253,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_render::mesh::inherit_weights)
.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 @@ -1363,7 +1363,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 @@ -1389,6 +1389,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 @@ -1400,22 +1401,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 @@ -1548,14 +1535,30 @@ 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() {
if let (Some(mesh), Some(weights)) = (gltf_node.mesh(), morph_weights) {
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(mesh) = gltf_node.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)?);
}
}
}

Expand Down
107 changes: 66 additions & 41 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 @@ -142,38 +189,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 @@ -182,6 +197,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 @@ -4,7 +4,7 @@ use bevy_ecs::prelude::*;
use bevy_render::sync_world::MainEntityHashMap;
use bevy_render::{
batching::NoAutomaticBatching,
mesh::morph::{MeshMorphWeights, MAX_MORPH_WEIGHTS},
mesh::morph::{MeshMorphWeights, MorphWeights, MAX_MORPH_WEIGHTS},
render_resource::{BufferUsages, RawBufferVec},
renderer::{RenderDevice, RenderQueue},
view::ViewVisibility,
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
23 changes: 2 additions & 21 deletions crates/bevy_render/src/mesh/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,31 +84,12 @@ impl Plugin for MeshPlugin {
}
}

/// [Inherit weights](inherit_weights) from glTF mesh parent entity to direct
/// bevy mesh child entities (ie: glTF primitive).
/// Adds morph target types.
pub struct MorphPlugin;
impl Plugin for MorphPlugin {
fn build(&self, app: &mut App) {
app.register_type::<MorphWeights>()
.register_type::<MeshMorphWeights>()
.add_systems(PostUpdate, inherit_weights);
}
}

/// 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());
}
.register_type::<MeshMorphWeights>();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: `MeshMorphWeights` is now a reference
pull_requests: [18465]
---

`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);
```

This change was made to improve runtime and compile-time performance. See the
`MorphWeights` documentation for examples of how to set up morph targets with
the new convention.