Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/stress_tests/many_morph_targets.rs
30632 views
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
post_process::motion_blur::MotionBlur,
7
prelude::*,
8
window::{PresentMode, WindowResolution},
9
winit::WinitSettings,
10
world_serialization::WorldInstanceReady,
11
};
12
use chacha20::ChaCha8Rng;
13
use core::{f32::consts::PI, str::FromStr};
14
use rand::{RngExt, SeedableRng};
15
16
/// Controls the morph weights.
17
#[derive(PartialEq)]
18
enum 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
33
impl 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)]
49
enum 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
58
impl 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)]
72
enum 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
92
impl 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)]
112
struct 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
134
fn 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)]
170
struct MorphAssets {
171
scene: Handle<WorldAsset>,
172
animations: Vec<(Handle<AnimationGraph>, AnimationNodeIndex)>,
173
}
174
175
#[derive(Component, Clone)]
176
struct AnimationToPlay {
177
graph_handle: Handle<AnimationGraph>,
178
index: AnimationNodeIndex,
179
speed: f32,
180
}
181
182
fn 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
189
fn 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
243
enum CycleState {
244
Spawn,
245
Despawn,
246
}
247
248
#[derive(Resource)]
249
struct State {
250
ticks: usize,
251
slot_count: usize,
252
spawned: Vec<(usize, Entity)>,
253
despawned: Vec<usize>,
254
cycle: CycleState,
255
}
256
257
impl 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)]
277
struct Rng(ChaCha8Rng);
278
279
// Randomly take `count` entries from the given `Vec` and return them.
280
fn 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
286
fn 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
366
fn 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
392
fn 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
}
411
412