From f4785568266630aec0d749fbedfa975a3ff6f1fd Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Wed, 6 May 2026 16:04:23 +0200 Subject: [PATCH 01/36] change animation behavior for root bone --- crates/bevy_animation/src/animation_curves.rs | 12 +++ crates/bevy_animation/src/lib.rs | 98 +++++++++++++------ 2 files changed, 80 insertions(+), 30 deletions(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index b08310d459fa5..294f0a3907f85 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -306,6 +306,18 @@ pub struct AnimatableCurveEvaluator { property: Box>, } +impl AnimatableCurveEvaluator { + pub(super) fn push_value(&mut self, value: A, weight: f32, graph_node: AnimationNodeIndex) { + self.evaluator + .stack + .push(BasicAnimationCurveEvaluatorStackElement { + value, + weight, + graph_node, + }); + } +} + impl AnimatableCurve where P: AnimatableProperty, diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index ac78cf01f1183..addb4185be369 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -34,17 +34,17 @@ use prelude::AnimationCurveEvaluator; use crate::{ graph::{AnimationGraphHandle, ThreadedAnimationGraphs}, - prelude::{AnimatableProperty, EvaluatorId}, + prelude::{AnimatableCurveEvaluator, AnimatableProperty, AnimatedField, EvaluatorId}, }; use bevy_app::{AnimationSystems, App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, AssetEventSystems, Assets}; use bevy_ecs::{prelude::*, resource::IsResource, world::EntityMutExcept}; -use bevy_math::FloatOrd; +use bevy_math::{FloatOrd, Vec3}; use bevy_platform::{collections::HashMap, hash::NoOpHash}; use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath}; use bevy_time::Time; -use bevy_transform::TransformSystems; +use bevy_transform::{components::Transform, TransformSystems}; use bevy_utils::{PreHashMap, PreHashMapExt, TypeIdMap}; use serde::{Deserialize, Serialize}; use thread_local::ThreadLocal; @@ -1108,6 +1108,8 @@ pub fn animate_targets( ); return; }; + // TODO: Temporaire + let root_target_id = AnimationTargetId::from_name(&Name::new("tmp")); // The graph might not have loaded yet. Safely bail. let Some(animation_graph) = graphs.get(animation_graph_id) else { @@ -1234,33 +1236,69 @@ pub fn animate_targets( let weight = active_animation.weight * animation_graph_node.weight; let seek_time = active_animation.seek_time; - for curve in curves { - // Fetch the curve evaluator. Curve evaluator types - // are unique to each property, but shared among all - // curve types. For example, given two curve types A - // and B, `RotationCurve` and `RotationCurve` - // will both yield a `RotationCurveEvaluator` and - // therefore will share the same evaluator in this - // table. - let curve_evaluator_id = (*curve.0).evaluator_id(); - let curve_evaluator = evaluation_state - .evaluators - .get_or_insert_with(curve_evaluator_id.clone(), || { - curve.0.create_evaluator() - }); - - evaluation_state - .current_evaluators - .insert(curve_evaluator_id); - - if let Err(err) = AnimationCurve::apply( - &*curve.0, - curve_evaluator, - seek_time, - weight, - animation_graph_node_index, - ) { - warn!("Animation application failed: {:?}", err); + if target_id == root_target_id { + let translation_field = animated_field!(Transform::translation); + // TODO: let rotation_field = animated_field!(Transform::rotation); + for curve in curves { + let curve_evaluator_id = (*curve.0).evaluator_id(); + let curve_evaluator = evaluation_state + .evaluators + .get_or_insert_with(curve_evaluator_id.clone(), || { + curve.0.create_evaluator() + }); + if curve_evaluator_id == translation_field.evaluator_id() { + let curve_evaluator = curve_evaluator + .downcast_mut::>() + .unwrap(); + let v1 = curve + .0 + .sample_clamped( + active_animation.last_seek_time.unwrap_or_default(), + ) + .downcast::() + .unwrap(); + let v2 = curve + .0 + .sample_clamped(active_animation.seek_time) + .downcast::() + .unwrap(); + // TODO: Handle loops + curve_evaluator.push_value( + *v2 - *v1, + weight, + animation_graph_node_index, + ); + } + } + } else { + for curve in curves { + // Fetch the curve evaluator. Curve evaluator types + // are unique to each property, but shared among all + // curve types. For example, given two curve types A + // and B, `RotationCurve` and `RotationCurve` + // will both yield a `RotationCurveEvaluator` and + // therefore will share the same evaluator in this + // table. + let curve_evaluator_id = (*curve.0).evaluator_id(); + let curve_evaluator = evaluation_state + .evaluators + .get_or_insert_with(curve_evaluator_id.clone(), || { + curve.0.create_evaluator() + }); + + evaluation_state + .current_evaluators + .insert(curve_evaluator_id); + + if let Err(err) = AnimationCurve::apply( + &*curve.0, + curve_evaluator, + seek_time, + weight, + animation_graph_node_index, + ) { + warn!("Animation application failed: {:?}", err); + } } } } From e23ec287209b53702e05ba20d33c0504e319339d Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Thu, 7 May 2026 10:08:43 +0200 Subject: [PATCH 02/36] add a test for root motion --- crates/bevy_animation/src/lib.rs | 113 ++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index addb4185be369..685a27627d0da 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1109,7 +1109,7 @@ pub fn animate_targets( return; }; // TODO: Temporaire - let root_target_id = AnimationTargetId::from_name(&Name::new("tmp")); + let root_target_id = AnimationTargetId::from_name(&Name::new("Root")); // The graph might not have loaded yet. Safely bail. let Some(animation_graph) = graphs.get(animation_graph_id) else { @@ -1236,6 +1236,8 @@ pub fn animate_targets( let weight = active_animation.weight * animation_graph_node.weight; let seek_time = active_animation.seek_time; + println!("seek time {}", seek_time); + if target_id == root_target_id { let translation_field = animated_field!(Transform::translation); // TODO: let rotation_field = animated_field!(Transform::rotation); @@ -1246,6 +1248,9 @@ pub fn animate_targets( .get_or_insert_with(curve_evaluator_id.clone(), || { curve.0.create_evaluator() }); + evaluation_state + .current_evaluators + .insert(curve_evaluator_id.clone()); if curve_evaluator_id == translation_field.evaluator_id() { let curve_evaluator = curve_evaluator .downcast_mut::>() @@ -1607,12 +1612,17 @@ impl<'a> Iterator for TriggeredEventsIter<'a> { #[cfg(test)] mod tests { + use std::time::Duration; + use crate::{ self as bevy_animation, prelude::{AnimatableCurve, AnimatableKeyframeCurve, AnimatedField}, }; + use bevy_app::{First, ScheduleRunnerPlugin}; + use bevy_asset::AssetPlugin; use bevy_math::Vec3; use bevy_reflect::map::{DynamicMap, Map}; + use bevy_time::{TimePlugin, TimeSystems, Virtual}; use bevy_transform::components::Transform; use super::*; @@ -1820,4 +1830,105 @@ mod tests { let value = clip.sample_clamped(animated_field!(Transform::translation), target_2, 1.0); assert_eq!(value, None); } + + #[test] + fn test_root_motion() { + let mut app = App::new(); + app.add_plugins(( + TimePlugin, + ScheduleRunnerPlugin::default(), + AssetPlugin::default(), + AnimationPlugin, + )); + // Force each update to 250ms + let step_duration = 0.25; + let clip_duration = 1.0; + app.add_systems( + First, + (move |mut time: ResMut>| { + time.advance_by(Duration::from_secs_f32(step_duration)) + }) + .after(TimeSystems), + ); + let mut clip = AnimationClip::default(); + let target_translation = Vec3::new(1., 2., 4.); + let root_target = AnimationTargetId::from_name(&Name::new("Root")); + let animatable_curve = AnimatableCurve::new( + animated_field!(Transform::translation), + AnimatableKeyframeCurve::new([ + (0.0, Vec3::new(0., 0., 0.)), + (clip_duration, target_translation), + ]) + .expect("Failed to create power level curve"), + ); + clip.add_curve_to_target(root_target, animatable_curve); + let mut graph = AnimationGraph::default(); + let clip_handle = { + let mut r_clips = app + .world_mut() + .get_resource_mut::>() + .unwrap(); + r_clips.add(clip) + }; + // animations settings + let once_speed = 0.5; + // TODO: set a speed so that one tick plays the animation more than once + // and don't end perfectly + let forever_speed = 0.5; + // Add multiple clips + let mut animation_player = AnimationPlayer::default(); + let once = graph.add_clip(clip_handle.clone(), 1.0, graph.root); + animation_player + .play(once) + .set_speed(once_speed) + .set_repeat(RepeatAnimation::Never); + let forever = graph.add_clip(clip_handle.clone(), 1.0, graph.root); + animation_player + .play(forever) + .set_speed(forever_speed) + .set_repeat(RepeatAnimation::Forever); + let graph_handle = { + let mut r_graphs = app + .world_mut() + .get_resource_mut::>() + .unwrap(); + AnimationGraphHandle(r_graphs.add(graph)) + }; + // Update to create the ThreadedAnimationGraph + app.update(); + // Spawn entities + let animator_entity = app.world_mut().spawn((animation_player, graph_handle)).id(); + let animated_entity = app + .world_mut() + .spawn(( + AnimatedBy(animator_entity), + root_target, + Transform::default(), + )) + .id(); + + for _ in 0..(clip_duration / once_speed) as u32 { + app.update(); + assert_eq!( + Transform::from_translation( + target_translation * step_duration * (once_speed + forever_speed) / 2. + ), + *app.world() + .get_entity(animated_entity) + .unwrap() + .get::() + .unwrap() + ); + } + app.update(); + // Since the single clip is ended only the forever should be active + assert_eq!( + Transform::from_translation(target_translation * step_duration * forever_speed), + *app.world() + .get_entity(animated_entity) + .unwrap() + .get::() + .unwrap() + ); + } } From ff1e5dda9bffbb4699dcda190510768018bdecf0 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Thu, 7 May 2026 14:36:47 +0200 Subject: [PATCH 03/36] handle animation loops --- crates/bevy_animation/src/lib.rs | 155 ++++++++++++++++++++++++------- 1 file changed, 122 insertions(+), 33 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 685a27627d0da..fc5480874ce76 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -31,6 +31,7 @@ use core::{ }; use graph::AnimationNodeType; use prelude::AnimationCurveEvaluator; +use std::ops::{Add, AddAssign, Sub, SubAssign}; use crate::{ graph::{AnimationGraphHandle, ThreadedAnimationGraphs}, @@ -40,7 +41,7 @@ use crate::{ use bevy_app::{AnimationSystems, App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, AssetEventSystems, Assets}; use bevy_ecs::{prelude::*, resource::IsResource, world::EntityMutExcept}; -use bevy_math::{FloatOrd, Vec3}; +use bevy_math::{FloatOrd, Vec3, VectorSpace}; use bevy_platform::{collections::HashMap, hash::NoOpHash}; use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath}; use bevy_time::Time; @@ -1077,6 +1078,54 @@ pub type AnimationEntityMut<'w, 's> = EntityMutExcept< ), >; +// TODO: Make a macro ? +fn root_motion_delta( + clip: &AnimationClip, + active_animation: &ActiveAnimation, + curve: &dyn AnimationCurve, +) -> T +where + T: Default + AddAssign + SubAssign + Add + Sub + 'static, +{ + let mut result = T::default(); + let reverse = active_animation.is_playback_reversed(); + let is_finished = active_animation.is_finished(); + + // Return early if the animation have finished on a previous tick. + if is_finished && !active_animation.just_completed { + return result; + } + + // The animation completed this tick, while still playing. + let looping = active_animation.just_completed && !is_finished; + + let Some(mut last_time) = active_animation.last_seek_time else { + return result; + }; + let this_time = active_animation.seek_time; + + if looping { + if reverse { + result += *(curve.sample_clamped(0.).downcast::().unwrap()) + - *(curve.sample_clamped(last_time).downcast::().unwrap()); + last_time = clip.duration; + } else { + result += *(curve.sample_clamped(clip.duration).downcast::().unwrap()) + - *(curve.sample_clamped(last_time).downcast::().unwrap()); + last_time = 0.; + } + } + + let diff = *(curve.sample_clamped(this_time).downcast::().unwrap()) + - *(curve.sample_clamped(last_time).downcast::().unwrap()); + if reverse { + result -= diff; + } else { + result += diff; + } + result +} + /// A system that modifies animation targets (e.g. bones in a skinned mesh) /// according to the currently-playing animations. pub fn animate_targets( @@ -1236,7 +1285,8 @@ pub fn animate_targets( let weight = active_animation.weight * animation_graph_node.weight; let seek_time = active_animation.seek_time; - println!("seek time {}", seek_time); + // TODO: remove before push + println!("seek time {} | clip_duration {}", seek_time, clip.duration); if target_id == root_target_id { let translation_field = animated_field!(Transform::translation); @@ -1251,25 +1301,15 @@ pub fn animate_targets( evaluation_state .current_evaluators .insert(curve_evaluator_id.clone()); + // TODO: Handle curves that shouldn't be modified if curve_evaluator_id == translation_field.evaluator_id() { let curve_evaluator = curve_evaluator .downcast_mut::>() .unwrap(); - let v1 = curve - .0 - .sample_clamped( - active_animation.last_seek_time.unwrap_or_default(), - ) - .downcast::() - .unwrap(); - let v2 = curve - .0 - .sample_clamped(active_animation.seek_time) - .downcast::() - .unwrap(); - // TODO: Handle loops + let value = + root_motion_delta(clip, active_animation, curve.0.as_ref()); curve_evaluator.push_value( - *v2 - *v1, + value, weight, animation_graph_node_index, ); @@ -1618,7 +1658,7 @@ mod tests { self as bevy_animation, prelude::{AnimatableCurve, AnimatableKeyframeCurve, AnimatedField}, }; - use bevy_app::{First, ScheduleRunnerPlugin}; + use bevy_app::{First, Last, ScheduleRunnerPlugin}; use bevy_asset::AssetPlugin; use bevy_math::Vec3; use bevy_reflect::map::{DynamicMap, Map}; @@ -1831,6 +1871,29 @@ mod tests { assert_eq!(value, None); } + fn compare_transform(expected: &Transform, found: &Transform) { + let transform_diff = expected.translation - found.translation; + const DELTA_ERROR: f32 = 0.00001; + assert!( + transform_diff.x.abs() <= DELTA_ERROR, + "Expected: {} | Found {}", + expected.translation.x, + found.translation.x + ); + assert!( + transform_diff.y.abs() <= DELTA_ERROR, + "Expected: {} | Found {}", + expected.translation.y, + found.translation.y + ); + assert!( + transform_diff.z.abs() <= DELTA_ERROR, + "Expected: {} | Found {}", + expected.translation.z, + found.translation.z + ); + } + #[test] fn test_root_motion() { let mut app = App::new(); @@ -1840,8 +1903,8 @@ mod tests { AssetPlugin::default(), AnimationPlugin, )); - // Force each update to 250ms - let step_duration = 0.25; + // Force each update to 300ms + let step_duration = 0.3; let clip_duration = 1.0; app.add_systems( First, @@ -1850,6 +1913,18 @@ mod tests { }) .after(TimeSystems), ); + // Auto remove finished animations + app.add_systems( + Last, + (move |q_players: Query<&mut AnimationPlayer>| { + for mut player in q_players { + player + .active_animations + .retain(|_, animation| !animation.is_finished()); + } + }) + .after(TimeSystems), + ); let mut clip = AnimationClip::default(); let target_translation = Vec3::new(1., 2., 4.); let root_target = AnimationTargetId::from_name(&Name::new("Root")); @@ -1870,11 +1945,9 @@ mod tests { .unwrap(); r_clips.add(clip) }; - // animations settings + // Animations settings let once_speed = 0.5; - // TODO: set a speed so that one tick plays the animation more than once - // and don't end perfectly - let forever_speed = 0.5; + let forever_speed = 1.0; // Add multiple clips let mut animation_player = AnimationPlayer::default(); let once = graph.add_clip(clip_handle.clone(), 1.0, graph.root); @@ -1907,28 +1980,44 @@ mod tests { )) .id(); - for _ in 0..(clip_duration / once_speed) as u32 { + let tick_count = clip_duration / once_speed / step_duration; + for _ in 0..tick_count as u32 { app.update(); - assert_eq!( - Transform::from_translation( - target_translation * step_duration * (once_speed + forever_speed) / 2. + compare_transform( + &Transform::from_translation( + target_translation * step_duration * (once_speed + forever_speed) / 2., ), - *app.world() + app.world() .get_entity(animated_entity) .unwrap() .get::() - .unwrap() + .unwrap(), ); } + // During this update once_clip will advance by less than step_duration app.update(); - // Since the single clip is ended only the forever should be active - assert_eq!( - Transform::from_translation(target_translation * step_duration * forever_speed), - *app.world() + compare_transform( + &Transform::from_translation( + target_translation + * step_duration + * ((tick_count - tick_count.floor()) * once_speed + forever_speed) + / 2., + ), + app.world() .get_entity(animated_entity) .unwrap() .get::() + .unwrap(), + ); + // Since the single clip is ended only the forever should be active + app.update(); + compare_transform( + &Transform::from_translation(target_translation * step_duration * forever_speed), + app.world() + .get_entity(animated_entity) .unwrap() + .get::() + .unwrap(), ); } } From bdb0238724a8a2157305072b64a9baf13688ed1a Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Thu, 7 May 2026 16:59:38 +0200 Subject: [PATCH 04/36] check backward direction --- crates/bevy_animation/src/lib.rs | 76 +++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index fc5480874ce76..3d195432d2222 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -41,7 +41,7 @@ use crate::{ use bevy_app::{AnimationSystems, App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, AssetEventSystems, Assets}; use bevy_ecs::{prelude::*, resource::IsResource, world::EntityMutExcept}; -use bevy_math::{FloatOrd, Vec3, VectorSpace}; +use bevy_math::{FloatOrd, Vec3}; use bevy_platform::{collections::HashMap, hash::NoOpHash}; use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath}; use bevy_time::Time; @@ -1116,13 +1116,8 @@ where } } - let diff = *(curve.sample_clamped(this_time).downcast::().unwrap()) + result += *(curve.sample_clamped(this_time).downcast::().unwrap()) - *(curve.sample_clamped(last_time).downcast::().unwrap()); - if reverse { - result -= diff; - } else { - result += diff; - } result } @@ -1950,14 +1945,14 @@ mod tests { let forever_speed = 1.0; // Add multiple clips let mut animation_player = AnimationPlayer::default(); - let once = graph.add_clip(clip_handle.clone(), 1.0, graph.root); + let once_animation = graph.add_clip(clip_handle.clone(), 1.0, graph.root); animation_player - .play(once) + .play(once_animation) .set_speed(once_speed) .set_repeat(RepeatAnimation::Never); - let forever = graph.add_clip(clip_handle.clone(), 1.0, graph.root); + let forever_animation = graph.add_clip(clip_handle.clone(), 1.0, graph.root); animation_player - .play(forever) + .play(forever_animation) .set_speed(forever_speed) .set_repeat(RepeatAnimation::Forever); let graph_handle = { @@ -1979,8 +1974,65 @@ mod tests { Transform::default(), )) .id(); - let tick_count = clip_duration / once_speed / step_duration; + + // Forward tests + for _ in 0..tick_count as u32 { + app.update(); + compare_transform( + &Transform::from_translation( + target_translation * step_duration * (once_speed + forever_speed) / 2., + ), + app.world() + .get_entity(animated_entity) + .unwrap() + .get::() + .unwrap(), + ); + } + // During this update once_clip will advance by less than step_duration + app.update(); + compare_transform( + &Transform::from_translation( + target_translation + * step_duration + * ((tick_count - tick_count.floor()) * once_speed + forever_speed) + / 2., + ), + app.world() + .get_entity(animated_entity) + .unwrap() + .get::() + .unwrap(), + ); + // Since the single clip is ended only the forever should be active + app.update(); + compare_transform( + &Transform::from_translation(target_translation * step_duration * forever_speed), + app.world() + .get_entity(animated_entity) + .unwrap() + .get::() + .unwrap(), + ); + + // Setup for backward + let once_speed = once_speed * -1.; + let forever_speed = forever_speed * -1.; + { + let mut player_entity = app.world_mut().get_entity_mut(animator_entity).unwrap(); + let mut animation_player = player_entity.get_mut::().unwrap(); + println!("{}", animation_player.active_animations.len()); + animation_player + .play(once_animation) + .set_speed(once_speed) + .seek_to(clip_duration); + animation_player + .play(forever_animation) + .set_speed(forever_speed) + .replay(); + } + // Backward tests for _ in 0..tick_count as u32 { app.update(); compare_transform( From a46589585492f9594bb0d57957dad186f25bb6fe Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Thu, 7 May 2026 18:33:38 +0200 Subject: [PATCH 05/36] add the RootMotion component --- crates/bevy_animation/src/animation_curves.rs | 4 +- crates/bevy_animation/src/lib.rs | 247 ++++++++++-------- crates/bevy_animation/src/morph.rs | 2 +- 3 files changed, 144 insertions(+), 109 deletions(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 294f0a3907f85..27351d038436b 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -425,7 +425,7 @@ impl AnimationCurveEvaluator for AnimatableCurveEvaluator { self.evaluator.push_blend_register(weight, graph_node) } - fn commit(&mut self, mut entity: AnimationEntityMut) -> Result<(), AnimationEvaluationError> { + fn commit(&mut self, mut entity: &mut AnimationEntityMut) -> Result<(), AnimationEvaluationError> { let property = self.property.get_mut(&mut entity)?; *property = self .evaluator @@ -720,7 +720,7 @@ pub trait AnimationCurveEvaluator: Downcast + Send + Sync + 'static { /// /// The property on the component must be overwritten with the value from /// the stack, not blended with it. - fn commit(&mut self, entity: AnimationEntityMut) -> Result<(), AnimationEvaluationError>; + fn commit(&mut self, entity: &mut AnimationEntityMut) -> Result<(), AnimationEvaluationError>; } impl_downcast!(AnimationCurveEvaluator); diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 3d195432d2222..74c2f7d66d76f 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -41,7 +41,7 @@ use crate::{ use bevy_app::{AnimationSystems, App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, AssetEventSystems, Assets}; use bevy_ecs::{prelude::*, resource::IsResource, world::EntityMutExcept}; -use bevy_math::{FloatOrd, Vec3}; +use bevy_math::{FloatOrd, Quat, Vec3}; use bevy_platform::{collections::HashMap, hash::NoOpHash}; use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath}; use bevy_time::Time; @@ -724,6 +724,13 @@ impl ActiveAnimation { } } +#[derive(Component, Default, Reflect)] +#[reflect(Component, Default)] +pub struct RootMotion { + pub translation_delta: Vec3, + pub rotation_delta: Quat, +} + /// Animation controls. /// /// Automatically added to any root animations of a scene when it is @@ -732,6 +739,7 @@ impl ActiveAnimation { #[reflect(Component, Default, Clone)] pub struct AnimationPlayer { active_animations: HashMap, + root_motion_target: Option, } // This is needed since `#[derive(Clone)]` does not generate optimized `clone_from`. @@ -739,11 +747,14 @@ impl Clone for AnimationPlayer { fn clone(&self) -> Self { Self { active_animations: self.active_animations.clone(), + root_motion_target: None, } } fn clone_from(&mut self, source: &Self) { self.active_animations.clone_from(&source.active_animations); + self.root_motion_target + .clone_from(&source.root_motion_target); } } @@ -984,6 +995,16 @@ impl AnimationPlayer { pub fn animation_mut(&mut self, animation: AnimationNodeIndex) -> Option<&mut ActiveAnimation> { self.active_animations.get_mut(&animation) } + + /// Returns the root motion target linked with this [`AnimationPlayer`] + pub fn root_motion_target(&self) -> Option { + self.root_motion_target + } + + /// Set the root motion target. Set to `None` if you want to disable root motion + pub fn set_root_motion_target(&mut self, target: Option) { + self.root_motion_target = target; + } } /// A system that triggers untargeted animation events for the currently-playing animations. @@ -1128,7 +1149,7 @@ pub fn animate_targets( clips: Res>, graphs: Res>, threaded_animation_graphs: Res, - players: Query<(&AnimationPlayer, &AnimationGraphHandle)>, + players: Query<(Entity, &AnimationPlayer, &AnimationGraphHandle)>, mut targets: Query< (Entity, &AnimationTargetId, &AnimatedBy, AnimationEntityMut), Without, @@ -1136,12 +1157,11 @@ pub fn animate_targets( animation_evaluation_state: Local>>, ) { // Evaluate all animation targets in parallel. - targets - .par_iter_mut() - .for_each(|(entity, &target_id, &AnimatedBy(player_id), entity_mut)| { - let (animation_player, animation_graph_id) = - if let Ok((player, graph_handle)) = players.get(player_id) { - (player, graph_handle.id()) + targets.par_iter_mut().for_each( + |(entity, &target_id, &AnimatedBy(player_id), mut entity_mut)| { + let (animation_player_entity, animation_player, animation_graph_id) = + if let Ok((player_entity, player, graph_handle)) = players.get(player_id) { + (player_entity, player, graph_handle.id()) } else { trace!( "Either an animation player {} or a graph was missing for the target \ @@ -1152,8 +1172,8 @@ pub fn animate_targets( ); return; }; - // TODO: Temporaire - let root_target_id = AnimationTargetId::from_name(&Name::new("Root")); + + let is_root_target = Some(target_id) == animation_player.root_motion_target; // The graph might not have loaded yet. Safely bail. let Some(animation_graph) = graphs.get(animation_graph_id) else { @@ -1283,7 +1303,7 @@ pub fn animate_targets( // TODO: remove before push println!("seek time {} | clip_duration {}", seek_time, clip.duration); - if target_id == root_target_id { + if is_root_target { let translation_field = animated_field!(Transform::translation); // TODO: let rotation_field = animated_field!(Transform::rotation); for curve in curves { @@ -1345,10 +1365,23 @@ pub fn animate_targets( } } - if let Err(err) = evaluation_state.commit_all(entity_mut) { + if let Err(err) = evaluation_state.commit_all(&mut entity_mut) { warn!("Animation application failed: {:?}", err); } - }); + + if is_root_target { + let mut root_motion_transform = entity_mut.get_mut::().unwrap(); + let translation_delta = core::mem::take(&mut root_motion_transform.translation); + let rotation_delta = core::mem::take(&mut root_motion_transform.rotation); + par_commands.command_scope(move |mut commands| { + commands.entity(animation_player_entity).insert(RootMotion { + translation_delta, + rotation_delta, + }); + }) + } + }, + ); } /// Adds animation support to an app @@ -1485,14 +1518,10 @@ impl AnimationEvaluationState { /// components being animated. fn commit_all( &mut self, - mut entity_mut: AnimationEntityMut, + entity_mut: &mut AnimationEntityMut, ) -> Result<(), AnimationEvaluationError> { - self.current_evaluators.clear(|id| { - self.evaluators - .get_mut(id) - .unwrap() - .commit(entity_mut.reborrow()) - }) + self.current_evaluators + .clear(|id| self.evaluators.get_mut(id).unwrap().commit(entity_mut)) } } @@ -1866,26 +1895,88 @@ mod tests { assert_eq!(value, None); } - fn compare_transform(expected: &Transform, found: &Transform) { - let transform_diff = expected.translation - found.translation; + fn compare_root_motion( + expected_translation: Vec3, + expected_rotation: Quat, + found: &RootMotion, + ) { + let translation_diff = expected_translation - found.translation_delta; const DELTA_ERROR: f32 = 0.00001; assert!( - transform_diff.x.abs() <= DELTA_ERROR, + translation_diff.x.abs() <= DELTA_ERROR, "Expected: {} | Found {}", - expected.translation.x, - found.translation.x + expected_translation.x, + found.translation_delta.x ); assert!( - transform_diff.y.abs() <= DELTA_ERROR, + translation_diff.y.abs() <= DELTA_ERROR, "Expected: {} | Found {}", - expected.translation.y, - found.translation.y + expected_translation.y, + found.translation_delta.y ); assert!( - transform_diff.z.abs() <= DELTA_ERROR, + translation_diff.z.abs() <= DELTA_ERROR, "Expected: {} | Found {}", - expected.translation.z, - found.translation.z + expected_translation.z, + found.translation_delta.z + ); + } + + fn root_motion_tests( + app: &mut App, + tick_count: f32, + target_translation: Vec3, + step_duration: f32, + once_speed: f32, + forever_speed: f32, + animator_entity: Entity, + animated_entity: Entity, + ) { + for _ in 0..tick_count as u32 { + app.update(); + compare_root_motion( + target_translation * step_duration * (once_speed + forever_speed) / 2., + Quat::IDENTITY, + app.world() + .get_entity(animator_entity) + .unwrap() + .get::() + .unwrap(), + ); + // Check if the transform is erased in the root target + assert_eq!( + Transform::IDENTITY, + *app.world() + .get_entity(animated_entity) + .unwrap() + .get::() + .unwrap() + ); + } + // During this update once_clip will advance by less than step_duration + app.update(); + compare_root_motion( + target_translation + * step_duration + * ((tick_count - tick_count.floor()) * once_speed + forever_speed) + / 2., + Quat::IDENTITY, + app.world() + .get_entity(animator_entity) + .unwrap() + .get::() + .unwrap(), + ); + // Since the single clip is ended only the forever should be active + app.update(); + compare_root_motion( + target_translation * step_duration * forever_speed, + Quat::IDENTITY, + app.world() + .get_entity(animator_entity) + .unwrap() + .get::() + .unwrap(), ); } @@ -1945,6 +2036,7 @@ mod tests { let forever_speed = 1.0; // Add multiple clips let mut animation_player = AnimationPlayer::default(); + animation_player.set_root_motion_target(Some(root_target)); let once_animation = graph.add_clip(clip_handle.clone(), 1.0, graph.root); animation_player .play(once_animation) @@ -1977,43 +2069,15 @@ mod tests { let tick_count = clip_duration / once_speed / step_duration; // Forward tests - for _ in 0..tick_count as u32 { - app.update(); - compare_transform( - &Transform::from_translation( - target_translation * step_duration * (once_speed + forever_speed) / 2., - ), - app.world() - .get_entity(animated_entity) - .unwrap() - .get::() - .unwrap(), - ); - } - // During this update once_clip will advance by less than step_duration - app.update(); - compare_transform( - &Transform::from_translation( - target_translation - * step_duration - * ((tick_count - tick_count.floor()) * once_speed + forever_speed) - / 2., - ), - app.world() - .get_entity(animated_entity) - .unwrap() - .get::() - .unwrap(), - ); - // Since the single clip is ended only the forever should be active - app.update(); - compare_transform( - &Transform::from_translation(target_translation * step_duration * forever_speed), - app.world() - .get_entity(animated_entity) - .unwrap() - .get::() - .unwrap(), + root_motion_tests( + &mut app, + tick_count, + target_translation, + step_duration, + once_speed, + forever_speed, + animator_entity, + animated_entity, ); // Setup for backward @@ -2022,7 +2086,6 @@ mod tests { { let mut player_entity = app.world_mut().get_entity_mut(animator_entity).unwrap(); let mut animation_player = player_entity.get_mut::().unwrap(); - println!("{}", animation_player.active_animations.len()); animation_player .play(once_animation) .set_speed(once_speed) @@ -2033,43 +2096,15 @@ mod tests { .replay(); } // Backward tests - for _ in 0..tick_count as u32 { - app.update(); - compare_transform( - &Transform::from_translation( - target_translation * step_duration * (once_speed + forever_speed) / 2., - ), - app.world() - .get_entity(animated_entity) - .unwrap() - .get::() - .unwrap(), - ); - } - // During this update once_clip will advance by less than step_duration - app.update(); - compare_transform( - &Transform::from_translation( - target_translation - * step_duration - * ((tick_count - tick_count.floor()) * once_speed + forever_speed) - / 2., - ), - app.world() - .get_entity(animated_entity) - .unwrap() - .get::() - .unwrap(), - ); - // Since the single clip is ended only the forever should be active - app.update(); - compare_transform( - &Transform::from_translation(target_translation * step_duration * forever_speed), - app.world() - .get_entity(animated_entity) - .unwrap() - .get::() - .unwrap(), + root_motion_tests( + &mut app, + tick_count, + target_translation, + step_duration, + once_speed, + forever_speed, + animator_entity, + animated_entity, ); } } diff --git a/crates/bevy_animation/src/morph.rs b/crates/bevy_animation/src/morph.rs index 1dc73f0976c82..bf2df3381970e 100644 --- a/crates/bevy_animation/src/morph.rs +++ b/crates/bevy_animation/src/morph.rs @@ -199,7 +199,7 @@ impl AnimationCurveEvaluator for WeightsCurveEvaluator { Ok(()) } - fn commit(&mut self, mut entity: AnimationEntityMut) -> Result<(), AnimationEvaluationError> { + fn commit(&mut self, entity: &mut AnimationEntityMut) -> Result<(), AnimationEvaluationError> { if self.stack_morph_target_weights.is_empty() { return Ok(()); } From 02bb39da0c28bca2d898fdb4b9e3262310455b9c Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Fri, 8 May 2026 12:32:19 +0200 Subject: [PATCH 06/36] add rotation to tests --- crates/bevy_animation/src/lib.rs | 188 ++++++++++++++++++++++++------- 1 file changed, 149 insertions(+), 39 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 74c2f7d66d76f..c3c45a1e5abfd 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -41,7 +41,7 @@ use crate::{ use bevy_app::{AnimationSystems, App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, AssetEventSystems, Assets}; use bevy_ecs::{prelude::*, resource::IsResource, world::EntityMutExcept}; -use bevy_math::{FloatOrd, Quat, Vec3}; +use bevy_math::{FloatOrd, Quat, Vec3, VectorSpace}; use bevy_platform::{collections::HashMap, hash::NoOpHash}; use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath}; use bevy_time::Time; @@ -731,6 +731,15 @@ pub struct RootMotion { pub rotation_delta: Quat, } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Reflect)] +#[reflect(Default, Clone)] +pub enum RootMotionMode { + // TODO: Add tests + Translation, + #[default] + TranslationAndRotation, +} + /// Animation controls. /// /// Automatically added to any root animations of a scene when it is @@ -740,6 +749,7 @@ pub struct RootMotion { pub struct AnimationPlayer { active_animations: HashMap, root_motion_target: Option, + root_motion_mode: RootMotionMode, } // This is needed since `#[derive(Clone)]` does not generate optimized `clone_from`. @@ -747,7 +757,8 @@ impl Clone for AnimationPlayer { fn clone(&self) -> Self { Self { active_animations: self.active_animations.clone(), - root_motion_target: None, + root_motion_target: self.root_motion_target.clone(), + root_motion_mode: self.root_motion_mode.clone(), } } @@ -755,6 +766,7 @@ impl Clone for AnimationPlayer { self.active_animations.clone_from(&source.active_animations); self.root_motion_target .clone_from(&source.root_motion_target); + self.root_motion_mode.clone_from(&source.root_motion_mode); } } @@ -1005,6 +1017,16 @@ impl AnimationPlayer { pub fn set_root_motion_target(&mut self, target: Option) { self.root_motion_target = target; } + + /// Returns the root motion mode + pub fn root_motion_mode(&self) -> RootMotionMode { + self.root_motion_mode + } + + /// Set the root motion mode to control how RootMotion is computed + pub fn set_root_motion_mode(&mut self, mode: RootMotionMode) { + self.root_motion_mode = mode; + } } /// A system that triggers untargeted animation events for the currently-playing animations. @@ -1099,16 +1121,12 @@ pub type AnimationEntityMut<'w, 's> = EntityMutExcept< ), >; -// TODO: Make a macro ? -fn root_motion_delta( +fn root_motion_translation_delta( clip: &AnimationClip, active_animation: &ActiveAnimation, curve: &dyn AnimationCurve, -) -> T -where - T: Default + AddAssign + SubAssign + Add + Sub + 'static, -{ - let mut result = T::default(); +) -> Vec3 { + let mut result = Vec3::ZERO; let reverse = active_animation.is_playback_reversed(); let is_finished = active_animation.is_finished(); @@ -1126,20 +1144,65 @@ where let this_time = active_animation.seek_time; if looping { + let last_translation = *(curve.sample_clamped(last_time).downcast::().unwrap()); if reverse { - result += *(curve.sample_clamped(0.).downcast::().unwrap()) - - *(curve.sample_clamped(last_time).downcast::().unwrap()); + result += *(curve.sample_clamped(0.).downcast::().unwrap()) - last_translation; last_time = clip.duration; } else { - result += *(curve.sample_clamped(clip.duration).downcast::().unwrap()) - - *(curve.sample_clamped(last_time).downcast::().unwrap()); + result += *(curve + .sample_clamped(clip.duration) + .downcast::() + .unwrap()) + - last_translation; last_time = 0.; } } - result += *(curve.sample_clamped(this_time).downcast::().unwrap()) - - *(curve.sample_clamped(last_time).downcast::().unwrap()); - result + result + *(curve.sample_clamped(this_time).downcast::().unwrap()) + - *(curve.sample_clamped(last_time).downcast::().unwrap()) +} + +fn root_motion_rotation_delta( + clip: &AnimationClip, + active_animation: &ActiveAnimation, + curve: &dyn AnimationCurve, +) -> Quat { + let mut result = Quat::IDENTITY; + let reverse = active_animation.is_playback_reversed(); + let is_finished = active_animation.is_finished(); + + // Return early if the animation have finished on a previous tick. + if is_finished && !active_animation.just_completed { + return result; + } + + // The animation completed this tick, while still playing. + let looping = active_animation.just_completed && !is_finished; + + let Some(mut last_time) = active_animation.last_seek_time else { + return result; + }; + let this_time = active_animation.seek_time; + + if looping { + let last_rotation = *(curve.sample_clamped(last_time).downcast::().unwrap()); + if reverse { + let current_rotation = *(curve.sample_clamped(0.).downcast::().unwrap()); + result = current_rotation * last_rotation.inverse(); + last_time = clip.duration; + } else { + let current_rotation = *(curve + .sample_clamped(clip.duration) + .downcast::() + .unwrap()); + result = current_rotation * last_rotation.inverse(); + last_time = 0.; + } + } + + (*(curve.sample_clamped(this_time).downcast::().unwrap()) + * (curve.sample_clamped(last_time).downcast::().unwrap()).inverse()) + * result } /// A system that modifies animation targets (e.g. bones in a skinned mesh) @@ -1305,7 +1368,7 @@ pub fn animate_targets( if is_root_target { let translation_field = animated_field!(Transform::translation); - // TODO: let rotation_field = animated_field!(Transform::rotation); + let rotation_field = animated_field!(Transform::rotation); for curve in curves { let curve_evaluator_id = (*curve.0).evaluator_id(); let curve_evaluator = evaluation_state @@ -1321,13 +1384,43 @@ pub fn animate_targets( let curve_evaluator = curve_evaluator .downcast_mut::>() .unwrap(); - let value = - root_motion_delta(clip, active_animation, curve.0.as_ref()); + let value = root_motion_translation_delta( + clip, + active_animation, + curve.0.as_ref(), + ); curve_evaluator.push_value( value, weight, animation_graph_node_index, ); + } else if curve_evaluator_id == rotation_field.evaluator_id() + && animation_player.root_motion_mode + == RootMotionMode::TranslationAndRotation + { + let curve_evaluator = curve_evaluator + .downcast_mut::>() + .unwrap(); + let value = root_motion_rotation_delta( + clip, + active_animation, + curve.0.as_ref(), + ); + curve_evaluator.push_value( + value, + weight, + animation_graph_node_index, + ); + } else { + if let Err(err) = AnimationCurve::apply( + &*curve.0, + curve_evaluator, + seek_time, + weight, + animation_graph_node_index, + ) { + warn!("Animation application failed: {:?}", err); + } } } } else { @@ -1895,37 +1988,47 @@ mod tests { assert_eq!(value, None); } + // TODO: Compare expected_rotation fn compare_root_motion( expected_translation: Vec3, expected_rotation: Quat, found: &RootMotion, ) { let translation_diff = expected_translation - found.translation_delta; - const DELTA_ERROR: f32 = 0.00001; + const TRANSLATION_DELTA_ERROR: f32 = 0.00001; + const ROTATION_DELTA_ERROR: f32 = 0.01; assert!( - translation_diff.x.abs() <= DELTA_ERROR, + translation_diff.x.abs() <= TRANSLATION_DELTA_ERROR, "Expected: {} | Found {}", expected_translation.x, found.translation_delta.x ); assert!( - translation_diff.y.abs() <= DELTA_ERROR, + translation_diff.y.abs() <= TRANSLATION_DELTA_ERROR, "Expected: {} | Found {}", expected_translation.y, found.translation_delta.y ); assert!( - translation_diff.z.abs() <= DELTA_ERROR, + translation_diff.z.abs() <= TRANSLATION_DELTA_ERROR, "Expected: {} | Found {}", expected_translation.z, found.translation_delta.z ); + assert!( + expected_rotation.angle_between(found.rotation_delta) <= ROTATION_DELTA_ERROR, + "Expected: {} | Found {} | Angle : {}", + expected_rotation, + found.rotation_delta, + expected_rotation.angle_between(found.rotation_delta) + ); } fn root_motion_tests( app: &mut App, tick_count: f32, target_translation: Vec3, + target_rotation: Quat, step_duration: f32, once_speed: f32, forever_speed: f32, @@ -1934,9 +2037,10 @@ mod tests { ) { for _ in 0..tick_count as u32 { app.update(); + let factor = step_duration * (once_speed + forever_speed) / 2.; compare_root_motion( - target_translation * step_duration * (once_speed + forever_speed) / 2., - Quat::IDENTITY, + target_translation * factor, + Quat::IDENTITY.slerp(target_rotation, factor), app.world() .get_entity(animator_entity) .unwrap() @@ -1955,12 +2059,10 @@ mod tests { } // During this update once_clip will advance by less than step_duration app.update(); + let factor = ((tick_count - tick_count.floor()) * once_speed + forever_speed) / 2.; compare_root_motion( - target_translation - * step_duration - * ((tick_count - tick_count.floor()) * once_speed + forever_speed) - / 2., - Quat::IDENTITY, + target_translation * step_duration * factor, + Quat::IDENTITY.slerp(target_rotation, factor), app.world() .get_entity(animator_entity) .unwrap() @@ -1969,9 +2071,10 @@ mod tests { ); // Since the single clip is ended only the forever should be active app.update(); + let factor = step_duration * forever_speed; compare_root_motion( - target_translation * step_duration * forever_speed, - Quat::IDENTITY, + target_translation * factor, + Quat::IDENTITY.slerp(target_rotation, factor), app.world() .get_entity(animator_entity) .unwrap() @@ -2013,16 +2116,21 @@ mod tests { ); let mut clip = AnimationClip::default(); let target_translation = Vec3::new(1., 2., 4.); + let target_rotation = + Quat::from_axis_angle(Vec3::Z, 1.0) * Quat::from_axis_angle(Vec3::X, 1.0); let root_target = AnimationTargetId::from_name(&Name::new("Root")); - let animatable_curve = AnimatableCurve::new( + let translation_curve = AnimatableCurve::new( animated_field!(Transform::translation), - AnimatableKeyframeCurve::new([ - (0.0, Vec3::new(0., 0., 0.)), - (clip_duration, target_translation), - ]) - .expect("Failed to create power level curve"), + AnimatableKeyframeCurve::new([(0.0, Vec3::ZERO), (clip_duration, target_translation)]) + .expect("Failed to create power level curve"), + ); + clip.add_curve_to_target(root_target, translation_curve); + let rotation_curve = AnimatableCurve::new( + animated_field!(Transform::rotation), + AnimatableKeyframeCurve::new([(0.0, Quat::IDENTITY), (clip_duration, target_rotation)]) + .expect("Failed to create power level curve"), ); - clip.add_curve_to_target(root_target, animatable_curve); + clip.add_curve_to_target(root_target, rotation_curve); let mut graph = AnimationGraph::default(); let clip_handle = { let mut r_clips = app @@ -2073,6 +2181,7 @@ mod tests { &mut app, tick_count, target_translation, + target_rotation, step_duration, once_speed, forever_speed, @@ -2100,6 +2209,7 @@ mod tests { &mut app, tick_count, target_translation, + target_rotation, step_duration, once_speed, forever_speed, From fe51b68f45ebfa7d0ebe7dbeee03c135842c4390 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Fri, 8 May 2026 13:13:29 +0200 Subject: [PATCH 07/36] refactor tests --- crates/bevy_animation/src/lib.rs | 116 +++++++++++-------------------- 1 file changed, 42 insertions(+), 74 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index c3c45a1e5abfd..88ae870ffc9de 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -31,7 +31,6 @@ use core::{ }; use graph::AnimationNodeType; use prelude::AnimationCurveEvaluator; -use std::ops::{Add, AddAssign, Sub, SubAssign}; use crate::{ graph::{AnimationGraphHandle, ThreadedAnimationGraphs}, @@ -41,7 +40,7 @@ use crate::{ use bevy_app::{AnimationSystems, App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, AssetEventSystems, Assets}; use bevy_ecs::{prelude::*, resource::IsResource, world::EntityMutExcept}; -use bevy_math::{FloatOrd, Quat, Vec3, VectorSpace}; +use bevy_math::{FloatOrd, Quat, Vec3}; use bevy_platform::{collections::HashMap, hash::NoOpHash}; use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath}; use bevy_time::Time; @@ -2026,27 +2025,19 @@ mod tests { fn root_motion_tests( app: &mut App, - tick_count: f32, + tick_count: u32, target_translation: Vec3, target_rotation: Quat, - step_duration: f32, - once_speed: f32, - forever_speed: f32, animator_entity: Entity, animated_entity: Entity, ) { - for _ in 0..tick_count as u32 { + let mut accumulated_deltas = RootMotion::default(); + for _ in 0..tick_count { app.update(); - let factor = step_duration * (once_speed + forever_speed) / 2.; - compare_root_motion( - target_translation * factor, - Quat::IDENTITY.slerp(target_rotation, factor), - app.world() - .get_entity(animator_entity) - .unwrap() - .get::() - .unwrap(), - ); + let root_motion = app.world().get::(animator_entity).unwrap(); + accumulated_deltas.translation_delta += root_motion.translation_delta; + accumulated_deltas.rotation_delta = + root_motion.rotation_delta * accumulated_deltas.rotation_delta; // Check if the transform is erased in the root target assert_eq!( Transform::IDENTITY, @@ -2057,30 +2048,7 @@ mod tests { .unwrap() ); } - // During this update once_clip will advance by less than step_duration - app.update(); - let factor = ((tick_count - tick_count.floor()) * once_speed + forever_speed) / 2.; - compare_root_motion( - target_translation * step_duration * factor, - Quat::IDENTITY.slerp(target_rotation, factor), - app.world() - .get_entity(animator_entity) - .unwrap() - .get::() - .unwrap(), - ); - // Since the single clip is ended only the forever should be active - app.update(); - let factor = step_duration * forever_speed; - compare_root_motion( - target_translation * factor, - Quat::IDENTITY.slerp(target_rotation, factor), - app.world() - .get_entity(animator_entity) - .unwrap() - .get::() - .unwrap(), - ); + compare_root_motion(target_translation, target_rotation, &accumulated_deltas); } #[test] @@ -2092,13 +2060,19 @@ mod tests { AssetPlugin::default(), AnimationPlugin, )); + // Animations settings + let play_count = 3; + // Choose tick_count such that ticks are not align perfectly with animation loops + let tick_count = 10; + let slow_speed = 0.5 as f32; + let fast_speed = 1. as f32; // Force each update to 300ms - let step_duration = 0.3; + let tick_duration = play_count as f32 / tick_count as f32; let clip_duration = 1.0; app.add_systems( First, (move |mut time: ResMut>| { - time.advance_by(Duration::from_secs_f32(step_duration)) + time.advance_by(Duration::from_secs_f32(tick_duration)) }) .after(TimeSystems), ); @@ -2139,22 +2113,19 @@ mod tests { .unwrap(); r_clips.add(clip) }; - // Animations settings - let once_speed = 0.5; - let forever_speed = 1.0; // Add multiple clips let mut animation_player = AnimationPlayer::default(); animation_player.set_root_motion_target(Some(root_target)); let once_animation = graph.add_clip(clip_handle.clone(), 1.0, graph.root); + /* animation_player + .play(once_animation) + .set_speed(slow_speed) + .set_repeat(RepeatAnimation::Count(play_count)); */ + let multiple_animation = graph.add_clip(clip_handle.clone(), 1.0, graph.root); animation_player - .play(once_animation) - .set_speed(once_speed) - .set_repeat(RepeatAnimation::Never); - let forever_animation = graph.add_clip(clip_handle.clone(), 1.0, graph.root); - animation_player - .play(forever_animation) - .set_speed(forever_speed) - .set_repeat(RepeatAnimation::Forever); + .play(multiple_animation) + .set_speed(fast_speed) + .set_repeat(RepeatAnimation::Count(play_count)); let graph_handle = { let mut r_graphs = app .world_mut() @@ -2174,45 +2145,42 @@ mod tests { Transform::default(), )) .id(); - let tick_count = clip_duration / once_speed / step_duration; + + let final_translation = target_translation * play_count as f32; + let final_rotation = Quat::IDENTITY.slerp(target_rotation, play_count as f32); // Forward tests root_motion_tests( &mut app, tick_count, - target_translation, - target_rotation, - step_duration, - once_speed, - forever_speed, + final_translation, + final_rotation, animator_entity, animated_entity, ); // Setup for backward - let once_speed = once_speed * -1.; - let forever_speed = forever_speed * -1.; + let slow_speed = slow_speed * -1.; + let fast_speed = fast_speed * -1.; { let mut player_entity = app.world_mut().get_entity_mut(animator_entity).unwrap(); let mut animation_player = player_entity.get_mut::().unwrap(); + /* animation_player + .play(once_animation) + .set_speed(slow_speed) + .seek_to(clip_duration); */ animation_player - .play(once_animation) - .set_speed(once_speed) - .seek_to(clip_duration); - animation_player - .play(forever_animation) - .set_speed(forever_speed) - .replay(); + .play(multiple_animation) + .set_repeat(RepeatAnimation::Count(play_count)) + .set_speed(fast_speed) + .repeat(); } // Backward tests root_motion_tests( &mut app, tick_count, - target_translation, - target_rotation, - step_duration, - once_speed, - forever_speed, + -final_translation, + final_rotation.inverse(), animator_entity, animated_entity, ); From 7b1c85c130567f6bf061ec4743af47499134bce9 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Fri, 8 May 2026 14:14:39 +0200 Subject: [PATCH 08/36] tests work with blending --- crates/bevy_animation/src/lib.rs | 52 ++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 88ae870ffc9de..f88f2bae529e6 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1987,7 +1987,6 @@ mod tests { assert_eq!(value, None); } - // TODO: Compare expected_rotation fn compare_root_motion( expected_translation: Vec3, expected_rotation: Quat, @@ -2064,11 +2063,12 @@ mod tests { let play_count = 3; // Choose tick_count such that ticks are not align perfectly with animation loops let tick_count = 10; - let slow_speed = 0.5 as f32; - let fast_speed = 1. as f32; - // Force each update to 300ms - let tick_duration = play_count as f32 / tick_count as f32; + let slow_speed = 1.0 as f32; + let fast_speed = 2.0 as f32; let clip_duration = 1.0; + let total_duration = (play_count as f32 * clip_duration) / slow_speed; + let tick_duration = total_duration / tick_count as f32; + // Force each update to a fixe duration app.add_systems( First, (move |mut time: ResMut>| { @@ -2096,13 +2096,13 @@ mod tests { let translation_curve = AnimatableCurve::new( animated_field!(Transform::translation), AnimatableKeyframeCurve::new([(0.0, Vec3::ZERO), (clip_duration, target_translation)]) - .expect("Failed to create power level curve"), + .expect("Failed to create curve"), ); clip.add_curve_to_target(root_target, translation_curve); let rotation_curve = AnimatableCurve::new( animated_field!(Transform::rotation), AnimatableKeyframeCurve::new([(0.0, Quat::IDENTITY), (clip_duration, target_rotation)]) - .expect("Failed to create power level curve"), + .expect("Failed to create curve"), ); clip.add_curve_to_target(root_target, rotation_curve); let mut graph = AnimationGraph::default(); @@ -2116,14 +2116,14 @@ mod tests { // Add multiple clips let mut animation_player = AnimationPlayer::default(); animation_player.set_root_motion_target(Some(root_target)); - let once_animation = graph.add_clip(clip_handle.clone(), 1.0, graph.root); - /* animation_player - .play(once_animation) - .set_speed(slow_speed) - .set_repeat(RepeatAnimation::Count(play_count)); */ - let multiple_animation = graph.add_clip(clip_handle.clone(), 1.0, graph.root); + let slow_animation = graph.add_clip(clip_handle.clone(), 1.0, graph.root); animation_player - .play(multiple_animation) + .play(slow_animation) + .set_speed(slow_speed) + .set_repeat(RepeatAnimation::Count(play_count)); + let fast_animation = graph.add_clip(clip_handle.clone(), 1.0, graph.root); + animation_player + .play(fast_animation) .set_speed(fast_speed) .set_repeat(RepeatAnimation::Count(play_count)); let graph_handle = { @@ -2146,8 +2146,13 @@ mod tests { )) .id(); - let final_translation = target_translation * play_count as f32; - let final_rotation = Quat::IDENTITY.slerp(target_rotation, play_count as f32); + // Compute how much time each animation will be applied according to there speed and duration + let both_animation_duration = play_count as f32 * clip_duration / fast_speed; + let slow_only_animation_duration = total_duration - both_animation_duration; + let applied_factor = slow_only_animation_duration * slow_speed + + both_animation_duration * (fast_speed + slow_speed) / 2.; + let final_translation = target_translation * applied_factor as f32; + let final_rotation = Quat::IDENTITY.slerp(target_rotation, applied_factor as f32); // Forward tests root_motion_tests( @@ -2165,16 +2170,19 @@ mod tests { { let mut player_entity = app.world_mut().get_entity_mut(animator_entity).unwrap(); let mut animation_player = player_entity.get_mut::().unwrap(); - /* animation_player - .play(once_animation) - .set_speed(slow_speed) - .seek_to(clip_duration); */ + assert_eq!(0, animation_player.active_animations.len()); animation_player - .play(multiple_animation) + .play(slow_animation) + .set_repeat(RepeatAnimation::Count(play_count)) + .set_speed(slow_speed) + .seek_to(clip_duration); + animation_player + .play(fast_animation) .set_repeat(RepeatAnimation::Count(play_count)) .set_speed(fast_speed) - .repeat(); + .seek_to(clip_duration); } + // Backward tests root_motion_tests( &mut app, From 08f5d85455412a322076df90794ec4a88741c510 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Fri, 8 May 2026 14:41:23 +0200 Subject: [PATCH 09/36] create fetch_curve_evaluator method --- crates/bevy_animation/src/lib.rs | 55 ++++++++++++++------------------ 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index f88f2bae529e6..d5968ae2a4202 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1218,6 +1218,8 @@ pub fn animate_targets( >, animation_evaluation_state: Local>>, ) { + let translation_field = animated_field!(Transform::translation); + let rotation_field = animated_field!(Transform::rotation); // Evaluate all animation targets in parallel. targets.par_iter_mut().for_each( |(entity, &target_id, &AnimatedBy(player_id), mut entity_mut)| { @@ -1362,23 +1364,11 @@ pub fn animate_targets( let weight = active_animation.weight * animation_graph_node.weight; let seek_time = active_animation.seek_time; - // TODO: remove before push - println!("seek time {} | clip_duration {}", seek_time, clip.duration); - if is_root_target { - let translation_field = animated_field!(Transform::translation); - let rotation_field = animated_field!(Transform::rotation); for curve in curves { let curve_evaluator_id = (*curve.0).evaluator_id(); - let curve_evaluator = evaluation_state - .evaluators - .get_or_insert_with(curve_evaluator_id.clone(), || { - curve.0.create_evaluator() - }); - evaluation_state - .current_evaluators - .insert(curve_evaluator_id.clone()); - // TODO: Handle curves that shouldn't be modified + let curve_evaluator = + evaluation_state.fetch_curve_evaluator(curve.0.as_ref()); if curve_evaluator_id == translation_field.evaluator_id() { let curve_evaluator = curve_evaluator .downcast_mut::>() @@ -1424,23 +1414,8 @@ pub fn animate_targets( } } else { for curve in curves { - // Fetch the curve evaluator. Curve evaluator types - // are unique to each property, but shared among all - // curve types. For example, given two curve types A - // and B, `RotationCurve` and `RotationCurve` - // will both yield a `RotationCurveEvaluator` and - // therefore will share the same evaluator in this - // table. - let curve_evaluator_id = (*curve.0).evaluator_id(); - let curve_evaluator = evaluation_state - .evaluators - .get_or_insert_with(curve_evaluator_id.clone(), || { - curve.0.create_evaluator() - }); - - evaluation_state - .current_evaluators - .insert(curve_evaluator_id); + let curve_evaluator = + evaluation_state.fetch_curve_evaluator(curve.0.as_ref()); if let Err(err) = AnimationCurve::apply( &*curve.0, @@ -1615,6 +1590,24 @@ impl AnimationEvaluationState { self.current_evaluators .clear(|id| self.evaluators.get_mut(id).unwrap().commit(entity_mut)) } + + /// Fetch the curve evaluator. Curve evaluator types + /// are unique to each property, but shared among all + /// curve types. For example, given two curve types A + /// and B, `RotationCurve` and `RotationCurve` + /// will both yield a `RotationCurveEvaluator` and + /// therefore will share the same evaluator in this + /// table. + fn fetch_curve_evaluator<'a>( + &'a mut self, + curve: &(dyn AnimationCurve + 'static), + ) -> &'a mut (dyn AnimationCurveEvaluator + 'static) { + let curve_evaluator = self + .evaluators + .get_or_insert_with(curve.evaluator_id(), || curve.create_evaluator()); + self.current_evaluators.insert(curve.evaluator_id()); + curve_evaluator + } } /// All the events from an [`AnimationClip`] that occurred this tick. From 22171afbc6fc694454da9484177523b7d3314fe3 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Fri, 8 May 2026 14:58:01 +0200 Subject: [PATCH 10/36] documentation --- crates/bevy_animation/src/lib.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index d5968ae2a4202..d926ba9972d50 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -723,18 +723,23 @@ impl ActiveAnimation { } } +/// Contains the root motion extracted by the [`AnimationPlayer`]. #[derive(Component, Default, Reflect)] #[reflect(Component, Default)] pub struct RootMotion { + /// Translation delta with the previous frame. pub translation_delta: Vec3, + /// Rotation delta with the previous frame. pub rotation_delta: Quat, } +/// How [`RootMotion`] should be extracted. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Reflect)] #[reflect(Default, Clone)] pub enum RootMotionMode { - // TODO: Add tests + /// Extract only translation from the root target. Translation, + /// Extract both translation and rotation from the root target. #[default] TranslationAndRotation, } @@ -1007,22 +1012,29 @@ impl AnimationPlayer { self.active_animations.get_mut(&animation) } - /// Returns the root motion target linked with this [`AnimationPlayer`] + /// Returns the root motion target linked with this [`AnimationPlayer`]. pub fn root_motion_target(&self) -> Option { self.root_motion_target } - /// Set the root motion target. Set to `None` if you want to disable root motion + /// Set the root motion target. Set to `None` if you want to disable root motion. + /// + /// When the root motion is active, [`Transform::translation`] and / or [`Transform::rotation`] will be + /// extracted from the root target according to the [`RootMotionMode`]. + /// + /// For example, if [`AnimationPlayer::root_motion_mode`] is set to [`RootMotionMode::TranslationAndRotation`], the + /// [`Transform::translation`] and [`Transform::rotation`] in the root target will always be the default value. + /// The delta between each frame will be stored in [`RootMotion`] in the [`AnimationPlayer`]'s entity. pub fn set_root_motion_target(&mut self, target: Option) { self.root_motion_target = target; } - /// Returns the root motion mode + /// Returns the [`RootMotionMode`]. pub fn root_motion_mode(&self) -> RootMotionMode { self.root_motion_mode } - /// Set the root motion mode to control how RootMotion is computed + /// Set the [`RootMotionMode`] to control how [`RootMotion`] is extracted. pub fn set_root_motion_mode(&mut self, mode: RootMotionMode) { self.root_motion_mode = mode; } From 811ff37afc15b2223e98b12e8208fbdabab7b760 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Sat, 9 May 2026 16:22:19 +0200 Subject: [PATCH 11/36] don't extract rotation if not configured --- crates/bevy_animation/src/lib.rs | 52 +++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index d926ba9972d50..9d701abb09187 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -724,7 +724,7 @@ impl ActiveAnimation { } /// Contains the root motion extracted by the [`AnimationPlayer`]. -#[derive(Component, Default, Reflect)] +#[derive(Debug, Component, Default, Reflect)] #[reflect(Component, Default)] pub struct RootMotion { /// Translation delta with the previous frame. @@ -734,7 +734,7 @@ pub struct RootMotion { } /// How [`RootMotion`] should be extracted. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Reflect)] +#[derive(Debug, Clone, Copy, Default, Reflect)] #[reflect(Default, Clone)] pub enum RootMotionMode { /// Extract only translation from the root target. @@ -744,6 +744,24 @@ pub enum RootMotionMode { TranslationAndRotation, } +impl RootMotionMode { + /// Returns true if the translation should be extracted + pub fn should_extract_translation(&self) -> bool { + match self { + Self::Translation => true, + Self::TranslationAndRotation => true, + } + } + + /// Returns true if the rotation should be extracted + pub fn should_extract_rotation(&self) -> bool { + match self { + Self::Translation => false, + Self::TranslationAndRotation => true, + } + } +} + /// Animation controls. /// /// Automatically added to any root animations of a scene when it is @@ -1377,11 +1395,18 @@ pub fn animate_targets( let seek_time = active_animation.seek_time; if is_root_target { + let extract_translation = animation_player + .root_motion_mode + .should_extract_translation(); + let extract_rotation = + animation_player.root_motion_mode.should_extract_rotation(); for curve in curves { let curve_evaluator_id = (*curve.0).evaluator_id(); let curve_evaluator = evaluation_state.fetch_curve_evaluator(curve.0.as_ref()); - if curve_evaluator_id == translation_field.evaluator_id() { + if extract_translation + && curve_evaluator_id == translation_field.evaluator_id() + { let curve_evaluator = curve_evaluator .downcast_mut::>() .unwrap(); @@ -1395,9 +1420,8 @@ pub fn animate_targets( weight, animation_graph_node_index, ); - } else if curve_evaluator_id == rotation_field.evaluator_id() - && animation_player.root_motion_mode - == RootMotionMode::TranslationAndRotation + } else if extract_rotation + && curve_evaluator_id == rotation_field.evaluator_id() { let curve_evaluator = curve_evaluator .downcast_mut::>() @@ -1450,8 +1474,20 @@ pub fn animate_targets( if is_root_target { let mut root_motion_transform = entity_mut.get_mut::().unwrap(); - let translation_delta = core::mem::take(&mut root_motion_transform.translation); - let rotation_delta = core::mem::take(&mut root_motion_transform.rotation); + let translation_delta = if animation_player + .root_motion_mode + .should_extract_translation() + { + core::mem::take(&mut root_motion_transform.translation) + } else { + Default::default() + }; + let rotation_delta = if animation_player.root_motion_mode.should_extract_rotation() + { + core::mem::take(&mut root_motion_transform.rotation) + } else { + Default::default() + }; par_commands.command_scope(move |mut commands| { commands.entity(animation_player_entity).insert(RootMotion { translation_delta, From 2010ea2eae7eafabc47018e4601fda03475dc350 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Sat, 9 May 2026 18:02:28 +0200 Subject: [PATCH 12/36] change test values to have bigger potential errors --- crates/bevy_animation/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 9d701abb09187..4f32e25a4c6e3 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -2130,7 +2130,7 @@ mod tests { .after(TimeSystems), ); let mut clip = AnimationClip::default(); - let target_translation = Vec3::new(1., 2., 4.); + let target_translation = Vec3::new(100., 200., 400.); let target_rotation = Quat::from_axis_angle(Vec3::Z, 1.0) * Quat::from_axis_angle(Vec3::X, 1.0); let root_target = AnimationTargetId::from_name(&Name::new("Root")); From 502f3175e52df7a9c8d0c460c3b7c02a8fdbf121 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Sat, 9 May 2026 18:53:39 +0200 Subject: [PATCH 13/36] add a system to remove RootMotion when it's disabled --- crates/bevy_animation/src/lib.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 4f32e25a4c6e3..e4ba96c808288 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -733,6 +733,21 @@ pub struct RootMotion { pub rotation_delta: Quat, } +/// A system that removes [`RootMotion`] from [`AnimationPlayer`] entities +/// with `root_motion_target` set to `None`. +/// +/// This happens when root motion is disabled after being active for at least one frame. +pub fn remove_disabled_root_motion( + mut query: Query<(Entity, &AnimationPlayer), With>, + mut commands: Commands, +) { + for (entity, player) in query.iter_mut() { + if player.root_motion_target.is_none() { + commands.entity(entity).remove::(); + } + } +} + /// How [`RootMotion`] should be extracted. #[derive(Debug, Clone, Copy, Default, Reflect)] #[reflect(Default, Clone)] @@ -1524,6 +1539,7 @@ impl Plugin for AnimationPlugin { // `PostUpdate`. For now, we just disable ambiguity testing // for this system. animate_targets.ambiguous_with_all(), + remove_disabled_root_motion, trigger_untargeted_animation_events, expire_completed_transitions, ) From b977aa1de69fe37a495b675ff721bf6a80d69185 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Sat, 9 May 2026 19:42:12 +0200 Subject: [PATCH 14/36] add root motion example --- Cargo.toml | 16 ++ assets/models/animated/FoxRootMotion.glb | Bin 0 -> 219604 bytes examples/README.md | 5 + examples/animation/root_motion.rs | 201 +++++++++++++++++++++++ 4 files changed, 222 insertions(+) create mode 100644 assets/models/animated/FoxRootMotion.glb create mode 100644 examples/animation/root_motion.rs diff --git a/Cargo.toml b/Cargo.toml index d60f7b1884307..ae09815e65a48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1751,6 +1751,22 @@ description = "Plays an animation from a glTF file with meshes with morph target category = "Animation" wasm = true +[[example]] +name = "root_motion" +path = "examples/animation/root_motion.rs" +doc-scrape-examples = true + +[package.metadata.example.root_motion] +name = "Root Motion" +description = """ +Plays an animation and use root motion to move the entity. + +When root motion is disabled only the mesh is moved so the position resets each time the animation resets. +When root motion is enabled, the mesh stays in place and the movement is transferred to the main entity. +""" +category = "Animation" +wasm = true + [[example]] name = "animated_transform" path = "examples/animation/animated_transform.rs" diff --git a/assets/models/animated/FoxRootMotion.glb b/assets/models/animated/FoxRootMotion.glb new file mode 100644 index 0000000000000000000000000000000000000000..77a9a9a7b290e15b66de6498e04dd61933488f88 GIT binary patch literal 219604 zcmeEvd7RDF|34u@vQx=kS;n~g!rXK2y_P6DQIvh`3}fs{NU4YxQYk7@q|SMh>yLse|mSoMAZy zJq&8&N_ToaPM6Q^@Y(DRr_Ynw#^y_F<4!tpuju@ zJuW|W;PH{xHow=K=1ZsPv^iW}no5#%8}si;YeU1LLHg{p20pi&J~g!Q6bvgmjy_{Y zkICsWY!a=i0UKOZTRI8aoL-01?)G^|b7KN&^=vjT_3ZGv?H;?=<+OW@<#Ty#ZtBb7 zcGw(#T1C1=gLT{OHm}R=a?oNqVMXmpE}%c%>9*P2UXRW0r)eqZ-tKbyeKtaw-|lkx z{9boj8%Mg+Wp`1xG#hl2bdC-b*3W_KS1|6*S=pJP?1@8c-V#H%r~7?&x6@A(M+@Yn zVHPYH&8W>ri$$~U^gF0KTOodEPMlu5!{a6s@cC$F{6zzF+6muI8=>FljK`xr_l2m_2p^x6|kLx*T@HEZIF3fP$vfP8cQE;q#M2NmGok z2+VdL&83gb(qYVP0h-A?$awsO7Q+|Oau^JRIw%cNdO;hM4 z_vfY&lLH`U<24%erTNpT7o$T1%3eZT0l?UxKZ}b=oQD~_KCjs0`F5c^dvGZd4 z<6U#Y#V&$AIS(>0m)}E{?Ib5m8aliVf{erCbdpBo#3+3E2>E0bHsW=;?5g=N@E1kD zF(1X?_s7mhA^80sAGv!sjfOmks~{%P0@9?r9d5UQS_g%hBIr}Ba1o!~K#!*Y`n1Z# zOqSyCk|(CFON5;wAiL}iKf%XI>s*LqCt0+^=5ms8r?n{nvY(cN{ELrf&slPf?P;zw zBML^x)pzpPNtrgs5Suf0NNv37giIgVBDoL>J$7T}{6?p4W9bcSQ($no3w@Y@c`vC( zE`SW0WPH5FK9|o$<0e-{aDX)?FdH6&{vpEShV8JYduZ}phNGeoYK*a9vMC;s9g)G( zL=&!TP6Of;ln9gsvCc(GsMgegS&6aRhu9pkV|S%fz^2ILrGVrhyY& z4ws)y!cA6M(7v0p3o>Fd6|XS?PMS;VwSHo!Egm$iv%>xbGonsbNxJ zr6nbc_S^k#nl4IG$l599Q=oQHe5Lg?^fjR5rie_Do?;~>bcW41eae|I+9o)U zLpLQJPMU&(gp%9{vHA_yNGS25L*ijVgi+j5L|`|?Lb4CaDF|PNg`~Bia>?oUI2;sr zJU$wKQR?hUr<9ZaGrXja(#>K^M*PPlUD4SvR=z|+S>On1y=X?s7L3H*VRTL&lY$Oq zQsloVp`lL-@Icu#;loE+3RM7P%#?N;2?*iCK`10QM0ixqhaw0kycOpk?A~<0i)uIm zJLR%ue*^=sjZ!=Vls?Kz$u>ws!&VAB46PzrtAkQ}x*P8vTxb-=^mNM{U4WGm2`RPa zqvXm?S)EZmP&rgE4P-8qqLPcX5h!frcMI=Q)nw$ZPKs>g#t4}OQJI{BkDQvDG6CV4+9A&SOEi$n`VMURmVdkYpNE(5d2rC`S3 zjHNG892>#eaDN3o`+d&BNK_aZ$$V)!X<}(Wl>Zq4(_at+Xuy_tWnlbpi6U;R^)-m^HM}E2w;U7i4mVEo)YpY zUr6AoxalHsOZ))I_#T4#36#7;|4lO**4@FiVr5pvTOaa_WH5?_lUL&+q zwV^igm<+6_Xrw7HGLaGmMp3pyYeq{=0o;fow7o%7;H1T*+JI0%DJ;3Y0vsDgPMM~W z>C;`Rv?wtuuyUF{BPJ3k$Rp!Y)5md}9`U#0fnR9UMyR7fdZ=oq7D7LM=&A`)fW${mj(mrU>K`g&?Ow{y$rad) zEY42J04*Nnk%b^}Qxv3RikuHE7C8hpMvM|58vLUZV)%j*#fLwgrk~unG4sX}7Y0S6 zcRQt7lnYT(N2^r4QRXL$Arw#mrCltS%cxk%8#s*AjcQHercEt?2u}&6NwkhMv4vS- z0cxT-fub~ZQO!wHO!so_ z^Wie;28tMp;X}|<72-7CUSnLezee$tiZnmP8`9n3q5DR@K}j^VZ)C?VN4k+8dI=X~ z!jvGD9JC`Q@#{n0afq#?6&SlEv<*Tr+t@gxD!d@$qtwW7=42pL=6j5YS73q0ZQ2N> z*>O@NE~t!*Q5Up8X*gLdxh zoii+Zbm11bu|3_iox_>dbn>tq>OUsJ-(qwdm9r<5i|%s93ON!A6^7hc2~s6aJ!-{L zJy$|G6ZPB)g;eT!6cRKumg;#E%9*I=ODLpL&mS*Aw=A`@D?m{!XQG{5frw%um3HJ2 z;ue?2XswM0OWETmLV0f%JPmTWj~SjBQ?>M0OWETmLV0S8()>)BI)qFByE zJ%<7j#X>6e911wl5?HF|P=KOX&P+W8B8r8S>M7umjN5Z4Kv67brk(;3#X?H;6mUq! z?Ku>nD3&u*Pl1SHA(eVg1sq6Y%f+RPGtM-MM7trjaaLv07bE!nR*ID6bmWUQ^3J+)t36X6sQ0}69s9{EpB;1N~NJofe0h~ zSZk;N1_+vIsDK4XDK%7p!icKY8Y-{>f@T^jkO5Lk4HdXBBA2y>DtHtNnrP@&VWU_| zrJ-8|jAH2G=2L9WNKY@sBdS=?OhXkmilvkqs(?ZFEa#986XG>27BtgPg^gksWkK`fMH}CmUBoansIXof+iYz6xaYMrG_eC&_JyIhHtVfRsu@ z+N6pTF_JxN4HfVJK{E{%*Z?Wkuz25Vz_1D9jU9l(y1lT7A3JICG1 zLO>*;527I{ecZUh)*ZN0TV6LVZ*uzJ3MZjYYRl`!T;U}2No{%ExV*{fgDY&U zk0-U|^`y4E9$elelu2!EJ-D_>O^yebHwk@GTU!sVZF2hH@+P5AYHRDowM|MNZ)(fy zO>KF-sV%P;m)Cl7QY>`5sX@V;8Wc#Gn~l8o{Go2NL^r9l727Gs8Bz@wkIs0f*u>N-%p0P!!9VtgW%- zRPyPCVj)2M?D58z5-1 z$n>qbIAnm7YL#6IT%14DE%A7xkpd(zWcsdr{InOJvYM~*`9s~PrJO(1Z5sPXf=A^# zinDf=Eqhc>gY4VVV|!G7qgc?y+tAl_;$dGbrPNRrQ>b%G4bLCy7CqJ7cUt!J%vol~ zx-^v~&o2|5>sqtN?Cw~z$8LC7^T+HpS+mEU?IyvGyG`^yg}BLyJKXiAIAC8)57rv_ zVs^8V;E&nPN`gOTKg*gwcDu})-xsq1mduR$Vm7s`dq^2VF}q+%X@q;C^kwEaRFp88 zlr@S$by6Coh&oS-{Qek}CZmx*MV!VyvtT`^{uo>*rBRB=cE%tz35{a-lhPBhAq*gEGEWjRvkR)a*<`J)?@&@|0UWzl7s>h^v-Cz&N z%~Z^aCo%9C^GKCn`mR#6HcGgif)Khb1GBqKAroo++6QV(^#L%Ede+mQ0Qk z`;1sJO2zn_hyx{-C(%pt#SpNhYVn^lo*_GcVj#NroRc^^)O_+4iNk z*TpLocQi}yTZRqLq&kWHs$3Go{-4ysU9n_`f?Gbx4JFkTp|=;tOjlA<9Q)n5WF{=; zyK_m6#D%+|NzPa7cj%JRD`l|6e2p$yy<*qi+OFtTjxno}j6^I}l3T}=(c+3lOA>m; zd3({d&z2+GG1c8mzNAC-LYs%Y9(Vfhm)JJl-J%JyY@-x z74s#%r1XmUo?dbi|AVLS#IAi3IF9|wUNY+#^Oe0M@#TqK`(*TrwfkfwVpl%7b&Os6 zBzlQCcTQ>@W54v5oDIi(?Jv1rV$qVsNMgSHm*hIed;>5!z3`wdIf+=TB(;v-l+ogi zMN2aC6^oUm^on^vE}7B$V>?Mgt(a%(l3Ye#%3$%O>{w!+l}l#m^s@L=lLKEYRFavk zSfC`gh$%zGm+~scJfoK6D#kpemedT#Ja?9yMC`gJHDCUelL>#yD-okCXX?=3`*ok9v#!fH# za9B=G_SoUma*RLcH0{;1Z`XcZd-f=JwRVr5eY%Ib(brDXn)d5-Lx-L{`*a*)qr>R5 zrq}lD+M{1z;|~JifKFYn=~DO?fz42V>bu@f`<&6-S%g-mTHPZ~{QDw_1J zh8$^Sv|+q$ys(?GlZ;=x%nXxa-G|X&3zVa_#u@KGH(2PE;dE9`huHo!`q_^(8~w5t z-ASWglS`xDGNHHD(`({w#=Gn3%Qy7%2zL6A{-jr<6StSTD(b!X)#W3oi>X=Jh27%_ zXrLpqCQiX+`hgsJt33SxApIPI zhrS)0lS%`QI1LDM@eLHH1+!SJfh(>7`a)EE10|OsQ3C|+_y!8pir1iThfp_qYg@@_ zq{k@Yn<&Ieh7qy+{#p3a!G+tC&n%*s-X4ShAXvJ$}eBZs$SW7+2?6wBW1tiHuT-$XHbqPILjQ zxo3)*DaA5dp&6H1A#Q=@*wHIsQaX?tPlzN!nN|13<4=j?Ehf)+POCKHNuV^*(i@(p zG~JgHd4=Kce@338bjUN}ZKAvBQ)|-l}^G;8|*4I^-Eo)09Gc-eU6T zSI0{wZUHgX%3En~ys|%$yoL70yZcKc zo(k~dGi3U?yi)aEI@}p=gGe-X%c-{jp4HTs4tE|4+*xUFJO`9$^cLD1KL=16aSPm8 zYM%=5;!8XFG1*dSp9=8eVehp7p4HT+f;&a;*0?L$>!Tk$rJv1&f2G^Aq2KvUIO#rG z)Bl^EeHK_ZdM++TWNehcJTc3(Q^(eHC7v>VIrO9wr_ z1@x?-V}U#??deq%iHC2|KfRZtl=7tnpPhbNJmLCT4xp6qv(xXOmrj9F0?IB6K!^VphOE&-u3|cBsN(dT> zeCY;YfkCSQSRJESLZFoADE5>NQsPqpyYbWEiDuAp0!r%!jaNCBN`X>B(CRS7dIc>o zXgPxx5VTaFlm{wys}mKg0i<-IqJW_BM5+Y>_{7;&qGT+7_QPg*s83&DN!*VykNJf37H-vA0SipWHlWhL0@PNRvO24=l($GPEeBwAT4Skz zHK>c1Fuu03yOIEFaRZ7KFuu~9G=@|Pq=G?}d`cKp4Zs3}mMdTZK}!gf^3=v|b!=lb z0IOpg>j9J!gT{-s6G13e0R;w(?@cG)fC2;VQi9Ox^u}@tEHD^51F8X7fY3?-s{fd?*78tY~Kq(<;b(CW{fKndj z&;!DyoI!6XF<8p;9J|*7g4Q!wN(>tB*-L~$D+n2{mP{mXJ%CbT(CUcAY5-P8ES3W( zL7_XsN#i0xc0>b=YF1Kq(Jf>{h2O z)&sDBpydE8vO!CE3j|sZAQcEIOu%1C2&Q!8qA&r!1q4e@Anj-re+LD>S5xpsrQx%3 zGP`9?8l5wSzMW+A8e0a&t)8PsP0ggQ)zDrBzkJf^@)!QyvDbMVp{Mx95%X@Yzt18QEi|K^z9*nL zIyr5L7cXvRosPn<8mG{Ov+xb?W)ksTcv98LQ~2s~vmO+~_7uK<#Y`ezCx789gi;VF zeE)`-fI=s~FEx0$$Tbimab#0|nltDQ(7UMH8c@crv%qloV!RiEkdI1>&+p_8ZZ z9WBZ0IdPp_9MxYwKnbar5MM6uw2lOd!4scd9IA+J2o!6UU zfb`iC2kkhWe73|fJ6kHJv_J!Yu%m zQe@$&nUmj}qQUyT2}hw)%I`}!jyM~4(GRL6$~F~!kVtCda5?DRI|)3MstBVGb`pwKCCX`<4}ms)b`_9jj|<8`7l%0v^Z(1~97m&hs< zT_iRmRTqivD837yGqp6wZIN^;bh5}g6*?u(KooXFqfT)x{HdA+y;d<%7xAO;Ck`)) ziSnmt79N|$o@hzK^Vkw^iN@v7T!LMq{i1QG@pg-!&(l)Q(B;FPLB!r-FN$(7>X3>`ZqOwv?cq?l=XTqz#udEAMg zYKTkHNFbrRi3gxjTBL*yH+`Etfq-I~=ncZDEw($wlPV9rCpeJ@ROyuBNtK73W=csN z2_!saROsZfr&f0w@5xQH6!D8q4~ryPiFh~}Z*5Mn*oq#EBO7`auISC#g%85fORqbR zoj~s(Z`zJd!Rf&|dINOyhTrEk1PZIpq6g>TX*t@)HBytCZ)rT?o(sx^yY?va*rVlw z5B#@kIP<10d5=Gy^Gf~itA!8jeLru(@(*(Lk6VYE2Abtpy=re-=E|Po`j7Vxd+z#A z@Rm*2hdX}KJACli>fqfK`-IQrWQPB^dQ-6FiBaK8^TvjkKM@ROWlRWH+%YYj{qGNH zZE6n)$82NBIsCjH1tTNS3^_Yofgcgb~F@v_JvUS za?67k=X?>`*nUH3L$_7IRp;&rADGfDeCw$Cq4X)Yhbz6%A^h%~uAx~UJr=Gs^V;wd z&%U%PCtn+08J-X`UK|VZ;QjSWzD;}QoBrWrH1?HecBb`xB^17VMp_v4$GQFU`fq}d zKGHheaZ}xVSN~zb5x2Jt|8a5cd|Zp^_kJ1doKYp5ciruI>%Q3>yzN}2aOWEu zG-3l~kw%@bD7!q^D*vm{lGiKcf4R6*Fyq*f&~J}D8miaiOWC>1;n0x3=Y{TlXLRtx z+usk(bS(@$wPsc@{djgL`_Mi4r$esIjkJ-WHlN*{zvxi=;Pb;LgwB+EF5mIv`$28< zy`c{_-_4xMrnETi! zwsYXO_k&YLTom?>Za{WpXdKy09`A;yp&Z4m7U?a|Bz zQ5N|Tw}@wKA7xQbY$trj!QkENeBs-!s2;}gHoc~D=%Ha9!)t!57Gv)?cC=;0E#mp| zGZTVmCS4R>w_#Bp&hz^1bza@+DH9&uWp>P*m)mtmFfyub`1tx}`DkmnUbyaPU#J82 zi~67)q0hl}M_Z_T=YyM1ws<16-u7ZX;uf)rm_)m{!n0s=^IwL9<_z6rifxode(cw* zFXTs_#~&>hxxXl0;Jl%Kz_msUyw&l!w0gIshbOkG5k?x<4rNQ=Ji@Vt@D|}1M0ksE z+(DdE42p0J8oeni!f|KUws&-nH7IM=CgCuTV-VppkK+#Brx=vSF{sH0Pei;FcUDu} zsaS4#q-Ig9nepzl$SjIM9ViA-+=+19p;(j0v4&z$9>*ZGchsNkE;xV^#7sVjNImVhX-)xR>3__ahCDN%VPN6K~1Y-@^j&8)D4=4s9 zjd(*@yx*wo@`w>@mQt)ioewVV6bVt>IZ1J6)jOjjpHZxtMX~0pCSU5;Q4AVEF^J+w zgyRm`ON3($+S>0wydUAXgFGmU_Pg=gt0E9J#9z|(v2iqtA6XCeC{=nMEQxt1{ zp;$vPDDTFi7=&ZQ^};zpn_9DQU*vL%H7`=EX}i5j0jx{(B$d9&+V@C`(I5Qz~mSW9%iZzI5 zoM)U{j6rvLmPR=4;Ci9m;kskLXj7;It~J^V`WeJO>W2Owv5FW*d}1s>yFgjwN1UT9 z@}mwYi##ZcYlm}=aRh0^0Lo%q#u{M=_%CP5$cRI#ut>u zyc%^w{gFm|qAdDG-bXz#CL)dg8D-J$wfUh@-pyas4KK;L-Kb@v@datL zBb3FlpiP;@GUP`;j5PLzZBoC{dVzSNbp+`dt=*8OafJrcxR%nmP!@GSJuxpu8vDXJ z1!-&(WznaVh|AI15P8t&AV2b;ze0Yjl`xkko@ig_x6zi-KO>Dfn3*j42h59*#x}8E ztXXi+f%v0)5lEA4wDv(+ypQ~-1JbB7%3>{pF{SZOH*cEsZn?1gbb5ZMnI5d)_@eNk zcbxgik35JC#NI^%8i$^{uuk~v#ZB^%zHL{N(1==>gdaHAHiWXl$ELR)TVp}^jVTlJ z?wNgm>;CV|5AR#+&I>NogMDtF8b0>ABZiJ_X%rfM;rQ@H-6DA?`(U6>sQKH&!)3pz zo4-1(b!d0JPT`hm_456dzX>jx5Dq`EyIDTUu3zX2on6s6{8RgP@{k{Cto3jnP=6fP z@KBvlvo8;XPORx4DpRLP=(8uY!+n>%7FuqenUjtr<;UDBV6YN=TQTX@wF9_ckzA98^`EB93e>V-|{mnnE2)-Ct9^O~w z%9y<@}(X>eLf$(E5HVym;dR)xjAogn}i*~f_Y@<+x_T9tR z{dIB7yrDj*KkAHQL7kBY@rIZ|n?mfN53g0FPN?EfWy1&mSe}RcXm>L^eiVG>-Dg9s zw~WX~9dI3S9*p(6xytD9(z#_rH|f>V`dx8b_+Fx`cCVLKW!mlGuR7iqIy=I(>Aey2 z!~1Ff7HtG=D!BcXjfXl;3wNMBM!awMke0s<4PQ8~S&ZG`7*8zqhXxNm6{`CC-aH&H z#>~kXjn{5vSZ&-0KG_+C8{1&%e z5Na~%k*v39an;bJ^BxNQb$i|X_p4P7&EGLMG^O3;`K~$-2fu&&*3jDam*$TN ztqHdJV`*sf-xc#)?s_n|^@aOFn-)~dN8K>)ApfW~?*-ppy*kv>aasN)RSyPR9bOZ< zWqE`A>n9%!-nMW-=%$`c@>}d(8vLj81EI<{*U0a7cx5ni`-)JF&UIpRKn!5N6@NJ$ z+?=sKwDQ%Hc{rcQk36V9_KV|1f8{xTRp{ToheD&;-WGatP4!Ut$ep2Cb!W!xwYJ}} zENJko@6tb1ZhCHT^m9u?@4Qd50CnPfwD0kfr&V~Qwl{R9cD=lQKl~EPxbpcJdf>E! zk;}e17CQ87Bt{l<4D_+sFVd(#>NB@e{zY*bfM3oOC z+z!&|jz1$R+2+}2f^SZ&9B$QPem?evd&p?luWsD_+$zTtp>h?5$LwjNEb`-?_UOg0 z2j{M>6FxWX_I&IY_q4HILw@8z?4g|^{xSZd-#~wY^N)T5{UrJj z%ZL-i2KqycJLntHb}+W09br7hImh|D;_1ers_#z-&zv+Zgs~OzhOrWD0kMs-3~`8K z!5nzXFO5Raj2j#N@}MmyM&lSUwxa!FOvG4;I^ek*o=>4H#yj)_7;A7(0R1`I0>&4_ z%+am2LM_g|8@g@Xnh^RW!~ps(0`(@!*;NJY!i7fUZ8J8e)Qq!hfz1&`$B%i zB+BBxACAkwKcB6aZ>C4MMnfKyU9hW3UMAIC4^hoUwwAZ4s4m0(4Wx080%ghH=0$6+ zg?fbRGOX8-E)f@^vJI-c^7J43g%4NH2@M|6B=7Iab;DCW z*LoPEZ%J#Nx2ve`L!I$H`G-8N`|v)_8`((oTm$E1WT;MF3##w_p!yEyr(vBYd0gvZ zjKH{qV<8_N;rb3`sqV<*TJOkDDs=2 z)w9?a>VW=j{hI1|U8x3~Lp7l9_*Hr3`yC36q1q7l+bd1ajZC5%a0k_ZV#l(G!SgKD zh8Xu_?!&nx-p6)O2aLrOb0b_snblkrPxH8rLtDU_4r4CHailTsqb%xxXRp{V+8xGy z%t??R&w^2w@{(rlac>suXsoZ1#xqcqMGPaZ9{=5w_w$PD z!lQCp=OKN^`>wpTi~EFw(<&SL7}0zW_g-*MhGav~PL!s0Lfj6@q8~s!VBLp0pl;|l zkj6Gqmh_LFaiBhCI^$jn%Hlp1?q7XTyI!a#?PIs2eQcz$wnAAPFWLp}hodh=AA8lF z?;>X=>%ZyWpv!c96z#p)B4QA>S{wP1 z#x_wF@1sp2wvjfI#l1@8L0P2H7El&rBDE9U!@+zBY0R%s7Wa1051_pujrc)XoD-ZY zTs!O+{Tq!p>i3YwaiJ`Z1#<@E!7~E7AKf1=!5_Vk{D>2zalNoj)D!hb--tBMGs@x_ zC7xa4J}}a_4~(*Sj*0OI;}p`^Cd%UZES}q9zewZWHOgY0jyV$MLP%r1kFr>&BR}R> zNMpT^vZzDwSk1g=Pc8|4eztl(=88BL-2a!yYKArv|32c!Ivt43Euo||C(i(|nyQ5I=4 zS&XL``*8mmY22?sSv(`a=bG@W8EHH_Kv}$x&r@OEi!|mrD2qDdxYockBuL{vJj&vD z@jmXiAdP$SD2sD~2h<`j01z&Sk#N z!|^^ES{AH&{h8bu8~)8h9=xwzP$zWr){k?)9{q0~>W||^9=wk_tZw#CaM|6<#f=TW z&cple$zKPTZQm2Dm0LC+dGJ2Y3F?W@9pSn5g;mRhuKejl@Q$81-7v{^66s zj4}&CO&(qveA~Yyv}o&s(9fO9hx-1$D0F1`lc5_Yd=p&cz9V$mz~@5~-~BphU$7x03oTkH;gcywvZ{aXK>AN+Ii zJ)u9h-5JA!dSV^a_Sb5mFMquwRIA6MA^qt3;DeRB$-2k4#>kGW`dIMvb<=YF)9(r) z?P^ygRP_RH?ypbZ6>6zp7)m=)U4HP)TQRaYc9g~Y?r|4~GTW~UUNQF05VpBtNTtw( zCv$_-hpvd3lZ92j3uf*fCEj@My_owrCpZ>tAKOHoF~7x}6Z2BEaeUSS?FjQ*%zH84 zMjOO7u^qHK%ny+t@rkl%YlwO57jcgK=%di?kRRJY9grXW1L}Y_j{Tx6+9TQ>+B@>& zeQY1^qkk}KAIE~aAwSLo>V`f6pOr$pL;ryH5ua$kxF3Rk18o*<+038d-VXW`v_aG# z^+erJXS5@#9i!oJMfN1H`of%c9x@?c+hANxf;ksrr`?IVpo8TG{a?(5yBB2y^t zuc5e)XR=r;{p`Fi(vaf*^Az`SA0FG8+_O&Jb476<+q~e@{Sj}y{{&W24sh-MI(cU) z?oXn)kNnss^5c0lwn>wkzYTVEILvSusXCXGP%#0 z+-Vf|aUPIo%>{Mx^erFfenoK~`Ee}BgY$s&W6l1F47z8z7(j6!bwe6?e3QS9JY19y zAdR}=T;ch3i>hVvI#AqSLUA9@N3ME!X=E40`a3DsUpMNtNH59(B;^1od#qFWyn&Ph z{6aav^mo6G^rX1|J;nXO6TXQop?shLiXHjf*{8}yV8_EIdQVy{5==#Xi zD&1s#$^j-;eJqk2n3kJCaUa{n`)%|K^QxSzEeOk2+wRs57>YdpNj9gnL0|c8%wExZi?% zM7Z~YHi&kI?W3*XecVSvOrlL;n}~VDHtK+~Xlv+CP!{{ec2E}kMPFfN*Qf*9CfYmN zBld-DB0tjDCi)6&6Lr8bqAc>`oSKwF`U=D(@}te755cn!v|02ac+O(xL-70s zZ4hK9Z(kA#4+N1)D3+O=3SU4 zU|xYV=5HvAYmFE{KZZ1p1!XZ_AO=upq_Itu#hMiL#GDgp%rj9Ib55+$aITOxlf@hu z^Ezx3Y0RZj7Gouzv7%idjdOysxJGE3*e23uvRK#ST;Y5ojc2|ni{r)n$d5GE7buIE z!FfR6f;9Rcl*Rk#uh71bMogkC+78wrXh%2}tYvWBagESk(AVL((7&Pn7}GJHsM?tOmH%5Co>&FFv1E)#1{bkaKSm@Licw+`$Ur!vOq54ETw%;?^YM~FxM zGfJOXRug9QjGPZex%Pwfr@nnkn9=XP`Cee8cZNQv`gF~Vp7_A6V*BSa^ffF0*j5_4 z%bBqaYDZ*5n9*--7%4u!eU`pq zY(|C|o%f(ePVVQ_2Ym4UHZ%H?J}sqKUPCsXa8@&;w;!J_$F{B**n51DW=6kXtBu_D zScd*+jdI(}=p8St6ff?%Nxw+{L^Gq?oV`TsyL+a>;>xX(= zBF*Ubzh}uGuc@td3qL8$=%>EuEw$yJY6q+D7G`wbXE(^1)&A1H8rxl%(fz-vFE;D5 z^auZFu9?ww{*_|OWfSxT7jG73wC^LQ=<(=C{g>9CX=e12GoJ<;UU94LsyIK_jJ|bU zHPQ96S^D1<(=;=BU(I3qm}=X!nj;?-W^}D9s>oCCG?I}mm*{5n6VBVk?#?sxlTY2B zVMbpwV3wTFW_i2IE*hhm(atw}zqRnex3oJlC+TK%`AwU*J~`!0t@C4(bTfLM<00+U zOK#Th?D?@~MqmH_-J;(M&m{i-2-=aFX8jHXzlLEKT&6l-*dJ4%{j zjSg`~NmH!RA?_$?iZwdK9VJb%MnK$A(iCenh&xJ}VvPoIM@dtx(ID<9X^J%(#2qF5 z(F414D^8oC*L`=2W=2!2(ID>p&osrFb`W>|XPRP-gt()mDb`4cJ4%{jjRtW?NmH!R zA?_$?iZwdK9VJb%Mu)hgq$$?u5OD5O@V- z?kH)BH9Eu{B~7uW9mE|aO|hmu#2qC~F+ziQp`V31Ap6dN%;zcq% zW_dNg<;4m4UWc*<=JjukbWNa`Q9fEG@PKUI?{v2UO_v?|RZ)^XUcGXUgFr#1n<`M0yrPt`+ zjH@cm=+7^HN%J-f=x1xzmS!}yFQG3bO?pbuSxHm-0{T+Yq^AI#mGsFD>jNLvU#4kC zbEFxa_QFDOuIDE0Or0^(j1D~im)L*FV14wQQ}M(fUhA0wc$+)@J-YMK`0{ z+<8`v`>u;j`y^j8rSoRXt~b^bo9k2+ZA|Dcf$nm4`Fe73g?Dnz=!e?fDu;F}pIgp* zC}2h(+_h4yJU&rwzGs9mqd&~9EdP9|x9qsPgJw#<&_jM%eu#K%cvbnK3Elj%^75|k zUx=!+glt>tF}q#51! z+goI4csXtBwO71lM*p+xesR0Ax$aN%NHaP#JY5{=dAWRTM`vnJO^?qjFV{BfE8qQb zlx9X>xvzqJUh6MA$jdb|`s45Ci3f+4lOH+y=w|fx8S~_enXNJ=h8EqtnN>)rLQ?DmVMqIl39Wc(+$JSu#|tY}`^eqo?hh zp?}<9pmymam4q1`%6~)C*0t52{I`iTqa&}{_1!lN6>r|zRyU)6-ZEWZfAKwm=JjsZ z%;+ZrZ-^Fthu)$`BWXs@Z`WV0Yx12oKD17l(XDM+dX1k~Xx;1d5@vMMXC}%IJ8HSM zYqn@+^jG!~`j8b(v;!~h*UaePFHQ6g)gRHme$6Az=)S)-*X8IH+R;B<(u`hftEaEn zwMKh=fzZw9lXAhf7smf5_ts^%>SlDVucEwurBj~y?UZIlZ+d*Hbo8jB4V-$nFr$C_ zc&xst#TN1P;0~G@{pr=S^$UAnEoxS)qM6ZW-<>Pp?fTf(tRI?aX0$%nAu6uS)a$qX zNSM)wd?Vy7Eq>OnZ&N*BMxXntmwep*inw{>&zc#%W7=A={gW%?s(zisop z_Szc{%#mjF&rg+;>!uEpJ?#6pnbDzrm&+R_RTGO&UZb1Qzg~H}cqQ$W$g4L%H=}v$`OH?uC2bF#WhGy2)A>T=PFQ$mbPlVd4}vA{%~8RCl?7bI{)-Y*=odc?aD1L2{XF>Kl8-%L$YP~*`C6T?sce|d^Dq% z{Bq*eni<{i;;+OND+kM*{W>u1HPBzla=s7nw7t?Ya`ho8p(u^K=@iNh_ z<#n>b;L5rgZ5AW&{`|XV$fuue6R7<9ZJHTftLg&L=f+GuYt382jDB`YcUk$38v2f! z4q-+=^X>ZFr@vmN)%`F>n$frayHLAm?PhJt>e14Srn*dn8ts3ksV>u@M*E-XWA*3h z2i{$k+qugX0W*5{8@K8oHrG2+>bB4; zOxU4yy75ZgjHdc77iv8vP4%4ywVs;Jo2^5wr=+RA)1lT=(!(RS%4)aG$vt!V;{h}J z>6N!icb}EHPmFvxU`A7&s6)-Dq^VBSpk`FkR43|CGpgwqdgxFyDru?{b*LGYG}VbZ z)Qn1+>O>7{MkP&kq7F5qlBPOQhni7IQ=OpB3i8y~y>v60>O>uC zMkP&kq6RgilBPOQgPKuEQ=KTFW>nIHc6#;D@}c78W-WCynrc7+^`Mfb8c;$#sHCX| zlu!>UX{rG=s0Woa)qpzGgG!oeKneArlBOC^hk8&+Qw=Dg9#qm)yGf|ylr+_D8BoV5 z>FO_(*K-^$>E3)=Go%0PldW&4ex>%{zxN6=`sX(DWJa4O0#6+szs-#P<>A>fZEshR z_WPh*Gn#5Q9qKqGO|_c_b)1r>+D(T#PDxYkrb8X4q^Wk(ppH}0RJ&zB9jBzJcGIDb zQ_{~iYNU%jb;J`Fbkfb}CmnZatDgKrglhKJ&1kCKbg1K$G}Uf8)Nx9>(Q_^I>mS;s zP1@N^H>0U`)1i)2(p0Nq7$wVMug zoRX&6Z5z~aN}6gn9qKqGO|_eZI!;Mb?WRK=r=5KV`uz7F4h+8U*==U@rAub$?`&)n*#7Koni);?od&g@l78UL z?)pnq>)mw5F3f1Ny;;0J)}Ez*PJ6j6|LQ5s=pzH>=-Gq9;?d=6+ndqX4x6b@TQEZm zY?8ChjGi)do}ANVN$#vQ{WD5KyLQdfXI#}d@bbns+stTcUx&W_XPU+$VO&a@=0w8$ zC~4Z4mas>yq-lR!!d|zMe*WUw`iZiKa@*h6PMFcj_|+tNEce;W_TTWnd%)H5#SIT= z`V~%TMju|#Twb|tl{RFfOD3Ro;8D`lz7BmUX|uhf68DvU34IsMzl60=(hHl-m5)}O z9C-J&tArW-;@o+1^|leY`I&!an9;OfAz|-ANz=ZEggp=?ZR<8q4r+EsZr2_c2h8a0 z9p}kw(pKgE^V#4GGy0w{XUkvDUMIYM*ETbHUZ0uLF=nc`vUaarGn!;|Xh%thTFupe zosuR_{82C0j5hNFc;C!k@ILWN@GVN3#-hWxlyoxVN+wS*+Xu$`ty|8OWA8~5*U|i# z(PnFe_mlCNW_xRR-|UPs)%#}Wn5o`3JD-Ko%(Pcx$_YGR=W;F5ZaOSI|&FXr* zpG^Ip3{T~p$@+ce?+?s7ut`fmOW;w`6!#_M07^QU8b6smhh%t?$su3vG+VxQT`N)b zhjN-3{e9(ea>aLB#Dm@p&5Smy{qeq8?T`1(&Ux{El6&2j&n%gp*Q9zsZSWkq<*AT( zWA>HX%;@Kb&y`ijwH7nh+|}NUHalNS^?ourVKUEyCF94Eshg7Fp*hiEev~xLtq$|7 zq?0*MNv0M~hKKg;b=bpK(zO4t!&!imZts~Vm+onmJLB&=17>tq<$3bsV~cVh`DAp! zjLsBYrT)+XZT`sv!i;V;v8(Jk@_=^dtp`K`S_d8_P3`N@my)hgd7i$8bZ*#wOu&q$ z@#-*kC2e+gU*f)UUkLY(HjN)6n~$EVeYopIVMaT1=jj7lRSaCz^REmu`osM<%NyQZ z73jF^FJVT{AKp&Bx#K}?=x+^mGy01k8|u$*Sgaj+Geb9{|4q--My_ZX_~M#b(u{uO zvDxywF&W!VJ=jY#qc7=rxxCn6_M<@k zv%(=!xm8tZMwbmX))!Z*B=2olQ<~AY&73QDJ-;K+{HIF7j9xa!CGYGqOkDVInr=p4 zO7nNcT_L?$r>mtIedq2p{h_K8h3iJAZbr{L_J`K+uOGCm#X)ICJ63j<@7sUTw$1uV zn9+A{Z>I0B^-;!&XF5tV`sqx+yuRVp8FwtSNi#a<$%cBLmltcd{+6Me(HmC&tsP$Y zbKpqUP120sb3rrNC485*sg_?iqX*C)ew_zaYHv!rZbs+)x*_BCmu}Uk4cHYhqj%*z z(0)OgdHTMIujZQ33lCdMvh?ybdx2MrT$Eh!ayU)U%)Mr<>9Bp5Lu~Tz`!I7<~`klwNhS= z6!#_M07{zTz79EnlBT$?Lk^&%DegV`x0^h zB~5XEE93x5n&Q3=Ie?O;xUWMFprk48YmfsdX^Q&-asVYwabJTRKuJ^FmyiP}X^Q(g zG{t=#asVYwabH3Xprk48>yQH|X^Q(2asVYwabG|Vprk48>yQH|X^Q(2 zasVYwaeo`+07{zT{#M8Vl=Ls(X6tKCJrg*5^=e^8Q{0!311M>V`|TkIP|_6lHOK*! zG{t=#asVa0%$PW;Dfp9dZCAO>tjB4xpqd?hD8Plr+VC9dZCAO>tj` z96(7^+}9unP|_6l1>^u~dezMu)V>aVDQUAD74OqL=wKI0+AL?r`^m)9WO$N^ zxykUD)%B^~H>>?qy`M}@k_=BWJ}Ma=v;7LZZ??x$;=VEm!#pjS94VRjo+OXuUV+&j z0^T>;fQ6Yr}nPTbU1a{oNeGwF5YuyX-<#dme(=wtrqJTzQ?Ry@(At-SWiU&Q6r zT1&a%xCrlQ7QHreoDv_6XfBzL=X2obGP2x^7IMY$GV+G&uZ~`Mo|&)4_B~=~+5~a5 z+OwjCHe3Ace^Y#v?-sA#-(S?f#x2ec>nPTa@`)Fp?I5=FXeMs1J3_wtKpXkUOC#mf z+gr$KhjV0upjZBqKSE};bV~l5@3)-OS6(Nsm6_jV%Fn;Zkbig2k@Xg2ME_}{W1T^_T9yY4rf%qckk#SK0WJ=;*<4c z9P)bd`f9cR5yw7mEqT0$-d-stKTekw=qp}LCKlh={gjq%`oX1YB0f-`QNnXmHiGiB|g9{HDdhP?PL8RaYa zIk&%MYFTmcqSr+GWt9V;_;-kTJ-!K?_~m86>&)}|=*|prsz*!7SDq*4%eGgOznuPD z>~hzTmAfAi%WGAZr^oyxyx&!qP0yVcSNX1x!`GY^ojcZ%!N>m=!S!|J%eNmDb8;`2 zpM7#F%2!fu=0z9E;h+B@d~cl<%OC$%@ZSyQ{2?0azl!xIhe^*1XT`D|Bjttje;2zZ zj*th>ofD^e{Wmav!2~(+#>WHw{+%hmsPI(an)k=ZkE+y@J8wT2?Vt7K`(u8*LYBGz zx9I1MPL`Ehg1ZEdkM$Uvb7$b&!=FUwEzos#pj{5}<@v?xKh}vg6+)tZlXYTLk88v; zXI_q8um7rrXk6{BC?C)F=?Pbe?0cRSk*1BrUq5V$UU_|(?~3<_%jKVC3wNgp@`jgu zV%ni;a_-pKV#>3lq_6H@VvlFMtf`+6Q!>WOnce;t8#agJ*`tHx13TKvizj5t=NblO z##jC2S0{rsU&EsJneUE&d&%YcOgXlnO;&n-ob10<$lqFxk6yVQmfsfWB96>?O?;R2 zsMzwrGotLcYemgJas|%^o{iQF&XRDqO4`qEIZX5rGXnf@!@!;AG(fQ@|=XvIJ z;dSD5VB9dS7-x(t{+s1^U0II#_;cpx{{Ou2km%RvL*ZNWgQ)uMzNo(ZIpdJ^6kSJNS6;uGHPYqOeZ!)7;PvLudEI~C)>>|@mKkkl=7=^j_qmBt+^`(; z@w)Q3_xy8Q^dGrR@RjG4`Pd(FUQpw^MWXt=?t-uECz+4qCdW(8ANX35kLQQyCFhYF zW&cOYOTM!IU_Oq=+ze#Kyp8P<#k~`{+s(_zs1+6 z{TA6X+tapkX(H0(ZNa$UE8~UvUfaJ;oNDlJ)Zdi!L;N}0D_^-C#vA7c9N##u@%5GF zr$u?cE1E|U%4I5H~7kO%*XMP^D2&;>{l80>@WE~+XsKnc?`#4 zZinreame<|SB@(s`53<(5BSP*+#cs0ocD6x!B>u_%*TK8=bXRtmE$e*4f@G0*LQWx z@h98L$se|rO}}X`2mIVZP7p$FeB37abH2~>%$`#jP{oUY$Z^55fnH5X#g zfM^}UpATDCQRJ-bF8T8|Lz{}sXK$9{JN-8>YW$5+dG3!t=lqf7IsZJi=UUNzL#DiG z*I2P@`=n_7!h8#IehVyH-&YP_^l0F*F5RN_)z2H|1-_ixJz8gRdnNf8FQMOiioc&K zuB*6S;{LgAVm;Qna>TSBXGitIHeMNs>i5OVr^Mn{sdhZ_ zyEy!EZ(;CvnKz{1YRG+Yq?+vVO4DfFrHx-J#ymVtyt3*=aq3TpaJ9We`t08aY&EON zUcJ5xIRJbeUd3IQ!*_S2tD=m#2L%{+c>WEU9%s9K2z& z;Lls%@rrOcDv9SVeMvmi@*=UY)=Ogf*rS1&!?ubWzOEpA+qQ^(Co2eUm*lity+vq6Y!)<+hacVpX~2gj{VkqoBE2SL%Kx$CG+t*vfpJq zv47+E#r})^B+IiuWV>fS!2XQ=7|Sys?oGsX$~ImQe7Hy$^SZ{{Pb#n(SvE7{NS`mi5k z9Pm1`A7X#TdT?CrG~x#_a&z~nUu3=553`?WwXvr3-Eu|L&vCoFpTzr1?BCcwvA<$J z!uc)RH`_J)51w~-i$?;d4z(9-4?M4IH#|=q7uf#Tj+me2S&r|se_%V~&)KfoUb!FM z|KQJg-I#Cvwrshe!AM#4r!lhMk^yqUi#c-oy20|7MWbYGF(i7Q`8K|}TJ*VXi@3vk zooF{JFIu;99?f|%^Y!0SLDYWlPw`^IYs8HkE|A0C?I>0}S~YsDT-z=@_g9t7SN^?C zqWo)z#gu{1h?$RU6LstF5}z&F8NK>4HL=xflf2*tBr ze~EhseI>@}HRMe@BBItc)gJmW`95FYM_&Ksz$kus9IPMD-#>F!iEbzV5DlMwUi`fIVp->gRYLw< zMYenCP@uujl_leg@pQ>=Jp*NHm6!AGI-QX*&JRE zKH5`0b4_3I`Ql!(OV=U7;O|y@m>BnGd)cK+^Mbz(dES?L{kF-nTVI!GTbJHj@#<7j z{gw7IIOtGd;a?eY@t}7CwLi*`y=MOyxa#`>@?hNw;-3AZx40c{m+j}N2SW0O{YS*2kAw1s+2tkI=XWokB)|P>zZiQ@ zmV9g7S;6%`|9#Q9Y@7f%6^jN*q^a~VR^pKelhpo17hb@XXyRDh3m-c$9|Um68jtW6YMwHk8!(f zx9oS=e%b%<=iDCqBj)GNSr5h~`}xk5hRC{g&P4acd7q2p9LKZ06}pLoT|X6!L(aqH zGjrsy1~Vnw3)>0%0mcdAgZ&iyB_0otpVxu?Q=clE#lDqi1p6OePsRoN5$>P;5yxF# z@2l-ygyVtJQF~`QXFqkR&nHjUXcXg@3_r#D=Ioc)PqE)&f5Lu&?V0VH_wm>-Y{}>> zmoNA?>L1t+*iLwUdA``*I8R|aVmapL`iJWxZjZ0L4$Q}P$o9^BY|m`(tOxV)=iDCG znVc_j{>WE8&tyJcS6+|l>#h~$-~L|kwbqD-#m?4;1@m#9&Ut%}<3EUtw|9_yUG^@$ zKmPI#(R`k9%{afIk(AZ`TTODi+z;cKuZ(l%<9wal=l=N0c{uasF8v^*OXo?V#*z+! zF@JX#*G-!rxNUFm=ylbGlNk?gTqKx}^=U3m$zXP&MZk!B?J7mb-k!8qx6Xa5RtPyppebKYCg;e=SWi zAOFqx;&tY0Nj|RQ_&)RTm2tp)?6){y=lH-^jvvg&`z(xC#s^<{{+W;KUY-x0CvK1J zhw;o;)`$665B9r!pZ5iLJY2W(-~2h}_k88~VLtAU`)51jE7v>B$NOTuPsa5sU%6go zKHe{5|HA%;uPn!Wyno00d0Z#+mFr~Y<9w6j495w+a(>EuoNscx;P}B;&QF<-+x^@o zWs46E1UQbd9$f!Vc&Unf=Bcj&oNw~EF6T{rtRA8>r)xWxN-9EUg`U^(7b;eNO-;rf~5I`0?rz6hT`uwUl;%*R)@Q|99< z`*l8l;4AO5F(1bv-cRClExxiG^YP!@KHq14ZimlXxL)VD#PN#bCEGR63txF&n2)cV z|8To}E!i%g7xI1v=YOmR%P}8cOY*UQswq>QSO}o zdG!2%_v@>?@wTY`(82)oGhdktE|kabpBZ=}t-K5#eJ6weo=*EEYjbST^9VjyI6XN} zlxdtEc>Dj6_a*RDcH94Z?`M!%QIxSs(xhm{ISryBghmyO(x_1?=QL_YBN|A9Aw`pt z%4wcUAtjYai3W+3aQ@G8vfb0}eebz`?|bk6cmMxB?{oKFd#&|dYp=cb+WS0*tI;jn zxy>uxQ{rlVr2%?6+x5hNaU17uhd`{~oHkeM@1O;JlSJ>cj{V%s| z^J4edV85{+`1=I>KC*lJLL0M>Pi*k_7$;BmvNcJbWupd#-0ww~+1$GyZIj@mZELta z#K!7WW7}8D;%&t3&)dAL9$^bcytT3D=xpn?u*62#tiP?x#ufFnop^xl&2OdkWxxEFb*xhmG*kMVkrX*)~xV&)4gP=6YtPiP;{)&2A_NQJXBhC-?n2@o%-*u@Oxo=e&X{KpU*h2upXAj z`Gn6eobP!3upG`KyzV&9@%m#v-WNEZaXw)C7#HUo&R>j+^Ah7>97mTsws{5nt#SV1 zyv2M>V|_Azaem@_#C~8qH-={0YPEHs!{i#rpu~Kc+DrwukLtKX9I(@t$lO)vwwb=R3}Ge19^?7$9&sl4FD4+u?gm^MXl& z^q4I+`13lJx6B(T0B-|ruwL}(K?1KGkE}1ucMwF~=wXBTST7Jx5zOt=-v+;5)Yh6L zaNiGYJFJ}~@Dd-i!S?X`O019V&Fk7nAZ&TL9v92$5A_rXAC0!j=RWTx&3ClH{u=!l zCombLX*1h-vcRQ;uIFPv`=1&ji2C-e-e2tZsw>U{lSO*`ub3&A9evRjzt{Y7)LsC_l-SNy z8X|bp=%MX^+Q9aGcF*L#t7&z@Hq&gTpsfFidOXn@q2OHVMO&}Cg9Hn1CE2!~ zVJGn6ezz6lVtMTE#b!c*XYAd2xqvML1zs-qZLvN6`|QlXEA{jETrowEoSIf|ALC*@ zpSzBNsY9;R>qmt57Wh0VvcaDV@aKdB@4N&_n)>zctMPkJd_TvZ3-CQ1=M~N;tdH{u z=QsYmfY%M@4c5c!j@KLKIhMoujPo1k1IELC;C#b)IFE6@VjPUycgqBU$>gWjIB#(t zV>zsc`8bbpe&RgFd5LkcU!z8h5dhQ0^?8f^#(9hL6~Fhyd5qT$^KstcyvFY-aNgm( z#_v1UCr=XGbj+#GYy4gY=M}bt{lNBceqp;fPjUWWIgEp8jCXB*f5BJJW42)%>;#v) z9~DArLe?Fx&vTsj_&mY)4P1|Li_ae{hvSIjiuVg{ z@jk?IxIW;xV!OD-dRPwM+ps-+?%@`{*TQo6+{5=dd~VluD8hw}lq*l#R{*1D^!}n3_A3o1?AK`l?Zt*=A%i()5{&_Eauf#3BM`Jl$_whM^>m+XRd5`7rdm~)e@%tbwhsUuT zelLaJ58-<@_7C&%IQ~5t{QEQb_hj(Twd0?2$KP)wEdY>~4nXPvu;73GsBpTnN44P? zKYif*;C$fw6o*y+pr2zh8E%{-e?Gj=GT(pl8*_bZUzO`eWga+^FP3;_l@(BpEr&L-|{?JEcm?O zY5ef>;m5ePDc|QF-gYDIe|SIsG#-BvKi-xS7Za)uOBO4x&&NhKH)2iY*SDd49CtES zvN-58mCaXXm$S*|ip&>&?KpqxW5UOQla=Z4zT=0}sh|6w%(b4+>5zRMhqsM=m|z>e!VyhS)Z5T9(m0A@w!?hUuP(8jMU}j<^7cDa|wuZ5bsf*mi77d z;QDwE^L-pg`5eJxeEUb^e>C=IvN$)?xwYmWp5|NLZ+=er!Sj&qFU~RC*84824L|k& zF$X-CpW_&VUqcRF_V9km_j%t*U*+e6`HEynoj;;^3`IEd@xklOpD8$Anp~{pvGH8^ zRu%`o##XfGJCuozAul5_a3(_ z0PjV{;OB~C z$+z;DypQ;iPdIA8ddUr$~Z%aAQE!;j&XB+~FpDVuS z$kwASzVdp!-}sTmhmQ$2Rv)*z7;(o`ee5u%JidItX*13)es~|aK7a41AHzJ{@-m!F z9+$U+`||R#bDWdG=Q+1r|2Y5a)=pkVe!hRym(O9$ug_5~P7Ue*jK|M=70$=sV#vo) z{^91z%i!4aZ9Sjk@oUUK{J4C}&r$Yp4Ec4${ePAlScY%qapd<6-fR55g^vsNNwzM$ z&)g#)J9$jrSNvc-evWuud0iYIzU9Y%#_pf=agJ~;@3ZV~#s6QA^1jLU z`MK1uv%EZqQ@_@K(#Pi}Umt%yKhIKruKYak9TCr+ujzPpU3!vhmQ%q-`1}qH&(yCScfa*va^V9@fhFoHJ%%g-2v*xF%FN5KO6C5_@2P) z)Q@wvWVo1Lhxv;QZVb;$e!PBOTnF>h^|gkI3x-yk~wH=Vdt=evk54{P~Y@ z_&xXYGapfZW#d2hGp@7pHJabQc+be!0r~Mi>+`XY?TfnH#eLk$#~6?C`Ht_uoZ3J3 z6*r&yajuW$@c!U=`16XazVNzr`hRGL1LyI7#^oQfGF>hvvipV{zkZw>d?JXy>4@xx=_KG$M=zWt*=*#BL<&do9uzuOIb>p&F@w~d&%JvsW<9P8gm(5)+=Dh5mjpG>ceA&Zc)yI<` zlc(kSGQB!J#^B>3tKnRW(VWGu@v&evIn^p62DT46ldBWH@zv`97~Fd+IR$l&-_)@pu`TZMi;wuTbXC zRIdFiePte}4y(Q|PjleBoqCynq0i4(_VD$d!>aGgjmu)p)0oe-{J9{TZ=D>TGcPNP z`JasQa=Z;bPFPOX=V?5K`((@M$!yk*^L;*^{KKEovbl3D9+S0PQLLxs`lP(PZhdSy zMLb>~SI$;FpX~GUc#P-OcwL)g6t~T?1nLghV{rbGE%QH;l zdit-nSnp3;eqYGhkm2Im&ect9NB(@3??;3;7u>sh*Oqf%_Af_ZDd5Iz(X{5WF_r8| zf@AitY%m`HvEUzlzW4t(eUJNj81AW$-+y~N$b2AaG9S!5H3dobul~pL=i_%*E`B5) zX}_WICG(lYQ$*{nnIY(xec(^?|KMC3!C9j<_4C&`X3xdLyPj^SzYUiM{CU(!D^9~IGLK9P9;ztdzr{(mhVit>%*H*|lK z@gnO7iKi&PNj#D!{Uz~8K6xJ@`6NDhA0h86r2OAc|7YtTng3+HCgVk(CnR4{y&>`c z{`mcS>-T@!Uq$u(ulhsoS4HPNxxVCjlJ^xdUr3tFA96mVeXY*$X+?Oy&-d5;PR@^{>)v(L zzwaRNNPiXiqliz=kE9jZFG!l)45!$ zdwhhy)T`g;`)fSN`w$sVvObaa#3il*IJ{SVeI(^cnw00~dspuJACe~ZN!pnEK8-sM z>c5{L^~w2=_Y+0>Bpzv>6X-!Jv`clv*~Jh?x} z`bO@5^8WYx`u{p#GQR(A`gebXM1B9&zu)D5<&*xB^_=7@x-XD?k|zBj`Q-jnw0}vw zhSH?{hU)*P<^SFB;m-&8pT8jIN3I8156FBWX;PldH*&xJE)N=5kABT}ay|{E6~%{~ zFL}Qq=S$9y{QP>k!c4GK=UDxHiu6}eyhuLj4|)ERd_~^}lJeyJiL9^W{e#SB^86^{;TmJ&v(Umzt%s+`Shzi zxj)H#Q(TXJ#rv!ARisbGhnz2||NDJ}^jG-H=RNZCEs00IZzAJI(q#Nde@L3Ve~|B& z6v>nK9TJbcFOd1G==>(*LEaBYJaYd3@6%%b{w(*8q{;jv`6T_<4wUa`ikt6d`0@C{QovuQT+e6`TKv{ zA9B4(T9JQ>;@wa@MfU!G=aca!X+`lN?;|8l-hWA&ybqK5ifHnFL(-%@MR+8i^iL5@ z>XWo0eUeY=E22q#a{eUUP(1SU33pNc<$W`0pX8JJq@kX%dg5N%`O3 zw+MfEA3@41!sGMp*Pl;Plpp`x-tYd28}#{>^oOJyimzz?WV}e4TwfB8q)GkX{r%_h z`yKDs^&sOx(u(c>ibw7@#d!aGJ<0d6ifGbbQl8{1qDlRR(u(j&z9M{5UJwi9vlqb(i^8QH5lX&F!MOK!Ug7EP0dV3_F^pCVp z(j*>fpY)HkN8&Y9p42Dt$@!4+Bl!)b$@w=_Uy;8g-$rGmKrOp<{rZviNIt1g$}6H3 z)kj7Azlu-lH`JaYdGdV?X^*58;gRx+XcCX4Nqv$}(*F(e^1q)&exBjyBhM%2LwgS@|zcqC2UU&(q!;w!RG^2z%vx!=foL)s(d$^A_FOX89Bh^&Vs z9%+xnC-F#m5}%9*sZaV#$}6HtJVo+KH5-u zGG3&=Bu)B9@=2PMS45Ng4W$*~|GW95zl!Yr-SQ-!B3hAuiuj7~NxmXHl27U@qDg&4 zG`U|$n%p0X@D%w+@)hBcd{SQ#P3kM6$@wax72%P5Me!x=k@^j#NjydL-;GbsR}ntx zAE{5$itrTi73B}fC-D^VN%@BQ|L>OP{g=Pbk~Dc=Qbd#bB&|rFow#40-hdd4q5&m)_}Dj2&@C^!3MAqYyz9X77z?VKq%M>wt?+n2iOUAfiMsbBEW922kZs=KqQC) z`$04~01kpf;4p{*u^N*DaZlOKrVO= zUVxV%54-~Tpa8rEh2RZ%3yQ!yPz*}Id+-5#1fM`D_zcRx7f=o=z*kTSzJc$c3j6@o zpa#?eDF6^c1Sv>E2C`5IHiF7f1*$?ds17xtCe(tBVH2ngbzoE24C+EX*c`Ti`p^Ix zLL+DlO<+sd3buxAU|VPk+d(tf9-6}rup{gQJHsx}0(OPnpe3||-C+;d6ZV3=VISBR zT0xZ9pGR%1P+Cc&$A!r{;jxh42l03ya`8SPVl`cAu>Y7$ON@Stx#*!2DL?|s2wsx?U6a^fI6a1s59z9)E)IeJy9>z8}&hbku|bGwn%`4s2}Q&2B3k+4h=%~$N>#TL(ov9T&~)UBW}um97MhLb zpt;Bo%|r9i0<;h!#I1Lz<+gbt$^6pP|eJUW6B z&{1>@C88vB9GyTX(J6EqC8IOwEJ{J=P%1i)E})Aj4N1@?bQxViSJ5?e9o;}T(Jgcv z-9dNJJ#-&EK}@0)0i5=o|Wus?ZNqjcQOWl5(XVQi!4`nqnxHQlc7B z%9IMFN~uxmlm?|qX;F=-CX_a%Lp7zEQM!~K)tqWU=~D)jA!S4vQzlePsuk6mYD2Z9 zOsRI18P%RLr#es_sZLa9staX7b)~vdmXsCMo$5jLqz-rTnOQ)O>0IwUAmwEvEdbB~$>l zlv+kDrvfPvC8kzTE2&k~YHAI&mI|WQQR}G<)JAF(wVB#N1ydnZD7BT^Ms25dP&=tz zR2UUbMNqq`J=9)m9~DVOQTwTA>Hu|+Iz%0&VyIXuj*6#_Pzlsg>KK(sB~izz6VyrS z6m^IV#G)h8UqApWcsH@a9>N<6Ux=G!lZc}%tyVO1EKJ|b~ zryf#|s0=ET%Ay`q+0+y2DV0M#qjIU|)C=k*l}Ejz@~HyqHC0Hxq25wO)H|w}Dxuy} zAE=MiC#sbCOqEezsB)@;`bt$&->C0Y74?IvrfR5KO3EGlkVZ5`(=G zRa%W!r!{CzT8nN>H=(s@9l9yqjMk;~=;m|_TAwzc4QV6Vm^PtX(yi#$bQ`)YZA!PJ z&FJ>DIo*NoNOz(;(_Lr_x+~p{wxq4-?sN~jC*6zgP4}Vu($=&MZA%MiA>EJePY<96 z(suM9+Mafx2h&67p|m6IL=U5#X&2g+9!|T_?z9Izf*whaqDRwX=&`gXJ&qnvPoTYM zZ+ap`adbR=gifH3(#PmTI*C3`pP*0Dr|8pk zGJS?VOQ+E1=v4YVeSyA6r_mDn5`CGzLSLn?(bwr4^iBE}eVe{R-=*)-_vr_8I{lD- zL}$>MbQb-X&ZeKxPw5=`8J$Z%r(e)7={))solh6gujxYi4gHobqTkWQbP4^Q{y=}E zKhdT1XS$63LYLDO^jEr){ziYNtLPteHC;p3(oz~QkUhQFm@#2mGOd`_OdF;xW6HE+%$W9! zIn#mZ$aG>lGhG-9rYqBpv1F{6?o1D+C)11R&GcdVGS-X@W6KB_A=8iP&kSG&GIq=$ z#-4Ft1~WsLp^PKr#0+Dc85hQt8P2#d?u-XBf*Hw-Vn#D#n6Zo}GmaV0OklhiZ)PGh ziJ8nyVWu+E7$0UjEMWqerOYyBITOf; z7%{VgS;?$oRx@juwM-DRj#|ypY`MV%{;uObPRz`M`W+J~5@t zXQqt#!jv-=%vYw8`Nn)_s+b>4HB-aXGExSxkVP!T(k#QWtPwPdZ>?raaXC) z2eU)ip{yh8#13PfSr^uo9nQM3?yLtpf*r|@Vn?%M*s-i9JB}UCPGG%QZ+0R(iJi<& zVW+awSRZye>&wnyXR@={+3Xy4F6+n6W9PFA*oEvOb}{SEE@1=MrR*|xIUC4|STVbT zUCFLuSF>x_wQLZ(j$O}gU^lXx*v;$~Hkb`zL)oqDHg-F^gWbvQV#C;QHiF&F?qT<` z``Ab}irvpfvj^CN>>>6r8^gx3acn$$giTkrm^c?hS{sDwBY`tG*BPxFmO2FK8f*9Bg8eOqVt&m__z%o*w5V`zJGpZ zhhb_HCcTH3>+a0`E^8%|_0TqNPpi18??Ye2ju~|p#Vq|KjeKkp@1nFkCT4lfw?GI$ z^8QZThei%f_BX1z&Rn%F!=yD!s%JvZj#tG2DcgF*C z04?CTf5njSFXH31J9Uf8+S_)Yb~GUvB<($%_igpL;giMU9cRwF=(tX660*HxD9G#D zCswDqiPybOT`~_%esm!PUET_!+V`@3HR#Yy0FTUCvcIF*w%I8o9@~StZC&R0sru|N zP(G<1_xNd9bi#SBk;9vS(z3G9wkL*|+p!Mgyic#s-%7KOZ_kPYc^!(?n!QomG4ad6 zqepWF{@6;Nf8hkub=EFQe%fmJ4lR=`Q!}f@FUEi3uuDg%Z+5OZqO-f*F^eqIoLx$2 z{3lc3qcWiH*aA1H`L#EL%^gp#|JqjB|H89gP_nzi>SBV&WxLat-FN#;ux;_WVm1;V zwzzn*`^B2XCY|Fu#T7Cmy?6+%&x>XaSX(u+{Z98;D?3;uE^ghIgS~eD@wcx@LwebE zifhXL6wK@UK2y3m!eoo{(D)t?FK!RZPE`gGmunX%Uq2J%aryJ~!AC95qAvkGq4;1~ zkI9=7n{bjUbt7Wula^d(y?r)@T8T__HO;X;4G68vo0uyS~tWq!TS0VrifeAfkUviXw zwi&XmF_?P`b`xi@8EHmqfVhWJH{exe&;8~Do0REQwd(*4(%-G?+hj5rs(yV$h5(7n z*vUXrKBxJ5bufEt$QlmI(yJyR7PM&tf>&qL(1SvW3p$kmM6iqiX`aC9{-{ z-#vU$FN(FTc00ND)aIs(YGR(p8ZR~6quvb+>{>JSO7zkUo#X{aUanB{3Vll*7SUk$ zqjTQ&&7G%!^u3PKa~puyhJ~*n@7o!18y4(tB4p=|Q#vdfX9p~53aTd9=T|wqO-3d?@$Ux0nmI~8>hQF|sbQYYB`eeqi#pkXr1mf)O)XDyUBXA{Cj|~m zo1b;l0(&N(Yx9l$(IUQY*iC(vtJmbEcSh5Jq375XBbAT2cVZ9JikH63PEhoR^k$oCbXzI zu++zNABY<0S5#%TcOQu06$e74S>5irv2(Rou+iK6k=IScQqHS{k!Q77#wKg;; z*V=pVsYu)6R-Ch=v`Fo2ZO>IL0k}WT;6;?`^w!6k&|sP#FhXIqk@uB^z=O&57e^>5|2ZUin^sRB(15|s}1zB2ce<&_G3ADy$c_L_~k(z+tQ zx<}0_qgKGb5zz1we(|f(SMy`dQ~aWz>wByY=k(kYudbP$(8USjR6;P$sxmU z<$~Ep43Ko)q&D|T@SMbp((2P<^;K#nAZ#4qAo7&OUt6wg-M?U!un!xxjoX3WT$J>m zm>)?^o4B=7hf({V@6xUnZKv*H)sDf?mDHRWUB@&4g2bwk5ha9hf~aRWfA zX2qRAKFAA*duqjAxs$JwKNiI zaGAIv^(;_ZuB)}1O#wQ+8;Kzh2W>f)I^^1g01(xWJ167kRg_l6#3<R50yn4Y*ZSM98BuAQ+% zBmy3xkmvpoWPJFRD?W;D!f-1WU z4^~YArIeA=loNxh64&0E+0wk5?&sGZ(H<~~CDN?egI@P8$Z* z1W%lz{91bf2N>7F@8kC${y=_35|83>F%>u2RZ>T;v?$EhZxZi5G z^w#Y*!fCyL33%>uG*fyiX|JnxMyS(>Op#jE#*2fbx%xxZK>0ea%~gw=R`#7LVUpKH zmfJ;m?%dyg$-6P8b7l!=fkFKWtk)mDc6`$Q6>D1piCJ-N8_$TG+R5!}RP}XxsBHeI zX?iLC;dTvBzDe)Y&L1zfG{4t+m{;oIq}TcT>=SK-zP5MXR?-@8-#w2EyS0b{AgO1v zy=H#a^ALffioV_Eev?{PWx3BQ>K2%BMmOzK+WoFzIlR6m<)y3imgPBv62XS-$e{6A zlcd49T~~}3*{+ET8*CM>^VP14;c0!#Cz0wUFVFXj=vS$eZ(yMd7rwpK;o9lvXZt4m z%{)>YlO@tmE9|@R<(J5}dgAd0K)mPl$Km$AgPbeE+U(73G`h3kj&McprS00Mj64?f z&goX%{OHA8ui<9-n=-JPpGB6x&R(WOGp+rwM?!hGB>@*BA7m$= zSPP5P=Pv9yWa^X~qDk!4BgeZ^r`7e|znK%ZWr0Uc&w_WFx^8o>u)@U_;S0h8hG#1s z4jsI|qW8Ji?Tn&a-I{i`VJOKQLDLfhol~>6#V!`_$*HzUm;@}gIB6t|Xn*SdYK@a^ z%x9GXFOwDHybpVv^+`Q6(W9m>8x^#JI-9IJwj$AE&x;?89Se%zd}@g<4LsEEkfq@m zsMo()UPTAXd8-^J)xI*ATe;n5Y~lFec}h1xx%$}?+T|Z}JsQuj5qnHZ{k}9->^kSi zO399~*~=|Ao)sswYnxXcp7Bl){o%=2O)(VTaogcB-e0@X(2ffptgd+5#O~~{*!9Uy zvvdK->G!sOSCDR8ds*{IVNcurmV4BUZ0(X>H;?Q8rKlGae~Hf7BAJ@hLXG?K0r;4f zKhZ98^Q#D6kvYj%)Hx}7LWi!MhHTGWBHcT~!6>VTI};FT!EN`QqoSGu%@4X8A02V{ zV78>=u2AbMd#KXO#V_3*E7q623A(Zm6dO~Zi;?e63$G5Nq`HsjqNWdQw>G=g>`->s z#4BZHz1=5=wppKgr_Ze=mz%tpU-UZiQO%{R2`yH|pA`zSL>JVax08mP882NaTB1q; z$xh9v$Iljjy*}b_R!-W3k&|AXd^Bxi@%->TI!NVQa~s#{OIB|h^*;FBWpmj_{jJ`v zrXE#3z`?|8J(Emb(%v=k-M+26I!`fj>z+K!Yed!OT+O%d-ndrfdM_#I<=S2kxr_Qb zNPWf(G#u%jHOf8GzW2&z?LNQk3iRyMxHEuRdgN=~Mx)LV#>opReM15qKgP8<EMR;E?__)xu8GjR1&H$$-94sYe-hjKodRscE^{D`6{SR*+|6rsZ5blH_GvC)LNvI^L zk_C9J1&qX|T$UpB4ZsN( zN?&z)V%{nsfUf#3Ie8s~3;-5Zn&9AuH|eGWKh?H&DY21Cn@50yG*|OgSE@PmjsSaV zvJ9%ue?B-t8gi_)lEmF}pl5`4Q&6tPa^Gr&S*>vNsr}J+sC3hlFH1pYQGkV;pWnj8 zuG8;HrY|c!{Yt|Le4YK;-tmYMWijw!Vl;AJvV!;H*pi9ym)e9yY?waizM5NTill`e zGjyNpF6$vp&F=&+-rwrh$OTVI&0EZ*Cw|dn#S}Q#s>N6%R{_N0s(?`cumE1O)=jNsE`C|Poo3`z@?OIZ9{kUaC zP)^fzLoJq+n*_eETKSThB-JbwsYxVT4#e$E(b?P4HNB>!dq({zP#XG8WT4}c}z^Ku}qFa}Mc82-uar0OFxZA5oaaxP#yQW4h zL@#=+c=YzrgJyxfoaY&p?U^fR*DM0O&%F-3gnd5^?zHl{OX7f-ENQ$_$fOnXhe`*P z4?XZ>RkJ0h-t_%cx&!I2Fq6(1{_bO9P4)wR`^-r7Gw5z_`FTtUY(b@8H~#kG^PF#$ zH&EraRntpV2Dj^5`)S!C=XR3{>D#x#>mFMcD7j2npnpyulqGJgjjcTR_>pw>!^qKb zHQ6m&Mec9*V`A4Gt%_pDyqo3eKB?BzE8nS>ZWg?_x@b_3P79Vjn%O?WH!^I)`NgJUl}|6|sOpzLnx8$Ea(76V zjP>cBd#BDXI@e{w+MvnI9iWx?;aau#oS7M+M;BGE8a%f6hvfq&(XAO8Op>u@O^TuBL>>436JG0D zpD-Su_ozoo#x^+0LpmyTa}V%(!i5&^n5k;5-ak#uc~C4`JagNjqTof@uT-*9Q#&Ak zvC}Lh3{2NSU875OJG*Q!kRGDMm-WP=!W5U+SE#6SW_eu&z@;{5U9jhXvX;o-tZ2?K z1iIJ`+Fq^~p?at~7=mVRx_r-)R@_*YZCMhTAJqE-x4Ls{Z*~FJ(sLt2zDPe!1P->} zQl;-|=JkBqr2Nb1r0WF`fYv$7O)sr^T)6dK6ECglZH~75UM70*xH$81lT(4>(Ox!r z&bAcYYJ!<`|ApN5k7k#VAvn=o69|n#Mex^FO~d<~t>((B*+Oe@$9kw~ zI~X<+KHmF4&-JjXno#<@&GZ{tLVqw*x%)`3soeVr3*aBH(|8PAmgUcVv+mysY?t)u zki4Ub{zcRG+1i^rj@&(~Qz`!z%=ehPw}YK<{qaFwz(Lm5(8cG|F`h-L`3h%2%LwE}ib{S6w;N=K33tKC;^FTD7eC zNdr~E>`D8|dW}73_wH8qcHigQcE1a24Yy1C`g74dp1Lc)16XqNr2Z-RZOViRyQI$g z;sZ;X2Duh=48I+E@ag5zOI@UzNsY#(9XUuZ2S#SX%9-?S>CEv*7hc=hLj3%Ar^V8t zM!JLV&qMF#?wf5GQxa4-ZT60Jfo~>SgVc7$dR9=Z3~vrdu3a3|Nf`V-C}5*>@A2NS zW88+@`W=T)VP`*hbmLf;@s)4%XPJK9S2?7n0G6({f)?L}x7LrFwO!g+twy)0-^s(l ziOYxYtGR7+SUJx@`10Z?Yi@6BNCdB6dEJ)AH0#(02-~vL5=F0+jH*|P{Pre9X-X)j znIU%{n7>dva@^#qUYd?q{Xx_2r%QJQK4~rZWGwQG+B@L;p&ePKAhGvlRdJ2LrnB^9 zW3%Yd*^geekn~)y6njXNY4K>{%xhb2-Uynex?-~sICk-OyC=HOI1HX;e(p_um|co?Xg8C%BA*YNcVN7!tG8t>Fl^A%MwvrF(gA!RY>EY6CtE!!I{IdtS-If}#MIvn=wlnB5N-EUlJv|cG#S1I_~g`f_UOPq9Ehr?qWt+tH+5-hEmFL=L{>7{1T z=mNv7u>0M%YCWWCyKfHZAzqxSY2;N^-qSEJPgLmVwbgy&;1SiI^N!~~DtOA~9;z)E zRp2l{XJg{`x6%(O#^u}+|M&!ZDFaC>4bL%7+9uun<{Cxy?eXG4=$1JL3wHN-p}$pW zU8-i&U6HpNS8^dJ8dwuFF|Kw%Q{TkzyQC#4#-&ckfc45b*oR90sE^kCpvz2w(4k?W_LVl=)*4Y9uAhn zYj02s=AfwB{gELd2GS{C9)whnotz(~na-S!@;>O7(9EXOGQaox+-LP&x+Zby2mQ$9 z71@mza6dHF?bf}xFKTQ#u)49+L4go zAMUz^qA0zb4nAn$)XhtgW>oWsQTt;rN5>Do?kb(e9U$)Ey_c$kMONAE*T!Ui2D-k0 z%e3C^ono)$fOQe>XL}VJ`=H!4zD={Y925D@GZ7c(+$|G;-J&PxYQhR77gudn-!(bD zN>Np+6%SN4EBUxy(AIo#Of+#psaK>a@UmZFD$MO~w^FI7I?T8Cr1*ZFI=5aIu`aqc z#Q|!jM+Xc7@kc6?w$K0Zw)BovE43fi=a4`i&p{$dAK>UWPnCb@)s3RNpwyl}fyq^bulNS{&{4V;I2V1#a z?|rGs`@N%po2rwKQHh3l_Q|SgYo@k#Ia>RnV}BQjQ8GK3PGat@%FI`{riA9&Ikp|sw(7>vYl2QQr)|G+ zo!WF@LmOBm;!c0*$?wzFIf-vB<9_ldQ{cHy$-ivK=cmu6grDzR?HoPzVVY&~bAtmp zz^YiJQ*lOQU%gj4#Z;&bz~f`J;g0#&<3tk8sM;?>v!Au!aRqGazv^ADobvBkqQ+Ki z&GMIhYn`OEqrhWT)jplK-xGL_F=PU$K$G1oHjj`>+U)L$JjPY*ojbk7*LI_8&YXMM z;`y8=j;q!ex~$eMD_H^OR(xHk>(@g%vv5$9!)f57;Te_7t&_Jnak=l-S;8LRbXDSz z+B*TF13yM~QEvKuK6eFtI76eP!{YE^-g}dxR)wF9q<4#w6QZ<~tVe@DF%l%aq?Wy} z>OXMe*!Ls5-gD3%zPb61j668%Bz%1Rz)AhVg&LjjzlrSkIOYkWzpdzQ7Rc4Y6DifA z2OG3B!FFjjCHUBT!x&SiTib?eJaBD!(XddYqT*+P*v{)#Z` z;Nha!XJ}?^^RLtM&JHd%mi?h}p;Co(meR>XA9uNVkGa-r@bzt}+@f9SiTqd~E)(Q& z7srao@Mg&l<6kJ>n>1!R>bSMbkjPHfB0b;lk@@Ze*8oY}^o>iBmD(CuORjO%McU8# zpj}+cTlpW$M7Lkv8CC1oc%VnK8@8DXH*i~xt>A0T7*zn?2hR#!Ha@`e&Dm7t5YODK zah;voe);Hpx7UxpH`laR0YYiJ5gOd5>jfR8)ApsbnDJcDVfjUaX&a|(KW!ZH`ktNe zR^jKN*B&f=*S&eGJ?xfD6KE#2%=(%Tv{C7-KX<)KlO8JQHu%D|G!N;GJG~;_b}ba# zAL_TuE5OP*ap{-M)6(Y63Gp!Rfv&;9S}#7FgkBR5abGu@t)9OBR@)`ZnUF49PYsLu z3WB~YSh|zDHC!1TP#*Q|mZ0jki*9>!H@_#v#{fy(2$FB_vi9Rjql_`NFiibLt`YeoE)^+eFiIWtts|M(l|7L09{|)65ir z`7_L~+;6J?DW*XG%F-L30TB0(QcizP4Q$@z{4=hCX3jh{a;T^h7HylIAk@1;9dNsA4*`NxWiUeN2hnfRYsQ6`9uxbCOns(-WZ7A5XS zJ5^S!Uug+-aX?gU zs$_azuY-ryEY=5Blez}Q&d%rIfKLDAyXPBy1~Xy)bN040bL4nB-nS*k&i%<=xw*3B z$exVPPWqp2T|m6l3nSp(@wLmtS7`T6*Lsm2sKPrQJh=-7&R;jX>T-ucl2^+=C|6xr` zWd{!nZb2oKk6zPV`)(R}`4tp+&(o?jdfak$LNyfR7apm6IV)!dzZ@S`ADic1+vcBQ zFm8b`RXjZW&6x{(rW-uN!(H{ouNH!3g=;fn{Sy4z6yCjaZOvg77QEx4WO>@_n*02) zB|~flw^@l>T(f}-^yW!tp3`4dk(R@)?E*A^Wul(Ayt^LOt zLwEEly+-k&_TcY4*9Hz~8QrchDE#0W|CaHqMpi%Hd@10i4;RipD|%nLcELls>Y+40 zu6F&Z;#CoA4?%}RJLm6e^HDd{d;wP?#7Zt69jvw-NiykvPcvbAMOX2q4uLOnzkZt7 zT?E8GR`!gdOxhWFHS&KBL)_1$K28g0HmFH6-+o)q?%K;Qix^moeJ#_j9$y>kACeW&}pKg?rJ^VeBAi~$zH(! z2vm<5V+=y31i5~RHCONaK{LT2gBE_?CeUm)f;Q<8rWN=?POg$6ES2N8fxr1e?_&!!C7))*U){fRqyHhO8C6jmc^&9X z^s~!PO?;8~zO#9&4f*ZXd^BE`H5rxeZfRJ1UCB!`M?-k>VOsk~qqn=n7LHmM;XALf zpXgQMMh%c?6*4oX-AC<;F74Q;ss!^+N1;z2up$2jHPf=A%4F2u^{voZx|73Lc8yv$ zbdb{wDk-IDL+5wN? zTvb6~my7*68>oR#782+08B+J1)gNAdzhn2>Ub^<>oyBOKVCVWzlRnut0`x>Z&@H-g zUmr==Zh^0d4Tw0=m|8NoI^%oE?$}lFSFd~tRPyvL21{{wn z_DcT-^R&{hHwNBfwb)ht+?2!vgh%W_8`v#MgT1E@G~fpl7((BS>SHk#j91o^w3w%1 zq8ZY*SmIOjp@uo%CVNTefV3m==X&1TWYywir*@xLSY&ux1%-!~FW<%eAJu(Qx3ugt zJi)+L{+Lr5<^FwD?6vTBlD%gN){6l^?5>x|vrv&KzoKGp@$EWL%cI z39}&}roVEzJ&J$vLhXFCSm|Ze*X)ZQ2OvY?&D8msKx7_O{NbKthVns?p-@TB_p%#x z-$lE08Wq2flV2^Y>Zz2Z*74bNRJ&TD5?ke|RdxctlYVgd?px~2S$+q$KK)=h zqDf6ZIBNO2A0yH?qXppjrVQgo4pNJ62;>J&c$hLaSg>P4gof9L5he$MINU4T@93=i z(MJMIHawi6U23lpFK%j5QUYcxg8&~CEimJL1|t8WhL;Wq>%M}+xX68-u67oP&vxlq znP%9C`)wYr%7x-WE#V>V`*<*UIk!ZB>s(!KC|P8}{mzXrHydt3KnQ`~go%$K$g|?@ zm6ygvB~wts8NXTeNTkV4iMs`GS^-fM5c6gVMTY!{R^{mL8hJYbhon_$H$QSQ&8al1 zc_eLah>EzY05B<^)H>vYkndG)EST|xHx{K5^5f7?{nvrLp4E90@*{M?X2b!Fs?60~ z%l+yQhtyxNQ@W4`5Bbr2cvUvcZNga>=W6y5yYlRLm_6!yk*3Vty4q2-J?V5PQwgnI z2+lARXS!+?7oj9N_rg+}H^)ADHsZXkIbNMefmOk^cZ`d<-(|X4!_DkUaBUwBmTN}U z+AD#F6O@F|%W%HnQCds!vz7hIv!M&n3oKXS!!eMX00SNx-;ws=euRNb8s4$sat?EV zlaxn^7DQ2Ej*bFzeN&80cNuRvyv2Q>>%p(qg5g7+uW|?-KCpOL=(*O{^lc00FnvJ! zQf`7Spx2rrP0=^O^6JMIj6G&g=y<*FHJ=V1`f&y^^D+(Xc5LXj;Y8nXU|sE={vU(Kel@Pinpm)`>xHl?!CYJ z|Nejf?#Fe`dEe)p=RD^*&v~}9F+1NDxzr)DL^ufw5&KVR;`Q& z()l%KEe}7MCSS0265Pc5V^F?`9;u9@$dISs5g_7|xjU-WGq#PTe! z3gY9e;6EqRB@kgmD*h7V;I+TF*|6ots);86E%_L#A&7Zb!y}j!KKu4+rJ;G06IlWnAMM%t>ud= zBK0A3Da(a~bR*1eA=_&%C6vKH4VOP zt6omzH9G$g0aA3>%k+86CcvCzmkuD$Hv@M`a^8^XzY)6VbK*86bHWU(W8gwmy5$m; zOy9vqlWMW8lm)i)qRRtrVJh`^WHMUs!0`%_R*240<=o^t=`KnjgbS)9VeR^uNXdr{2!+9Ysa@n5;C%4vkpDY60qI#rSf{m zvRHLAWhTkvqI}r>EthP&QsWMAe<8a`PUpE_SYi##Iz~(YWAe6cPK+hlNiabGj%V8a z(@y=4Y%#E@Nu96EFc_KPO{pO`l+Omi7VgJ3s9->4dN(aS8RI4~SU}R#h%6_2&J-@c z)?weteNZ>Uraxa*s#;mw0WO>mu*cIdV$Hf=lWwZ)otTp#998rM%+5ME4q(u3nmB$M zv-0M|bO1IBlKz$={W8>rAdK)%uOaR6u=w=ZAOp53;rWQ`49xWw5#$}A&s9(sdQ2dt z6X0^H83y33A1H1W*tE*?9W1gM-}$4^`T${-2%GWxhmmWxu{8@H3nkOwQf@K~{bM0P zB$luZYILRNb8u#!M&PI;EdRMkw%T$pn+$Z_7sjznWcQV`nd{QVEse z4r57{SRN)^fdN5QBKNUP1g(~B^aGYLpAPwV^&i^OR$b58-%oon$IwcxU5!&XozP?| zD#U!awP>`9_4{Z}TUt!enaX2%`)A{&SFhuq__n$rBT3)Q-zAxbFFGPluU|#f8%e^n zkVxjhWOcRL+Z2ujlw-&0S;f_Owqf9F5bON%bFjcD!~f6q1nd367scS%6pw6kfipWmOO zzM?VTf6Ps`awb3KeCoFiHKpY?X*u57YNNfwctu8Vgj`(_ZEN4yB^Ga0>BRHxvaKlZ5EgcWt{i5GV`4&cnD20rDmzL$HizWyl#plK8J)IwJuxR4!qF?+i z*SBCDt~~{)wYNp-lr`vU=m2pv-Rh^g-Rf}ESfvtbd?wadO z9-JNOh~xrmkwDSx^!PIQOvUwGrFQvT_Hh=uhOnE&?E4T!5X(f=bKdnW!A2XVS2=L; zB@oz)xIJ52zOO5=Uq+3Ow;N|ldgd?t;yu_FmVeZY=R&SZ%_&F zY7K$em%3*=xD%xjhnqF=at{#%gaRmUk)c<)5%*DBz`Feud2As&5&pd>eeoF4{_k;W`ieEl)1OfG<5w? zie#pb<*O&{gUxx6$h`)@p1;MhhX4LxR!AHfP=Qn28zDIfB2M$K&~@qh7bZde!C|0- zFoly|Vj%8ep5pW^K%?DYQxMen;3|3o0`%-dIz^q7H1RQIO)uj;08kT_gdjk`1So+n z34L#MQN&x$pAHe%mg}WAv}5%fFL!uj&xq$|p}!h}PwktDM~P>U?PP=>{F-1q7QQso zHVt2!XZm?uG+{xjXnHH&Ll&iJb(Cj#%&Kws({58*-d_=csTourUoob>?(w$wS)V+^ zojv0;Hle+Rx{05jGI{n2zVxKg(x?wP@_FU2hC)C6>C-xU=TyD^5F4Zkt@QN76b8Nb zAs73eZ2c|2Y#V)VBcZU5rdBx{e!{h`RqN!l;l{?vftc%+uc`>n-q@W5Kp&tpHp71U zDCwch&6Ve~NUJ31Po&E7cbLcJYRFcp0I87A5pE%y<$6CJ|58Ey&8CJAq(0Ixjk!FT zW*)<(VK?4gU#**eyP<O-n)e?$5)_VIxmQp4I z^02mYBVT)``QZ`m-uS?Z?2`UUnH5=wrni8y19BXw{JEcTD!T9)t&fb&IA82>6cdP7 zWSj@+dzLaphddDegl7XmK|uqJt_KRnzSvmmbGmFS(Fs;JV^XuU^Bnf4=yIUs zvZvh1wGwyTy860SDIf#6o|c@bBC+mX4)!$Mzu2jJz%MGuio5uBGF(w3W+= zB30=dqFY0T%m%fmzVq295WFTPN@u;VJ0cd}Jb**Q>EF-3&STIfe&?pe70$h1YN=ma zhACI;xlgcfOBEIQH+&#}5v6H{DPKG8f8eXRy0&VSxrJ#!!qNvg+lbiLtIv|j=h(Ed zPid=Zb2rL+M`;haEm-4^A3r?Up9e?d1(ct&&j_C1cgQ19{fWD`dWHL|UcW$f?^go5 zV<)0v_rHH6|NOGc&=6(pV-jO(mNsHaV~xa@j*oh^SjNmwO-{DdpbssabN1>ts#i{? zP@gTo)mWPRP@}9E@hJDWAU>$wyw>?UJwCxi7w4Ev

