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