Skip to content

Commit 0339de9

Browse files
authored
Add many_morph_targets stress test (#18536)
## Objective I wanted to benchmark the morph target changes in #18465. I also wanted to test morph targets on multiple meshes, which is not covered by existing examples. ## Solution Add a stress test for morph targets, similar to `many_cubes` and `many_foxes`. Spawns a ton of meshes (defaults to 1024) and animates their morph target weights. ![425571366-b043c16c-6e6a-491e-a0bd-5ece630d7bf8](https://github.com/user-attachments/assets/86ef26a4-ad00-46fa-9e5a-0aa4238023e3) ## Testing ```sh cargo run --example many_morph_targets # Test different mesh counts. cargo run --example many_morph_targets -- --count 42 ``` Tested on Win10/Vulkan/Nvidia, Wasm/WebGL/Chrome/Win10/Nvidia.
1 parent f61e18f commit 0339de9

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3519,6 +3519,17 @@ description = "Loads an animated fox model and spawns lots of them. Good for tes
35193519
category = "Stress Tests"
35203520
wasm = true
35213521

3522+
[[example]]
3523+
name = "many_morph_targets"
3524+
path = "examples/stress_tests/many_morph_targets.rs"
3525+
doc-scrape-examples = true
3526+
3527+
[package.metadata.example.many_morph_targets]
3528+
name = "Many Morph Targets"
3529+
description = "Simple benchmark to test rendering many meshes with animated morph targets."
3530+
category = "Stress Tests"
3531+
wasm = true
3532+
35223533
[[example]]
35233534
name = "many_glyphs"
35243535
path = "examples/stress_tests/many_glyphs.rs"

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,7 @@ Example | Description
539539
[Many Glyphs](../examples/stress_tests/many_glyphs.rs) | Simple benchmark to test text rendering.
540540
[Many Gradients](../examples/stress_tests/many_gradients.rs) | Stress test for gradient rendering performance
541541
[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
542+
[Many Morph Targets](../examples/stress_tests/many_morph_targets.rs) | Simple benchmark to test rendering many meshes with animated morph targets.
542543
[Many Sprite Meshes](../examples/stress_tests/many_sprite_meshes.rs) | Displays many sprite meshes in a grid arrangement! Used for performance testing. Use `--colored` to enable color tinted sprites.
543544
[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.
544545
[Many Text2d](../examples/stress_tests/many_text2d.rs) | Displays many Text2d! Used for performance testing.
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
//! Simple benchmark to test rendering many meshes with animated morph targets.
2+
3+
use argh::FromArgs;
4+
use bevy::{
5+
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
6+
prelude::*,
7+
scene::SceneInstanceReady,
8+
window::{PresentMode, WindowResolution},
9+
winit::WinitSettings,
10+
};
11+
use core::{f32::consts::PI, str::FromStr};
12+
use rand::{Rng, SeedableRng};
13+
use rand_chacha::ChaCha8Rng;
14+
15+
/// Controls the morph weights.
16+
#[derive(PartialEq)]
17+
enum ArgWeights {
18+
/// Weights will be animated by an `AnimationClip`.
19+
Animated,
20+
21+
/// Set all the weights to one.
22+
One,
23+
24+
/// Set all the weights to zero, minimizing vertex shader cost.
25+
Zero,
26+
27+
/// Set all the weights to a very small value, so the pixel shader cost
28+
/// should be similar to `Zero` but vertex shader cost the same as `One`.
29+
Tiny,
30+
}
31+
32+
impl FromStr for ArgWeights {
33+
type Err = String;
34+
35+
fn from_str(s: &str) -> Result<Self, Self::Err> {
36+
match s {
37+
"animated" => Ok(Self::Animated),
38+
"zero" => Ok(Self::Zero),
39+
"one" => Ok(Self::One),
40+
"tiny" => Ok(Self::Tiny),
41+
_ => Err("must be 'animated', 'one', `zero`, or 'tiny'".into()),
42+
}
43+
}
44+
}
45+
46+
/// Controls the camera.
47+
#[derive(PartialEq)]
48+
enum ArgCamera {
49+
/// Fill the screen with meshes.
50+
Near,
51+
52+
/// Zoom far out. This is used to reduce pixel shader costs and so emphasize
53+
/// vertex shader costs.
54+
Far,
55+
}
56+
57+
impl FromStr for ArgCamera {
58+
type Err = String;
59+
60+
fn from_str(s: &str) -> Result<Self, Self::Err> {
61+
match s {
62+
"near" => Ok(Self::Near),
63+
"far" => Ok(Self::Far),
64+
_ => Err("must be 'near' or 'far'".into()),
65+
}
66+
}
67+
}
68+
69+
/// `many_morph_targets` stress test
70+
#[derive(FromArgs, Resource)]
71+
struct Args {
72+
/// number of meshes - default = 1024
73+
#[argh(option, default = "1024")]
74+
count: usize,
75+
76+
/// options: 'animated', 'one', 'zero', 'tiny' - default = 'animated'
77+
#[argh(option, default = "ArgWeights::Animated")]
78+
weights: ArgWeights,
79+
80+
/// options: 'near', 'far' - default = 'near'
81+
#[argh(option, default = "ArgCamera::Near")]
82+
camera: ArgCamera,
83+
}
84+
85+
fn main() {
86+
// `from_env` panics on the web
87+
#[cfg(not(target_arch = "wasm32"))]
88+
let args: Args = argh::from_env();
89+
#[cfg(target_arch = "wasm32")]
90+
let args = Args::from_args(&[], &[]).unwrap();
91+
92+
App::new()
93+
.add_plugins((
94+
DefaultPlugins.set(WindowPlugin {
95+
primary_window: Some(Window {
96+
title: "Many Morph Targets".to_string(),
97+
present_mode: PresentMode::AutoNoVsync,
98+
resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
99+
..Default::default()
100+
}),
101+
..Default::default()
102+
}),
103+
FrameTimeDiagnosticsPlugin::default(),
104+
LogDiagnosticsPlugin::default(),
105+
))
106+
.insert_resource(WinitSettings::continuous())
107+
.insert_resource(GlobalAmbientLight {
108+
brightness: 1000.0,
109+
..Default::default()
110+
})
111+
.insert_resource(args)
112+
.add_systems(Startup, setup)
113+
.run();
114+
}
115+
116+
#[derive(Component, Clone)]
117+
struct AnimationToPlay {
118+
graph_handle: Handle<AnimationGraph>,
119+
index: AnimationNodeIndex,
120+
speed: f32,
121+
}
122+
123+
impl AnimationToPlay {
124+
fn with_speed(&self, speed: f32) -> Self {
125+
AnimationToPlay {
126+
speed,
127+
..self.clone()
128+
}
129+
}
130+
}
131+
132+
fn setup(
133+
args: Res<Args>,
134+
asset_server: Res<AssetServer>,
135+
mut graphs: ResMut<Assets<AnimationGraph>>,
136+
mut commands: Commands,
137+
) {
138+
const ASSET_PATH: &str = "models/animated/MorphStressTest.gltf";
139+
140+
let scene = SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(ASSET_PATH)));
141+
142+
let mut rng = ChaCha8Rng::seed_from_u64(856673);
143+
144+
let animations = (0..3)
145+
.map(|gltf_index| {
146+
let (graph, index) = AnimationGraph::from_clip(
147+
asset_server.load(GltfAssetLabel::Animation(gltf_index).from_asset(ASSET_PATH)),
148+
);
149+
AnimationToPlay {
150+
graph_handle: graphs.add(graph),
151+
index,
152+
speed: 1.0,
153+
}
154+
})
155+
.collect::<Vec<_>>();
156+
157+
// Arrange the meshes in a grid.
158+
159+
let count = args.count;
160+
let x_dim = ((count as f32).sqrt().ceil() as usize).max(1);
161+
let y_dim = count.div_ceil(x_dim);
162+
163+
for mesh_index in 0..count {
164+
let animation = animations[mesh_index.rem_euclid(animations.len())].clone();
165+
166+
let x = 2.5 + (5.0 * ((mesh_index.rem_euclid(x_dim) as f32) - ((x_dim as f32) * 0.5)));
167+
let y = -2.2 - (3.0 * ((mesh_index.div_euclid(x_dim) as f32) - ((y_dim as f32) * 0.5)));
168+
169+
// Randomly vary the animation speed so that the number of morph targets
170+
// active on each frame is more likely to be stable.
171+
172+
let animation_speed = rng.random_range(0.5..=1.5);
173+
174+
commands
175+
.spawn((
176+
animation.with_speed(animation_speed),
177+
scene.clone(),
178+
Transform::from_xyz(x, y, 0.0),
179+
))
180+
.observe(play_animation)
181+
.observe(set_weights);
182+
}
183+
184+
commands.spawn((
185+
DirectionalLight::default(),
186+
Transform::from_rotation(Quat::from_rotation_z(PI / 2.0)),
187+
));
188+
189+
let camera_distance = (x_dim as f32)
190+
* match args.camera {
191+
ArgCamera::Near => 4.0,
192+
ArgCamera::Far => 200.0,
193+
};
194+
195+
commands.spawn((
196+
Camera3d::default(),
197+
Transform::from_xyz(0.0, 0.0, camera_distance).looking_at(Vec3::ZERO, Vec3::Y),
198+
));
199+
}
200+
201+
fn play_animation(
202+
trigger: On<SceneInstanceReady>,
203+
mut commands: Commands,
204+
args: Res<Args>,
205+
children: Query<&Children>,
206+
animations_to_play: Query<&AnimationToPlay>,
207+
mut players: Query<&mut AnimationPlayer>,
208+
) {
209+
if args.weights == ArgWeights::Animated
210+
&& let Ok(animation_to_play) = animations_to_play.get(trigger.entity)
211+
{
212+
for child in children.iter_descendants(trigger.entity) {
213+
if let Ok(mut player) = players.get_mut(child) {
214+
commands
215+
.entity(child)
216+
.insert(AnimationGraphHandle(animation_to_play.graph_handle.clone()));
217+
218+
player
219+
.play(animation_to_play.index)
220+
.repeat()
221+
.set_speed(animation_to_play.speed);
222+
}
223+
}
224+
}
225+
}
226+
227+
fn set_weights(
228+
trigger: On<SceneInstanceReady>,
229+
args: Res<Args>,
230+
children: Query<&Children>,
231+
mut weight_components: Query<&mut MorphWeights>,
232+
) {
233+
if let Some(weight_value) = match args.weights {
234+
ArgWeights::One => Some(1.0),
235+
ArgWeights::Zero => Some(0.0),
236+
ArgWeights::Tiny => Some(0.00001),
237+
_ => None,
238+
} {
239+
for child in children.iter_descendants(trigger.entity) {
240+
if let Ok(mut weight_component) = weight_components.get_mut(child) {
241+
weight_component.weights_mut().fill(weight_value);
242+
}
243+
}
244+
}
245+
}

0 commit comments

Comments
 (0)