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

Conversation

greeble-dev
Copy link
Contributor

@greeble-dev greeble-dev commented Mar 21, 2025

Objective

Summary

I want to make bevy_animation independent of bevy_render (see also #18543, #18437). I'm assuming this is good for compile times and alternative renderers.

The final dependency is a system ordering that ensures morph target weights are written by bevy_animation before they're read by bevy_render:

animate_targets.before(bevy_render::mesh::inherit_weights)

This PR removes the dependency by eliminating inherit_weights. The changes are also an optimisation and make the pipeline more flexible.

Users who just import glTFs and use bevy_animation are not affected. Users who make lower level changes might need to update their code.

Background

The morph target pipeline lets main world entities set weights which are then automatically copied into per-mesh render buffers.

The current pipeline is:

  • bevy_animation::animate_targets or user code sets weights on a MorphWeights component (basically a Vec<f32>).
  • In the main world update, bevy_render::inherit_weights copies weights from MorphWeights to any MeshMorphWeights components in child entities. MeshMorphWeights is also a Vec<f32>.
  • In the render world extraction, bevy_render::extract_morphs finds any entities with both a Mesh3d and MeshMorphWeights, then copies the weights to per-mesh render buffers.

The separation between MorphWeights and MeshMorphWeights allows a single MorphWeights component in a parent entity to drive multiple meshes in child entities.

Changes

In the new pipeline, MeshMorphWeights is not a copy of MorphWeights. Instead it's a reference to the entity containing MorphWeights.

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

Any code that writes MorphWeights is unchanged. inherit_weights disappears - instead extract_morphs uses MeshMorphWeights to find and copy MorphWeights directly into render buffers.

In theory this is more flexible as meshes are not required to be a child of the entity with MorphWeights - although in practice I'd guess that's a niche requirement. Performance improves due to reduced copies and the child search becoming a direct entity lookup.

Breakage

Changing MeshMorphWeights from a copy to a reference could affect some users.

Unaffected: Users who import a glTF scene and drive the MorphWeights component themselves or via bevy_animation.

Possibly Affected: Users who set up their own pipeline or modify the glTF pipeline. In particular, if they're manipulating MeshMorphWeights directly then they'll have to restructure to use MorphWeights.

I did a github code search and found only one case that would break. They can fix this by rearranging the components.

Performance

Tested on many_morph_targets (#18536):

  • extract_morphs goes from 42.5us to 48.9us (+6.4us).
  • inherit_weights goes from 30.3us to zero.
  • Throughput of the two systems goes from 14 to 21 meshes per us (+50%).

I expect a small memory saving due to one less copy of the weights per mesh.

There's room for further optimisation. The render buffers have a copy of the weights for each mesh, but multiple meshes could be sharing weights.

Testing

cargo run --example morph_targets
cargo run --example many_morph_targets
cargo run --example scene_viewer -- assets/models/animated/MorphStressTest.gltf

Also tested a few other glTFs, and hacked the glTF importer to simulate some valid and invalid combinations (missing weights, mismatched arrays).

Alternatives

The pipeline still feels a little awkward, particularly the case where MeshMorphWeights might reference a MorphWeights on the same entity.

Some alternatives:

  1. Make MeshMorphWeights an enum that's either an entity reference or a Vec<f32> of weights.
    • This would be simpler for someone who wants to assign weights directly to a single mesh.
    • But not clear if that's common enough to justify the small complexity and memory cost.
  2. Remove MeshMorphWeights, change MorphWeights to be an enum that's either an entity reference or a Vec<f32> of weights.
    • Maximum simplicity, but...
    • Changes the MorphWeights interface, which means breaking more user code.
    • Also not clear what happens to the awkward MorphWeights::first_mesh variable.

I kinda like 2, but will be controversial. Maybe something for a follow up PR.

Comment on lines 147 to 152
pub fn clear_weights(&mut self) {
self.weights.clear();
}
pub fn extend_weights(&mut self, weights: &[f32]) {
self.weights.extend(weights);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were added so that MorphWeights supports everything that MeshMorphWeights did.

Personally I'd rather make MorphWeights members public and avoid the need for this interface.

Comment on lines 1564 to 1570
// Create `MorphWeights`. The weights will be copied from `mesh.weights()`
// if present. If not then the weights are zero.
//
// The glTF spec says that all primitives 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
// biggest, leaving any unspecified weights at zero.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I played it safe here, to the point of accepting malformed glTFs. I wasn't sure if the importer is intended to be strict or permissive.

@alice-i-cecile alice-i-cecile added C-Code-Quality A section of code that is hard to understand or change A-Animation Make things move and change over time X-Contentious There are nontrivial implications that should be thought through S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Mar 23, 2025
@greeble-dev greeble-dev marked this pull request as ready for review March 28, 2025 12:55
@greeble-dev
Copy link
Contributor Author

Changed from draft to ready for review. Documentation has been updated, and it now fully removes the bevy_animation dependency on bevy_render.

@greeble-dev greeble-dev added S-Needs-Review Needs reviewer attention (from anyone!) to move forward M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide A-Rendering Drawing game state to the screen and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels May 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Animation Make things move and change over time A-Rendering Drawing game state to the screen C-Code-Quality A section of code that is hard to understand or change M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Contentious There are nontrivial implications that should be thought through
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

Feature gate bevy_pbr in bevy_animation
2 participants