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 chacha20::ChaCha8Rng;
12use rand::{RngExt, SeedableRng};
13
14const FOX_PATH: &str = "models/animated/Fox.glb";
15
16fn main() {
17 App::new()
18 .insert_resource(GlobalAmbientLight {
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.get(step.trigger().target)?.translation();
53 for _ in 0..14 {
55 let horizontal = seeded_rng.0.random::<Dir2>() * seeded_rng.0.random_range(8.0..12.0);
56 let vertical = seeded_rng.0.random_range(0.0..4.0);
57 let size = seeded_rng.0.random_range(0.2..1.0);
58
59 commands.spawn((
60 Particle {
61 lifetime_timer: Timer::from_seconds(
62 seeded_rng.0.random_range(0.2..0.6),
63 TimerMode::Once,
64 ),
65 size,
66 velocity: Vec3::new(horizontal.x, vertical, horizontal.y) * 10.0,
67 },
68 Mesh3d(particle.mesh.clone()),
69 MeshMaterial3d(particle.material.clone()),
70 Transform {
71 translation,
72 scale: Vec3::splat(size),
73 ..Default::default()
74 },
75 ));
76 }
77 Ok(())
78}
79
80fn setup(
81 mut commands: Commands,
82 asset_server: Res<AssetServer>,
83 mut meshes: ResMut<Assets<Mesh>>,
84 mut materials: ResMut<Assets<StandardMaterial>>,
85 mut graphs: ResMut<Assets<AnimationGraph>>,
86) {
87 let (graph, index) = AnimationGraph::from_clip(
89 asset_server.load(GltfAssetLabel::Animation(2).from_asset(FOX_PATH)),
91 );
92
93 let graph_handle = graphs.add(graph);
95 commands.insert_resource(Animations {
96 index,
97 graph_handle,
98 });
99
100 commands.spawn((
102 Camera3d::default(),
103 Transform::from_xyz(100.0, 100.0, 150.0).looking_at(Vec3::new(0.0, 20.0, 0.0), Vec3::Y),
104 ));
105
106 commands.spawn((
108 Mesh3d(meshes.add(Plane3d::default().mesh().size(500000.0, 500000.0))),
109 MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
110 ));
111
112 commands.spawn((
114 Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
115 DirectionalLight {
116 shadow_maps_enabled: true,
117 ..default()
118 },
119 CascadeShadowConfigBuilder {
120 first_cascade_far_bound: 200.0,
121 maximum_distance: 400.0,
122 ..default()
123 }
124 .build(),
125 ));
126
127 commands.spawn(WorldAssetRoot(
129 asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_PATH)),
130 ));
131
132 let seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
135 commands.insert_resource(SeededRng(seeded_rng));
136}
137
138fn setup_scene_once_loaded(
141 mut commands: Commands,
142 animations: Res<Animations>,
143 feet: Res<FoxFeetTargets>,
144 graphs: Res<Assets<AnimationGraph>>,
145 mut clips: ResMut<Assets<AnimationClip>>,
146 mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
147) {
148 fn get_clip<'a>(
149 node: AnimationNodeIndex,
150 graph: &AnimationGraph,
151 clips: &'a mut Assets<AnimationClip>,
152 ) -> &'a mut AnimationClip {
153 let node = graph.get(node).unwrap();
154 let clip = match &node.node_type {
155 AnimationNodeType::Clip(handle) => clips.get_mut(handle),
156 _ => unreachable!(),
157 };
158 clip.unwrap().into_inner()
159 }
160
161 for (entity, mut player) in &mut players {
162 let graph = graphs.get(&animations.graph_handle).unwrap();
165 let running_animation = get_clip(animations.index, graph, &mut clips);
166
167 running_animation.add_event_to_target(feet.front_left, 0.625, Step);
171 running_animation.add_event_to_target(feet.front_right, 0.5, Step);
172 running_animation.add_event_to_target(feet.back_left, 0.0, Step);
173 running_animation.add_event_to_target(feet.back_right, 0.125, Step);
174
175 let mut transitions = AnimationTransitions::new();
178
179 transitions
184 .play(&mut player, animations.index, Duration::ZERO)
185 .repeat();
186
187 commands
188 .entity(entity)
189 .insert(AnimationGraphHandle(animations.graph_handle.clone()))
190 .insert(transitions);
191 }
192}
193
194fn simulate_particles(
195 mut commands: Commands,
196 mut query: Query<(Entity, &mut Transform, &mut Particle)>,
197 time: Res<Time>,
198) {
199 for (entity, mut transform, mut particle) in &mut query {
200 if particle.lifetime_timer.tick(time.delta()).just_finished() {
201 commands.entity(entity).despawn();
202 return;
203 }
204
205 transform.translation += particle.velocity * time.delta_secs();
206 transform.scale = Vec3::splat(particle.size.lerp(0.0, particle.lifetime_timer.fraction()));
207 particle
208 .velocity
209 .smooth_nudge(&Vec3::ZERO, 4.0, time.delta_secs());
210 }
211}
212
213#[derive(Component)]
214struct Particle {
215 lifetime_timer: Timer,
216 size: f32,
217 velocity: Vec3,
218}
219
220#[derive(Resource)]
221struct ParticleAssets {
222 mesh: Handle<Mesh>,
223 material: Handle<StandardMaterial>,
224}
225
226impl FromWorld for ParticleAssets {
227 fn from_world(world: &mut World) -> Self {
228 Self {
229 mesh: world.add_asset::<Mesh>(Sphere::new(10.0)),
230 material: world.add_asset::<StandardMaterial>(StandardMaterial {
231 base_color: WHITE.into(),
232 ..Default::default()
233 }),
234 }
235 }
236}
237
238#[derive(Resource)]
240struct FoxFeetTargets {
241 front_right: AnimationTargetId,
242 front_left: AnimationTargetId,
243 back_left: AnimationTargetId,
244 back_right: AnimationTargetId,
245}
246
247impl Default for FoxFeetTargets {
248 fn default() -> Self {
249 let hip_node = ["root", "_rootJoint", "b_Root_00", "b_Hip_01"];
250 let front_left_foot = hip_node.iter().chain(
251 [
252 "b_Spine01_02",
253 "b_Spine02_03",
254 "b_LeftUpperArm_09",
255 "b_LeftForeArm_010",
256 "b_LeftHand_011",
257 ]
258 .iter(),
259 );
260 let front_right_foot = hip_node.iter().chain(
261 [
262 "b_Spine01_02",
263 "b_Spine02_03",
264 "b_RightUpperArm_06",
265 "b_RightForeArm_07",
266 "b_RightHand_08",
267 ]
268 .iter(),
269 );
270 let back_left_foot = hip_node.iter().chain(
271 [
272 "b_LeftLeg01_015",
273 "b_LeftLeg02_016",
274 "b_LeftFoot01_017",
275 "b_LeftFoot02_018",
276 ]
277 .iter(),
278 );
279 let back_right_foot = hip_node.iter().chain(
280 [
281 "b_RightLeg01_019",
282 "b_RightLeg02_020",
283 "b_RightFoot01_021",
284 "b_RightFoot02_022",
285 ]
286 .iter(),
287 );
288 Self {
289 front_left: AnimationTargetId::from_iter(front_left_foot),
290 front_right: AnimationTargetId::from_iter(front_right_foot),
291 back_left: AnimationTargetId::from_iter(back_left_foot),
292 back_right: AnimationTargetId::from_iter(back_right_foot),
293 }
294 }
295}