Path: blob/main/examples/stress_tests/many_morph_targets.rs
30632 views
//! Simple benchmark to test rendering many meshes with animated morph targets.12use argh::FromArgs;3use bevy::{4diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},5post_process::motion_blur::MotionBlur,6prelude::*,7window::{PresentMode, WindowResolution},8winit::WinitSettings,9world_serialization::WorldInstanceReady,10};11use chacha20::ChaCha8Rng;12use core::{f32::consts::PI, str::FromStr};13use rand::{RngExt, SeedableRng};1415/// Controls the morph weights.16#[derive(PartialEq)]17enum ArgWeights {18/// Weights will be animated by an `AnimationClip`.19Animated,2021/// Set all the weights to one.22One,2324/// Set all the weights to zero, minimizing vertex shader cost.25Zero,2627/// Set all the weights to a very small value, so the pixel shader cost28/// should be similar to `Zero` but vertex shader cost the same as `One`.29Tiny,30}3132impl FromStr for ArgWeights {33type Err = String;3435fn from_str(s: &str) -> Result<Self, Self::Err> {36match 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}4546/// Controls the camera.47#[derive(PartialEq)]48enum ArgCamera {49/// Fill the screen with meshes.50Near,5152/// Zoom far out. This is used to reduce pixel shader costs and so emphasize53/// vertex shader costs.54Far,55}5657impl FromStr for ArgCamera {58type Err = String;5960fn from_str(s: &str) -> Result<Self, Self::Err> {61match s {62"near" => Ok(Self::Near),63"far" => Ok(Self::Far),64_ => Err("must be 'near' or 'far'".into()),65}66}67}6869/// Controls how the meshes spawn.70#[derive(PartialEq)]71enum ArgSpawning {72/// All meshes will spawn in one frame.73Instant,7475/// One mesh will spawn per frame.76Gradual,7778/// Spawn one mesh per frame in a consistent order until all are spawned,79/// then despawn one mesh per frame in the same order, and repeat.80RegularCycle,8182/// Spawn one mesh per frame in a random order until all are spawned, then83/// despawn one mesh per frame in a random order, and repeat.84RandomCycle,8586/// All meshes will spawn in one frame, and after that one mesh will spawn87/// and one mesh will despawn per frame.88RandomSteady,89}9091impl FromStr for ArgSpawning {92type Err = String;9394fn from_str(s: &str) -> Result<Self, Self::Err> {95match s {96"instant" => Ok(Self::Instant),97"gradual" => Ok(Self::Gradual),98"regular-cycle" => Ok(Self::RegularCycle),99"random-cycle" => Ok(Self::RandomCycle),100"random-steady" => Ok(Self::RandomSteady),101_ => Err(102"must be 'instant', 'gradual', 'regular-cycle', 'random-cycle', or 'random-steady'"103.into(),104),105}106}107}108109/// `many_morph_targets` stress test110#[derive(FromArgs, Resource)]111struct Args {112/// number of meshes - default = 1024113#[argh(option, default = "1024")]114count: usize,115116/// options: 'animated', 'one', 'zero', 'tiny' - default = 'animated'117#[argh(option, default = "ArgWeights::Animated")]118weights: ArgWeights,119120/// options: 'near', 'far' - default = 'near'121#[argh(option, default = "ArgCamera::Near")]122camera: ArgCamera,123124/// options: 'instant', 'gradual', 'regular-cycle', 'random-cycle', 'random-steady' - default = 'instant'125#[argh(option, default = "ArgSpawning::Instant")]126spawning: ArgSpawning,127128/// enable motion blur129#[argh(switch)]130motion_blur: bool,131}132133fn main() {134// `from_env` panics on the web135#[cfg(not(target_arch = "wasm32"))]136let args: Args = argh::from_env();137#[cfg(target_arch = "wasm32")]138let args = Args::from_args(&[], &[]).unwrap();139140App::new()141.add_plugins((142DefaultPlugins.set(WindowPlugin {143primary_window: Some(Window {144title: "Many Morph Targets".to_string(),145present_mode: PresentMode::AutoNoVsync,146resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),147..Default::default()148}),149..Default::default()150}),151FrameTimeDiagnosticsPlugin::default(),152LogDiagnosticsPlugin::default(),153))154.insert_resource(WinitSettings::continuous())155.insert_resource(GlobalAmbientLight {156brightness: 1000.0,157..Default::default()158})159.insert_resource(MorphAssets::default())160.insert_resource(Rng(ChaCha8Rng::seed_from_u64(856673)))161.insert_resource(State::new(&args))162.insert_resource(args)163.add_systems(Startup, setup)164.add_systems(Update, update)165.run();166}167168#[derive(Resource, Default)]169struct MorphAssets {170scene: Handle<WorldAsset>,171animations: Vec<(Handle<AnimationGraph>, AnimationNodeIndex)>,172}173174#[derive(Component, Clone)]175struct AnimationToPlay {176graph_handle: Handle<AnimationGraph>,177index: AnimationNodeIndex,178speed: f32,179}180181fn dims(count: usize) -> (usize, usize) {182let x_dim = ((count as f32).sqrt().ceil() as usize).max(1);183let y_dim = count.div_ceil(x_dim);184185(x_dim, y_dim)186}187188fn setup(189args: Res<Args>,190mut commands: Commands,191mut assets: ResMut<MorphAssets>,192asset_server: Res<AssetServer>,193mut graphs: ResMut<Assets<AnimationGraph>>,194state: Res<State>,195) {196let (x_dim, _) = dims(state.slot_count);197198commands.spawn((199DirectionalLight::default(),200Transform::from_rotation(Quat::from_rotation_z(PI / 2.0)),201));202203let camera_distance = (x_dim as f32)204* match args.camera {205ArgCamera::Near => 4.0,206ArgCamera::Far => 200.0,207};208209let mut camera = commands.spawn((210Camera3d::default(),211Transform::from_xyz(0.0, 0.0, camera_distance).looking_at(Vec3::ZERO, Vec3::Y),212));213214if args.motion_blur {215camera.insert((216MotionBlur {217// Use an unrealistically large shutter angle so that motion blur is clearly visible.218shutter_angle: 3.0,219..Default::default()220},221// MSAA and MotionBlur are not compatible on WebGL.222#[cfg(all(feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu")))]223Msaa::Off,224));225}226227const ASSET_PATH: &str = "models/animated/MorphStressTest.gltf";228229*assets = MorphAssets {230scene: asset_server.load(GltfAssetLabel::Scene(0).from_asset(ASSET_PATH)),231animations: (0..3)232.map(|gltf_index| {233let (graph, index) = AnimationGraph::from_clip(234asset_server.load(GltfAssetLabel::Animation(gltf_index).from_asset(ASSET_PATH)),235);236(graphs.add(graph), index)237})238.collect::<Vec<_>>(),239}240}241242enum CycleState {243Spawn,244Despawn,245}246247#[derive(Resource)]248struct State {249ticks: usize,250slot_count: usize,251spawned: Vec<(usize, Entity)>,252despawned: Vec<usize>,253cycle: CycleState,254}255256impl State {257fn new(args: &Args) -> State {258// The `RandomSteady` case allocates double the number of slots but only259// keeps half occupied.260let slot_count = match args.spawning {261ArgSpawning::RandomSteady => args.count * 2,262_ => args.count,263};264265State {266ticks: 0,267slot_count,268spawned: Default::default(),269despawned: (0..slot_count).collect::<Vec<_>>(),270cycle: CycleState::Spawn,271}272}273}274275#[derive(Resource)]276struct Rng(ChaCha8Rng);277278// Randomly take `count` entries from the given `Vec` and return them.279fn take_random<T>(rng: &mut ChaCha8Rng, from: &mut Vec<T>, count: usize) -> Vec<T> {280(0..count)281.map(|_| from.swap_remove(rng.random_range(..from.len())))282.collect()283}284285fn update(286args: Res<Args>,287mut commands: Commands,288mut state: ResMut<State>,289mut rng: ResMut<Rng>,290assets: Res<MorphAssets>,291) {292state.ticks += 1;293294if state.spawned.is_empty() {295state.cycle = CycleState::Spawn;296} else if state.despawned.is_empty() {297state.cycle = CycleState::Despawn;298}299300let mut to_spawn = Vec::<usize>::default();301let mut to_despawn = Vec::<(usize, Entity)>::default();302303match args.spawning {304ArgSpawning::Instant => to_spawn = std::mem::take(&mut state.despawned),305ArgSpawning::Gradual => to_spawn = state.despawned.pop().into_iter().collect(),306ArgSpawning::RegularCycle => match state.cycle {307CycleState::Spawn => to_spawn.push(state.despawned.pop().unwrap()),308CycleState::Despawn => to_despawn.push(state.spawned.pop().unwrap()),309},310ArgSpawning::RandomCycle => match state.cycle {311CycleState::Spawn => to_spawn = take_random(&mut rng.0, &mut state.despawned, 1),312CycleState::Despawn => to_despawn = take_random(&mut rng.0, &mut state.spawned, 1),313},314ArgSpawning::RandomSteady => {315if state.spawned.is_empty() {316let spawn_count = state.slot_count / 2;317to_spawn = take_random(&mut rng.0, &mut state.despawned, spawn_count);318} else {319to_spawn = take_random(&mut rng.0, &mut state.despawned, 1);320to_despawn = take_random(&mut rng.0, &mut state.spawned, 1);321}322}323}324325for (mesh_index, entity) in to_despawn {326commands.entity(entity).despawn();327state.despawned.push(mesh_index);328}329330for mesh_index in to_spawn {331// Arrange the meshes in a grid.332333let (x_dim, y_dim) = dims(state.slot_count);334335let x = 2.5 + (5.0 * ((mesh_index.rem_euclid(x_dim) as f32) - ((x_dim as f32) * 0.5)));336let y = -2.2 - (3.0 * ((mesh_index.div_euclid(x_dim) as f32) - ((y_dim as f32) * 0.5)));337338// Vary the animation speed so that the number of morph targets339// active on each frame is more likely to be stable.340341let speed = ((mesh_index as f32) * 0.1).rem_euclid(1.0) + 0.5;342343let animation_asset =344assets.animations[mesh_index.rem_euclid(assets.animations.len())].clone();345let animation = AnimationToPlay {346graph_handle: animation_asset.0.clone(),347index: animation_asset.1,348speed,349};350351let entity = commands352.spawn((353animation,354Transform::from_xyz(x, y, 0.0),355WorldAssetRoot(assets.scene.clone()),356))357.observe(play_animation)358.observe(set_weights)359.id();360361state.spawned.push((mesh_index, entity));362}363}364365fn play_animation(366trigger: On<WorldInstanceReady>,367mut commands: Commands,368args: Res<Args>,369children: Query<&Children>,370animations_to_play: Query<&AnimationToPlay>,371mut players: Query<&mut AnimationPlayer>,372) {373if args.weights == ArgWeights::Animated374&& let Ok(animation_to_play) = animations_to_play.get(trigger.entity)375{376for child in children.iter_descendants(trigger.entity) {377if let Ok(mut player) = players.get_mut(child) {378commands379.entity(child)380.insert(AnimationGraphHandle(animation_to_play.graph_handle.clone()));381382player383.play(animation_to_play.index)384.repeat()385.set_speed(animation_to_play.speed);386}387}388}389}390391fn set_weights(392trigger: On<WorldInstanceReady>,393args: Res<Args>,394children: Query<&Children>,395mut weight_components: Query<&mut MorphWeights>,396) {397if let Some(weight_value) = match args.weights {398ArgWeights::One => Some(1.0),399ArgWeights::Zero => Some(0.0),400ArgWeights::Tiny => Some(0.00001),401_ => None,402} {403for child in children.iter_descendants(trigger.entity) {404if let Ok(mut weight_component) = weight_components.get_mut(child) {405weight_component.weights_mut().fill(weight_value);406}407}408}409}410411412