diff --git a/Cargo.toml b/Cargo.toml index af2d9d356cb59..7666127bf13a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3011,6 +3011,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/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 diff --git a/examples/stress_tests/many_morph_targets.rs b/examples/stress_tests/many_morph_targets.rs new file mode 100644 index 0000000000000..dcf3c23211507 --- /dev/null +++ b/examples/stress_tests/many_morph_targets.rs @@ -0,0 +1,253 @@ +//! Simple benchmark to test rendering many meshes with animated morph targets. + +use argh::FromArgs; +use bevy::{ + diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, + prelude::*, + scene::SceneInstanceReady, + window::{PresentMode, WindowResolution}, + winit::{UpdateMode, WinitSettings}, +}; +use core::{f32::consts::PI, str::FromStr}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +/// Controls the weight values. +#[derive(PartialEq)] +enum ArgWeights { + /// Weights will change over time and be a mix of zero and non-zero. + Animated, + + /// Set all the weights to one. + One, + + /// Set all the weights to zero, minimizing vertex shader cost. + Zero, + + /// Set all the weights to a very small value, so the pixel shader cost + /// should be similar to `Zero` but vertex shader cost the same as `One`. + Tiny, +} + +impl FromStr for ArgWeights { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "animated" => Ok(Self::Animated), + "zero" => Ok(Self::Zero), + "one" => Ok(Self::One), + "tiny" => Ok(Self::Tiny), + _ => Err("must be 'animated', 'one', `zero`, or 'tiny'".into()), + } + } +} + +/// Controls the camera. +#[derive(PartialEq)] +enum ArgCamera { + /// Keep all the meshes in view and at a reasonable size. + Default, + + /// Zoom far out. This is used to reduce pixel shader costs and so emphasize + /// vertex shader costs. + Far, +} + +impl FromStr for ArgCamera { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "default" => Ok(Self::Default), + "far" => Ok(Self::Far), + _ => Err("must be 'default' or 'far'".into()), + } + } +} + +/// `many_morph_targets` stress test +#[derive(FromArgs, Resource)] +struct Args { + /// number of meshes + #[argh(option, default = "1024")] + count: usize, + + /// options: 'animated', 'one', 'zero', 'tiny' + #[argh(option, default = "ArgWeights::Animated")] + weights: ArgWeights, + + /// options: 'default', 'far' + #[argh(option, default = "ArgCamera::Default")] + camera: ArgCamera, +} + +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, Clone)] +struct AnimationToPlay { + graph_handle: Handle, + index: AnimationNodeIndex, + speed: f32, +} + +impl AnimationToPlay { + fn with_speed(&self, speed: f32) -> Self { + AnimationToPlay { + speed, + ..self.clone() + } + } +} + +fn setup( + args: Res, + asset_server: Res, + mut graphs: ResMut>, + mut commands: Commands, +) { + const ASSET_PATH: &str = "models/animated/MorphStressTest.gltf"; + + let scene = SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(ASSET_PATH))); + + let mut rng = ChaCha8Rng::seed_from_u64(856673); + + let animations = (0..3) + .map(|gltf_index| { + let (graph, index) = AnimationGraph::from_clip( + asset_server.load(GltfAssetLabel::Animation(gltf_index).from_asset(ASSET_PATH)), + ); + AnimationToPlay { + graph_handle: graphs.add(graph), + index, + speed: 1.0, + } + }) + .collect::>(); + + // Arrange the meshes in a grid. + + let count = args.count; + 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 = animations[mesh_index.rem_euclid(animations.len())].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))); + + // Randomly vary the animation speed so that the number of morph targets + // active on each frame is more likely to be stable. + + let animation_speed = rng.r#gen::() + 0.5; + + commands + .spawn(( + animation.with_speed(animation_speed), + scene.clone(), + Transform::from_xyz(x, y, 0.0), + )) + .observe(play_animation) + .observe(set_weights); + } + + commands.spawn(( + DirectionalLight::default(), + Transform::from_rotation(Quat::from_rotation_z(PI / 2.0)), + )); + + let camera_distance = (x_dim as f32) + * match args.camera { + ArgCamera::Default => 4.0, + ArgCamera::Far => 200.0, + }; + + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 0.0, camera_distance).looking_at(Vec3::ZERO, Vec3::Y), + )); +} + +fn play_animation( + trigger: Trigger, + mut commands: Commands, + args: Res, + children: Query<&Children>, + animations_to_play: Query<&AnimationToPlay>, + mut players: Query<&mut AnimationPlayer>, +) { + if args.weights != ArgWeights::Animated { + return; + } + + 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) { + commands + .entity(child) + .insert(AnimationGraphHandle(animation_to_play.graph_handle.clone())); + + player + .play(animation_to_play.index) + .repeat() + .set_speed(animation_to_play.speed); + } + } + } +} + +fn set_weights( + trigger: Trigger, + args: Res, + children: Query<&Children>, + mut weight_components: Query<&mut MorphWeights>, +) { + let weight_value = match args.weights { + ArgWeights::One => Some(1.0), + ArgWeights::Zero => Some(0.0), + ArgWeights::Tiny => Some(0.00001), + _ => None, + }; + + if let Some(weight_value) = weight_value { + for child in children.iter_descendants(trigger.target()) { + if let Ok(mut weight_component) = weight_components.get_mut(child) { + weight_component.weights_mut().fill(weight_value); + } + } + } +}