use std::{
any::TypeId,
collections::{HashMap, HashSet},
time::Duration,
};
use bevy::{
animation::{
ActiveAnimation, AnimationClip, RepeatAnimation,
graph::{AnimationGraph, AnimationGraphHandle, AnimationNodeIndex},
transition::AnimationTransitions,
},
asset::{AssetLoader, LoadContext, io::Reader},
gltf::Gltf,
prelude::*,
reflect::{FromReflect, TypePath, TypeRegistry, TypeRegistryArc},
};
use jackdaw_bsn::{BsnPatch, BsnValue, bsn_value_to_reflect, parse_bsn_text};
use serde::{Deserialize, Serialize};
use crate::{
AnimationSet, AnimationSources, AnimationStateFinished, descendants_named,
tag_animation_targets,
};
const EQUAL_EPSILON: f32 = 1.0e-5;
const WEIGHT_EPSILON: f32 = 1.0e-4;
#[derive(Reflect, Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[reflect(Default)]
pub struct AnimationGraphDef {
pub parameters: Vec<AnimationParameterDef>,
pub states: Vec<AnimationGraphState>,
pub transitions: Vec<AnimationTransitionDef>,
pub entry: String,
}
#[derive(Reflect, Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[reflect(Default)]
pub struct AnimationParameterDef {
pub name: String,
pub kind: AnimationParameterKind,
pub default: f32,
}
#[derive(Reflect, Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)]
#[reflect(Default)]
pub enum AnimationParameterKind {
#[default]
Float,
Bool,
Trigger,
}
#[derive(Reflect, Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[reflect(Default)]
pub struct AnimationClipRef {
pub source: String,
pub clip: String,
}
#[derive(Reflect, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[reflect(Default)]
pub struct AnimationGraphState {
pub name: String,
pub motion: AnimationMotion,
pub looped: bool,
pub speed: f32,
pub position: Vec2,
}
impl Default for AnimationGraphState {
fn default() -> Self {
Self {
name: String::new(),
motion: AnimationMotion::default(),
looped: true,
speed: 1.0,
position: Vec2::ZERO,
}
}
}
#[derive(Reflect, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[reflect(Default)]
pub enum AnimationMotion {
Clip(AnimationClipRef),
Blend1d {
parameter: String,
points: Vec<AnimationBlendPoint>,
},
}
impl Default for AnimationMotion {
fn default() -> Self {
Self::Clip(AnimationClipRef::default())
}
}
#[derive(Reflect, Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[reflect(Default)]
pub struct AnimationBlendPoint {
pub threshold: f32,
pub clip: AnimationClipRef,
}
#[derive(Reflect, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[reflect(Default)]
pub struct AnimationTransitionDef {
pub from: String,
pub to: String,
pub conditions: Vec<AnimationCondition>,
pub require_all: bool,
pub exit_time: Option<f32>,
pub crossfade_secs: f32,
}
impl Default for AnimationTransitionDef {
fn default() -> Self {
Self {
from: String::new(),
to: String::new(),
conditions: Vec::new(),
require_all: true,
exit_time: None,
crossfade_secs: 0.15,
}
}
}
#[derive(Reflect, Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[reflect(Default)]
pub struct AnimationCondition {
pub parameter: String,
pub op: AnimationConditionOp,
pub value: f32,
}
#[derive(Reflect, Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)]
#[reflect(Default)]
pub enum AnimationConditionOp {
#[default]
Greater,
GreaterOrEqual,
Less,
LessOrEqual,
Equal,
NotEqual,
}
impl AnimationConditionOp {
fn holds(self, parameter: f32, value: f32) -> bool {
match self {
Self::Greater => parameter > value,
Self::GreaterOrEqual => parameter >= value,
Self::Less => parameter < value,
Self::LessOrEqual => parameter <= value,
Self::Equal => (parameter - value).abs() <= EQUAL_EPSILON,
Self::NotEqual => (parameter - value).abs() > EQUAL_EPSILON,
}
}
}
#[derive(Asset, TypePath, Debug, Clone, Default)]
pub struct AnimationGraphAsset {
pub def: AnimationGraphDef,
}
#[derive(Component, Reflect, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[reflect(Component, Default)]
pub struct AnimationGraphRef {
pub path: String,
pub skeleton_root: String,
}
impl Default for AnimationGraphRef {
fn default() -> Self {
Self {
path: String::new(),
skeleton_root: "Armature".to_string(),
}
}
}
#[derive(Component, Debug)]
pub struct AnimationGraphSource(
pub Handle<AnimationGraphAsset>,
);
#[derive(Component, Reflect, Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[reflect(Component, Default)]
pub struct AnimationParams {
values: HashMap<String, f32>,
pending: HashSet<String>,
}
impl AnimationParams {
pub fn set_float(&mut self, name: impl Into<String>, value: f32) {
self.values.insert(name.into(), value);
}
pub fn set_bool(&mut self, name: impl Into<String>, value: bool) {
self.values
.insert(name.into(), if value { 1.0 } else { 0.0 });
}
pub fn trigger(&mut self, name: impl Into<String>) {
self.pending.insert(name.into());
}
pub fn float(&self, name: &str) -> f32 {
self.values.get(name).copied().unwrap_or_default()
}
pub fn flag(&self, name: &str) -> bool {
self.float(name) != 0.0
}
pub fn triggered(&self, name: &str) -> bool {
self.pending.contains(name)
}
pub fn clear_trigger(&mut self, name: &str) {
self.pending.remove(name);
}
fn seed(&mut self, name: &str, value: f32) {
if !self.values.contains_key(name) {
self.values.insert(name.to_string(), value);
}
}
}
#[derive(Component, Reflect, Debug, Clone, Default, PartialEq)]
#[reflect(Component, Default)]
pub struct AnimationGraphPlayback {
pub state: String,
pub since_secs: f32,
pub transition: Option<AnimationGraphTransition>,
}
#[derive(Reflect, Debug, Clone, Default, PartialEq)]
#[reflect(Default)]
pub struct AnimationGraphTransition {
pub from: String,
pub remaining_secs: f32,
}
#[derive(Debug)]
struct BoundClip {
node: AnimationNodeIndex,
threshold: f32,
duration: f32,
source: String,
clip: String,
}
#[derive(Debug)]
struct BoundState {
node: AnimationNodeIndex,
blend: bool,
parameter: String,
looped: bool,
speed: f32,
clips: Vec<BoundClip>,
}
#[derive(Component, Debug)]
pub struct AnimationGraphBound {
pub player: Entity,
pub graph: Handle<AnimationGraph>,
states: Vec<BoundState>,
by_name: HashMap<String, usize>,
sources: Vec<String>,
}
impl AnimationGraphBound {
pub(crate) fn leading_clip_of(
&self,
player: &AnimationPlayer,
state: &str,
) -> Option<(&str, &str, AnimationNodeIndex)> {
let bound = self.states.get(*self.by_name.get(state)?)?;
let clip = leading_clip(player, bound)?;
Some((clip.source.as_str(), clip.clip.as_str(), clip.node))
}
pub fn node(&self, state: &str) -> Option<AnimationNodeIndex> {
self.by_name
.get(state)
.map(|&index| self.states[index].node)
}
}
#[derive(Debug)]
pub enum AnimationGraphLoadError {
Io(String),
Parse(String),
NoGraph,
Fields,
Unregistered,
}
impl std::fmt::Display for AnimationGraphLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(err) => write!(f, "could not read the graph: {err}"),
Self::Parse(err) => write!(f, "could not parse the graph: {err}"),
Self::NoGraph => write!(f, "the file holds no AnimationGraphDef"),
Self::Fields => write!(f, "a field of the graph does not read as its type"),
Self::Unregistered => write!(f, "AnimationGraphDef is not registered for reflection"),
}
}
}
impl std::error::Error for AnimationGraphLoadError {}
#[derive(TypePath)]
pub struct AnimationGraphLoader {
registry: TypeRegistryArc,
}
impl FromWorld for AnimationGraphLoader {
fn from_world(world: &mut World) -> Self {
Self {
registry: world.resource::<AppTypeRegistry>().0.clone(),
}
}
}
impl AssetLoader for AnimationGraphLoader {
type Asset = AnimationGraphAsset;
type Settings = ();
type Error = AnimationGraphLoadError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader
.read_to_end(&mut bytes)
.await
.map_err(|err| AnimationGraphLoadError::Io(err.to_string()))?;
let text = jackdaw_bsn::document_text_from_bytes(&bytes, load_context.path().path())
.map_err(|err| AnimationGraphLoadError::Parse(err.to_string()))?;
let def = parse_animation_graph(&text, &self.registry.read())?;
Ok(AnimationGraphAsset { def })
}
fn extensions(&self) -> &[&str] {
&["animgraph.bsn", "animgraph.bsb"]
}
}
pub fn parse_animation_graph(
text: &str,
registry: &TypeRegistry,
) -> Result<AnimationGraphDef, AnimationGraphLoadError> {
let ast =
parse_bsn_text(text).map_err(|err| AnimationGraphLoadError::Parse(err.to_string()))?;
if registry.get(TypeId::of::<AnimationGraphDef>()).is_none() {
return Err(AnimationGraphLoadError::Unregistered);
}
let wanted = AnimationGraphDef::type_path();
for &root in &ast.roots {
let Some(patches) = ast.get_patches(root) else {
continue;
};
for &patch_entity in &patches.0 {
match ast.get_patch(patch_entity) {
Some(BsnPatch::Type(path)) if path == wanted => {
return Ok(AnimationGraphDef::default());
}
Some(BsnPatch::Struct(data)) if data.type_path == wanted => {
let value = bsn_value_to_reflect(
&BsnValue::Struct(data.clone()),
TypeId::of::<AnimationGraphDef>(),
registry,
None,
)
.ok_or(AnimationGraphLoadError::Fields)?;
return AnimationGraphDef::from_reflect(&*value)
.ok_or(AnimationGraphLoadError::Fields);
}
_ => {}
}
}
}
Err(AnimationGraphLoadError::NoGraph)
}
impl AnimationSet {
pub fn to_graph(&self) -> AnimationGraphDef {
let states = self
.states
.iter()
.map(|def| AnimationGraphState {
name: def.name.clone(),
motion: AnimationMotion::Clip(AnimationClipRef {
source: self.sources.get(def.source).cloned().unwrap_or_default(),
clip: def.clip.clone(),
}),
looped: def.looped,
speed: def.speed,
..AnimationGraphState::default()
})
.collect();
let transitions = self
.states
.iter()
.filter_map(|def| {
let next = def.then.as_ref()?;
let crossfade_secs = self
.states
.iter()
.find(|other| &other.name == next)
.map_or(def.transition_secs, |other| other.transition_secs);
Some(AnimationTransitionDef {
from: def.name.clone(),
to: next.clone(),
exit_time: Some(1.0),
crossfade_secs,
..AnimationTransitionDef::default()
})
})
.collect();
AnimationGraphDef {
parameters: Vec::new(),
states,
transitions,
entry: self.default_state.clone(),
}
}
}
pub fn register_animation_graph_types(app: &mut App) {
app.register_type::<AnimationGraphDef>()
.register_type::<AnimationParameterDef>()
.register_type::<AnimationParameterKind>()
.register_type::<AnimationClipRef>()
.register_type::<AnimationGraphState>()
.register_type::<AnimationMotion>()
.register_type::<AnimationBlendPoint>()
.register_type::<AnimationTransitionDef>()
.register_type::<AnimationCondition>()
.register_type::<AnimationConditionOp>()
.register_type::<AnimationGraphRef>()
.register_type::<AnimationParams>()
.register_type::<AnimationGraphPlayback>()
.register_type::<AnimationGraphTransition>();
}
#[derive(Component)]
pub(crate) struct AnimationGraphUnread;
pub(crate) fn bind_animation_graphs(
mut commands: Commands,
asset_server: Res<AssetServer>,
assets: Res<Assets<AnimationGraphAsset>>,
gltfs: Res<Assets<Gltf>>,
clips: Res<Assets<AnimationClip>>,
mut graphs: ResMut<Assets<AnimationGraph>>,
unbound: Query<
(
Entity,
&AnimationGraphRef,
Option<&AnimationGraphSource>,
Option<&AnimationSources>,
Option<&AnimationParams>,
Option<&AnimationGraphUnread>,
),
Without<AnimationGraphBound>,
>,
children: Query<&Children>,
names: Query<&Name>,
) {
for (entity, graph_ref, source, sources, params, unread) in &unbound {
if graph_ref.path.is_empty() {
continue;
}
let Some(source) = source else {
commands
.entity(entity)
.insert(AnimationGraphSource(asset_server.load(&graph_ref.path)));
continue;
};
let Some(def) = assets.get(&source.0).map(|asset| &asset.def) else {
if unread.is_none() && asset_server.load_state(&source.0).is_failed() {
warn!("animation graph `{}` could not be read", graph_ref.path);
commands.entity(entity).insert(AnimationGraphUnread);
}
continue;
};
let wanted = graph_sources(def);
let Some(sources) = sources.filter(|held| held.0.len() == wanted.len()) else {
commands.entity(entity).insert(AnimationSources(
wanted.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 files: HashMap<&str, &Gltf> = wanted
.iter()
.map(String::as_str)
.zip(loaded.iter().copied())
.collect();
let Some(&root) =
descendants_named(entity, &graph_ref.skeleton_root, &children, &names).first()
else {
continue;
};
let mut graph = AnimationGraph::new();
let mut states = Vec::new();
let mut by_name = HashMap::new();
for state in &def.states {
let Some(bound) = compile_state(state, &files, &clips, &mut graph) else {
continue;
};
by_name.insert(state.name.clone(), states.len());
states.push(bound);
}
let graph = graphs.add(graph);
commands.queue(move |world: &mut World| {
tag_animation_targets(world, root);
});
let mut player = AnimationPlayer::default();
let mut transitions = AnimationTransitions::new();
for state in states.iter().filter(|state| state.blend) {
for clip in &state.clips {
player
.play(clip.node)
.set_repeat(RepeatAnimation::Forever)
.set_speed(state.speed)
.set_weight(0.0);
}
}
match by_name.get(&def.entry) {
Some(&index) => enter_state(
&states[index],
Duration::ZERO,
&mut player,
&mut transitions,
),
None if def.entry.is_empty() => {}
None => warn!("animation graph has no entry state named `{}`", def.entry),
}
commands
.entity(root)
.insert((player, AnimationGraphHandle(graph.clone()), transitions));
let mut params = params.cloned().unwrap_or_default();
for parameter in &def.parameters {
if parameter.kind != AnimationParameterKind::Trigger {
params.seed(¶meter.name, parameter.default);
}
}
commands.entity(entity).insert((
params,
AnimationGraphPlayback {
state: def.entry.clone(),
since_secs: 0.0,
transition: None,
},
AnimationGraphBound {
player: root,
graph,
states,
by_name,
sources: wanted,
},
));
}
}
pub(crate) fn rebind_edited_graphs(
mut commands: Commands,
mut edits: MessageReader<AssetEvent<AnimationGraphAsset>>,
assets: Res<Assets<AnimationGraphAsset>>,
bound: Query<(Entity, &AnimationGraphSource, &AnimationGraphBound)>,
) {
let edited: Vec<AssetId<AnimationGraphAsset>> = edits
.read()
.filter_map(|edit| match edit {
AssetEvent::Modified { id } => Some(*id),
_ => None,
})
.collect();
if edited.is_empty() {
return;
}
for (entity, source, bound) in &bound {
if !edited.contains(&source.0.id()) {
continue;
}
let mut entity = commands.entity(entity);
entity.remove::<AnimationGraphBound>();
if assets
.get(&source.0)
.is_some_and(|asset| graph_sources(&asset.def) != bound.sources)
{
entity.remove::<AnimationSources>();
}
}
}
pub(crate) fn advance_animation_graphs(
time: Res<Time>,
assets: Res<Assets<AnimationGraphAsset>>,
mut graphs: ResMut<Assets<AnimationGraph>>,
mut graphed: Query<(
Entity,
&AnimationGraphSource,
&AnimationGraphBound,
&mut AnimationParams,
&mut AnimationGraphPlayback,
)>,
mut players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
mut finished: MessageWriter<AnimationStateFinished>,
) {
let delta = time.delta_secs();
for (entity, source, bound, mut params, mut playback) in &mut graphed {
let Some(def) = assets.get(&source.0).map(|asset| &asset.def) else {
continue;
};
let Ok((mut player, mut transitions)) = players.get_mut(bound.player) else {
continue;
};
let current = bound
.by_name
.get(&playback.state)
.map(|&index| &bound.states[index]);
if let Some(state) = current
&& !state.looped
&& ran_out(&player, state)
{
finished.write(AnimationStateFinished {
entity,
state: playback.state.clone(),
});
}
let normalized = current.map_or(1.0, |state| normalized_time(&player, state));
match pick_transition(def, bound, ¶ms, &playback.state, normalized) {
Some(taken) => {
let transition = &def.transitions[taken];
for condition in &transition.conditions {
if parameter_kind(def, &condition.parameter) == AnimationParameterKind::Trigger
{
params.clear_trigger(&condition.parameter);
}
}
enter_state(
&bound.states[bound.by_name[&transition.to]],
Duration::from_secs_f32(transition.crossfade_secs.max(0.0)),
&mut player,
&mut transitions,
);
playback.transition =
(transition.crossfade_secs > 0.0).then(|| AnimationGraphTransition {
from: playback.state.clone(),
remaining_secs: transition.crossfade_secs,
});
playback.state = transition.to.clone();
playback.since_secs = 0.0;
}
None => {
playback.since_secs += delta;
if let Some(fade) = &mut playback.transition {
fade.remaining_secs -= delta;
if fade.remaining_secs <= 0.0 {
playback.transition = None;
}
}
}
}
write_weights(bound, ¶ms, &mut player, &mut graphs);
}
}
fn graph_sources(def: &AnimationGraphDef) -> Vec<String> {
let mut sources: Vec<String> = Vec::new();
for state in &def.states {
for clip in motion_clips(&state.motion) {
if !sources.contains(&clip.source) {
sources.push(clip.source.clone());
}
}
}
sources
}
fn motion_clips(motion: &AnimationMotion) -> Vec<&AnimationClipRef> {
match motion {
AnimationMotion::Clip(clip) => vec![clip],
AnimationMotion::Blend1d { points, .. } => points.iter().map(|point| &point.clip).collect(),
}
}
fn compile_state(
state: &AnimationGraphState,
files: &HashMap<&str, &Gltf>,
clips: &Assets<AnimationClip>,
graph: &mut AnimationGraph,
) -> Option<BoundState> {
match &state.motion {
AnimationMotion::Clip(clip_ref) => {
let (handle, duration) = resolve_clip(&state.name, clip_ref, files, clips)?;
let node = graph.add_clip(handle, 1.0, graph.root);
Some(BoundState {
node,
blend: false,
parameter: String::new(),
looped: state.looped,
speed: state.speed,
clips: vec![BoundClip {
node,
threshold: 0.0,
duration,
source: clip_ref.source.clone(),
clip: clip_ref.clip.clone(),
}],
})
}
AnimationMotion::Blend1d { parameter, points } => {
let blend = graph.add_blend(0.0, graph.root);
let mut bound: Vec<BoundClip> = points
.iter()
.filter_map(|point| {
let (handle, duration) = resolve_clip(&state.name, &point.clip, files, clips)?;
Some(BoundClip {
node: graph.add_clip(handle, 1.0, blend),
threshold: point.threshold,
duration,
source: point.clip.source.clone(),
clip: point.clip.clip.clone(),
})
})
.collect();
if bound.is_empty() {
warn!("animation state `{}` blends no clips", state.name);
return None;
}
bound.sort_by(|a, b| a.threshold.total_cmp(&b.threshold));
Some(BoundState {
node: blend,
blend: true,
parameter: parameter.clone(),
looped: state.looped,
speed: state.speed,
clips: bound,
})
}
}
}
fn resolve_clip(
state: &str,
clip_ref: &AnimationClipRef,
files: &HashMap<&str, &Gltf>,
clips: &Assets<AnimationClip>,
) -> Option<(Handle<AnimationClip>, f32)> {
let Some(gltf) = files.get(clip_ref.source.as_str()) else {
warn!(
"animation state `{state}` wants clips from `{}`, which the graph does not hold",
clip_ref.source
);
return None;
};
let Some(handle) = gltf.named_animations.get(clip_ref.clip.as_str()) else {
let available: Vec<&str> = gltf.named_animations.keys().map(|name| &**name).collect();
warn!(
"animation state `{state}` wants clip `{}`, and its source holds {available:?}",
clip_ref.clip
);
return None;
};
let duration = clips.get(handle).map_or(0.0, AnimationClip::duration);
Some((handle.clone(), duration))
}
fn enter_state(
bound: &BoundState,
crossfade: Duration,
player: &mut AnimationPlayer,
transitions: &mut AnimationTransitions,
) {
let repeat = if bound.looped {
RepeatAnimation::Forever
} else {
RepeatAnimation::Never
};
let active = transitions.play(player, bound.node, crossfade);
active.set_repeat(repeat).set_speed(bound.speed);
if bound.blend {
active.set_weight(if crossfade.is_zero() { 1.0 } else { 0.0 });
}
}
fn pick_transition(
def: &AnimationGraphDef,
bound: &AnimationGraphBound,
params: &AnimationParams,
current: &str,
normalized: f32,
) -> Option<usize> {
def.transitions.iter().position(|transition| {
(transition.from.is_empty() || transition.from == current)
&& transition.to != current
&& bound.by_name.contains_key(&transition.to)
&& transition
.exit_time
.is_none_or(|exit| normalized >= exit - EQUAL_EPSILON)
&& conditions_hold(def, params, transition)
})
}
fn conditions_hold(
def: &AnimationGraphDef,
params: &AnimationParams,
transition: &AnimationTransitionDef,
) -> bool {
if transition.conditions.is_empty() {
return true;
}
let mut holds = transition.conditions.iter().map(|condition| {
if parameter_kind(def, &condition.parameter) == AnimationParameterKind::Trigger {
params.triggered(&condition.parameter)
} else {
condition
.op
.holds(params.float(&condition.parameter), condition.value)
}
});
if transition.require_all {
holds.all(|held| held)
} else {
holds.any(|held| held)
}
}
fn parameter_kind(def: &AnimationGraphDef, name: &str) -> AnimationParameterKind {
def.parameters
.iter()
.find(|parameter| parameter.name == name)
.map_or(AnimationParameterKind::Float, |parameter| parameter.kind)
}
fn normalized_time(player: &AnimationPlayer, bound: &BoundState) -> f32 {
let Some(clip) = leading_clip(player, bound) else {
return 1.0;
};
let Some(active) = player.animation(clip.node) else {
return 1.0;
};
if active.is_finished() || active.just_completed() || clip.duration <= 0.0 {
return 1.0;
}
(active.seek_time() / clip.duration).clamp(0.0, 1.0)
}
fn leading_clip<'a>(player: &AnimationPlayer, bound: &'a BoundState) -> Option<&'a BoundClip> {
bound
.clips
.iter()
.max_by(|a, b| clip_weight(player, a).total_cmp(&clip_weight(player, b)))
}
fn clip_weight(player: &AnimationPlayer, clip: &BoundClip) -> f32 {
player
.animation(clip.node)
.map_or(0.0, ActiveAnimation::weight)
}
fn ran_out(player: &AnimationPlayer, bound: &BoundState) -> bool {
bound.clips.iter().any(|clip| {
player
.animation(clip.node)
.is_some_and(|active| active.just_completed() && active.is_finished())
})
}
fn write_weights(
bound: &AnimationGraphBound,
params: &AnimationParams,
player: &mut AnimationPlayer,
graphs: &mut Assets<AnimationGraph>,
) {
let mut shares: Vec<(AnimationNodeIndex, f32)> = Vec::new();
for state in bound.states.iter().filter(|state| state.blend) {
shares.push((
state.node,
player
.animation(state.node)
.map_or(0.0, ActiveAnimation::weight),
));
let mix = blend_weights(&state.clips, params.float(&state.parameter));
for (clip, weight) in state.clips.iter().zip(mix) {
if let Some(active) = player.animation_mut(clip.node) {
active.set_weight(weight);
}
}
}
if shares.is_empty() {
return;
}
let Some(graph) = graphs.get_mut_untracked(&bound.graph) else {
return;
};
for (index, share) in shares {
if let Some(node) = graph.get_mut(index)
&& (node.weight - share).abs() > WEIGHT_EPSILON
{
node.weight = share;
}
}
}
fn blend_weights(clips: &[BoundClip], value: f32) -> Vec<f32> {
let mut weights = vec![0.0; clips.len()];
let Some(last) = clips.len().checked_sub(1) else {
return weights;
};
if value <= clips[0].threshold {
weights[0] = 1.0;
return weights;
}
if value >= clips[last].threshold {
weights[last] = 1.0;
return weights;
}
for (index, pair) in clips.windows(2).enumerate() {
let (low, high) = (pair[0].threshold, pair[1].threshold);
if value < low || value > high {
continue;
}
let span = high - low;
let toward = if span > 0.0 {
(value - low) / span
} else {
1.0
};
weights[index] = 1.0 - toward;
weights[index + 1] = toward;
break;
}
weights
}