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