Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions perro_source/core/perro_animation/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use perro_scene::{Node3DField, NodeField};
use std::borrow::Cow;
mod panim;
mod panim_tree;
pub use panim::parse_panim;
pub use panim_tree::{AnimationBlendTreeDef, parse_panimtree};

#[derive(Clone, Debug, Default)]
pub struct AnimationClip {
Expand Down
39 changes: 39 additions & 0 deletions perro_source/core/perro_animation/src/panim_tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use std::borrow::Cow;

#[derive(Clone, Debug, Default)]
pub struct AnimationBlendTreeDef {
pub slots: Cow<'static, [Cow<'static, str>]>,
}

pub fn parse_panimtree(source: &str) -> Result<AnimationBlendTreeDef, String> {
let mut slots = Vec::new();
for line in source.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((k, v)) = line.split_once('=')
&& k.trim() == "slot"
{
slots.push(Cow::Owned(v.trim().to_string()));
}
}
if slots.is_empty() {
return Err("panimtree contains no slots".to_string());
}
Ok(AnimationBlendTreeDef {
slots: Cow::Owned(slots),
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parse_slots() {
let src = "slot = Idle\nslot = Walk\nslot = Run";
let def = parse_panimtree(src).unwrap();
assert_eq!(def.slots.len(), 3);
}
}
4 changes: 4 additions & 0 deletions perro_source/core/perro_ids/src/ids.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ define_generational!(
AnimationID,
"Animation ID - allocated by animation system. Index + generation."
);
define_generational!(
AnimationMixClipID,
"Animation mix clip ID - allocated by animation mix system. Index + generation."
);
define_generational!(
LightID,
"Light ID — allocated by light system. Index + generation."
Expand Down
285 changes: 285 additions & 0 deletions perro_source/core/perro_nodes/src/nodes/animation_tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
use perro_ids::{AnimationID, AnimationMixClipID};
use std::collections::HashMap;

#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum AnimationMixerSlotState {
Playing,
Paused,
#[default]
Stopped,
}

#[derive(Clone, Debug, Default)]
pub struct AnimationMixerSlot {
pub animation: AnimationID,
pub time_seconds: f32,
pub speed: f32,
pub state: AnimationMixerSlotState,
pub looping: bool,
pub weight: f32,
}

#[derive(Clone, Debug, Default)]
pub struct AnimationMixerGraph {
pub nodes: Vec<AnimationMixerNode>,
pub output: Option<usize>,
}

#[derive(Clone, Debug)]
pub struct AnimationMixerNode {
pub kind: AnimationMixerNodeKind,
}

#[derive(Clone, Debug)]
pub enum AnimationMixerNodeKind {
SlotRef {
slot_index: usize,
},
BlendN {
inputs: Vec<usize>,
weights: Vec<f32>,
},
AddN {
inputs: Vec<usize>,
weights: Vec<f32>,
},
Invert {
input: usize,
},
Output {
input: usize,
},
}

#[derive(Clone, Debug, Default)]
pub struct AnimationMixerInternalData {
pub last_resolved_animation: AnimationID,
}

#[derive(Clone, Debug)]
pub struct AnimationMixer {
pub slots: Vec<AnimationMixerSlot>,
pub graph: AnimationMixerGraph,
pub blend_tree: AnimationMixClipID,
pub reverse_slot_lookup: HashMap<AnimationID, Vec<usize>>,
pub internal: AnimationMixerInternalData,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AnimationMixerError {
InvalidSlot(usize),
InvalidNode(usize),
InvalidNodeInput { node_id: usize, input_index: usize },
}

impl AnimationMixer {
pub fn new(slot_count: usize) -> Self {
Self {
slots: (0..slot_count)
.map(|_| AnimationMixerSlot {
speed: 1.0,
looping: true,
weight: 1.0,
..AnimationMixerSlot::default()
})
.collect(),
graph: AnimationMixerGraph::default(),
blend_tree: AnimationMixClipID::nil(),
reverse_slot_lookup: HashMap::new(),
internal: AnimationMixerInternalData::default(),
}
}

pub fn set_mix_clip(&mut self, mix_clip: AnimationMixClipID) {
self.blend_tree = mix_clip;
}

pub fn set_slot_animation(
&mut self,
slot_index: usize,
animation: AnimationID,
) -> Result<(), AnimationMixerError> {
let Some(slot) = self.slots.get_mut(slot_index) else {
return Err(AnimationMixerError::InvalidSlot(slot_index));
};
if !slot.animation.is_nil() {
if let Some(v) = self.reverse_slot_lookup.get_mut(&slot.animation) {
v.retain(|idx| *idx != slot_index);
}
}
slot.animation = animation;
self.reverse_slot_lookup
.entry(animation)
.or_default()
.push(slot_index);
Ok(())
}

pub fn slot_indices_for_animation(&self, animation: AnimationID) -> &[usize] {
self.reverse_slot_lookup
.get(&animation)
.map_or(&[], |v| v.as_slice())
}

pub fn play_slot(&mut self, slot_index: usize) -> Result<(), AnimationMixerError> {
self.set_slot_state(slot_index, AnimationMixerSlotState::Playing)
}
pub fn pause_slot(&mut self, slot_index: usize) -> Result<(), AnimationMixerError> {
self.set_slot_state(slot_index, AnimationMixerSlotState::Paused)
}
pub fn stop_slot(&mut self, slot_index: usize) -> Result<(), AnimationMixerError> {
self.set_slot_state(slot_index, AnimationMixerSlotState::Stopped)?;
self.seek_slot(slot_index, 0.0)
}
pub fn seek_slot(
&mut self,
slot_index: usize,
time_seconds: f32,
) -> Result<(), AnimationMixerError> {
let Some(slot) = self.slots.get_mut(slot_index) else {
return Err(AnimationMixerError::InvalidSlot(slot_index));
};
slot.time_seconds = time_seconds.max(0.0);
Ok(())
}

pub fn play_all(&mut self) {
for slot in &mut self.slots {
slot.state = AnimationMixerSlotState::Playing;
}
}
pub fn pause_all(&mut self) {
for slot in &mut self.slots {
slot.state = AnimationMixerSlotState::Paused;
}
}
pub fn stop_all(&mut self) {
for slot in &mut self.slots {
slot.state = AnimationMixerSlotState::Stopped;
slot.time_seconds = 0.0;
}
}

pub fn play_animation(&mut self, animation: AnimationID) {
for idx in self.slot_indices_for_animation(animation).to_vec() {
let _ = self.play_slot(idx);
}
}
pub fn pause_animation(&mut self, animation: AnimationID) {
for idx in self.slot_indices_for_animation(animation).to_vec() {
let _ = self.pause_slot(idx);
}
}
pub fn seek_animation(&mut self, animation: AnimationID, time_seconds: f32) {
for idx in self.slot_indices_for_animation(animation).to_vec() {
let _ = self.seek_slot(idx, time_seconds);
}
}

pub fn set_blend_weight(
&mut self,
node_id: usize,
input_index: usize,
weight: f32,
) -> Result<(), AnimationMixerError> {
let Some(node) = self.graph.nodes.get_mut(node_id) else {
return Err(AnimationMixerError::InvalidNode(node_id));
};
match &mut node.kind {
AnimationMixerNodeKind::BlendN { weights, .. }
| AnimationMixerNodeKind::AddN { weights, .. } => {
let Some(input) = weights.get_mut(input_index) else {
return Err(AnimationMixerError::InvalidNodeInput {
node_id,
input_index,
});
};
*input = weight;
Ok(())
}
_ => Err(AnimationMixerError::InvalidNodeInput {
node_id,
input_index,
}),
}
}

pub fn evaluate_output_animation(&self) -> Result<Option<AnimationID>, AnimationMixerError> {
let Some(output) = self.graph.output else {
return Ok(None);
};
self.eval_node(output)
}

fn eval_node(&self, node_id: usize) -> Result<Option<AnimationID>, AnimationMixerError> {
let Some(node) = self.graph.nodes.get(node_id) else {
return Err(AnimationMixerError::InvalidNode(node_id));
};
match &node.kind {
AnimationMixerNodeKind::SlotRef { slot_index } => Ok(self
.slots
.get(*slot_index)
.map(|s| s.animation)
.filter(|id| !id.is_nil())),
AnimationMixerNodeKind::BlendN { inputs, weights }
| AnimationMixerNodeKind::AddN { inputs, weights } => {
if inputs.is_empty() {
return Ok(None);
}
let mut best: Option<(AnimationID, f32)> = None;
for (i, input) in inputs.iter().enumerate() {
let Some(anim) = self.eval_node(*input)? else {
continue;
};
let w = *weights.get(i).unwrap_or(&1.0);
if best.as_ref().is_none_or(|(_, bw)| w > *bw) {
best = Some((anim, w));
}
}
Ok(best.map(|v| v.0))
}
AnimationMixerNodeKind::Invert { input } | AnimationMixerNodeKind::Output { input } => {
self.eval_node(*input)
}
}
}

fn set_slot_state(
&mut self,
slot_index: usize,
state: AnimationMixerSlotState,
) -> Result<(), AnimationMixerError> {
let Some(slot) = self.slots.get_mut(slot_index) else {
return Err(AnimationMixerError::InvalidSlot(slot_index));
};
slot.state = state;
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn animation_tree_core_api() {
let mut tree = AnimationMixer::new(3);
assert_eq!(tree.slots.len(), 3);
tree.set_slot_animation(0, AnimationID::from_u32(10))
.unwrap();
tree.set_slot_animation(1, AnimationID::from_u32(11))
.unwrap();
tree.set_slot_animation(2, AnimationID::from_u32(11))
.unwrap();
assert_eq!(
tree.slot_indices_for_animation(AnimationID::from_u32(11)),
&[1, 2]
);
tree.play_slot(1).unwrap();
tree.pause_slot(1).unwrap();
tree.seek_slot(1, 2.5).unwrap();
tree.stop_slot(1).unwrap();
}
}

pub type AnimationTree = AnimationMixer;
2 changes: 2 additions & 0 deletions perro_source/core/perro_nodes/src/nodes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
pub mod animation_player;
pub mod animation_tree;
pub mod node_2d;
pub mod node_3d;
pub mod node_registry;

pub use animation_player::*;
pub use animation_tree::*;
pub use node_2d::*;
pub use node_3d::*;
pub use node_registry::*;
Expand Down
4 changes: 3 additions & 1 deletion perro_source/core/perro_nodes/src/nodes/node_registry.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::ambient_light_3d::AmbientLight3D;
use crate::animation_player::AnimationPlayer;
use crate::animation_tree::AnimationMixer;
use crate::camera_2d::Camera2D;
use crate::camera_3d::Camera3D;
use crate::mesh_instance_3d::MeshInstance3D;
Expand Down Expand Up @@ -844,6 +845,7 @@ define_scene_nodes! {
UiTreeList => (UiBox, UiTreeList, Renderable::False, InternalUpdate::False, InternalFixedUpdate::False)
}
resource: {
AnimationPlayer => (None, AnimationPlayer, Renderable::False, InternalUpdate::True, InternalFixedUpdate::False)
AnimationPlayer => (None, AnimationPlayer, Renderable::False, InternalUpdate::True, InternalFixedUpdate::False),
AnimationMixer => (None, AnimationMixer, Renderable::False, InternalUpdate::True, InternalFixedUpdate::False)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub fn internal_update_node<RT, RS, IP>(
IP: InputAPI + ?Sized,
{
nodes::animation_player::internal_update(ctx, res, ipt, id);
nodes::animation_tree::internal_update(ctx, res, ipt, id);
nodes::particle_emitter_3d::internal_update(ctx, res, ipt, id);
}

Expand All @@ -27,5 +28,6 @@ pub fn internal_fixed_update_node<RT, RS, IP>(
IP: InputAPI + ?Sized,
{
nodes::animation_player::internal_fixed_update(ctx, res, ipt, id);
nodes::animation_tree::internal_fixed_update(ctx, res, ipt, id);
nodes::particle_emitter_3d::internal_fixed_update(ctx, res, ipt, id);
}
Loading
Loading