Skip to main content

many_foxes/
many_foxes.rs

1//! Loads animations from a skinned glTF, spawns many of them, and plays the
2//! animation to stress test skinned meshes.
3
4use 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)]
18/// `many_foxes` stress test
19struct Args {
20    /// whether all foxes run in sync.
21    #[argh(switch)]
22    sync: bool,
23
24    /// total number of foxes.
25    #[argh(option, default = "1000")]
26    count: usize,
27
28    /// enable motion blur.
29    #[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    // `from_env` panics on the web
43    #[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    // Insert a resource with the current scene information
123    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    // Foxes
138    // Concentric rings of foxes, running in opposite directions. The rings are spaced at 2m radius intervals.
139    // The foxes in each ring are spaced at least 2m apart around its circumference.'
140
141    // NOTE: This fox model faces +z
142    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    // Camera
197    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                // Use an unrealistically large shutter angle so that motion blur is clearly visible.
213                shutter_angle: 3.0,
214                ..Default::default()
215            },
216            // MSAA and MotionBlur are not compatible on WebGL.
217            #[cfg(all(feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu")))]
218            Msaa::Off,
219        ));
220    }
221
222    // Plane
223    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    // Light
229    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
250// Once the scene is loaded, start the animation
251fn 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}