#![deny(missing_docs)]
use std::{
collections::{HashMap, HashSet},
time::Duration,
};
use bevy::{
animation::{
ActiveAnimation, AnimatedBy, AnimationTargetId, RepeatAnimation,
graph::{AnimationGraph, AnimationGraphHandle, AnimationNodeIndex},
transition::AnimationTransitions,
},
asset::AssetPlugin,
gltf::Gltf,
mesh::skinning::SkinnedMesh,
prelude::*,
};
use serde::{Deserialize, Serialize};
pub mod events;
pub mod graph;
pub use events::{AnimationEvent, ClipEvent, ClipPass, ClipPlayhead, fire_clip_events};
pub use graph::{
AnimationBlendPoint, AnimationClipRef, AnimationCondition, AnimationConditionOp,
AnimationGraphAsset, AnimationGraphBound, AnimationGraphDef, AnimationGraphLoadError,
AnimationGraphLoader, AnimationGraphPlayback, AnimationGraphRef, AnimationGraphSource,
AnimationGraphState, AnimationGraphTransition, AnimationMotion, AnimationParameterDef,
AnimationParameterKind, AnimationParams, AnimationTransitionDef, parse_animation_graph,
register_animation_graph_types,
};
#[derive(Component, Reflect, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[reflect(Component, Default)]
pub struct AnimationSet {
pub sources: Vec<String>,
pub states: Vec<AnimationStateDef>,
pub default_state: String,
pub skeleton_root: String,
}
impl Default for AnimationSet {
fn default() -> Self {
Self {
sources: Vec::new(),
states: Vec::new(),
default_state: String::new(),
skeleton_root: "Armature".to_string(),
}
}
}
#[derive(Reflect, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[reflect(Default)]
pub struct AnimationStateDef {
pub name: String,
pub source: usize,
pub clip: String,
pub looped: bool,
pub transition_secs: f32,
pub speed: f32,
pub then: Option<String>,
}
impl Default for AnimationStateDef {
fn default() -> Self {
Self {
name: String::new(),
source: 0,
clip: String::new(),
looped: true,
transition_secs: 0.15,
speed: 1.0,
then: None,
}
}
}
#[derive(Component, Reflect, Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
#[reflect(Component, Default)]
pub struct AnimationState(
pub String,
);
#[derive(Component, Debug, Default)]
pub struct AnimationSources(
pub Vec<Handle<Gltf>>,
);
#[derive(Component, Debug)]
pub struct AnimationSetBound {
pub player: Entity,
pub nodes: HashMap<String, AnimationNodeIndex>,
pub graph: Handle<AnimationGraph>,
pub parts: Vec<Entity>,
pub warned_states: HashSet<String>,
}
#[derive(Message, Debug, Clone, PartialEq, Eq)]
pub struct AnimationStateFinished {
pub entity: Entity,
pub state: String,
}
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AnimationSetSystems;
pub struct AnimationRuntimePlugin;
impl Plugin for AnimationRuntimePlugin {
fn build(&self, app: &mut App) {
register_animation_set_types(app);
graph::register_animation_graph_types(app);
let graph_assets_can_be_loaded = app.is_plugin_added::<AssetPlugin>();
if graph_assets_can_be_loaded {
app.init_asset::<AnimationGraphAsset>()
.init_asset_loader::<AnimationGraphLoader>();
}
app.add_message::<AnimationStateFinished>()
.add_message::<AnimationEvent>()
.add_systems(
Update,
(
bind_animation_sets,
merge_part_skeletons,
apply_animation_state,
report_finished_states,
)
.chain()
.in_set(AnimationSetSystems)
.run_if(
resource_exists::<AssetServer>
.and_then(resource_exists::<Assets<Gltf>>)
.and_then(resource_exists::<Assets<AnimationGraph>>),
),
)
.add_systems(
Update,
(
graph::rebind_edited_graphs,
graph::bind_animation_graphs,
graph::advance_animation_graphs,
)
.chain()
.in_set(AnimationSetSystems)
.run_if(
resource_exists::<AssetServer>
.and_then(resource_exists::<Assets<Gltf>>)
.and_then(resource_exists::<Assets<AnimationGraph>>)
.and_then(resource_exists::<Assets<AnimationGraphAsset>>),
),
)
.add_systems(
Update,
(write_clip_playheads, fire_clip_events)
.chain()
.in_set(AnimationSetSystems)
.after(apply_animation_state)
.after(graph::advance_animation_graphs)
.before(report_finished_states),
);
}
}
pub fn register_animation_set_types(app: &mut App) {
app.register_type::<AnimationSet>()
.register_type::<AnimationStateDef>()
.register_type::<AnimationState>()
.register_type::<ClipEvent>();
}
fn bind_animation_sets(
mut commands: Commands,
asset_server: Res<AssetServer>,
gltfs: Res<Assets<Gltf>>,
mut graphs: ResMut<Assets<AnimationGraph>>,
unbound: Query<
(
Entity,
&AnimationSet,
Option<&AnimationGraphRef>,
Option<&AnimationSources>,
Option<&AnimationState>,
),
Without<AnimationSetBound>,
>,
children: Query<&Children>,
names: Query<&Name>,
) {
for (entity, set, graph_ref, sources, state) in &unbound {
if graph_ref.is_some_and(|graph_ref| !graph_ref.path.is_empty()) {
continue;
}
let Some(sources) = sources else {
commands.entity(entity).insert(AnimationSources(
set.sources
.iter()
.map(|path| asset_server.load(path))
.collect(),
));
continue;
};
let Some(loaded) = sources
.0
.iter()
.map(|handle| gltfs.get(handle))
.collect::<Option<Vec<_>>>()
else {
continue;
};
let Some(&root) = descendants_named(entity, &set.skeleton_root, &children, &names).first()
else {
continue;
};
let mut graph = AnimationGraph::new();
let mut nodes = HashMap::new();
for def in &set.states {
let Some(gltf) = loaded.get(def.source) else {
warn!(
"animation state `{}` wants source {} of {}",
def.name,
def.source,
loaded.len()
);
continue;
};
let Some(clip) = gltf.named_animations.get(def.clip.as_str()) else {
let available: Vec<&str> =
gltf.named_animations.keys().map(|name| &**name).collect();
warn!(
"animation state `{}` wants clip `{}`, and its source holds {:?}",
def.name, def.clip, available
);
continue;
};
let node = graph.add_clip(clip.clone(), 1.0, graph.root);
nodes.insert(def.name.clone(), node);
}
let graph = graphs.add(graph);
commands.queue(move |world: &mut World| {
tag_animation_targets(world, root);
});
let wanted = state.map_or(set.default_state.as_str(), |state| state.0.as_str());
let mut player = AnimationPlayer::default();
let mut transitions = AnimationTransitions::new();
let mut warned_states = HashSet::new();
match resolve_state(set, &nodes, wanted) {
Some((def, node)) => play_state(def, node, &mut player, &mut transitions),
None if wanted.is_empty() => {}
None => {
warn!("animation set has no state named `{wanted}`");
warned_states.insert(wanted.to_string());
}
}
commands
.entity(root)
.insert((player, AnimationGraphHandle(graph.clone()), transitions));
if state.is_none() {
commands
.entity(entity)
.insert(AnimationState(wanted.to_string()));
}
commands.entity(entity).insert(AnimationSetBound {
player: root,
nodes,
graph,
parts: Vec::new(),
warned_states,
});
}
}
fn merge_part_skeletons(
mut commands: Commands,
mut sets: Query<(
Entity,
&AnimationSet,
&mut AnimationSetBound,
Option<&AnimationState>,
)>,
children: Query<&Children>,
names: Query<&Name>,
child_of: Query<&ChildOf>,
animated: Query<(), With<AnimationPlayer>>,
mut skins: Query<&mut SkinnedMesh>,
) {
for (entity, set, mut bound, state) in &mut sets {
let primary = bound.player;
let candidates: Vec<Entity> =
descendants_named(entity, &set.skeleton_root, &children, &names)
.into_iter()
.filter(|&part| part != primary && !animated.contains(part))
.collect();
if candidates.is_empty() {
continue;
}
let primary_joints: HashMap<String, Entity> = descendants(primary, &children)
.into_iter()
.filter_map(|joint| names.get(joint).ok().map(|name| (name.to_string(), joint)))
.collect();
let primary_parent = child_of
.get(primary)
.map_or(entity, |&ChildOf(parent)| parent);
for part in candidates {
let part_root = part_subtree_root(part, primary, &child_of, &children);
let part_meshes: Vec<Entity> = descendants(part_root, &children)
.into_iter()
.filter(|&mesh| skins.contains(mesh))
.collect();
let Some(remapped) = remap_joints(&part_meshes, &primary_joints, &skins, &names) else {
warn!(
"an animated part does not answer to the same bone names as the skeleton it \
was placed under, so it keeps its own"
);
bind_extra_root(&mut commands, part, &bound, set, state);
bound.parts.push(part);
continue;
};
for (mesh, joints) in remapped {
if let Ok(mut skin) = skins.get_mut(mesh) {
skin.joints = joints;
}
commands.entity(mesh).insert(ChildOf(primary_parent));
}
commands.entity(part).despawn();
}
}
}
fn part_subtree_root(
part: Entity,
primary: Entity,
child_of: &Query<&ChildOf>,
children: &Query<&Children>,
) -> Entity {
child_of
.get(part)
.map(|&ChildOf(parent)| parent)
.ok()
.filter(|&parent| !descendants(parent, children).contains(&primary))
.unwrap_or(part)
}
fn apply_animation_state(
mut sets: Query<
(&AnimationSet, &mut AnimationSetBound, &AnimationState),
Changed<AnimationState>,
>,
mut players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
) {
for (set, mut bound, state) in &mut sets {
let Some((def, node)) = resolve_state(set, &bound.nodes, &state.0) else {
if !state.0.is_empty() && bound.warned_states.insert(state.0.clone()) {
warn!("animation set has no state named `{}`", state.0);
}
continue;
};
let roots: Vec<Entity> = std::iter::once(bound.player)
.chain(bound.parts.iter().copied())
.collect();
for root in roots {
let Ok((mut player, mut transitions)) = players.get_mut(root) else {
continue;
};
play_state(def, node, &mut player, &mut transitions);
}
}
}
#[derive(Clone, Copy)]
struct PlayingClip<'a> {
source: &'a str,
clip: &'a str,
seek: f32,
pass: ClipPass,
}
impl<'a> PlayingClip<'a> {
fn read(source: &'a str, clip: &'a str, active: &ActiveAnimation) -> Self {
let wrapped = active.just_completed() && !active.is_finished();
Self {
source,
clip,
seek: active.seek_time(),
pass: match (active.is_playback_reversed(), wrapped) {
(false, false) => ClipPass::Forward,
(false, true) => ClipPass::ForwardWrapped,
(true, false) => ClipPass::Backward,
(true, true) => ClipPass::BackwardWrapped,
},
}
}
}
pub fn write_clip_playheads(
mut commands: Commands,
sets: Query<
(Entity, &AnimationSet, &AnimationSetBound, &AnimationState),
Without<AnimationGraphBound>,
>,
graphs: Query<(Entity, &AnimationGraphBound, &graph::AnimationGraphPlayback)>,
players: Query<&AnimationPlayer>,
installed: Query<&AnimationGraphHandle>,
children: Query<&Children>,
names: Query<&Name>,
marked: Query<(), With<ClipEvent>>,
mut playheads: Query<&mut ClipPlayhead>,
) {
for (owner, set, bound, state) in &sets {
if !plays_its_own_graph(bound.player, &bound.graph, &installed) {
continue;
}
let playing = resolve_state(set, &bound.nodes, &state.0).and_then(|(def, node)| {
let active = players.get(bound.player).ok()?.animation(node)?;
let source = set.sources.get(def.source).map_or("", String::as_str);
Some(PlayingClip::read(source, def.clip.as_str(), active))
});
write_playheads_under(
owner,
playing,
&mut commands,
&children,
&names,
&marked,
&mut playheads,
);
}
for (owner, bound, playback) in &graphs {
if !plays_its_own_graph(bound.player, &bound.graph, &installed) {
continue;
}
let playing = players.get(bound.player).ok().and_then(|player| {
let (source, clip, node) = bound.leading_clip_of(player, &playback.state)?;
Some(PlayingClip::read(source, clip, player.animation(node)?))
});
write_playheads_under(
owner,
playing,
&mut commands,
&children,
&names,
&marked,
&mut playheads,
);
}
}
fn plays_its_own_graph(
player: Entity,
graph: &Handle<AnimationGraph>,
installed: &Query<&AnimationGraphHandle>,
) -> bool {
installed
.get(player)
.is_ok_and(|handle| handle.0.id() == graph.id())
}
fn write_playheads_under(
owner: Entity,
playing: Option<PlayingClip<'_>>,
commands: &mut Commands,
children: &Query<&Children>,
names: &Query<&Name>,
marked: &Query<(), With<ClipEvent>>,
playheads: &mut Query<&mut ClipPlayhead>,
) {
let Ok(kids) = children.get(owner) else {
return;
};
for row in kids.iter() {
let holds_events = children
.get(row)
.is_ok_and(|keys| keys.iter().any(|key| marked.contains(key)));
if !holds_events {
continue;
}
let name = names.get(row).map_or("", Name::as_str);
let played = playing.filter(|clip| stands_for_clip(name, clip.source, clip.clip));
match (playheads.get_mut(row), played) {
(Ok(mut playhead), Some(clip)) => playhead.advance_to(clip.seek, clip.pass),
(Ok(_), None) => {
commands.entity(row).remove::<ClipPlayhead>();
}
(Err(_), Some(clip)) => {
commands.entity(row).insert(ClipPlayhead {
last: clip.seek,
now: clip.seek,
pass: ClipPass::Forward,
});
}
(Err(_), None) => {}
}
}
}
pub fn stands_for_clip(name: &str, source: &str, clip: &str) -> bool {
match name.split_once('#') {
Some((file, named)) => file == source && named == clip,
None => name == clip,
}
}
fn report_finished_states(
mut sets: Query<(
Entity,
&AnimationSet,
&AnimationSetBound,
&mut AnimationState,
)>,
players: Query<(&AnimationPlayer, &AnimationTransitions)>,
mut finished: MessageWriter<AnimationStateFinished>,
) {
for (entity, set, bound, mut state) in &mut sets {
let Some((def, node)) = resolve_state(set, &bound.nodes, &state.0) else {
continue;
};
if def.looped {
continue;
}
let Ok((player, transitions)) = players.get(bound.player) else {
continue;
};
if transitions.get_main_animation() != Some(node) {
continue;
}
let ran_out_this_frame = player
.animation(node)
.is_some_and(|active| active.just_completed() && active.is_finished());
if !ran_out_this_frame {
continue;
}
finished.write(AnimationStateFinished {
entity,
state: state.0.clone(),
});
if let Some(next) = &def.then {
state.0 = next.clone();
}
}
}
fn resolve_state<'a>(
set: &'a AnimationSet,
nodes: &HashMap<String, AnimationNodeIndex>,
wanted: &str,
) -> Option<(&'a AnimationStateDef, AnimationNodeIndex)> {
let def = set.states.iter().find(|def| def.name == wanted)?;
let node = *nodes.get(wanted)?;
Some((def, node))
}
fn play_state(
def: &AnimationStateDef,
node: AnimationNodeIndex,
player: &mut AnimationPlayer,
transitions: &mut AnimationTransitions,
) {
let already_running = transitions.get_main_animation() == Some(node)
&& player
.animation(node)
.is_some_and(|active| !active.is_finished());
if already_running {
return;
}
let repeat = if def.looped {
RepeatAnimation::Forever
} else {
RepeatAnimation::Never
};
transitions
.play(player, node, Duration::from_secs_f32(def.transition_secs))
.set_repeat(repeat)
.set_speed(def.speed);
}
fn bind_extra_root(
commands: &mut Commands,
root: Entity,
bound: &AnimationSetBound,
set: &AnimationSet,
state: Option<&AnimationState>,
) {
commands.queue(move |world: &mut World| {
tag_animation_targets(world, root);
});
let wanted = state.map_or(set.default_state.as_str(), |state| state.0.as_str());
let mut player = AnimationPlayer::default();
let mut transitions = AnimationTransitions::new();
if let Some((def, node)) = resolve_state(set, &bound.nodes, wanted) {
play_state(def, node, &mut player, &mut transitions);
}
commands.entity(root).insert((
player,
AnimationGraphHandle(bound.graph.clone()),
transitions,
));
}
fn remap_joints(
meshes: &[Entity],
primary_joints: &HashMap<String, Entity>,
skins: &Query<&mut SkinnedMesh>,
names: &Query<&Name>,
) -> Option<Vec<(Entity, Vec<Entity>)>> {
let mut remapped = Vec::with_capacity(meshes.len());
for &mesh in meshes {
let skin = skins.get(mesh).ok()?;
let joints = skin
.joints
.iter()
.map(|&joint| primary_joints.get(names.get(joint).ok()?.as_str()).copied())
.collect::<Option<Vec<_>>>()?;
remapped.push((mesh, joints));
}
Some(remapped)
}
pub fn tag_animation_targets(world: &mut World, root: Entity) -> Vec<Entity> {
let mut tagged = Vec::new();
tag_from(world, root, root, &mut Vec::new(), &mut tagged);
tagged
}
fn tag_from(
world: &mut World,
root: Entity,
entity: Entity,
path: &mut Vec<Name>,
tagged: &mut Vec<Entity>,
) {
let Some(name) = world.get::<Name>(entity).cloned() else {
return;
};
path.push(name);
if world.get::<AnimationTargetId>(entity).is_none() {
world
.entity_mut(entity)
.insert((AnimationTargetId::from_names(path.iter()), AnimatedBy(root)));
tagged.push(entity);
}
let kids: Vec<Entity> = world
.get::<Children>(entity)
.map(|kids| kids.iter().collect())
.unwrap_or_default();
for child in kids {
tag_from(world, root, child, path, tagged);
}
path.pop();
}
pub(crate) fn descendants_named(
root: Entity,
wanted: &str,
children: &Query<&Children>,
names: &Query<&Name>,
) -> Vec<Entity> {
descendants(root, children)
.into_iter()
.skip(1)
.filter(|&entity| names.get(entity).is_ok_and(|name| name.as_str() == wanted))
.collect()
}
fn descendants(root: Entity, children: &Query<&Children>) -> Vec<Entity> {
let mut found = vec![root];
let mut visited = 0;
while visited < found.len() {
let entity = found[visited];
visited += 1;
if let Ok(kids) = children.get(entity) {
found.extend(kids.iter());
}
}
found
}