Skip to main content

many_morph_targets/
many_morph_targets.rs

1//! Simple benchmark to test rendering many meshes with animated morph targets.
2
3use argh::FromArgs;
4use bevy::{
5    diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
6    post_process::motion_blur::MotionBlur,
7    prelude::*,
8    window::{PresentMode, WindowResolution},
9    winit::WinitSettings,
10    world_serialization::WorldInstanceReady,
11};
12use chacha20::ChaCha8Rng;
13use core::{f32::consts::PI, str::FromStr};
14use rand::{RngExt, SeedableRng};
15
16/// Controls the morph weights.
17#[derive(PartialEq)]
18enum ArgWeights {
19    /// Weights will be animated by an `AnimationClip`.
20    Animated,
21
22    /// Set all the weights to one.
23    One,
24
25    /// Set all the weights to zero, minimizing vertex shader cost.
26    Zero,
27
28    /// Set all the weights to a very small value, so the pixel shader cost
29    /// should be similar to `Zero` but vertex shader cost the same as `One`.
30    Tiny,
31}
32
33impl FromStr for ArgWeights {
34    type Err = String;
35
36    fn from_str(s: &str) -> Result<Self, Self::Err> {
37        match s {
38            "animated" => Ok(Self::Animated),
39            "zero" => Ok(Self::Zero),
40            "one" => Ok(Self::One),
41            "tiny" => Ok(Self::Tiny),
42            _ => Err("must be 'animated', 'one', `zero`, or 'tiny'".into()),
43        }
44    }
45}
46
47/// Controls the camera.
48#[derive(PartialEq)]
49enum ArgCamera {
50    /// Fill the screen with meshes.
51    Near,
52
53    /// Zoom far out. This is used to reduce pixel shader costs and so emphasize
54    /// vertex shader costs.
55    Far,
56}
57
58impl FromStr for ArgCamera {
59    type Err = String;
60
61    fn from_str(s: &str) -> Result<Self, Self::Err> {
62        match s {
63            "near" => Ok(Self::Near),
64            "far" => Ok(Self::Far),
65            _ => Err("must be 'near' or 'far'".into()),
66        }
67    }
68}
69
70/// Controls how the meshes spawn.
71#[derive(PartialEq)]
72enum ArgSpawning {
73    /// All meshes will spawn in one frame.
74    Instant,
75
76    /// One mesh will spawn per frame.
77    Gradual,
78
79    /// Spawn one mesh per frame in a consistent order until all are spawned,
80    /// then despawn one mesh per frame in the same order, and repeat.
81    RegularCycle,
82
83    /// Spawn one mesh per frame in a random order until all are spawned, then
84    /// despawn one mesh per frame in a random order, and repeat.
85    RandomCycle,
86
87    /// All meshes will spawn in one frame, and after that one mesh will spawn
88    /// and one mesh will despawn per frame.
89    RandomSteady,
90}
91
92impl FromStr for ArgSpawning {
93    type Err = String;
94
95    fn from_str(s: &str) -> Result<Self, Self::Err> {
96        match s {
97            "instant" => Ok(Self::Instant),
98            "gradual" => Ok(Self::Gradual),
99            "regular-cycle" => Ok(Self::RegularCycle),
100            "random-cycle" => Ok(Self::RandomCycle),
101            "random-steady" => Ok(Self::RandomSteady),
102            _ => Err(
103                "must be 'instant', 'gradual', 'regular-cycle', 'random-cycle', or 'random-steady'"
104                    .into(),
105            ),
106        }
107    }
108}
109
110/// `many_morph_targets` stress test
111#[derive(FromArgs, Resource)]
112struct Args {
113    /// number of meshes - default = 1024
114    #[argh(option, default = "1024")]
115    count: usize,
116
117    /// options: 'animated', 'one', 'zero', 'tiny' - default = 'animated'
118    #[argh(option, default = "ArgWeights::Animated")]
119    weights: ArgWeights,
120
121    /// options: 'near', 'far' - default = 'near'
122    #[argh(option, default = "ArgCamera::Near")]
123    camera: ArgCamera,
124
125    /// options: 'instant', 'gradual', 'regular-cycle', 'random-cycle', 'random-steady' - default = 'instant'
126    #[argh(option, default = "ArgSpawning::Instant")]
127    spawning: ArgSpawning,
128
129    /// enable motion blur
130    #[argh(switch)]
131    motion_blur: bool,
132}
133
134fn main() {
135    // `from_env` panics on the web
136    #[cfg(not(target_arch = "wasm32"))]
137    let args: Args = argh::from_env();
138    #[cfg(target_arch = "wasm32")]
139    let args = Args::from_args(&[], &[]).unwrap();
140
141    App::new()
142        .add_plugins((
143            DefaultPlugins.set(WindowPlugin {
144                primary_window: Some(Window {
145                    title: "Many Morph Targets".to_string(),
146                    present_mode: PresentMode::AutoNoVsync,
147                    resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
148                    ..Default::default()
149                }),
150                ..Default::default()
151            }),
152            FrameTimeDiagnosticsPlugin::default(),
153            LogDiagnosticsPlugin::default(),
154        ))
155        .insert_resource(WinitSettings::continuous())
156        .insert_resource(GlobalAmbientLight {
157            brightness: 1000.0,
158            ..Default::default()
159        })
160        .insert_resource(MorphAssets::default())
161        .insert_resource(Rng(ChaCha8Rng::seed_from_u64(856673)))
162        .insert_resource(State::new(&args))
163        .insert_resource(args)
164        .add_systems(Startup, setup)
165        .add_systems(Update, update)
166        .run();
167}
168
169#[derive(Resource, Default)]
170struct MorphAssets {
171    scene: Handle<WorldAsset>,
172    animations: Vec<(Handle<AnimationGraph>, AnimationNodeIndex)>,
173}
174
175#[derive(Component, Clone)]
176struct AnimationToPlay {
177    graph_handle: Handle<AnimationGraph>,
178    index: AnimationNodeIndex,
179    speed: f32,
180}
181
182fn dims(count: usize) -> (usize, usize) {
183    let x_dim = ((count as f32).sqrt().ceil() as usize).max(1);
184    let y_dim = count.div_ceil(x_dim);
185
186    (x_dim, y_dim)
187}
188
189fn setup(
190    args: Res<Args>,
191    mut commands: Commands,
192    mut assets: ResMut<MorphAssets>,
193    asset_server: Res<AssetServer>,
194    mut graphs: ResMut<Assets<AnimationGraph>>,
195    state: Res<State>,
196) {
197    let (x_dim, _) = dims(state.slot_count);
198
199    commands.spawn((
200        DirectionalLight::default(),
201        Transform::from_rotation(Quat::from_rotation_z(PI / 2.0)),
202    ));
203
204    let camera_distance = (x_dim as f32)
205        * match args.camera {
206            ArgCamera::Near => 4.0,
207            ArgCamera::Far => 200.0,
208        };
209
210    let mut camera = commands.spawn((
211        Camera3d::default(),
212        Transform::from_xyz(0.0, 0.0, camera_distance).looking_at(Vec3::ZERO, Vec3::Y),
213    ));
214
215    if args.motion_blur {
216        camera.insert((
217            MotionBlur {
218                // Use an unrealistically large shutter angle so that motion blur is clearly visible.
219                shutter_angle: 3.0,
220                ..Default::default()
221            },
222            // MSAA and MotionBlur are not compatible on WebGL.
223            #[cfg(all(feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu")))]
224            Msaa::Off,
225        ));
226    }
227
228    const ASSET_PATH: &str = "models/animated/MorphStressTest.gltf";
229
230    *assets = MorphAssets {
231        scene: asset_server.load(GltfAssetLabel::Scene(0).from_asset(ASSET_PATH)),
232        animations: (0..3)
233            .map(|gltf_index| {
234                let (graph, index) = AnimationGraph::from_clip(
235                    asset_server.load(GltfAssetLabel::Animation(gltf_index).from_asset(ASSET_PATH)),
236                );
237                (graphs.add(graph), index)
238            })
239            .collect::<Vec<_>>(),
240    }
241}
242
243enum CycleState {
244    Spawn,
245    Despawn,
246}
247
248#[derive(Resource)]
249struct State {
250    ticks: usize,
251    slot_count: usize,
252    spawned: Vec<(usize, Entity)>,
253    despawned: Vec<usize>,
254    cycle: CycleState,
255}
256
257impl State {
258    fn new(args: &Args) -> State {
259        // The `RandomSteady` case allocates double the number of slots but only
260        // keeps half occupied.
261        let slot_count = match args.spawning {
262            ArgSpawning::RandomSteady => args.count * 2,
263            _ => args.count,
264        };
265
266        State {
267            ticks: 0,
268            slot_count,
269            spawned: Default::default(),
270            despawned: (0..slot_count).collect::<Vec<_>>(),
271            cycle: CycleState::Spawn,
272        }
273    }
274}
275
276#[derive(Resource)]
277struct Rng(ChaCha8Rng);
278
279// Randomly take `count` entries from the given `Vec` and return them.
280fn take_random<T>(rng: &mut ChaCha8Rng, from: &mut Vec<T>, count: usize) -> Vec<T> {
281    (0..count)
282        .map(|_| from.swap_remove(rng.random_range(..from.len())))
283        .collect()
284}
285
286fn update(
287    args: Res<Args>,
288    mut commands: Commands,
289    mut state: ResMut<State>,
290    mut rng: ResMut<Rng>,
291    assets: Res<MorphAssets>,
292) {
293    state.ticks += 1;
294
295    if state.spawned.is_empty() {
296        state.cycle = CycleState::Spawn;
297    } else if state.despawned.is_empty() {
298        state.cycle = CycleState::Despawn;
299    }
300
301    let mut to_spawn = Vec::<usize>::default();
302    let mut to_despawn = Vec::<(usize, Entity)>::default();
303
304    match args.spawning {
305        ArgSpawning::Instant => to_spawn = std::mem::take(&mut state.despawned),
306        ArgSpawning::Gradual => to_spawn = state.despawned.pop().into_iter().collect(),
307        ArgSpawning::RegularCycle => match state.cycle {
308            CycleState::Spawn => to_spawn.push(state.despawned.pop().unwrap()),
309            CycleState::Despawn => to_despawn.push(state.spawned.pop().unwrap()),
310        },
311        ArgSpawning::RandomCycle => match state.cycle {
312            CycleState::Spawn => to_spawn = take_random(&mut rng.0, &mut state.despawned, 1),
313            CycleState::Despawn => to_despawn = take_random(&mut rng.0, &mut state.spawned, 1),
314        },
315        ArgSpawning::RandomSteady => {
316            if state.spawned.is_empty() {
317                let spawn_count = state.slot_count / 2;
318                to_spawn = take_random(&mut rng.0, &mut state.despawned, spawn_count);
319            } else {
320                to_spawn = take_random(&mut rng.0, &mut state.despawned, 1);
321                to_despawn = take_random(&mut rng.0, &mut state.spawned, 1);
322            }
323        }
324    }
325
326    for (mesh_index, entity) in to_despawn {
327        commands.entity(entity).despawn();
328        state.despawned.push(mesh_index);
329    }
330
331    for mesh_index in to_spawn {
332        // Arrange the meshes in a grid.
333
334        let (x_dim, y_dim) = dims(state.slot_count);
335
336        let x = 2.5 + (5.0 * ((mesh_index.rem_euclid(x_dim) as f32) - ((x_dim as f32) * 0.5)));
337        let y = -2.2 - (3.0 * ((mesh_index.div_euclid(x_dim) as f32) - ((y_dim as f32) * 0.5)));
338
339        // Vary the animation speed so that the number of morph targets
340        // active on each frame is more likely to be stable.
341
342        let speed = ((mesh_index as f32) * 0.1).rem_euclid(1.0) + 0.5;
343
344        let animation_asset =
345            assets.animations[mesh_index.rem_euclid(assets.animations.len())].clone();
346        let animation = AnimationToPlay {
347            graph_handle: animation_asset.0.clone(),
348            index: animation_asset.1,
349            speed,
350        };
351
352        let entity = commands
353            .spawn((
354                animation,
355                Transform::from_xyz(x, y, 0.0),
356                WorldAssetRoot(assets.scene.clone()),
357            ))
358            .observe(play_animation)
359            .observe(set_weights)
360            .id();
361
362        state.spawned.push((mesh_index, entity));
363    }
364}
365
366fn play_animation(
367    trigger: On<WorldInstanceReady>,
368    mut commands: Commands,
369    args: Res<Args>,
370    children: Query<&Children>,
371    animations_to_play: Query<&AnimationToPlay>,
372    mut players: Query<&mut AnimationPlayer>,
373) {
374    if args.weights == ArgWeights::Animated
375        && let Ok(animation_to_play) = animations_to_play.get(trigger.entity)
376    {
377        for child in children.iter_descendants(trigger.entity) {
378            if let Ok(mut player) = players.get_mut(child) {
379                commands
380                    .entity(child)
381                    .insert(AnimationGraphHandle(animation_to_play.graph_handle.clone()));
382
383                player
384                    .play(animation_to_play.index)
385                    .repeat()
386                    .set_speed(animation_to_play.speed);
387            }
388        }
389    }
390}
391
392fn set_weights(
393    trigger: On<WorldInstanceReady>,
394    args: Res<Args>,
395    children: Query<&Children>,
396    mut weight_components: Query<&mut MorphWeights>,
397) {
398    if let Some(weight_value) = match args.weights {
399        ArgWeights::One => Some(1.0),
400        ArgWeights::Zero => Some(0.0),
401        ArgWeights::Tiny => Some(0.00001),
402        _ => None,
403    } {
404        for child in children.iter_descendants(trigger.entity) {
405            if let Ok(mut weight_component) = weight_components.get_mut(child) {
406                weight_component.weights_mut().fill(weight_value);
407            }
408        }
409    }
410}