`Kr7GcYvpJzrl%kHWh}WWeD74giyQ2~gaslW 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 z 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, + mut q_transform: Query<&mut Transform>, +) { + for (root_motion, apply_to) in q_root_motion { + let mut transform = q_transform.get_mut(apply_to.0).unwrap(); + // If your model is scaled, you probably want to scale the RootMotion accordingly + // By default, the RootMotion is not affected by the scale + let scaled_delta = Vec3 { + x: root_motion.translation_delta.x * transform.scale.x, + y: root_motion.translation_delta.y * transform.scale.y, + z: root_motion.translation_delta.z * transform.scale.z, + }; + transform.translation += scaled_delta; + + // We reset the fox position before it leaves the ground. + if transform.translation.z > 70. { + transform.translation = ORIGIN_POSITION; + } + } +} + +#[derive(Resource)] +struct RootMotionTargetId(AnimationTargetId); + +fn toggle_root_motion( + keys: Res>, + mut help_text: Single<&mut Text, With>, + mut q_transform: Query<&mut Transform>, + root_motion_target_id: Option>, + player: Single<(&mut AnimationPlayer, &ApplyRootMotionTo)>, +) { + if let Some(root_motion_target_id) = root_motion_target_id { + if keys.just_pressed(KeyCode::Space) { + let (mut animation_player, apply_to) = player.into_inner(); + if animation_player.root_motion_target().is_none() { + animation_player.set_root_motion_target(Some(root_motion_target_id.0)); + help_text.0 = HELP_TEXT.to_string() + "(current: On)"; + } else { + animation_player.set_root_motion_target(None); + help_text.0 = HELP_TEXT.to_string() + "(current: Off)"; + } + q_transform.get_mut(apply_to.0).unwrap().translation = ORIGIN_POSITION; + } + } +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + asset_server: Res, + mut materials: ResMut>, +) { + commands + // Spawn the fox + .spawn(( + WorldAssetRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(MODEL))), + Transform::from_scale(Vec3::splat(0.10)).with_translation(ORIGIN_POSITION), + )) + // When the scene is ready, launch the animation and link the scene with the main entity to apply root motion. + .observe( + |trigger: On, + q_children: Query<&Children>, + q_name: Query<&Name>, + q_animation_target_id: Query<&AnimationTargetId>, + asset_server: Res, + mut q_animation_player: Query<&mut AnimationPlayer>, + mut animation_graphs: ResMut>, + mut commands: Commands| { + for scene in q_children.get(trigger.event_target()).unwrap().iter() { + let Ok(scene_children) = q_children.get(scene) else { + continue; + }; + for scene_child in scene_children { + if let Ok(mut animation_player) = q_animation_player.get_mut(*scene_child) { + let mut animation_graph = AnimationGraph::new(); + let clip_handle = + asset_server.load(GltfAssetLabel::Animation(2).from_asset(MODEL)); + let animation_node_index = + animation_graph.add_clip(clip_handle, 1.0, animation_graph.root); + animation_player + .play(animation_node_index) + .set_repeat(RepeatAnimation::Forever) + .set_speed(0.5); + commands.entity(*scene_child).insert(AnimationGraphHandle( + animation_graphs.add(animation_graph), + )); + // Here we dig in the rig hierarchy to find the root bone. + let root_motion_target_id = find_root_bone_recursive( + *scene_child, + &q_children, + &q_name, + &q_animation_target_id, + &Name::new("b_Root_00"), + ) + .unwrap(); + // You can choose if you want to get Translation + Rotation + // or only Translation with RootMotionMode. + // By default, it's Translation + Rotation. + animation_player.set_root_motion_mode(RootMotionMode::Translation); + commands + .entity(*scene_child) + .insert(ApplyRootMotionTo(trigger.event_target())); + commands.insert_resource(RootMotionTargetId(root_motion_target_id)); + return; + } + } + } + error!("Animation Player wasn't found"); + }, + ); + + // Some light to see something + commands.spawn(( + DirectionalLight { + illuminance: 10_000., + shadow_maps_enabled: true, + ..Default::default() + }, + CascadeShadowConfigBuilder { + maximum_distance: 500., + ..Default::default() + } + .build(), + Transform::from_xyz(8., 16., 8.).looking_at(Vec3::ZERO, Vec3::Y), + )); + + // Ground plane + commands.spawn(( + Mesh3d(meshes.add(Plane3d::default().mesh().size(50., 150.))), + MeshMaterial3d(materials.add(Color::from(SILVER))), + )); + + // The camera + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-60., 60., 100.).looking_at(Vec3::new(0., 0., 30.), Vec3::Y), + )); + // Help Text + commands.spawn(( + HelpTextMarker, + Text::new(HELP_TEXT.to_string() + "(current: Off)"), + Node { + position_type: PositionType::Absolute, + top: px(12), + left: px(12), + ..default() + }, + )); +} + +/// Finds the root node so we can configure the [`AnimationPlayer`] to extract the motion from it. +fn find_root_bone_recursive( + entity: Entity, + q_children: &Query<&Children>, + q_name: &Query<&Name>, + q_animation_target_id: &Query<&AnimationTargetId>, + name: &Name, +) -> Option { + if let Ok(entity_name) = q_name.get(entity) + && name == entity_name + { + return q_animation_target_id.get(entity).ok().copied(); + } + if let Ok(children) = q_children.get(entity) { + for child in children { + let found = + find_root_bone_recursive(*child, q_children, q_name, q_animation_target_id, name); + if found.is_some() { + return found; + } + } + } + None +} From 7fcad9ceb5ea1720544deb846119dc64ca98e09e Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Sat, 9 May 2026 20:05:41 +0200 Subject: [PATCH 15/36] add release note --- _release-content/release-notes/root_motion.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 _release-content/release-notes/root_motion.md diff --git a/_release-content/release-notes/root_motion.md b/_release-content/release-notes/root_motion.md new file mode 100644 index 0000000000000..06cd357850196 --- /dev/null +++ b/_release-content/release-notes/root_motion.md @@ -0,0 +1,12 @@ +--- +title: Root Motion +authors: ["@Hilpogar", "@emberlightstudios"] +pull_requests: [24201] +--- + +In animation and game development, root motion is a technique where the movement of a character is driven directly by the animation’s root bone instead of being controlled purely by code or physics. +It is used to create more natural, accurate movements such as walking, climbing, or attacks, by synchronizing the character’s in-game position with the animation itself. + +You can now use this technique by using the `set_root_motion_target` method in the `AnimationPlayer`. To do so, you need to get the `AnimationTargetId` of your model's root motion bone. +It can be the root bone or any bone you want. When a bone is configured to be used for root motion, its position and / or rotation will be erased each frame and the delta with the previous +frame is stored in the `RootMotion` component inside the `AnimationPlayer`'s entity. You can configure if the root motion should extract translation and rotation or just translation with the `set_root_motion_mode` method in `AnimationPlayer`. From 23b2fe61dfe72f417b610a539e0bf14d761a27c5 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Sat, 9 May 2026 20:06:34 +0200 Subject: [PATCH 16/36] missed formatting --- crates/bevy_animation/src/animation_curves.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 27351d038436b..f0d522004d34e 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -425,7 +425,10 @@ impl AnimationCurveEvaluator for AnimatableCurveEvaluator { self.evaluator.push_blend_register(weight, graph_node) } - fn commit(&mut self, mut entity: &mut AnimationEntityMut) -> Result<(), AnimationEvaluationError> { + fn commit( + &mut self, + mut entity: &mut AnimationEntityMut, + ) -> Result<(), AnimationEvaluationError> { let property = self.property.get_mut(&mut entity)?; *property = self .evaluator From 74a855a271ce0009a54f85c87941b1a9d7145307 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Sat, 9 May 2026 20:13:10 +0200 Subject: [PATCH 17/36] clippy --- crates/bevy_animation/src/animation_curves.rs | 7 ++--- crates/bevy_animation/src/lib.rs | 25 +++++++++--------- examples/animation/root_motion.rs | 26 +++++++++---------- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index f0d522004d34e..b8297c044c530 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -425,11 +425,8 @@ impl AnimationCurveEvaluator for AnimatableCurveEvaluator { self.evaluator.push_blend_register(weight, graph_node) } - fn commit( - &mut self, - mut entity: &mut AnimationEntityMut, - ) -> Result<(), AnimationEvaluationError> { - let property = self.property.get_mut(&mut entity)?; + fn commit(&mut self, entity: &mut AnimationEntityMut) -> Result<(), AnimationEvaluationError> { + let property = self.property.get_mut(entity)?; *property = self .evaluator .stack diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index e4ba96c808288..691353e6eee02 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -763,8 +763,7 @@ impl RootMotionMode { /// Returns true if the translation should be extracted pub fn should_extract_translation(&self) -> bool { match self { - Self::Translation => true, - Self::TranslationAndRotation => true, + Self::Translation | Self::TranslationAndRotation => true, } } @@ -794,8 +793,8 @@ impl Clone for AnimationPlayer { fn clone(&self) -> Self { Self { active_animations: self.active_animations.clone(), - root_motion_target: self.root_motion_target.clone(), - root_motion_mode: self.root_motion_mode.clone(), + root_motion_target: self.root_motion_target, + root_motion_mode: self.root_motion_mode, } } @@ -1508,7 +1507,7 @@ pub fn animate_targets( translation_delta, rotation_delta, }); - }) + }); } }, ); @@ -1825,7 +1824,7 @@ impl<'a> Iterator for TriggeredEventsIter<'a> { #[cfg(test)] mod tests { - use std::time::Duration; + use core::time::Duration; use crate::{ self as bevy_animation, @@ -2120,8 +2119,8 @@ mod tests { let play_count = 3; // Choose tick_count such that ticks are not align perfectly with animation loops let tick_count = 10; - let slow_speed = 1.0 as f32; - let fast_speed = 2.0 as f32; + let slow_speed = 1.0; + let fast_speed = 2.0; let clip_duration = 1.0; let total_duration = (play_count as f32 * clip_duration) / slow_speed; let tick_duration = total_duration / tick_count as f32; @@ -2129,7 +2128,7 @@ mod tests { app.add_systems( First, (move |mut time: ResMut>| { - time.advance_by(Duration::from_secs_f32(tick_duration)) + time.advance_by(Duration::from_secs_f32(tick_duration)); }) .after(TimeSystems), ); @@ -2208,8 +2207,8 @@ mod tests { let slow_only_animation_duration = total_duration - both_animation_duration; let applied_factor = slow_only_animation_duration * slow_speed + both_animation_duration * (fast_speed + slow_speed) / 2.; - let final_translation = target_translation * applied_factor as f32; - let final_rotation = Quat::IDENTITY.slerp(target_rotation, applied_factor as f32); + let final_translation = target_translation * applied_factor; + let final_rotation = Quat::IDENTITY.slerp(target_rotation, applied_factor); // Forward tests root_motion_tests( @@ -2222,8 +2221,8 @@ mod tests { ); // Setup for backward - let slow_speed = slow_speed * -1.; - let fast_speed = fast_speed * -1.; + let slow_speed = -slow_speed; + let fast_speed = -fast_speed; { let mut player_entity = app.world_mut().get_entity_mut(animator_entity).unwrap(); let mut animation_player = player_entity.get_mut::().unwrap(); diff --git a/examples/animation/root_motion.rs b/examples/animation/root_motion.rs index c56682d0bc43f..69d3942121fb5 100644 --- a/examples/animation/root_motion.rs +++ b/examples/animation/root_motion.rs @@ -6,9 +6,9 @@ use bevy::{ }; use bevy_animation::{RepeatAnimation, RootMotion, RootMotionMode}; -const MODEL: &'static str = "models/animated/FoxRootMotion.glb"; +const MODEL: &str = "models/animated/FoxRootMotion.glb"; const ORIGIN_POSITION: Vec3 = Vec3::new(0., 0., -50.); -const HELP_TEXT: &'static str = "Press 'Space' to toggle root motion "; +const HELP_TEXT: &str = "Press 'Space' to toggle root motion "; fn main() { App::new() @@ -56,18 +56,18 @@ fn toggle_root_motion( root_motion_target_id: Option>, player: Single<(&mut AnimationPlayer, &ApplyRootMotionTo)>, ) { - if let Some(root_motion_target_id) = root_motion_target_id { - if keys.just_pressed(KeyCode::Space) { - let (mut animation_player, apply_to) = player.into_inner(); - if animation_player.root_motion_target().is_none() { - animation_player.set_root_motion_target(Some(root_motion_target_id.0)); - help_text.0 = HELP_TEXT.to_string() + "(current: On)"; - } else { - animation_player.set_root_motion_target(None); - help_text.0 = HELP_TEXT.to_string() + "(current: Off)"; - } - q_transform.get_mut(apply_to.0).unwrap().translation = ORIGIN_POSITION; + if let Some(root_motion_target_id) = root_motion_target_id + && keys.just_pressed(KeyCode::Space) + { + let (mut animation_player, apply_to) = player.into_inner(); + if animation_player.root_motion_target().is_none() { + animation_player.set_root_motion_target(Some(root_motion_target_id.0)); + help_text.0 = HELP_TEXT.to_string() + "(current: On)"; + } else { + animation_player.set_root_motion_target(None); + help_text.0 = HELP_TEXT.to_string() + "(current: Off)"; } + q_transform.get_mut(apply_to.0).unwrap().translation = ORIGIN_POSITION; } } From 8b7f25954e998c157f8c8d362e68fceb10ec09c1 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Sat, 9 May 2026 20:45:32 +0200 Subject: [PATCH 18/36] ci errors --- crates/bevy_animation/src/lib.rs | 2 +- examples/README.md | 1 - examples/animation/root_motion.rs | 8 +++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 691353e6eee02..6971754e36a43 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -2124,7 +2124,7 @@ mod tests { let clip_duration = 1.0; let total_duration = (play_count as f32 * clip_duration) / slow_speed; let tick_duration = total_duration / tick_count as f32; - // Force each update to a fixe duration + // Force each update to a fix duration app.add_systems( First, (move |mut time: ResMut>| { diff --git a/examples/README.md b/examples/README.md index d427b26c6ca52..8d694e7ae476b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -230,7 +230,6 @@ Example | Description When root motion is disabled only the mesh is moved so the position resets each time the animation resets. When root motion is enabled, the mesh stays in place and the movement is transferred to the main entity. - ### Application Example | Description diff --git a/examples/animation/root_motion.rs b/examples/animation/root_motion.rs index 69d3942121fb5..53dc6fd580084 100644 --- a/examples/animation/root_motion.rs +++ b/examples/animation/root_motion.rs @@ -1,10 +1,12 @@ //! Demonstrates the usage of root motion. use bevy::{ - animation::AnimationTargetId, color::palettes::css::SILVER, light::CascadeShadowConfigBuilder, - prelude::*, world_serialization::WorldInstanceReady, + animation::{AnimationTargetId, RepeatAnimation, RootMotion, RootMotionMode}, + color::palettes::css::SILVER, + light::CascadeShadowConfigBuilder, + prelude::*, + world_serialization::WorldInstanceReady, }; -use bevy_animation::{RepeatAnimation, RootMotion, RootMotionMode}; const MODEL: &str = "models/animated/FoxRootMotion.glb"; const ORIGIN_POSITION: Vec3 = Vec3::new(0., 0., -50.); From bb3fd8738415486ab7f92f984e9eac43e3154285 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Sat, 9 May 2026 21:07:33 +0200 Subject: [PATCH 19/36] fix description causing issues --- Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ae09815e65a48..41dd879bbd263 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1762,8 +1762,7 @@ description = """ Plays an animation and use root motion to move the entity. When root motion is disabled only the mesh is moved so the position resets each time the animation resets. -When root motion is enabled, the mesh stays in place and the movement is transferred to the main entity. -""" +When root motion is enabled, the mesh stays in place and the movement is transferred to the main entity.""" category = "Animation" wasm = true From 7ff8e63910e09c7b4ea5805e1ec6fdac607ca07a Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Sat, 9 May 2026 21:16:51 +0200 Subject: [PATCH 20/36] add PartialEq to RootMotionMode --- crates/bevy_animation/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 6971754e36a43..4de0dbaf2d550 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -749,7 +749,7 @@ pub fn remove_disabled_root_motion( } /// How [`RootMotion`] should be extracted. -#[derive(Debug, Clone, Copy, Default, Reflect)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Reflect)] #[reflect(Default, Clone)] pub enum RootMotionMode { /// Extract only translation from the root target. From 0f122e2a70f27f9e87d12b68636ccf5c73506b33 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Sun, 10 May 2026 07:30:13 +0200 Subject: [PATCH 21/36] Update examples/animation/root_motion.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François Mockers --- examples/animation/root_motion.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/animation/root_motion.rs b/examples/animation/root_motion.rs index 53dc6fd580084..b767fe6849c6a 100644 --- a/examples/animation/root_motion.rs +++ b/examples/animation/root_motion.rs @@ -34,11 +34,7 @@ fn apply_root_motion( let mut transform = q_transform.get_mut(apply_to.0).unwrap(); // If your model is scaled, you probably want to scale the RootMotion accordingly // By default, the RootMotion is not affected by the scale - let scaled_delta = Vec3 { - x: root_motion.translation_delta.x * transform.scale.x, - y: root_motion.translation_delta.y * transform.scale.y, - z: root_motion.translation_delta.z * transform.scale.z, - }; + let scaled_delta = root_motion.translation_delta * transform.scale; transform.translation += scaled_delta; // We reset the fox position before it leaves the ground. From 5cd174e1e4c642c1ac777d926c07e789b1d827c9 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Sun, 10 May 2026 07:30:38 +0200 Subject: [PATCH 22/36] Update crates/bevy_animation/src/lib.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François Mockers --- crates/bevy_animation/src/lib.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 4de0dbaf2d550..0ec4b8645296d 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -800,9 +800,8 @@ impl Clone for AnimationPlayer { fn clone_from(&mut self, source: &Self) { self.active_animations.clone_from(&source.active_animations); - self.root_motion_target - .clone_from(&source.root_motion_target); - self.root_motion_mode.clone_from(&source.root_motion_mode); + root_motion_target: self.root_motion_target; + root_motion_mode: self.root_motion_mode; } } From a1e95e7be1dc954d4597ce2051111fc79e09d8cf Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Sun, 10 May 2026 07:36:37 +0200 Subject: [PATCH 23/36] fix suggestion --- crates/bevy_animation/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 0ec4b8645296d..782267fd56011 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -800,8 +800,8 @@ impl Clone for AnimationPlayer { fn clone_from(&mut self, source: &Self) { self.active_animations.clone_from(&source.active_animations); - root_motion_target: self.root_motion_target; - root_motion_mode: self.root_motion_mode; + self.root_motion_target = source.root_motion_target; + self.root_motion_mode = source.root_motion_mode; } } From 9931310ce56395e1e30c5615278e5c2a0589a9d0 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Sun, 10 May 2026 23:09:26 +0200 Subject: [PATCH 24/36] clean RootMotion when AnimationPlayer is removed --- crates/bevy_animation/src/lib.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 782267fd56011..da296f194a129 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -39,7 +39,12 @@ use crate::{ use bevy_app::{AnimationSystems, App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, AssetEventSystems, Assets}; -use bevy_ecs::{prelude::*, resource::IsResource, world::EntityMutExcept}; +use bevy_ecs::{ + lifecycle::HookContext, + prelude::*, + resource::IsResource, + world::{DeferredWorld, EntityMutExcept}, +}; use bevy_math::{FloatOrd, Quat, Vec3}; use bevy_platform::{collections::HashMap, hash::NoOpHash}; use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath}; @@ -782,6 +787,7 @@ impl RootMotionMode { /// spawned. #[derive(Component, Default, Reflect)] #[reflect(Component, Default, Clone)] +#[component(on_remove=Self::on_remove)] pub struct AnimationPlayer { active_animations: HashMap, root_motion_target: Option, @@ -1069,6 +1075,14 @@ impl AnimationPlayer { pub fn set_root_motion_mode(&mut self, mode: RootMotionMode) { self.root_motion_mode = mode; } + + fn on_remove(mut world: DeferredWorld<'_>, context: HookContext) { + // Removes potential [`RootMotion`] added by the [`AnimationPlayer`] + world + .commands() + .entity(context.entity) + .remove::(); + } } /// A system that triggers untargeted animation events for the currently-playing animations. From 65cbf7d94ad9a7befec0af236d8d9ab2d2d2c691 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Mon, 11 May 2026 08:54:48 +0200 Subject: [PATCH 25/36] test if RootMotion is removed with AnimationPlayer --- crates/bevy_animation/src/lib.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index da296f194a129..6e05f955d421f 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -2262,4 +2262,13 @@ mod tests { animated_entity, ); } + + #[test] + fn test_root_motion_cleaning() { + let mut world = World::new(); + let mut animation_player_entity = + world.spawn((AnimationPlayer::default(), RootMotion::default())); + animation_player_entity.remove::(); + assert!(animation_player_entity.get::().is_none()); + } } From cb434d996439dd5db8732289dd75d71bfbe49458 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Mon, 11 May 2026 09:02:42 +0200 Subject: [PATCH 26/36] check if disabling root_motion_target removes RootMotion the next frame --- crates/bevy_animation/src/lib.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 6e05f955d421f..9dc7a043ac898 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -2261,6 +2261,19 @@ mod tests { animator_entity, animated_entity, ); + + // Test if RootMotion is removed when root motion is disabled + app.world_mut() + .entity_mut(animator_entity) + .get_mut::() + .unwrap() + .root_motion_target = None; + app.update(); + assert!(app + .world_mut() + .entity_mut(animator_entity) + .get::() + .is_none()); } #[test] From 7c289a9916a810f742d860bbef8c1df346e6e9d5 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Wed, 20 May 2026 09:30:33 +0200 Subject: [PATCH 27/36] apply root motion after animation systems --- examples/animation/root_motion.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/animation/root_motion.rs b/examples/animation/root_motion.rs index b767fe6849c6a..792f1aa8ff06a 100644 --- a/examples/animation/root_motion.rs +++ b/examples/animation/root_motion.rs @@ -2,6 +2,7 @@ use bevy::{ animation::{AnimationTargetId, RepeatAnimation, RootMotion, RootMotionMode}, + app::AnimationSystems, color::palettes::css::SILVER, light::CascadeShadowConfigBuilder, prelude::*, @@ -16,7 +17,14 @@ fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems(Update, (apply_root_motion, toggle_root_motion)) + .add_systems(Update, toggle_root_motion) + // We apply the root motion after the animations are processed, but before propagating the transform + .add_systems( + PostUpdate, + apply_root_motion + .after(AnimationSystems) + .before(TransformSystems::Propagate), + ) .run(); } From c6074c8a5c1821e8965490490e334954925a6f88 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Wed, 20 May 2026 09:40:09 +0200 Subject: [PATCH 28/36] add track_caller to test helper functions --- crates/bevy_animation/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 9dc7a043ac898..2ddb82c7ac046 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -2056,6 +2056,7 @@ mod tests { assert_eq!(value, None); } + #[track_caller] fn compare_root_motion( expected_translation: Vec3, expected_rotation: Quat, @@ -2091,6 +2092,7 @@ mod tests { ); } + #[track_caller] fn root_motion_tests( app: &mut App, tick_count: u32, From 24ab2cc2680df74f8cc5375f247011b983490e02 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Tue, 26 May 2026 13:51:04 +0200 Subject: [PATCH 29/36] add Clone and PartialEq to RootMotion --- crates/bevy_animation/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 2ddb82c7ac046..27be26ef4d30b 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -729,7 +729,7 @@ impl ActiveAnimation { } /// Contains the root motion extracted by the [`AnimationPlayer`]. -#[derive(Debug, Component, Default, Reflect)] +#[derive(Debug, Clone, Component, Default, PartialEq, Reflect)] #[reflect(Component, Default)] pub struct RootMotion { /// Translation delta with the previous frame. From 0430376320f48e8207679544584e52ff2861a5cf Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Tue, 26 May 2026 14:11:53 +0200 Subject: [PATCH 30/36] move find_root_bone_recursive in bevy_animation --- crates/bevy_animation/src/lib.rs | 26 ++++++++++++++++++++++++++ examples/animation/root_motion.rs | 26 +------------------------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 27be26ef4d30b..2dedcff76c109 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -753,6 +753,32 @@ pub fn remove_disabled_root_motion( } } +/// Finds the root bone by recursively searching in the `entity` hierarchy an entity with the requested name. +/// `entity` itself is tested so you can call this function directly on the rig entity. +pub fn find_root_bone_recursive( + entity: Entity, + q_children: &Query<&Children>, + q_name: &Query<&Name>, + q_animation_target_id: &Query<&AnimationTargetId>, + name: &Name, +) -> Option { + if let Ok(entity_name) = q_name.get(entity) + && name == entity_name + { + return q_animation_target_id.get(entity).ok().copied(); + } + if let Ok(children) = q_children.get(entity) { + for child in children { + let found = + find_root_bone_recursive(*child, q_children, q_name, q_animation_target_id, name); + if found.is_some() { + return found; + } + } + } + None +} + /// How [`RootMotion`] should be extracted. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Reflect)] #[reflect(Default, Clone)] diff --git a/examples/animation/root_motion.rs b/examples/animation/root_motion.rs index 792f1aa8ff06a..80084c1a29224 100644 --- a/examples/animation/root_motion.rs +++ b/examples/animation/root_motion.rs @@ -8,6 +8,7 @@ use bevy::{ prelude::*, world_serialization::WorldInstanceReady, }; +use bevy_animation::find_root_bone_recursive; const MODEL: &str = "models/animated/FoxRootMotion.glb"; const ORIGIN_POSITION: Vec3 = Vec3::new(0., 0., -50.); @@ -180,28 +181,3 @@ fn setup( }, )); } - -/// Finds the root node so we can configure the [`AnimationPlayer`] to extract the motion from it. -fn find_root_bone_recursive( - entity: Entity, - q_children: &Query<&Children>, - q_name: &Query<&Name>, - q_animation_target_id: &Query<&AnimationTargetId>, - name: &Name, -) -> Option { - if let Ok(entity_name) = q_name.get(entity) - && name == entity_name - { - return q_animation_target_id.get(entity).ok().copied(); - } - if let Ok(children) = q_children.get(entity) { - for child in children { - let found = - find_root_bone_recursive(*child, q_children, q_name, q_animation_target_id, name); - if found.is_some() { - return found; - } - } - } - None -} From dc9edede134144b8e181a78e954c7825a3e95031 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Tue, 26 May 2026 15:14:00 +0200 Subject: [PATCH 31/36] move root motion config from AnimationPlayer to RootMotionConfig --- crates/bevy_animation/src/lib.rs | 145 +++++++++++++----------------- examples/animation/root_motion.rs | 44 +++++---- 2 files changed, 91 insertions(+), 98 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 2dedcff76c109..15b659453a009 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -738,21 +738,6 @@ pub struct RootMotion { pub rotation_delta: Quat, } -/// A system that removes [`RootMotion`] from [`AnimationPlayer`] entities -/// with `root_motion_target` set to `None`. -/// -/// This happens when root motion is disabled after being active for at least one frame. -pub fn remove_disabled_root_motion( - mut query: Query<(Entity, &AnimationPlayer), With>, - mut commands: Commands, -) { - for (entity, player) in query.iter_mut() { - if player.root_motion_target.is_none() { - commands.entity(entity).remove::(); - } - } -} - /// Finds the root bone by recursively searching in the `entity` hierarchy an entity with the requested name. /// `entity` itself is tested so you can call this function directly on the rig entity. pub fn find_root_bone_recursive( @@ -807,6 +792,23 @@ impl RootMotionMode { } } +/// Configures the root motion for the entity. This must be placed in the same entity as the [`AnimationPlayer`]. +#[derive(Debug, Component, Clone, Copy, PartialEq, Eq, Reflect)] +#[component(on_remove = Self::on_remove)] +pub struct RootMotionConfig { + /// Describes which components of the bone should be extracted. + pub root_motion_mode: RootMotionMode, + /// The bone used for root motion. Depending on the [`RootMotionMode`], + /// that means that the bone will no longer translate and / or rotate. + /// You can easily find the root bone by name using [`find_root_bone_recursive`]. + pub root_motion_target: AnimationTargetId, +} +impl RootMotionConfig { + fn on_remove(mut world: DeferredWorld, ctx: HookContext) { + world.commands().entity(ctx.entity).remove::(); + } +} + /// Animation controls. /// /// Automatically added to any root animations of a scene when it is @@ -816,8 +818,6 @@ impl RootMotionMode { #[component(on_remove=Self::on_remove)] pub struct AnimationPlayer { active_animations: HashMap, - root_motion_target: Option, - root_motion_mode: RootMotionMode, } // This is needed since `#[derive(Clone)]` does not generate optimized `clone_from`. @@ -825,15 +825,11 @@ impl Clone for AnimationPlayer { fn clone(&self) -> Self { Self { active_animations: self.active_animations.clone(), - root_motion_target: self.root_motion_target, - root_motion_mode: self.root_motion_mode, } } fn clone_from(&mut self, source: &Self) { self.active_animations.clone_from(&source.active_animations); - self.root_motion_target = source.root_motion_target; - self.root_motion_mode = source.root_motion_mode; } } @@ -1075,33 +1071,6 @@ impl AnimationPlayer { self.active_animations.get_mut(&animation) } - /// Returns the root motion target linked with this [`AnimationPlayer`]. - pub fn root_motion_target(&self) -> Option { - self.root_motion_target - } - - /// Set the root motion target. Set to `None` if you want to disable root motion. - /// - /// When the root motion is active, [`Transform::translation`] and / or [`Transform::rotation`] will be - /// extracted from the root target according to the [`RootMotionMode`]. - /// - /// For example, if [`AnimationPlayer::root_motion_mode`] is set to [`RootMotionMode::TranslationAndRotation`], the - /// [`Transform::translation`] and [`Transform::rotation`] in the root target will always be the default value. - /// The delta between each frame will be stored in [`RootMotion`] in the [`AnimationPlayer`]'s entity. - pub fn set_root_motion_target(&mut self, target: Option) { - self.root_motion_target = target; - } - - /// Returns the [`RootMotionMode`]. - pub fn root_motion_mode(&self) -> RootMotionMode { - self.root_motion_mode - } - - /// Set the [`RootMotionMode`] to control how [`RootMotion`] is extracted. - pub fn set_root_motion_mode(&mut self, mode: RootMotionMode) { - self.root_motion_mode = mode; - } - fn on_remove(mut world: DeferredWorld<'_>, context: HookContext) { // Removes potential [`RootMotion`] added by the [`AnimationPlayer`] world @@ -1200,6 +1169,7 @@ pub type AnimationEntityMut<'w, 's> = EntityMutExcept< AnimatedBy, AnimationPlayer, AnimationGraphHandle, + RootMotionConfig, ), >; @@ -1294,7 +1264,12 @@ pub fn animate_targets( clips: Res>, graphs: Res>, threaded_animation_graphs: Res, - players: Query<(Entity, &AnimationPlayer, &AnimationGraphHandle)>, + players: Query<( + Entity, + &AnimationPlayer, + &AnimationGraphHandle, + Option<&RootMotionConfig>, + )>, mut targets: Query< (Entity, &AnimationTargetId, &AnimatedBy, AnimationEntityMut), Without, @@ -1306,21 +1281,23 @@ pub fn animate_targets( // Evaluate all animation targets in parallel. targets.par_iter_mut().for_each( |(entity, &target_id, &AnimatedBy(player_id), mut entity_mut)| { - let (animation_player_entity, animation_player, animation_graph_id) = - if let Ok((player_entity, player, graph_handle)) = players.get(player_id) { - (player_entity, player, graph_handle.id()) - } else { - trace!( - "Either an animation player {} or a graph was missing for the target \ + let ( + animation_player_entity, + animation_player, + animation_graph_id, + maybe_root_motion_config, + ) = if let Ok((player_entity, player, graph_handle, config)) = players.get(player_id) { + (player_entity, player, graph_handle.id(), config) + } else { + trace!( + "Either an animation player {} or a graph was missing for the target \ entity {} ({:?}); no animations will play this frame", - player_id, - entity_mut.id(), - entity_mut.get::(), - ); - return; - }; - - let is_root_target = Some(target_id) == animation_player.root_motion_target; + player_id, + entity_mut.id(), + entity_mut.get::(), + ); + return; + }; // The graph might not have loaded yet. Safely bail. let Some(animation_graph) = graphs.get(animation_graph_id) else { @@ -1333,6 +1310,10 @@ pub fn animate_targets( return; }; + // Get root motion configuration + let is_root_target = maybe_root_motion_config + .is_some_and(|config| config.root_motion_target == target_id); + // Determine which mask groups this animation target belongs to. let target_mask = animation_graph .mask_groups @@ -1447,12 +1428,11 @@ pub fn animate_targets( let weight = active_animation.weight * animation_graph_node.weight; let seek_time = active_animation.seek_time; - if is_root_target { - let extract_translation = animation_player - .root_motion_mode - .should_extract_translation(); + if is_root_target && let Some(config) = maybe_root_motion_config { + let extract_translation = + config.root_motion_mode.should_extract_translation(); let extract_rotation = - animation_player.root_motion_mode.should_extract_rotation(); + config.root_motion_mode.should_extract_rotation(); for curve in curves { let curve_evaluator_id = (*curve.0).evaluator_id(); let curve_evaluator = @@ -1525,18 +1505,14 @@ pub fn animate_targets( warn!("Animation application failed: {:?}", err); } - if is_root_target { + if is_root_target && let Some(config) = maybe_root_motion_config { let mut root_motion_transform = entity_mut.get_mut::().unwrap(); - let translation_delta = if animation_player - .root_motion_mode - .should_extract_translation() - { + let translation_delta = if config.root_motion_mode.should_extract_translation() { core::mem::take(&mut root_motion_transform.translation) } else { Default::default() }; - let rotation_delta = if animation_player.root_motion_mode.should_extract_rotation() - { + let rotation_delta = if config.root_motion_mode.should_extract_rotation() { core::mem::take(&mut root_motion_transform.rotation) } else { Default::default() @@ -1577,7 +1553,6 @@ impl Plugin for AnimationPlugin { // `PostUpdate`. For now, we just disable ambiguity testing // for this system. animate_targets.ambiguous_with_all(), - remove_disabled_root_motion, trigger_untargeted_animation_events, expire_completed_transitions, ) @@ -2212,7 +2187,6 @@ mod tests { }; // Add multiple clips let mut animation_player = AnimationPlayer::default(); - animation_player.set_root_motion_target(Some(root_target)); let slow_animation = graph.add_clip(clip_handle.clone(), 1.0, graph.root); animation_player .play(slow_animation) @@ -2233,7 +2207,17 @@ mod tests { // Update to create the ThreadedAnimationGraph app.update(); // Spawn entities - let animator_entity = app.world_mut().spawn((animation_player, graph_handle)).id(); + let animator_entity = app + .world_mut() + .spawn(( + animation_player, + graph_handle, + RootMotionConfig { + root_motion_target: root_target, + root_motion_mode: Default::default(), + }, + )) + .id(); let animated_entity = app .world_mut() .spawn(( @@ -2293,9 +2277,8 @@ mod tests { // Test if RootMotion is removed when root motion is disabled app.world_mut() .entity_mut(animator_entity) - .get_mut::() - .unwrap() - .root_motion_target = None; + .remove::(); + app.update(); assert!(app .world_mut() diff --git a/examples/animation/root_motion.rs b/examples/animation/root_motion.rs index 80084c1a29224..e6c22213f104f 100644 --- a/examples/animation/root_motion.rs +++ b/examples/animation/root_motion.rs @@ -8,7 +8,7 @@ use bevy::{ prelude::*, world_serialization::WorldInstanceReady, }; -use bevy_animation::find_root_bone_recursive; +use bevy_animation::{find_root_bone_recursive, RootMotionConfig}; const MODEL: &str = "models/animated/FoxRootMotion.glb"; const ORIGIN_POSITION: Vec3 = Vec3::new(0., 0., -50.); @@ -57,22 +57,29 @@ fn apply_root_motion( struct RootMotionTargetId(AnimationTargetId); fn toggle_root_motion( + mut commands: Commands, keys: Res>, mut help_text: Single<&mut Text, With>, mut q_transform: Query<&mut Transform>, root_motion_target_id: Option>, - player: Single<(&mut AnimationPlayer, &ApplyRootMotionTo)>, + player: Single<(Entity, Option<&RootMotionConfig>, &ApplyRootMotionTo)>, ) { if let Some(root_motion_target_id) = root_motion_target_id && keys.just_pressed(KeyCode::Space) { - let (mut animation_player, apply_to) = player.into_inner(); - if animation_player.root_motion_target().is_none() { - animation_player.set_root_motion_target(Some(root_motion_target_id.0)); - help_text.0 = HELP_TEXT.to_string() + "(current: On)"; - } else { - animation_player.set_root_motion_target(None); - help_text.0 = HELP_TEXT.to_string() + "(current: Off)"; + let (player_entity, root_motion_config, apply_to) = player.into_inner(); + match root_motion_config { + Some(_) => { + help_text.0 = HELP_TEXT.to_string() + "(current: Off)"; + commands.entity(player_entity).remove::(); + } + None => { + help_text.0 = HELP_TEXT.to_string() + "(current: On)"; + commands.entity(player_entity).insert(RootMotionConfig { + root_motion_mode: RootMotionMode::Translation, + root_motion_target: root_motion_target_id.0, + }); + } } q_transform.get_mut(apply_to.0).unwrap().translation = ORIGIN_POSITION; } @@ -127,13 +134,16 @@ fn setup( &Name::new("b_Root_00"), ) .unwrap(); - // You can choose if you want to get Translation + Rotation - // or only Translation with RootMotionMode. - // By default, it's Translation + Rotation. - animation_player.set_root_motion_mode(RootMotionMode::Translation); - commands - .entity(*scene_child) - .insert(ApplyRootMotionTo(trigger.event_target())); + commands.entity(*scene_child).insert(( + RootMotionConfig { + // You can choose if you want to get Translation + Rotation + // or only Translation with RootMotionMode. + // By default, it's Translation + Rotation. + root_motion_mode: RootMotionMode::Translation, + root_motion_target: root_motion_target_id, + }, + ApplyRootMotionTo(trigger.event_target()), + )); commands.insert_resource(RootMotionTargetId(root_motion_target_id)); return; } @@ -172,7 +182,7 @@ fn setup( // Help Text commands.spawn(( HelpTextMarker, - Text::new(HELP_TEXT.to_string() + "(current: Off)"), + Text::new(HELP_TEXT.to_string() + "(current: On)"), Node { position_type: PositionType::Absolute, top: px(12), From b6e8ed5394b071357485ff92eaaaa0e6b59866de Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Tue, 26 May 2026 15:23:25 +0200 Subject: [PATCH 32/36] document RootMotionMode match --- crates/bevy_animation/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 15b659453a009..7831c3fcedb06 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -778,6 +778,8 @@ pub enum RootMotionMode { impl RootMotionMode { /// Returns true if the translation should be extracted pub fn should_extract_translation(&self) -> bool { + // Note: All cases are explicitly handled so we don't forget to change + // this match if new options are added. match self { Self::Translation | Self::TranslationAndRotation => true, } From 70437d0319e1377e8354c1e2f6855d570be6dcb0 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Tue, 26 May 2026 15:32:59 +0200 Subject: [PATCH 33/36] document FoxRootMotion.glb --- assets/models/animated/FoxRootMotion.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 assets/models/animated/FoxRootMotion.md diff --git a/assets/models/animated/FoxRootMotion.md b/assets/models/animated/FoxRootMotion.md new file mode 100644 index 0000000000000..212095736f92f --- /dev/null +++ b/assets/models/animated/FoxRootMotion.md @@ -0,0 +1,4 @@ +# What's FoxRootMotion.glb + +This model is the same as Fox.glb but with an additional curve for translation in the `b_Root_00` bone of the `run` animation. +It's used to illustrate the root motion in the root_motion example. From a4163d5acbdfcf74f1d00cc95932832cdcf4fba2 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Tue, 26 May 2026 16:52:37 +0200 Subject: [PATCH 34/36] add a migration guide for AnimationCurveEvaluator --- .../animation_curve_evaluator_commit.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 _release-content/migration-guides/animation_curve_evaluator_commit.md diff --git a/_release-content/migration-guides/animation_curve_evaluator_commit.md b/_release-content/migration-guides/animation_curve_evaluator_commit.md new file mode 100644 index 0000000000000..3e1219946e7ff --- /dev/null +++ b/_release-content/migration-guides/animation_curve_evaluator_commit.md @@ -0,0 +1,19 @@ +--- +title: Change function signature of commit in AnimationCurveEvaluator +pull_requests: [24201] +--- + +The root motion feature needs to access the animated entity after the curves are applied. For this reason, the commit method of +AnimationCurveEvaluator changed from : + +```rust + fn commit(&mut self, entity: AnimationEntityMut) -> Result<(), AnimationEvaluationError>; +``` + +to + +```rust + fn commit(&mut self, entity: &mut AnimationEntityMut) -> Result<(), AnimationEvaluationError>; +``` + +Passing the AnimationEntityMut by mutable reference is now necessary. From 2d79ad2277560e78002074bf58df03de54916f16 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Tue, 26 May 2026 17:01:20 +0200 Subject: [PATCH 35/36] fix ci import issue --- examples/animation/root_motion.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/animation/root_motion.rs b/examples/animation/root_motion.rs index e6c22213f104f..4f708e312e272 100644 --- a/examples/animation/root_motion.rs +++ b/examples/animation/root_motion.rs @@ -1,14 +1,16 @@ //! Demonstrates the usage of root motion. use bevy::{ - animation::{AnimationTargetId, RepeatAnimation, RootMotion, RootMotionMode}, + animation::{ + find_root_bone_recursive, AnimationTargetId, RepeatAnimation, RootMotion, RootMotionConfig, + RootMotionMode, + }, app::AnimationSystems, color::palettes::css::SILVER, light::CascadeShadowConfigBuilder, prelude::*, world_serialization::WorldInstanceReady, }; -use bevy_animation::{find_root_bone_recursive, RootMotionConfig}; const MODEL: &str = "models/animated/FoxRootMotion.glb"; const ORIGIN_POSITION: Vec3 = Vec3::new(0., 0., -50.); From 6775369bd23124d6a4489b1fe33b8a37a2177b93 Mon Sep 17 00:00:00 2001 From: Hilpogar Date: Wed, 27 May 2026 13:34:04 +0200 Subject: [PATCH 36/36] change animated_field! to remove the need to import AnimatedField --- crates/bevy_animation/src/animation_curves.rs | 7 ++++--- crates/bevy_animation/src/lib.rs | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index b8297c044c530..dc564950f43d2 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -805,9 +805,10 @@ where #[macro_export] macro_rules! animated_field { ($component:ident::$field:tt) => { - AnimatedField::new_unchecked(stringify!($field), |component: &mut $component| { - &mut component.$field - }) + $crate::animation_curves::AnimatedField::new_unchecked( + stringify!($field), + |component: &mut $component| &mut component.$field, + ) }; } diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 7831c3fcedb06..929d57796ae2e 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -34,7 +34,7 @@ use prelude::AnimationCurveEvaluator; use crate::{ graph::{AnimationGraphHandle, ThreadedAnimationGraphs}, - prelude::{AnimatableCurveEvaluator, AnimatableProperty, AnimatedField, EvaluatorId}, + prelude::{AnimatableCurveEvaluator, AnimatableProperty, EvaluatorId}, }; use bevy_app::{AnimationSystems, App, Plugin, PostUpdate}; @@ -1844,7 +1844,7 @@ mod tests { use crate::{ self as bevy_animation, - prelude::{AnimatableCurve, AnimatableKeyframeCurve, AnimatedField}, + prelude::{AnimatableCurve, AnimatableKeyframeCurve}, }; use bevy_app::{First, Last, ScheduleRunnerPlugin}; use bevy_asset::AssetPlugin;