1use 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#[derive(PartialEq)]
18enum ArgWeights {
19 Animated,
21
22 One,
24
25 Zero,
27
28 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#[derive(PartialEq)]
49enum ArgCamera {
50 Near,
52
53 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#[derive(PartialEq)]
72enum ArgSpawning {
73 Instant,
75
76 Gradual,
78
79 RegularCycle,
82
83 RandomCycle,
86
87 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#[derive(FromArgs, Resource)]
112struct Args {
113 #[argh(option, default = "1024")]
115 count: usize,
116
117 #[argh(option, default = "ArgWeights::Animated")]
119 weights: ArgWeights,
120
121 #[argh(option, default = "ArgCamera::Near")]
123 camera: ArgCamera,
124
125 #[argh(option, default = "ArgSpawning::Instant")]
127 spawning: ArgSpawning,
128
129 #[argh(switch)]
131 motion_blur: bool,
132}
133
134fn main() {
135 #[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 shutter_angle: 3.0,
220 ..Default::default()
221 },
222 #[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 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
279fn 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 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 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}