From 109eadde90cf18ee7c1e9a72441e15c286b0a4c7 Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Thu, 20 Mar 2025 18:34:10 +0000 Subject: [PATCH 01/17] Added `many_morph_targets` stress test. --- Cargo.toml | 11 ++ examples/stress_tests/many_morph_targets.rs | 124 ++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 examples/stress_tests/many_morph_targets.rs diff --git a/Cargo.toml b/Cargo.toml index 5872daf97017b..5fe9b57e48a48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2998,6 +2998,17 @@ description = "Loads an animated fox model and spawns lots of them. Good for tes category = "Stress Tests" wasm = true +[[example]] +name = "many_morph_targets" +path = "examples/stress_tests/many_morph_targets.rs" +doc-scrape-examples = true + +[package.metadata.example.many_morph_targets] +name = "Many Morph Targets" +description = "Simple benchmark to test rendering many meshes with animated morph targets." +category = "Stress Tests" +wasm = true + [[example]] name = "many_glyphs" path = "examples/stress_tests/many_glyphs.rs" diff --git a/examples/stress_tests/many_morph_targets.rs b/examples/stress_tests/many_morph_targets.rs new file mode 100644 index 0000000000000..968cd64c0030f --- /dev/null +++ b/examples/stress_tests/many_morph_targets.rs @@ -0,0 +1,124 @@ +//! TODO + +use argh::FromArgs; +use bevy::{ + diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, + prelude::*, + scene::SceneInstanceReady, + window::{PresentMode, WindowResolution}, + winit::{UpdateMode, WinitSettings}, +}; +use std::f32::consts::PI; + +/// TODO +#[derive(FromArgs, Resource)] +struct Args { + /// TODO + #[argh(option, default = "1024")] + count: usize, +} + +fn main() { + // `from_env` panics on the web + #[cfg(not(target_arch = "wasm32"))] + let args: Args = argh::from_env(); + #[cfg(target_arch = "wasm32")] + let args = Args::from_args(&[], &[]).unwrap(); + + App::new() + .add_plugins(( + DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Many Morph Targets".to_string(), + present_mode: PresentMode::AutoNoVsync, + resolution: WindowResolution::new(1920.0, 1080.0) + .with_scale_factor_override(1.0), + ..default() + }), + ..Default::default() + }), + FrameTimeDiagnosticsPlugin::default(), + LogDiagnosticsPlugin::default(), + )) + .insert_resource(WinitSettings { + focused_mode: UpdateMode::Continuous, + unfocused_mode: UpdateMode::Continuous, + }) + .insert_resource(AmbientLight { + brightness: 1000.0, + ..Default::default() + }) + .insert_resource(args) + .add_systems(Startup, setup) + .run(); +} + +#[derive(Component)] +struct AnimationToPlay(Handle); + +fn setup(asset_server: Res, args: Res, mut commands: Commands) { + let scene_handle = asset_server + .load(GltfAssetLabel::Scene(0).from_asset("models/animated/MorphStressTest.gltf")); + + let animation_handles = (0..3) + .map(|index| { + asset_server.load( + GltfAssetLabel::Animation(index).from_asset("models/animated/MorphStressTest.gltf"), + ) + }) + .collect::>(); + + let count = args.count.max(1); + let x_dim = ((count as f32).sqrt().ceil() as usize).max(1); + let y_dim = count.div_ceil(x_dim); + + for mesh_index in 0..count { + let animation_index = mesh_index.rem_euclid(animation_handles.len()); + let animation_handle = animation_handles[animation_index].clone(); + + let x = 2.5 + (5.0 * ((mesh_index.rem_euclid(x_dim) as f32) - ((x_dim as f32) * 0.5))); + let y = -2.2 - (3.0 * ((mesh_index.div_euclid(x_dim) as f32) - ((y_dim as f32) * 0.5))); + + commands + .spawn(( + AnimationToPlay(animation_handle), + SceneRoot(scene_handle.clone()), + Transform::from_xyz(x, y, 0.0), + )) + .observe(play_animation); + } + + commands.spawn(( + DirectionalLight::default(), + Transform::from_rotation(Quat::from_rotation_z(PI / 2.0)), + )); + + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 0.0, (x_dim as f32) * 4.0).looking_at(Vec3::ZERO, Vec3::Y), + )); +} + +fn play_animation( + trigger: Trigger, + mut commands: Commands, + children: Query<&Children>, + animations_to_play: Query<&AnimationToPlay>, + mut players: Query<&mut AnimationPlayer>, + mut graphs: ResMut>, +) { + if let Ok(animation_to_play) = animations_to_play.get(trigger.target()) { + for child in children.iter_descendants(trigger.target()) { + if let Ok(mut player) = players.get_mut(child) { + let (graph, animation_index) = + AnimationGraph::from_clip(animation_to_play.0.clone()); + + commands + .entity(child) + .insert(AnimationGraphHandle(graphs.add(graph))); + + player.play(animation_index).repeat(); + } + } + } +} From 3dacafecab92a29997a3bad588de3088b6212f9c Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Thu, 20 Mar 2025 19:53:40 +0000 Subject: [PATCH 02/17] First pass at replacing `inherit_weights` with extraction. --- crates/bevy_animation/src/lib.rs | 4 +-- crates/bevy_gltf/src/loader/mod.rs | 40 ++++++++++++----------------- crates/bevy_mesh/src/morph.rs | 29 +++------------------ crates/bevy_pbr/src/render/morph.rs | 9 ++++--- crates/bevy_render/src/mesh/mod.rs | 23 ++--------------- 5 files changed, 28 insertions(+), 77 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index b3ecc085c605b..ca3434b1d5d70 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1256,9 +1256,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, ) diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 6ebcb9b58aa7c..be3a860ea57a0 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -1387,8 +1387,6 @@ fn load_node( // Map node index to entity node_index_to_entity_map.insert(gltf_node.index(), node.id()); - let mut morph_weights = None; - 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 if !settings.load_meshes.is_empty() { @@ -1413,6 +1411,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? @@ -1424,22 +1423,13 @@ 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. + // TODO: Should this check that `target_count` equals + // `mesh.weights.len()`? Only necessary to catch malformed assets. + // + // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#morph-targets // > All morph target accessors MUST have the same count as // > the accessors of the original primitive. - mesh_entity.insert(MeshMorphWeights::new(weights).unwrap()); + mesh_entity.insert(MeshMorphWeights(parent_entity)); } mesh_entity.insert(Aabb::from_min_max( Vec3::from_slice(&bounds.min), @@ -1572,14 +1562,16 @@ 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() { + if let Some(morph_weights) = mesh.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(morph_weights.into(), first_mesh)?); + } } } diff --git a/crates/bevy_mesh/src/morph.rs b/crates/bevy_mesh/src/morph.rs index a8ff3be037d5a..cdce9cde39352 100644 --- a/crates/bevy_mesh/src/morph.rs +++ b/crates/bevy_mesh/src/morph.rs @@ -155,32 +155,9 @@ impl MorphWeights { /// 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, -} -impl MeshMorphWeights { - pub fn new(weights: Vec) -> Result { - 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(); - } - pub fn extend_weights(&mut self, weights: &[f32]) { - self.weights.extend(weights); - } -} +#[derive(Reflect, Debug, Clone, Component)] +#[reflect(Debug, Component, Clone)] +pub struct MeshMorphWeights(#[entities] pub Entity); /// Attributes **differences** used for morph targets. /// diff --git a/crates/bevy_pbr/src/render/morph.rs b/crates/bevy_pbr/src/render/morph.rs index 4b1ed68ce87a3..4fef1f7bbcbbf 100644 --- a/crates/bevy_pbr/src/render/morph.rs +++ b/crates/bevy_pbr/src/render/morph.rs @@ -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, @@ -110,6 +110,7 @@ pub fn extract_morphs( morph_indices: ResMut, uniform: ResMut, query: Extract>, + weights_query: Extract>, ) { // Borrow check workaround. let (morph_indices, uniform) = (morph_indices.into_inner(), uniform.into_inner()); @@ -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::(&mut uniform.current_buffer); diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index fbd530c14da42..a1c0234456ba9 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -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::() - .register_type::() - .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, Changed)>, - mut morph_primitives: Query<&mut MeshMorphWeights, With>, -) { - 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::(); } } From a2d982d9a479e344e6b0d06217db8183be7fd8e5 Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Fri, 21 Mar 2025 11:04:52 +0000 Subject: [PATCH 03/17] Fixed previous commit regressing https://github.com/bevyengine/bevy/issues/9863. Also made the weight count more forgiving, just in case. --- crates/bevy_gltf/src/loader/mod.rs | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index be3a860ea57a0..1365146d0b0a0 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -1,6 +1,7 @@ mod extensions; mod gltf_ext; +use core::iter::repeat_n; use std::{ io::Error, path::{Path, PathBuf}, @@ -1387,6 +1388,8 @@ fn load_node( // Map node index to entity node_index_to_entity_map.insert(gltf_node.index(), node.id()); + 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 if !settings.load_meshes.is_empty() { @@ -1423,12 +1426,7 @@ fn load_node( let target_count = primitive.morph_targets().len(); if target_count != 0 { - // TODO: Should this check that `target_count` equals - // `mesh.weights.len()`? Only necessary to catch malformed assets. - // - // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#morph-targets - // > All morph target accessors MUST have the same count as - // > the accessors of the original primitive. + max_morph_target_count = max_morph_target_count.max(target_count); mesh_entity.insert(MeshMorphWeights(parent_entity)); } mesh_entity.insert(Aabb::from_min_max( @@ -1563,14 +1561,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) = gltf_node.mesh() { - if let Some(morph_weights) = mesh.weights() { + // 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. + if (max_morph_target_count > 0) || mesh.weights().is_some() { + let mut weights = Vec::new(); + + if let Some(mesh_weights) = mesh.weights() { + weights = mesh_weights.to_vec(); + } + if max_morph_target_count > weights.len() { + weights.extend(repeat_n(0.0, max_morph_target_count - weights.len())); + } + 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(morph_weights.into(), first_mesh)?); + node.insert(MorphWeights::new(weights, first_mesh)?); } } } From afda57b06ea22eb7b21f6077aa59c7e0c369fa2b Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Fri, 21 Mar 2025 15:46:07 +0000 Subject: [PATCH 04/17] Added methods so that the `MorphWeights` interface matches the old `MeshMorphWeights`. This might smooth upgrades. --- crates/bevy_mesh/src/morph.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/bevy_mesh/src/morph.rs b/crates/bevy_mesh/src/morph.rs index cdce9cde39352..cd029438a87bd 100644 --- a/crates/bevy_mesh/src/morph.rs +++ b/crates/bevy_mesh/src/morph.rs @@ -142,6 +142,12 @@ impl MorphWeights { pub fn weights_mut(&mut self) -> &mut [f32] { &mut self.weights } + pub fn clear_weights(&mut self) { + self.weights.clear(); + } + pub fn extend_weights(&mut self, weights: &[f32]) { + self.weights.extend(weights); + } } /// Control a specific [`Mesh`] instance's [morph targets]. These control the weights of From 0cef429a4ef1ff8046d577bc0885fdee75bcee19 Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Fri, 21 Mar 2025 15:50:55 +0000 Subject: [PATCH 05/17] Added TODO comments. --- crates/bevy_mesh/src/morph.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_mesh/src/morph.rs b/crates/bevy_mesh/src/morph.rs index cd029438a87bd..b328964c7b666 100644 --- a/crates/bevy_mesh/src/morph.rs +++ b/crates/bevy_mesh/src/morph.rs @@ -97,6 +97,8 @@ impl MorphTargetImage { } } +/// TODO: Update this documentation. +/// /// 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 @@ -150,6 +152,8 @@ impl MorphWeights { } } +/// TODO: Update this documentation. +/// /// 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 From e61bcde82209a76de3343dc419ec0ec752be641c Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:14:28 +0000 Subject: [PATCH 06/17] Fixed clippy after merging main. --- crates/bevy_gltf/src/loader/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 89530a3826ba0..fcf71523ea5fa 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -1,7 +1,6 @@ mod extensions; mod gltf_ext; -use core::iter::repeat_n; use std::{ io::Error, path::{Path, PathBuf}, @@ -1576,7 +1575,10 @@ fn load_node( weights = mesh_weights.to_vec(); } if max_morph_target_count > weights.len() { - weights.extend(repeat_n(0.0, max_morph_target_count - weights.len())); + weights.extend(core::iter::repeat_n( + 0.0, + max_morph_target_count - weights.len(), + )); } let primitive_label = mesh.primitives().next().map(|p| GltfAssetLabel::Primitive { From c8fdc2a4da4b41c7154b66e8ef3407be604adaba Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:17:06 +0000 Subject: [PATCH 07/17] Fixed example docs. --- examples/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/README.md b/examples/README.md index 202c41a4f1d98..6c8f8668470b1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -502,6 +502,7 @@ Example | Description [Many Gizmos](../examples/stress_tests/many_gizmos.rs) | Test rendering of many gizmos [Many Glyphs](../examples/stress_tests/many_glyphs.rs) | Simple benchmark to test text rendering. [Many Lights](../examples/stress_tests/many_lights.rs) | Simple benchmark to test rendering many point lights. Run with `WGPU_SETTINGS_PRIO=webgl2` to restrict to uniform buffers and max 256 lights +[Many Morph Targets](../examples/stress_tests/many_morph_targets.rs) | Simple benchmark to test rendering many meshes with animated morph targets. [Many Sprites](../examples/stress_tests/many_sprites.rs) | Displays many sprites in a grid arrangement! Used for performance testing. Use `--colored` to enable color tinted sprites. [Many Text2d](../examples/stress_tests/many_text2d.rs) | Displays many Text2d! Used for performance testing. [Text Pipeline](../examples/stress_tests/text_pipeline.rs) | Text Pipeline benchmark From c7778ff8a78d382e0f7ec259794d8f99588af34d Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Sat, 22 Mar 2025 14:01:21 +0000 Subject: [PATCH 08/17] Simplified code. --- crates/bevy_gltf/src/loader/mod.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index fcf71523ea5fa..ac09775823ce7 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -1569,16 +1569,10 @@ fn load_node( // number if present. We're more forgiving and take whichever is // biggest, leaving any unspecified weights at zero. if (max_morph_target_count > 0) || mesh.weights().is_some() { - let mut weights = Vec::new(); + let mut weights = Vec::from(mesh.weights().unwrap_or(&[])); - if let Some(mesh_weights) = mesh.weights() { - weights = mesh_weights.to_vec(); - } if max_morph_target_count > weights.len() { - weights.extend(core::iter::repeat_n( - 0.0, - max_morph_target_count - weights.len(), - )); + weights.resize(max_morph_target_count, 0.0); } let primitive_label = mesh.primitives().next().map(|p| GltfAssetLabel::Primitive { From f207386bb3d979e198278f6328dcee863bd2ef1d Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Wed, 26 Mar 2025 15:20:21 +0000 Subject: [PATCH 09/17] Removed `bevy_animation` dependency on `bevy_render`. --- crates/bevy_animation/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 37bc05aaffa0f..50e3ce741a827 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -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" } From 7f5d4e91c935c18596f2ec0019c3de69c8ada1f6 Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:14:47 +0000 Subject: [PATCH 10/17] First pass at documentation. --- crates/bevy_mesh/src/morph.rs | 80 ++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/crates/bevy_mesh/src/morph.rs b/crates/bevy_mesh/src/morph.rs index b328964c7b666..6ec3da3a55311 100644 --- a/crates/bevy_mesh/src/morph.rs +++ b/crates/bevy_mesh/src/morph.rs @@ -97,19 +97,65 @@ impl MorphTargetImage { } } -/// TODO: Update this documentation. +/// Controls the [morph targets] of one or more meshes. /// -/// 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. +/// Any entity can contain a `MorphWeights` component. An entity with a `Mesh3d` +/// component can then reference it by adding a [`MeshMorphWeights`] +/// component. /// -/// 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). +/// Here, a single `MorphTargets` component is used to drive multiple meshes: /// -/// Add this to the parent of one or more [`Entities`](`Entity`) with a `Mesh3d` with a [`MeshMorphWeights`]. +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_asset::prelude::*; +/// # use bevy_mesh::*; +/// # use bevy_mesh::morph::*; +/// # #[derive(Component)] +/// # struct Mesh3d(Handle); +/// fn setup(mut commands: Commands, meshes: &[Handle]) { +/// // Create the `MorphWeights` component. +/// let weights_component = MorphWeights::new( +/// vec![0.0, 0.5, 1.0], +/// Some(meshes[0].clone()) +/// ).unwrap(); +/// +/// // Spawn an entity that contains the weights. Or we could have added them to +/// // an existing entity. +/// let weights_entity = commands.spawn(weights_component).id(); +/// +/// for mesh in meshes { +/// // The `MeshMorphWeights` component references the entity with `MorphWeights`. +/// commands.spawn(( +/// MeshMorphWeights(weights_entity), +/// Mesh3d(mesh.clone()) +/// )); +/// } +/// } +/// ``` +/// +/// Any changes to the `MorphWeights` component are automatically copied to the +/// mesh. +/// +/// In the simplest case, a `MorphTargets` component and a mesh can be in one +/// entity. The `MeshMorphTargets` component is still required. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_asset::prelude::*; +/// # use bevy_mesh::*; +/// # use bevy_mesh::morph::*; +/// # #[derive(Component)] +/// # struct Mesh3d(Handle); +/// # fn setup(mut commands: Commands, mesh: Handle) { +/// # let weights_component = MorphWeights::new(vec![0.0, 0.5, 1.0], Some(mesh.clone())).unwrap(); +/// let entity = commands.spawn(weights_component).id(); +/// +/// commands.entity(entity).insert( +/// (MeshMorphWeights(entity), +/// Mesh3d(mesh.clone()) +/// )); +/// # } +/// ``` /// /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation #[derive(Reflect, Default, Debug, Clone, Component)] @@ -152,17 +198,9 @@ impl MorphWeights { } } -/// TODO: Update this documentation. -/// -/// 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. +/// Controls the [morph targets] of a specific `Mesh3d` by referencing an +/// entity with a [`MorphWeights`] component. Multiple meshes can reference a +/// single `MorphWeights` component. /// /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation #[derive(Reflect, Debug, Clone, Component)] From 8fedc158996a68f8c4cb43d929938b8be9d37dc6 Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:24:43 +0000 Subject: [PATCH 11/17] Simplified documentation code. --- crates/bevy_mesh/src/morph.rs | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/crates/bevy_mesh/src/morph.rs b/crates/bevy_mesh/src/morph.rs index 6ec3da3a55311..6db58c733f73b 100644 --- a/crates/bevy_mesh/src/morph.rs +++ b/crates/bevy_mesh/src/morph.rs @@ -100,8 +100,7 @@ impl MorphTargetImage { /// Controls the [morph targets] of one or more meshes. /// /// Any entity can contain a `MorphWeights` component. An entity with a `Mesh3d` -/// component can then reference it by adding a [`MeshMorphWeights`] -/// component. +/// component can then reference it by using a [`MeshMorphWeights`] component. /// /// Here, a single `MorphTargets` component is used to drive multiple meshes: /// @@ -110,25 +109,20 @@ impl MorphTargetImage { /// # use bevy_asset::prelude::*; /// # use bevy_mesh::*; /// # use bevy_mesh::morph::*; -/// # #[derive(Component)] -/// # struct Mesh3d(Handle); -/// fn setup(mut commands: Commands, meshes: &[Handle]) { +/// fn setup(mut commands: Commands, mesh_entities: &[Entity]) { /// // Create the `MorphWeights` component. /// let weights_component = MorphWeights::new( /// vec![0.0, 0.5, 1.0], -/// Some(meshes[0].clone()) +/// None, /// ).unwrap(); /// -/// // Spawn an entity that contains the weights. Or we could have added them to +/// // Spawn an entity to contain the weights. Or we could have added them to /// // an existing entity. /// let weights_entity = commands.spawn(weights_component).id(); /// -/// for mesh in meshes { +/// for &mesh_entity in mesh_entities { /// // The `MeshMorphWeights` component references the entity with `MorphWeights`. -/// commands.spawn(( -/// MeshMorphWeights(weights_entity), -/// Mesh3d(mesh.clone()) -/// )); +/// commands.entity(mesh_entity).insert(MeshMorphWeights(weights_entity)); /// } /// } /// ``` @@ -144,15 +138,11 @@ impl MorphTargetImage { /// # use bevy_asset::prelude::*; /// # use bevy_mesh::*; /// # use bevy_mesh::morph::*; -/// # #[derive(Component)] -/// # struct Mesh3d(Handle); -/// # fn setup(mut commands: Commands, mesh: Handle) { -/// # let weights_component = MorphWeights::new(vec![0.0, 0.5, 1.0], Some(mesh.clone())).unwrap(); -/// let entity = commands.spawn(weights_component).id(); -/// -/// commands.entity(entity).insert( -/// (MeshMorphWeights(entity), -/// Mesh3d(mesh.clone()) +/// # fn setup(mut commands: Commands, mesh_entity: Entity) { +/// # let weights_component = MorphWeights::new(vec![0.0, 0.5, 1.0], None).unwrap(); +/// commands.entity(mesh_entity).insert(( +/// weights_component, +/// MeshMorphWeights(mesh_entity), /// )); /// # } /// ``` From 00401ebd5d90dfb20ea155d225e27f0641a51e5f Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:28:09 +0000 Subject: [PATCH 12/17] Removed redundant uses. --- crates/bevy_mesh/src/morph.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/bevy_mesh/src/morph.rs b/crates/bevy_mesh/src/morph.rs index 6db58c733f73b..0ee29ea3a1ee3 100644 --- a/crates/bevy_mesh/src/morph.rs +++ b/crates/bevy_mesh/src/morph.rs @@ -106,8 +106,6 @@ impl MorphTargetImage { /// /// ``` /// # use bevy_ecs::prelude::*; -/// # use bevy_asset::prelude::*; -/// # use bevy_mesh::*; /// # use bevy_mesh::morph::*; /// fn setup(mut commands: Commands, mesh_entities: &[Entity]) { /// // Create the `MorphWeights` component. @@ -135,8 +133,6 @@ impl MorphTargetImage { /// /// ``` /// # use bevy_ecs::prelude::*; -/// # use bevy_asset::prelude::*; -/// # use bevy_mesh::*; /// # use bevy_mesh::morph::*; /// # fn setup(mut commands: Commands, mesh_entity: Entity) { /// # let weights_component = MorphWeights::new(vec![0.0, 0.5, 1.0], None).unwrap(); From fbb11c9451d76c1d98adaa42d37822a6c04c7857 Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:42:28 +0000 Subject: [PATCH 13/17] Removed `many_morph_targets` stress test - this will be covered by #18536. --- Cargo.toml | 11 -- examples/stress_tests/many_morph_targets.rs | 124 -------------------- 2 files changed, 135 deletions(-) delete mode 100644 examples/stress_tests/many_morph_targets.rs diff --git a/Cargo.toml b/Cargo.toml index 7666127bf13a9..af2d9d356cb59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3011,17 +3011,6 @@ description = "Loads an animated fox model and spawns lots of them. Good for tes category = "Stress Tests" wasm = true -[[example]] -name = "many_morph_targets" -path = "examples/stress_tests/many_morph_targets.rs" -doc-scrape-examples = true - -[package.metadata.example.many_morph_targets] -name = "Many Morph Targets" -description = "Simple benchmark to test rendering many meshes with animated morph targets." -category = "Stress Tests" -wasm = true - [[example]] name = "many_glyphs" path = "examples/stress_tests/many_glyphs.rs" diff --git a/examples/stress_tests/many_morph_targets.rs b/examples/stress_tests/many_morph_targets.rs deleted file mode 100644 index 968cd64c0030f..0000000000000 --- a/examples/stress_tests/many_morph_targets.rs +++ /dev/null @@ -1,124 +0,0 @@ -//! TODO - -use argh::FromArgs; -use bevy::{ - diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, - prelude::*, - scene::SceneInstanceReady, - window::{PresentMode, WindowResolution}, - winit::{UpdateMode, WinitSettings}, -}; -use std::f32::consts::PI; - -/// TODO -#[derive(FromArgs, Resource)] -struct Args { - /// TODO - #[argh(option, default = "1024")] - count: usize, -} - -fn main() { - // `from_env` panics on the web - #[cfg(not(target_arch = "wasm32"))] - let args: Args = argh::from_env(); - #[cfg(target_arch = "wasm32")] - let args = Args::from_args(&[], &[]).unwrap(); - - App::new() - .add_plugins(( - DefaultPlugins.set(WindowPlugin { - primary_window: Some(Window { - title: "Many Morph Targets".to_string(), - present_mode: PresentMode::AutoNoVsync, - resolution: WindowResolution::new(1920.0, 1080.0) - .with_scale_factor_override(1.0), - ..default() - }), - ..Default::default() - }), - FrameTimeDiagnosticsPlugin::default(), - LogDiagnosticsPlugin::default(), - )) - .insert_resource(WinitSettings { - focused_mode: UpdateMode::Continuous, - unfocused_mode: UpdateMode::Continuous, - }) - .insert_resource(AmbientLight { - brightness: 1000.0, - ..Default::default() - }) - .insert_resource(args) - .add_systems(Startup, setup) - .run(); -} - -#[derive(Component)] -struct AnimationToPlay(Handle); - -fn setup(asset_server: Res, args: Res, mut commands: Commands) { - let scene_handle = asset_server - .load(GltfAssetLabel::Scene(0).from_asset("models/animated/MorphStressTest.gltf")); - - let animation_handles = (0..3) - .map(|index| { - asset_server.load( - GltfAssetLabel::Animation(index).from_asset("models/animated/MorphStressTest.gltf"), - ) - }) - .collect::>(); - - let count = args.count.max(1); - let x_dim = ((count as f32).sqrt().ceil() as usize).max(1); - let y_dim = count.div_ceil(x_dim); - - for mesh_index in 0..count { - let animation_index = mesh_index.rem_euclid(animation_handles.len()); - let animation_handle = animation_handles[animation_index].clone(); - - let x = 2.5 + (5.0 * ((mesh_index.rem_euclid(x_dim) as f32) - ((x_dim as f32) * 0.5))); - let y = -2.2 - (3.0 * ((mesh_index.div_euclid(x_dim) as f32) - ((y_dim as f32) * 0.5))); - - commands - .spawn(( - AnimationToPlay(animation_handle), - SceneRoot(scene_handle.clone()), - Transform::from_xyz(x, y, 0.0), - )) - .observe(play_animation); - } - - commands.spawn(( - DirectionalLight::default(), - Transform::from_rotation(Quat::from_rotation_z(PI / 2.0)), - )); - - commands.spawn(( - Camera3d::default(), - Transform::from_xyz(0.0, 0.0, (x_dim as f32) * 4.0).looking_at(Vec3::ZERO, Vec3::Y), - )); -} - -fn play_animation( - trigger: Trigger, - mut commands: Commands, - children: Query<&Children>, - animations_to_play: Query<&AnimationToPlay>, - mut players: Query<&mut AnimationPlayer>, - mut graphs: ResMut>, -) { - if let Ok(animation_to_play) = animations_to_play.get(trigger.target()) { - for child in children.iter_descendants(trigger.target()) { - if let Ok(mut player) = players.get_mut(child) { - let (graph, animation_index) = - AnimationGraph::from_clip(animation_to_play.0.clone()); - - commands - .entity(child) - .insert(AnimationGraphHandle(graphs.add(graph))); - - player.play(animation_index).repeat(); - } - } - } -} From 1ea9d8ce5b68bb24bdea79c8aa0796227f952264 Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:47:47 +0000 Subject: [PATCH 14/17] Oops, missed this. --- examples/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index 6c8f8668470b1..202c41a4f1d98 100644 --- a/examples/README.md +++ b/examples/README.md @@ -502,7 +502,6 @@ Example | Description [Many Gizmos](../examples/stress_tests/many_gizmos.rs) | Test rendering of many gizmos [Many Glyphs](../examples/stress_tests/many_glyphs.rs) | Simple benchmark to test text rendering. [Many Lights](../examples/stress_tests/many_lights.rs) | Simple benchmark to test rendering many point lights. Run with `WGPU_SETTINGS_PRIO=webgl2` to restrict to uniform buffers and max 256 lights -[Many Morph Targets](../examples/stress_tests/many_morph_targets.rs) | Simple benchmark to test rendering many meshes with animated morph targets. [Many Sprites](../examples/stress_tests/many_sprites.rs) | Displays many sprites in a grid arrangement! Used for performance testing. Use `--colored` to enable color tinted sprites. [Many Text2d](../examples/stress_tests/many_text2d.rs) | Displays many Text2d! Used for performance testing. [Text Pipeline](../examples/stress_tests/text_pipeline.rs) | Text Pipeline benchmark From bb11563a855c3788ee55a5b4a2b55eb1f8e5c35c Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:36:21 +0000 Subject: [PATCH 15/17] Comment tweak. --- crates/bevy_gltf/src/loader/mod.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 6cc6661f86ffd..b658147452768 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -1558,13 +1558,14 @@ 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) = gltf_node.mesh() { - // Create `MorphWeights`. The weights will be copied from `mesh.weights()` - // if present. If not then the weights are zero. + // 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 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. + // 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(&[])); From 9693d0c8acedb5dd7ce19674190d1201ea2ad9a1 Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:18:31 +0100 Subject: [PATCH 16/17] Better documentation. --- crates/bevy_mesh/src/morph.rs | 58 ++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/crates/bevy_mesh/src/morph.rs b/crates/bevy_mesh/src/morph.rs index 0ee29ea3a1ee3..b3bb4f7a142a5 100644 --- a/crates/bevy_mesh/src/morph.rs +++ b/crates/bevy_mesh/src/morph.rs @@ -97,48 +97,61 @@ impl MorphTargetImage { } } -/// Controls the [morph targets] of one or more meshes. +/// A component that controls the [morph targets] of one or more `Mesh3d` +/// components. /// -/// Any entity can contain a `MorphWeights` component. An entity with a `Mesh3d` -/// component can then reference it by using a [`MeshMorphWeights`] component. +/// 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. /// -/// Here, a single `MorphTargets` component is used to drive multiple meshes: +/// 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::*; -/// fn setup(mut commands: Commands, mesh_entities: &[Entity]) { +/// # #[derive(Component)] +/// # struct Mesh3d(Handle); +/// fn setup(mut commands: Commands, mesh_handle: Handle) { /// // 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. Or we could have added them to -/// // an existing entity. +/// // Spawn an entity to contain the weights. /// let weights_entity = commands.spawn(weights_component).id(); /// -/// for &mesh_entity in mesh_entities { -/// // The `MeshMorphWeights` component references the entity with `MorphWeights`. -/// commands.entity(mesh_entity).insert(MeshMorphWeights(weights_entity)); -/// } +/// // 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), +/// )); /// } /// ``` /// -/// Any changes to the `MorphWeights` component are automatically copied to the -/// mesh. -/// -/// In the simplest case, a `MorphTargets` component and a mesh can be in one -/// entity. The `MeshMorphTargets` component is still required. +/// 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); /// # fn setup(mut commands: Commands, mesh_entity: Entity) { /// # let weights_component = MorphWeights::new(vec![0.0, 0.5, 1.0], None).unwrap(); -/// commands.entity(mesh_entity).insert(( -/// weights_component, -/// MeshMorphWeights(mesh_entity), +/// # let mesh_handle = Handle::::default(); +/// let weights_entity = commands.spawn(weights_component).id(); +/// +/// commands.entity(weights_entity).insert(( +/// Mesh3d(mesh_handle.clone()), +/// MeshMorphWeights(weights_entity), /// )); /// # } /// ``` @@ -184,9 +197,10 @@ impl MorphWeights { } } -/// Controls the [morph targets] of a specific `Mesh3d` by referencing an -/// entity with a [`MorphWeights`] component. Multiple meshes can reference a -/// single `MorphWeights` component. +/// 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)] From dc64b9f4e6fc8dbc06c141f97c18bf7754e1d332 Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Fri, 2 May 2025 14:15:08 +0100 Subject: [PATCH 17/17] Added migration guide. --- .../meshmorphweights_is_now_a_reference.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 release-content/migration-guides/meshmorphweights_is_now_a_reference.md diff --git a/release-content/migration-guides/meshmorphweights_is_now_a_reference.md b/release-content/migration-guides/meshmorphweights_is_now_a_reference.md new file mode 100644 index 0000000000000..c038f6d203a5b --- /dev/null +++ b/release-content/migration-guides/meshmorphweights_is_now_a_reference.md @@ -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 } ++ 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.