1use std::{f32::consts::PI, time::Duration};
5
6use argh::FromArgs;
7use bevy::{
8 diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
9 gltf::GltfPlugin,
10 light::CascadeShadowConfigBuilder,
11 mesh::MeshCompressionArgs,
12 post_process::motion_blur::MotionBlur,
13 prelude::*,
14 window::{PresentMode, WindowResolution},
15 winit::WinitSettings,
16 world_serialization::WorldInstanceReady,
17};
18
19#[derive(FromArgs, Resource)]
20struct Args {
22 #[argh(switch)]
24 sync: bool,
25
26 #[argh(option, default = "1000")]
28 count: usize,
29
30 #[argh(switch)]
32 motion_blur: bool,
33
34 #[argh(switch)]
36 mesh_compression: bool,
37}
38
39#[derive(Resource)]
40struct Foxes {
41 count: usize,
42 speed: f32,
43 moving: bool,
44 sync: bool,
45}
46
47fn main() {
48 #[cfg(not(target_arch = "wasm32"))]
50 let args: Args = argh::from_env();
51 #[cfg(target_arch = "wasm32")]
52 let args = Args::from_args(&[], &[]).unwrap();
53
54 App::new()
55 .add_plugins((
56 DefaultPlugins
57 .set(WindowPlugin {
58 primary_window: Some(Window {
59 title: "🦊🦊🦊 Many Foxes! 🦊🦊🦊".into(),
60 present_mode: PresentMode::AutoNoVsync,
61 resolution: WindowResolution::new(1920, 1080)
62 .with_scale_factor_override(1.0),
63 ..default()
64 }),
65 ..default()
66 })
67 .set(GltfPlugin {
68 mesh_compression: if args.mesh_compression {
69 MeshCompressionArgs::regular()
70 } else {
71 MeshCompressionArgs::none()
72 },
73 ..default()
74 }),
75 FrameTimeDiagnosticsPlugin::default(),
76 LogDiagnosticsPlugin::default(),
77 ))
78 .insert_resource(StaticTransformOptimizations::Disabled)
79 .insert_resource(WinitSettings::continuous())
80 .insert_resource(Foxes {
81 count: args.count,
82 speed: 2.0,
83 moving: true,
84 sync: args.sync,
85 })
86 .insert_resource(args)
87 .add_systems(Startup, setup)
88 .add_systems(
89 Update,
90 (
91 keyboard_animation_control,
92 update_fox_rings.after(keyboard_animation_control),
93 ),
94 )
95 .run();
96}
97
98#[derive(Resource)]
99struct Animations {
100 node_indices: Vec<AnimationNodeIndex>,
101 graph: Handle<AnimationGraph>,
102}
103
104const RING_SPACING: f32 = 2.0;
105const FOX_SPACING: f32 = 2.0;
106
107#[derive(Component, Clone, Copy)]
108enum RotationDirection {
109 CounterClockwise,
110 Clockwise,
111}
112
113impl RotationDirection {
114 fn sign(&self) -> f32 {
115 match self {
116 RotationDirection::CounterClockwise => 1.0,
117 RotationDirection::Clockwise => -1.0,
118 }
119 }
120}
121
122#[derive(Component)]
123struct Ring {
124 radius: f32,
125}
126
127fn setup(
128 mut commands: Commands,
129 asset_server: Res<AssetServer>,
130 mut meshes: ResMut<Assets<Mesh>>,
131 mut materials: ResMut<Assets<StandardMaterial>>,
132 mut animation_graphs: ResMut<Assets<AnimationGraph>>,
133 foxes: Res<Foxes>,
134 args: Res<Args>,
135) {
136 warn!(include_str!("warning_string.txt"));
137
138 let animation_clips = [
140 asset_server.load(GltfAssetLabel::Animation(2).from_asset("models/animated/Fox.glb")),
141 asset_server.load(GltfAssetLabel::Animation(1).from_asset("models/animated/Fox.glb")),
142 asset_server.load(GltfAssetLabel::Animation(0).from_asset("models/animated/Fox.glb")),
143 ];
144 let mut animation_graph = AnimationGraph::new();
145 let node_indices = animation_graph
146 .add_clips(animation_clips, 1.0, animation_graph.root)
147 .collect();
148 commands.insert_resource(Animations {
149 node_indices,
150 graph: animation_graphs.add(animation_graph),
151 });
152
153 let fox_handle =
159 asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb"));
160
161 let ring_directions = [
162 (
163 Quat::from_rotation_y(PI),
164 RotationDirection::CounterClockwise,
165 ),
166 (Quat::IDENTITY, RotationDirection::Clockwise),
167 ];
168
169 let mut ring_index = 0;
170 let mut radius = RING_SPACING;
171 let mut foxes_remaining = foxes.count;
172
173 info!("Spawning {} foxes...", foxes.count);
174
175 while foxes_remaining > 0 {
176 let (base_rotation, ring_direction) = ring_directions[ring_index % 2];
177 let ring_parent = commands
178 .spawn((
179 Transform::default(),
180 Visibility::default(),
181 ring_direction,
182 Ring { radius },
183 ))
184 .id();
185
186 let circumference = PI * 2. * radius;
187 let foxes_in_ring = ((circumference / FOX_SPACING) as usize).min(foxes_remaining);
188 let fox_spacing_angle = circumference / (foxes_in_ring as f32 * radius);
189
190 for fox_i in 0..foxes_in_ring {
191 let fox_angle = fox_i as f32 * fox_spacing_angle;
192 let (s, c) = ops::sin_cos(fox_angle);
193 let (x, z) = (radius * c, radius * s);
194
195 commands.entity(ring_parent).with_children(|builder| {
196 builder
197 .spawn((
198 WorldAssetRoot(fox_handle.clone()),
199 Transform::from_xyz(x, 0.0, z)
200 .with_scale(Vec3::splat(0.01))
201 .with_rotation(base_rotation * Quat::from_rotation_y(-fox_angle)),
202 ))
203 .observe(setup_scene_once_loaded);
204 });
205 }
206
207 foxes_remaining -= foxes_in_ring;
208 radius += RING_SPACING;
209 ring_index += 1;
210 }
211
212 let zoom = 0.8;
214 let translation = Vec3::new(
215 radius * 1.25 * zoom,
216 radius * 0.5 * zoom,
217 radius * 1.5 * zoom,
218 );
219 let mut camera = commands.spawn((
220 Camera3d::default(),
221 Transform::from_translation(translation)
222 .looking_at(0.2 * Vec3::new(translation.x, 0.0, translation.z), Vec3::Y),
223 ));
224
225 if args.motion_blur {
226 camera.insert((
227 MotionBlur {
228 shutter_angle: 3.0,
230 ..Default::default()
231 },
232 #[cfg(all(feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu")))]
234 Msaa::Off,
235 ));
236 }
237
238 commands.spawn((
240 Mesh3d(meshes.add(Plane3d::default().mesh().size(5000.0, 5000.0))),
241 MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
242 ));
243
244 commands.spawn((
246 Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
247 DirectionalLight {
248 shadow_maps_enabled: true,
249 ..default()
250 },
251 CascadeShadowConfigBuilder {
252 first_cascade_far_bound: 0.9 * radius,
253 maximum_distance: 2.8 * radius,
254 ..default()
255 }
256 .build(),
257 ));
258
259 println!("Animation controls:");
260 println!(" - spacebar: play / pause");
261 println!(" - arrow up / down: speed up / slow down animation playback");
262 println!(" - arrow left / right: seek backward / forward");
263 println!(" - return: change animation");
264}
265
266fn setup_scene_once_loaded(
268 scene_ready: On<WorldInstanceReady>,
269 animations: Res<Animations>,
270 foxes: Res<Foxes>,
271 mut commands: Commands,
272 children: Query<&Children>,
273 mut players: Query<&mut AnimationPlayer>,
274) {
275 for child in children.iter_descendants(scene_ready.entity) {
276 if let Ok(mut player) = players.get_mut(child) {
277 let playing_animation = player.play(animations.node_indices[0]).repeat();
278 if !foxes.sync {
279 playing_animation.seek_to(scene_ready.entity.index_u32() as f32 / 10.0);
280 }
281 commands.entity(child).insert((
282 AnimationGraphHandle(animations.graph.clone()),
283 AnimationTransitions::default(),
284 ));
285 }
286 }
287}
288
289fn update_fox_rings(
290 time: Res<Time>,
291 foxes: Res<Foxes>,
292 mut rings: Query<(&Ring, &RotationDirection, &mut Transform)>,
293) {
294 if !foxes.moving {
295 return;
296 }
297
298 let dt = time.delta_secs();
299 for (ring, rotation_direction, mut transform) in &mut rings {
300 let angular_velocity = foxes.speed / ring.radius;
301 transform.rotate_y(rotation_direction.sign() * angular_velocity * dt);
302 }
303}
304
305fn keyboard_animation_control(
306 keyboard_input: Res<ButtonInput<KeyCode>>,
307 mut animation_player: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
308 animations: Res<Animations>,
309 mut current_animation: Local<usize>,
310 mut foxes: ResMut<Foxes>,
311) {
312 if keyboard_input.just_pressed(KeyCode::Space) {
313 foxes.moving = !foxes.moving;
314 }
315
316 if keyboard_input.just_pressed(KeyCode::ArrowUp) {
317 foxes.speed *= 1.25;
318 }
319
320 if keyboard_input.just_pressed(KeyCode::ArrowDown) {
321 foxes.speed *= 0.8;
322 }
323
324 if keyboard_input.just_pressed(KeyCode::Enter) {
325 *current_animation = (*current_animation + 1) % animations.node_indices.len();
326 }
327
328 for (mut player, mut transitions) in &mut animation_player {
329 if keyboard_input.just_pressed(KeyCode::Space) {
330 if player.all_paused() {
331 player.resume_all();
332 } else {
333 player.pause_all();
334 }
335 }
336
337 if keyboard_input.just_pressed(KeyCode::ArrowUp) {
338 player.adjust_speeds(1.25);
339 }
340
341 if keyboard_input.just_pressed(KeyCode::ArrowDown) {
342 player.adjust_speeds(0.8);
343 }
344
345 if keyboard_input.just_pressed(KeyCode::ArrowLeft) {
346 player.seek_all_by(-0.1);
347 }
348
349 if keyboard_input.just_pressed(KeyCode::ArrowRight) {
350 player.seek_all_by(0.1);
351 }
352
353 if keyboard_input.just_pressed(KeyCode::Enter) {
354 transitions
355 .play(
356 &mut player,
357 animations.node_indices[*current_animation],
358 Duration::from_millis(250),
359 )
360 .repeat();
361 }
362 }
363}