From 2c5f0dfc05c4b381e18c51aa590d4689a7d1c3b6 Mon Sep 17 00:00:00 2001 From: Tiernan DeFranco <126631791+TiernanDeFranco@users.noreply.github.com> Date: Fri, 8 May 2026 08:48:14 -0700 Subject: [PATCH 1/2] Add panimtree parser baseline and AnimationMixer alias --- perro_source/core/perro_animation/src/lib.rs | 2 + .../core/perro_animation/src/panim_tree.rs | 39 +++ perro_source/core/perro_ids/src/ids.rs | 4 + .../perro_nodes/src/nodes/animation_tree.rs | 285 ++++++++++++++++++ .../core/perro_nodes/src/nodes/mod.rs | 2 + .../perro_nodes/src/nodes/node_registry.rs | 4 +- .../perro_internal_updates/src/lib.rs | 2 + .../src/nodes/animation_tree.rs | 39 +++ .../perro_internal_updates/src/nodes/mod.rs | 2 + 9 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 perro_source/core/perro_animation/src/panim_tree.rs create mode 100644 perro_source/core/perro_nodes/src/nodes/animation_tree.rs create mode 100644 perro_source/runtime_project/perro_internal_updates/src/nodes/animation_tree.rs diff --git a/perro_source/core/perro_animation/src/lib.rs b/perro_source/core/perro_animation/src/lib.rs index 00aae9925..eba8336ae 100644 --- a/perro_source/core/perro_animation/src/lib.rs +++ b/perro_source/core/perro_animation/src/lib.rs @@ -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 { diff --git a/perro_source/core/perro_animation/src/panim_tree.rs b/perro_source/core/perro_animation/src/panim_tree.rs new file mode 100644 index 000000000..452bc52e7 --- /dev/null +++ b/perro_source/core/perro_animation/src/panim_tree.rs @@ -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 { + 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); + } +} diff --git a/perro_source/core/perro_ids/src/ids.rs b/perro_source/core/perro_ids/src/ids.rs index 16258706d..9b3d6acaf 100644 --- a/perro_source/core/perro_ids/src/ids.rs +++ b/perro_source/core/perro_ids/src/ids.rs @@ -171,6 +171,10 @@ define_generational!( AnimationID, "Animation ID - allocated by animation system. Index + generation." ); +define_generational!( + AnimationBlendTreeID, + "Animation blend tree ID - allocated by animation blend tree system. Index + generation." +); define_generational!( LightID, "Light ID — allocated by light system. Index + generation." diff --git a/perro_source/core/perro_nodes/src/nodes/animation_tree.rs b/perro_source/core/perro_nodes/src/nodes/animation_tree.rs new file mode 100644 index 000000000..b0296cdbd --- /dev/null +++ b/perro_source/core/perro_nodes/src/nodes/animation_tree.rs @@ -0,0 +1,285 @@ +use perro_ids::{AnimationBlendTreeID, AnimationID}; +use std::collections::HashMap; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum AnimationTreeSlotState { + Playing, + Paused, + #[default] + Stopped, +} + +#[derive(Clone, Debug, Default)] +pub struct AnimationTreeSlot { + pub animation: AnimationID, + pub time_seconds: f32, + pub speed: f32, + pub state: AnimationTreeSlotState, + pub looping: bool, + pub weight: f32, +} + +#[derive(Clone, Debug, Default)] +pub struct AnimationTreeGraph { + pub nodes: Vec, + pub output: Option, +} + +#[derive(Clone, Debug)] +pub struct AnimationTreeNode { + pub kind: AnimationTreeNodeKind, +} + +#[derive(Clone, Debug)] +pub enum AnimationTreeNodeKind { + SlotRef { + slot_index: usize, + }, + BlendN { + inputs: Vec, + weights: Vec, + }, + AddN { + inputs: Vec, + weights: Vec, + }, + Invert { + input: usize, + }, + Output { + input: usize, + }, +} + +#[derive(Clone, Debug, Default)] +pub struct AnimationTreeInternalData { + pub last_resolved_animation: AnimationID, +} + +#[derive(Clone, Debug)] +pub struct AnimationTree { + pub slots: Vec, + pub graph: AnimationTreeGraph, + pub blend_tree: AnimationBlendTreeID, + pub reverse_slot_lookup: HashMap>, + pub internal: AnimationTreeInternalData, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AnimationTreeError { + InvalidSlot(usize), + InvalidNode(usize), + InvalidNodeInput { node_id: usize, input_index: usize }, +} + +impl AnimationTree { + pub fn new(slot_count: usize) -> Self { + Self { + slots: (0..slot_count) + .map(|_| AnimationTreeSlot { + speed: 1.0, + looping: true, + weight: 1.0, + ..AnimationTreeSlot::default() + }) + .collect(), + graph: AnimationTreeGraph::default(), + blend_tree: AnimationBlendTreeID::nil(), + reverse_slot_lookup: HashMap::new(), + internal: AnimationTreeInternalData::default(), + } + } + + pub fn set_blend_tree(&mut self, blend_tree: AnimationBlendTreeID) { + self.blend_tree = blend_tree; + } + + pub fn set_slot_animation( + &mut self, + slot_index: usize, + animation: AnimationID, + ) -> Result<(), AnimationTreeError> { + let Some(slot) = self.slots.get_mut(slot_index) else { + return Err(AnimationTreeError::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<(), AnimationTreeError> { + self.set_slot_state(slot_index, AnimationTreeSlotState::Playing) + } + pub fn pause_slot(&mut self, slot_index: usize) -> Result<(), AnimationTreeError> { + self.set_slot_state(slot_index, AnimationTreeSlotState::Paused) + } + pub fn stop_slot(&mut self, slot_index: usize) -> Result<(), AnimationTreeError> { + self.set_slot_state(slot_index, AnimationTreeSlotState::Stopped)?; + self.seek_slot(slot_index, 0.0) + } + pub fn seek_slot( + &mut self, + slot_index: usize, + time_seconds: f32, + ) -> Result<(), AnimationTreeError> { + let Some(slot) = self.slots.get_mut(slot_index) else { + return Err(AnimationTreeError::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 = AnimationTreeSlotState::Playing; + } + } + pub fn pause_all(&mut self) { + for slot in &mut self.slots { + slot.state = AnimationTreeSlotState::Paused; + } + } + pub fn stop_all(&mut self) { + for slot in &mut self.slots { + slot.state = AnimationTreeSlotState::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<(), AnimationTreeError> { + let Some(node) = self.graph.nodes.get_mut(node_id) else { + return Err(AnimationTreeError::InvalidNode(node_id)); + }; + match &mut node.kind { + AnimationTreeNodeKind::BlendN { weights, .. } + | AnimationTreeNodeKind::AddN { weights, .. } => { + let Some(input) = weights.get_mut(input_index) else { + return Err(AnimationTreeError::InvalidNodeInput { + node_id, + input_index, + }); + }; + *input = weight; + Ok(()) + } + _ => Err(AnimationTreeError::InvalidNodeInput { + node_id, + input_index, + }), + } + } + + pub fn evaluate_output_animation(&self) -> Result, AnimationTreeError> { + let Some(output) = self.graph.output else { + return Ok(None); + }; + self.eval_node(output) + } + + fn eval_node(&self, node_id: usize) -> Result, AnimationTreeError> { + let Some(node) = self.graph.nodes.get(node_id) else { + return Err(AnimationTreeError::InvalidNode(node_id)); + }; + match &node.kind { + AnimationTreeNodeKind::SlotRef { slot_index } => Ok(self + .slots + .get(*slot_index) + .map(|s| s.animation) + .filter(|id| !id.is_nil())), + AnimationTreeNodeKind::BlendN { inputs, weights } + | AnimationTreeNodeKind::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)) + } + AnimationTreeNodeKind::Invert { input } | AnimationTreeNodeKind::Output { input } => { + self.eval_node(*input) + } + } + } + + fn set_slot_state( + &mut self, + slot_index: usize, + state: AnimationTreeSlotState, + ) -> Result<(), AnimationTreeError> { + let Some(slot) = self.slots.get_mut(slot_index) else { + return Err(AnimationTreeError::InvalidSlot(slot_index)); + }; + slot.state = state; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn animation_tree_core_api() { + let mut tree = AnimationTree::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 AnimationMixer = AnimationTree; diff --git a/perro_source/core/perro_nodes/src/nodes/mod.rs b/perro_source/core/perro_nodes/src/nodes/mod.rs index 221100af5..936f78ce8 100644 --- a/perro_source/core/perro_nodes/src/nodes/mod.rs +++ b/perro_source/core/perro_nodes/src/nodes/mod.rs @@ -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::*; diff --git a/perro_source/core/perro_nodes/src/nodes/node_registry.rs b/perro_source/core/perro_nodes/src/nodes/node_registry.rs index f18d0cc3e..d00c3acc9 100644 --- a/perro_source/core/perro_nodes/src/nodes/node_registry.rs +++ b/perro_source/core/perro_nodes/src/nodes/node_registry.rs @@ -1,5 +1,6 @@ use crate::ambient_light_3d::AmbientLight3D; use crate::animation_player::AnimationPlayer; +use crate::animation_tree::AnimationTree; use crate::camera_2d::Camera2D; use crate::camera_3d::Camera3D; use crate::mesh_instance_3d::MeshInstance3D; @@ -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), + AnimationTree => (None, AnimationTree, Renderable::False, InternalUpdate::True, InternalFixedUpdate::False) } } diff --git a/perro_source/runtime_project/perro_internal_updates/src/lib.rs b/perro_source/runtime_project/perro_internal_updates/src/lib.rs index d2a36f080..e6c08fa9c 100644 --- a/perro_source/runtime_project/perro_internal_updates/src/lib.rs +++ b/perro_source/runtime_project/perro_internal_updates/src/lib.rs @@ -13,6 +13,7 @@ pub fn internal_update_node( 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); } @@ -27,5 +28,6 @@ pub fn internal_fixed_update_node( 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); } diff --git a/perro_source/runtime_project/perro_internal_updates/src/nodes/animation_tree.rs b/perro_source/runtime_project/perro_internal_updates/src/nodes/animation_tree.rs new file mode 100644 index 000000000..723a4a5ea --- /dev/null +++ b/perro_source/runtime_project/perro_internal_updates/src/nodes/animation_tree.rs @@ -0,0 +1,39 @@ +use crate::prelude::*; +use perro_nodes::{AnimationTree, AnimationTreeSlotState}; + +type SelfNodeType = AnimationTree; + +pub fn internal_update( + ctx: &mut RuntimeWindow<'_, RT>, + _res: &ResourceWindow<'_, R>, + _ipt_w: &InputWindow<'_, IP>, + id: NodeID, +) where + RT: RuntimeAPI + ?Sized, + R: ResourceAPI + ?Sized, + IP: InputAPI + ?Sized, +{ + let delta = delta_time!(ctx).max(0.0); + with_node_mut!(ctx, SelfNodeType, id, |tree| { + for slot in &mut tree.slots { + if slot.state == AnimationTreeSlotState::Playing { + slot.time_seconds += delta * slot.speed; + } + } + if let Ok(Some(animation)) = tree.evaluate_output_animation() { + tree.internal.last_resolved_animation = animation; + } + }); +} + +pub fn internal_fixed_update( + _run: &mut RuntimeWindow<'_, RT>, + _res_w: &ResourceWindow<'_, R>, + _ipt_w: &InputWindow<'_, IP>, + _id: NodeID, +) where + RT: RuntimeAPI + ?Sized, + R: ResourceAPI + ?Sized, + IP: InputAPI + ?Sized, +{ +} diff --git a/perro_source/runtime_project/perro_internal_updates/src/nodes/mod.rs b/perro_source/runtime_project/perro_internal_updates/src/nodes/mod.rs index 07057c0dd..d33ec2eca 100644 --- a/perro_source/runtime_project/perro_internal_updates/src/nodes/mod.rs +++ b/perro_source/runtime_project/perro_internal_updates/src/nodes/mod.rs @@ -1,2 +1,4 @@ pub mod animation_player; pub mod particle_emitter_3d; + +pub mod animation_tree; From 87a5a697c1967a7d9e44dc3b1d09b42da30a03ee Mon Sep 17 00:00:00 2001 From: Tiernan DeFranco <126631791+TiernanDeFranco@users.noreply.github.com> Date: Fri, 8 May 2026 08:48:29 -0700 Subject: [PATCH 2/2] Refocus animation mixer naming and panimmix scene field plumbing --- perro_source/core/perro_ids/src/ids.rs | 4 +- .../perro_nodes/src/nodes/animation_tree.rs | 110 +++++++++--------- .../perro_nodes/src/nodes/node_registry.rs | 4 +- .../src/nodes/animation_tree.rs | 6 +- .../prepare/nodes/three_d/animation.rs | 19 +++ .../perro_scene/src/node_fields.rs | 10 ++ 6 files changed, 91 insertions(+), 62 deletions(-) diff --git a/perro_source/core/perro_ids/src/ids.rs b/perro_source/core/perro_ids/src/ids.rs index 9b3d6acaf..3d16b03fa 100644 --- a/perro_source/core/perro_ids/src/ids.rs +++ b/perro_source/core/perro_ids/src/ids.rs @@ -172,8 +172,8 @@ define_generational!( "Animation ID - allocated by animation system. Index + generation." ); define_generational!( - AnimationBlendTreeID, - "Animation blend tree ID - allocated by animation blend tree system. Index + generation." + AnimationMixClipID, + "Animation mix clip ID - allocated by animation mix system. Index + generation." ); define_generational!( LightID, diff --git a/perro_source/core/perro_nodes/src/nodes/animation_tree.rs b/perro_source/core/perro_nodes/src/nodes/animation_tree.rs index b0296cdbd..5df4d51d9 100644 --- a/perro_source/core/perro_nodes/src/nodes/animation_tree.rs +++ b/perro_source/core/perro_nodes/src/nodes/animation_tree.rs @@ -1,8 +1,8 @@ -use perro_ids::{AnimationBlendTreeID, AnimationID}; +use perro_ids::{AnimationID, AnimationMixClipID}; use std::collections::HashMap; #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] -pub enum AnimationTreeSlotState { +pub enum AnimationMixerSlotState { Playing, Paused, #[default] @@ -10,28 +10,28 @@ pub enum AnimationTreeSlotState { } #[derive(Clone, Debug, Default)] -pub struct AnimationTreeSlot { +pub struct AnimationMixerSlot { pub animation: AnimationID, pub time_seconds: f32, pub speed: f32, - pub state: AnimationTreeSlotState, + pub state: AnimationMixerSlotState, pub looping: bool, pub weight: f32, } #[derive(Clone, Debug, Default)] -pub struct AnimationTreeGraph { - pub nodes: Vec, +pub struct AnimationMixerGraph { + pub nodes: Vec, pub output: Option, } #[derive(Clone, Debug)] -pub struct AnimationTreeNode { - pub kind: AnimationTreeNodeKind, +pub struct AnimationMixerNode { + pub kind: AnimationMixerNodeKind, } #[derive(Clone, Debug)] -pub enum AnimationTreeNodeKind { +pub enum AnimationMixerNodeKind { SlotRef { slot_index: usize, }, @@ -52,55 +52,55 @@ pub enum AnimationTreeNodeKind { } #[derive(Clone, Debug, Default)] -pub struct AnimationTreeInternalData { +pub struct AnimationMixerInternalData { pub last_resolved_animation: AnimationID, } #[derive(Clone, Debug)] -pub struct AnimationTree { - pub slots: Vec, - pub graph: AnimationTreeGraph, - pub blend_tree: AnimationBlendTreeID, +pub struct AnimationMixer { + pub slots: Vec, + pub graph: AnimationMixerGraph, + pub blend_tree: AnimationMixClipID, pub reverse_slot_lookup: HashMap>, - pub internal: AnimationTreeInternalData, + pub internal: AnimationMixerInternalData, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum AnimationTreeError { +pub enum AnimationMixerError { InvalidSlot(usize), InvalidNode(usize), InvalidNodeInput { node_id: usize, input_index: usize }, } -impl AnimationTree { +impl AnimationMixer { pub fn new(slot_count: usize) -> Self { Self { slots: (0..slot_count) - .map(|_| AnimationTreeSlot { + .map(|_| AnimationMixerSlot { speed: 1.0, looping: true, weight: 1.0, - ..AnimationTreeSlot::default() + ..AnimationMixerSlot::default() }) .collect(), - graph: AnimationTreeGraph::default(), - blend_tree: AnimationBlendTreeID::nil(), + graph: AnimationMixerGraph::default(), + blend_tree: AnimationMixClipID::nil(), reverse_slot_lookup: HashMap::new(), - internal: AnimationTreeInternalData::default(), + internal: AnimationMixerInternalData::default(), } } - pub fn set_blend_tree(&mut self, blend_tree: AnimationBlendTreeID) { - self.blend_tree = blend_tree; + 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<(), AnimationTreeError> { + ) -> Result<(), AnimationMixerError> { let Some(slot) = self.slots.get_mut(slot_index) else { - return Err(AnimationTreeError::InvalidSlot(slot_index)); + return Err(AnimationMixerError::InvalidSlot(slot_index)); }; if !slot.animation.is_nil() { if let Some(v) = self.reverse_slot_lookup.get_mut(&slot.animation) { @@ -121,23 +121,23 @@ impl AnimationTree { .map_or(&[], |v| v.as_slice()) } - pub fn play_slot(&mut self, slot_index: usize) -> Result<(), AnimationTreeError> { - self.set_slot_state(slot_index, AnimationTreeSlotState::Playing) + 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<(), AnimationTreeError> { - self.set_slot_state(slot_index, AnimationTreeSlotState::Paused) + 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<(), AnimationTreeError> { - self.set_slot_state(slot_index, AnimationTreeSlotState::Stopped)?; + 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<(), AnimationTreeError> { + ) -> Result<(), AnimationMixerError> { let Some(slot) = self.slots.get_mut(slot_index) else { - return Err(AnimationTreeError::InvalidSlot(slot_index)); + return Err(AnimationMixerError::InvalidSlot(slot_index)); }; slot.time_seconds = time_seconds.max(0.0); Ok(()) @@ -145,17 +145,17 @@ impl AnimationTree { pub fn play_all(&mut self) { for slot in &mut self.slots { - slot.state = AnimationTreeSlotState::Playing; + slot.state = AnimationMixerSlotState::Playing; } } pub fn pause_all(&mut self) { for slot in &mut self.slots { - slot.state = AnimationTreeSlotState::Paused; + slot.state = AnimationMixerSlotState::Paused; } } pub fn stop_all(&mut self) { for slot in &mut self.slots { - slot.state = AnimationTreeSlotState::Stopped; + slot.state = AnimationMixerSlotState::Stopped; slot.time_seconds = 0.0; } } @@ -181,15 +181,15 @@ impl AnimationTree { node_id: usize, input_index: usize, weight: f32, - ) -> Result<(), AnimationTreeError> { + ) -> Result<(), AnimationMixerError> { let Some(node) = self.graph.nodes.get_mut(node_id) else { - return Err(AnimationTreeError::InvalidNode(node_id)); + return Err(AnimationMixerError::InvalidNode(node_id)); }; match &mut node.kind { - AnimationTreeNodeKind::BlendN { weights, .. } - | AnimationTreeNodeKind::AddN { weights, .. } => { + AnimationMixerNodeKind::BlendN { weights, .. } + | AnimationMixerNodeKind::AddN { weights, .. } => { let Some(input) = weights.get_mut(input_index) else { - return Err(AnimationTreeError::InvalidNodeInput { + return Err(AnimationMixerError::InvalidNodeInput { node_id, input_index, }); @@ -197,32 +197,32 @@ impl AnimationTree { *input = weight; Ok(()) } - _ => Err(AnimationTreeError::InvalidNodeInput { + _ => Err(AnimationMixerError::InvalidNodeInput { node_id, input_index, }), } } - pub fn evaluate_output_animation(&self) -> Result, AnimationTreeError> { + pub fn evaluate_output_animation(&self) -> Result, AnimationMixerError> { let Some(output) = self.graph.output else { return Ok(None); }; self.eval_node(output) } - fn eval_node(&self, node_id: usize) -> Result, AnimationTreeError> { + fn eval_node(&self, node_id: usize) -> Result, AnimationMixerError> { let Some(node) = self.graph.nodes.get(node_id) else { - return Err(AnimationTreeError::InvalidNode(node_id)); + return Err(AnimationMixerError::InvalidNode(node_id)); }; match &node.kind { - AnimationTreeNodeKind::SlotRef { slot_index } => Ok(self + AnimationMixerNodeKind::SlotRef { slot_index } => Ok(self .slots .get(*slot_index) .map(|s| s.animation) .filter(|id| !id.is_nil())), - AnimationTreeNodeKind::BlendN { inputs, weights } - | AnimationTreeNodeKind::AddN { inputs, weights } => { + AnimationMixerNodeKind::BlendN { inputs, weights } + | AnimationMixerNodeKind::AddN { inputs, weights } => { if inputs.is_empty() { return Ok(None); } @@ -238,7 +238,7 @@ impl AnimationTree { } Ok(best.map(|v| v.0)) } - AnimationTreeNodeKind::Invert { input } | AnimationTreeNodeKind::Output { input } => { + AnimationMixerNodeKind::Invert { input } | AnimationMixerNodeKind::Output { input } => { self.eval_node(*input) } } @@ -247,10 +247,10 @@ impl AnimationTree { fn set_slot_state( &mut self, slot_index: usize, - state: AnimationTreeSlotState, - ) -> Result<(), AnimationTreeError> { + state: AnimationMixerSlotState, + ) -> Result<(), AnimationMixerError> { let Some(slot) = self.slots.get_mut(slot_index) else { - return Err(AnimationTreeError::InvalidSlot(slot_index)); + return Err(AnimationMixerError::InvalidSlot(slot_index)); }; slot.state = state; Ok(()) @@ -263,7 +263,7 @@ mod tests { #[test] fn animation_tree_core_api() { - let mut tree = AnimationTree::new(3); + let mut tree = AnimationMixer::new(3); assert_eq!(tree.slots.len(), 3); tree.set_slot_animation(0, AnimationID::from_u32(10)) .unwrap(); @@ -282,4 +282,4 @@ mod tests { } } -pub type AnimationMixer = AnimationTree; +pub type AnimationTree = AnimationMixer; diff --git a/perro_source/core/perro_nodes/src/nodes/node_registry.rs b/perro_source/core/perro_nodes/src/nodes/node_registry.rs index d00c3acc9..bd4867615 100644 --- a/perro_source/core/perro_nodes/src/nodes/node_registry.rs +++ b/perro_source/core/perro_nodes/src/nodes/node_registry.rs @@ -1,6 +1,6 @@ use crate::ambient_light_3d::AmbientLight3D; use crate::animation_player::AnimationPlayer; -use crate::animation_tree::AnimationTree; +use crate::animation_tree::AnimationMixer; use crate::camera_2d::Camera2D; use crate::camera_3d::Camera3D; use crate::mesh_instance_3d::MeshInstance3D; @@ -846,6 +846,6 @@ define_scene_nodes! { } resource: { AnimationPlayer => (None, AnimationPlayer, Renderable::False, InternalUpdate::True, InternalFixedUpdate::False), - AnimationTree => (None, AnimationTree, Renderable::False, InternalUpdate::True, InternalFixedUpdate::False) + AnimationMixer => (None, AnimationMixer, Renderable::False, InternalUpdate::True, InternalFixedUpdate::False) } } diff --git a/perro_source/runtime_project/perro_internal_updates/src/nodes/animation_tree.rs b/perro_source/runtime_project/perro_internal_updates/src/nodes/animation_tree.rs index 723a4a5ea..a2d8953ba 100644 --- a/perro_source/runtime_project/perro_internal_updates/src/nodes/animation_tree.rs +++ b/perro_source/runtime_project/perro_internal_updates/src/nodes/animation_tree.rs @@ -1,7 +1,7 @@ use crate::prelude::*; -use perro_nodes::{AnimationTree, AnimationTreeSlotState}; +use perro_nodes::{AnimationMixer, AnimationMixerSlotState}; -type SelfNodeType = AnimationTree; +type SelfNodeType = AnimationMixer; pub fn internal_update( ctx: &mut RuntimeWindow<'_, RT>, @@ -16,7 +16,7 @@ pub fn internal_update( let delta = delta_time!(ctx).max(0.0); with_node_mut!(ctx, SelfNodeType, id, |tree| { for slot in &mut tree.slots { - if slot.state == AnimationTreeSlotState::Playing { + if slot.state == AnimationMixerSlotState::Playing { slot.time_seconds += delta * slot.speed; } } diff --git a/perro_source/runtime_project/perro_runtime/src/runtime/scene_loader/prepare/nodes/three_d/animation.rs b/perro_source/runtime_project/perro_runtime/src/runtime/scene_loader/prepare/nodes/three_d/animation.rs index e0700dc3f..499c66815 100644 --- a/perro_source/runtime_project/perro_runtime/src/runtime/scene_loader/prepare/nodes/three_d/animation.rs +++ b/perro_source/runtime_project/perro_runtime/src/runtime/scene_loader/prepare/nodes/three_d/animation.rs @@ -93,3 +93,22 @@ fn parse_animation_bindings(value: &SceneValue) -> Option> Some(out) } + + +fn build_animation_mixer(data: &SceneDefNodeData) -> AnimationMixer { + let mut node = AnimationMixer::new(0); + if let Some(source) = extract_animation_mixer_source(data) { + node.set_mix_clip(perro_ids::AnimationMixClipID::from_u64(perro_ids::string_to_u64(&source))); + } + node +} + +fn extract_animation_mixer_source(data: &SceneDefNodeData) -> Option { + if data.ty != "AnimationMixer" { return None; } + data.fields.iter().find_map(|(name, value)| { + (resolve_node_field("AnimationMixer", name) + == Some(NodeField::AnimationMixer(AnimationMixerField::Mixer))) + .then(|| as_asset_source(value)) + .flatten() + }) +} diff --git a/perro_source/runtime_project/perro_scene/src/node_fields.rs b/perro_source/runtime_project/perro_scene/src/node_fields.rs index a071690ab..078a017b9 100644 --- a/perro_source/runtime_project/perro_scene/src/node_fields.rs +++ b/perro_source/runtime_project/perro_scene/src/node_fields.rs @@ -16,6 +16,7 @@ pub enum NodeField { Camera3D(Camera3DField), ParticleEmitter3D(ParticleEmitter3DField), AnimationPlayer(AnimationPlayerField), + AnimationMixer(AnimationMixerField), Light3D(Light3DField), Sky3D(Sky3DField), RayLight3D(RayLight3DField), @@ -147,6 +148,11 @@ pub enum AnimationPlayerField { Playback, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AnimationMixerField { + Mixer, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Light3DField { Color, @@ -362,6 +368,10 @@ pub fn resolve_node_field(node_type_name: &str, field: &str) -> Option Some(NodeField::AnimationPlayer(AnimationPlayerField::Playback)), _ => None, }, + NodeType::AnimationMixer => match field { + "mixer" => Some(NodeField::AnimationMixer(AnimationMixerField::Mixer)), + _ => None, + }, NodeType::AmbientLight3D => resolve_light3d_common(field).map(NodeField::Light3D), NodeType::Sky3D => resolve_sky3d_field(field).map(NodeField::Sky3D), NodeType::RayLight3D => match field {