1use std::{f32::consts::PI, time::Duration};
4
5use bevy::{
6 animation::{AnimationEvent, AnimationTargetId},
7 color::palettes::css::WHITE,
8 light::CascadeShadowConfigBuilder,
9 prelude::*,
10};
11use rand::{Rng, SeedableRng};
12use rand_chacha::ChaCha8Rng;
13
14const FOX_PATH: &str = "models/animated/Fox.glb";
15
16fn main() {
17 App::new()
18 .insert_resource(AmbientLight {
19 color: Color::WHITE,
20 brightness: 2000.,
21 ..default()
22 })
23 .add_plugins(DefaultPlugins)
24 .init_resource::<ParticleAssets>()
25 .init_resource::<FoxFeetTargets>()
26 .add_systems(Startup, setup)
27 .add_systems(Update, setup_scene_once_loaded)
28 .add_systems(Update, simulate_particles)
29 .add_observer(observe_on_step)
30 .run();
31}
32
33#[derive(Resource)]
34struct SeededRng(ChaCha8Rng);
35
36#[derive(Resource)]
37struct Animations {
38 index: AnimationNodeIndex,
39 graph_handle: Handle<AnimationGraph>,
40}
41
42#[derive(AnimationEvent, Reflect, Clone)]
43struct Step;
44
45fn observe_on_step(
46 step: On<Step>,
47 particle: Res<ParticleAssets>,
48 mut commands: Commands,
49 transforms: Query<&GlobalTransform>,
50 mut seeded_rng: ResMut<SeededRng>,
51) -> Result {
52 let translation = transforms
53 .get(step.trigger().animation_player)?
54 .translation();
55 for _ in 0..14 {
57 let horizontal = seeded_rng.0.random::<Dir2>() * seeded_rng.0.random_range(8.0..12.0);
58 let vertical = seeded_rng.0.random_range(0.0..4.0);
59 let size = seeded_rng.0.random_range(0.2..1.0);
60
61 commands.spawn((
62 Particle {
63 lifetime_timer: Timer::from_seconds(
64 seeded_rng.0.random_range(0.2..0.6),
65 TimerMode::Once,
66 ),
67 size,
68 velocity: Vec3::new(horizontal.x, vertical, horizontal.y) * 10.0,
69 },
70 Mesh3d(particle.mesh.clone()),
71 MeshMaterial3d(particle.material.clone()),
72 Transform {
73 translation,
74 scale: Vec3::splat(size),
75 ..Default::default()
76 },
77 ));
78 }
79 Ok(())
80}
81
82fn setup(
83 mut commands: Commands,
84 asset_server: Res<AssetServer>,
85 mut meshes: ResMut<Assets<Mesh>>,
86 mut materials: ResMut<Assets<StandardMaterial>>,
87 mut graphs: ResMut<Assets<AnimationGraph>>,
88) {
89 let (graph, index) = AnimationGraph::from_clip(
91 asset_server.load(GltfAssetLabel::Animation(2).from_asset(FOX_PATH)),
93 );
94
95 let graph_handle = graphs.add(graph);
97 commands.insert_resource(Animations {
98 index,
99 graph_handle,
100 });
101
102 commands.spawn((
104 Camera3d::default(),
105 Transform::from_xyz(100.0, 100.0, 150.0).looking_at(Vec3::new(0.0, 20.0, 0.0), Vec3::Y),
106 ));
107
108 commands.spawn((
110 Mesh3d(meshes.add(Plane3d::default().mesh().size(500000.0, 500000.0))),
111 MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
112 ));
113
114 commands.spawn((
116 Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
117 DirectionalLight {
118 shadows_enabled: true,
119 ..default()
120 },
121 CascadeShadowConfigBuilder {
122 first_cascade_far_bound: 200.0,
123 maximum_distance: 400.0,
124 ..default()
125 }
126 .build(),
127 ));
128
129 commands.spawn(SceneRoot(
131 asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_PATH)),
132 ));
133
134 let seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
137 commands.insert_resource(SeededRng(seeded_rng));
138}
139
140fn setup_scene_once_loaded(
143 mut commands: Commands,
144 animations: Res<Animations>,
145 feet: Res<FoxFeetTargets>,
146 graphs: Res<Assets<AnimationGraph>>,
147 mut clips: ResMut<Assets<AnimationClip>>,
148 mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
149) {
150 fn get_clip<'a>(
151 node: AnimationNodeIndex,
152 graph: &AnimationGraph,
153 clips: &'a mut Assets<AnimationClip>,
154 ) -> &'a mut AnimationClip {
155 let node = graph.get(node).unwrap();
156 let clip = match &node.node_type {
157 AnimationNodeType::Clip(handle) => clips.get_mut(handle),
158 _ => unreachable!(),
159 };
160 clip.unwrap()
161 }
162
163 for (entity, mut player) in &mut players {
164 let graph = graphs.get(&animations.graph_handle).unwrap();
167 let running_animation = get_clip(animations.index, graph, &mut clips);
168
169 running_animation.add_event_to_target(feet.front_left, 0.625, Step);
173 running_animation.add_event_to_target(feet.front_right, 0.5, Step);
174 running_animation.add_event_to_target(feet.back_left, 0.0, Step);
175 running_animation.add_event_to_target(feet.back_right, 0.125, Step);
176
177 let mut transitions = AnimationTransitions::new();
180
181 transitions
186 .play(&mut player, animations.index, Duration::ZERO)
187 .repeat();
188
189 commands
190 .entity(entity)
191 .insert(AnimationGraphHandle(animations.graph_handle.clone()))
192 .insert(transitions);
193 }
194}
195
196fn simulate_particles(
197 mut commands: Commands,
198 mut query: Query<(Entity, &mut Transform, &mut Particle)>,
199 time: Res<Time>,
200) {
201 for (entity, mut transform, mut particle) in &mut query {
202 if particle.lifetime_timer.tick(time.delta()).just_finished() {
203 commands.entity(entity).despawn();
204 return;
205 }
206
207 transform.translation += particle.velocity * time.delta_secs();
208 transform.scale = Vec3::splat(particle.size.lerp(0.0, particle.lifetime_timer.fraction()));
209 particle
210 .velocity
211 .smooth_nudge(&Vec3::ZERO, 4.0, time.delta_secs());
212 }
213}
214
215#[derive(Component)]
216struct Particle {
217 lifetime_timer: Timer,
218 size: f32,
219 velocity: Vec3,
220}
221
222#[derive(Resource)]
223struct ParticleAssets {
224 mesh: Handle<Mesh>,
225 material: Handle<StandardMaterial>,
226}
227
228impl FromWorld for ParticleAssets {
229 fn from_world(world: &mut World) -> Self {
230 Self {
231 mesh: world.add_asset::<Mesh>(Sphere::new(10.0)),
232 material: world.add_asset::<StandardMaterial>(StandardMaterial {
233 base_color: WHITE.into(),
234 ..Default::default()
235 }),
236 }
237 }
238}
239
240#[derive(Resource)]
242struct FoxFeetTargets {
243 front_right: AnimationTargetId,
244 front_left: AnimationTargetId,
245 back_left: AnimationTargetId,
246 back_right: AnimationTargetId,
247}
248
249impl Default for FoxFeetTargets {
250 fn default() -> Self {
251 let hip_node = ["root", "_rootJoint", "b_Root_00", "b_Hip_01"];
252 let front_left_foot = hip_node.iter().chain(
253 [
254 "b_Spine01_02",
255 "b_Spine02_03",
256 "b_LeftUpperArm_09",
257 "b_LeftForeArm_010",
258 "b_LeftHand_011",
259 ]
260 .iter(),
261 );
262 let front_right_foot = hip_node.iter().chain(
263 [
264 "b_Spine01_02",
265 "b_Spine02_03",
266 "b_RightUpperArm_06",
267 "b_RightForeArm_07",
268 "b_RightHand_08",
269 ]
270 .iter(),
271 );
272 let back_left_foot = hip_node.iter().chain(
273 [
274 "b_LeftLeg01_015",
275 "b_LeftLeg02_016",
276 "b_LeftFoot01_017",
277 "b_LeftFoot02_018",
278 ]
279 .iter(),
280 );
281 let back_right_foot = hip_node.iter().chain(
282 [
283 "b_RightLeg01_019",
284 "b_RightLeg02_020",
285 "b_RightFoot01_021",
286 "b_RightFoot02_022",
287 ]
288 .iter(),
289 );
290 Self {
291 front_left: AnimationTargetId::from_iter(front_left_foot),
292 front_right: AnimationTargetId::from_iter(front_right_foot),
293 back_left: AnimationTargetId::from_iter(back_left_foot),
294 back_right: AnimationTargetId::from_iter(back_right_foot),
295 }
296 }
297}