1use 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#[derive(PartialEq)]
20enum ArgWeights {
21 Animated,
23
24 One,
26
27 Zero,
29
30 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#[derive(PartialEq)]
51enum ArgCamera {
52 Near,
54
55 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#[derive(PartialEq)]
74enum ArgSpawning {
75 Instant,
77
78 Gradual,
80
81 RegularCycle,
84
85 RandomCycle,
88
89 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#[derive(FromArgs, Resource)]
114struct Args {
115 #[argh(option, default = "1024")]
117 count: usize,
118
119 #[argh(option, default = "ArgWeights::Animated")]
121 weights: ArgWeights,
122
123 #[argh(option, default = "ArgCamera::Near")]
125 camera: ArgCamera,
126
127 #[argh(option, default = "ArgSpawning::Instant")]
129 spawning: ArgSpawning,
130
131 #[argh(switch)]
133 motion_blur: bool,
134
135 #[argh(switch)]
137 mesh_compression: bool,
138}
139
140fn main() {
141 #[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 shutter_angle: 3.0,
238 ..default()
239 },
240 #[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 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
297fn 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 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 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}