aac.AnimationStore(folder) seeds a clip from an existing .anim asset and returns a normal ClipBuilder, so pre-made clips work anywhere generated clips do and can still be edited (looping, keyframes, toggles, blend shapes). The Unity side clones the asset into the container and applies the script's changes to the clone, leaving the original untouched. - ClipData gains optional source; looping becomes Option<bool> so an untouched store clip keeps the asset's own looping setting. - Add rust/examples/animation_store.rhai, a worked store example.
270 lines
7.6 KiB
Rust
270 lines
7.6 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
|
|
/// The complete description of one animator controller, produced entirely by a Rhai script.
|
|
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct ControllerGraph {
|
|
pub system_name: String,
|
|
pub asset_key: String,
|
|
pub parameters: Vec<Parameter>,
|
|
pub clips: Vec<ClipData>,
|
|
pub blend_trees: Vec<BlendTreeData>,
|
|
pub controller: Controller,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct Controller {
|
|
pub layers: Vec<Layer>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct Layer {
|
|
/// The layer suffix as written in the script. Unity's layer name is derived from it by the C# side.
|
|
pub name: String,
|
|
pub state_machine: StateMachine,
|
|
}
|
|
|
|
/// Grid position in Unity's animator graph. Serialized as an object so C# can bind it directly.
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct GridPos {
|
|
pub x: i32,
|
|
pub y: i32,
|
|
}
|
|
|
|
impl GridPos {
|
|
pub fn new(x: i32, y: i32) -> Self {
|
|
Self { x, y }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct StateMachine {
|
|
/// `None` for a layer's root machine, `Some` for a sub-state machine.
|
|
pub name: Option<String>,
|
|
pub position: GridPos,
|
|
pub states: Vec<State>,
|
|
pub sub_machines: Vec<StateMachine>,
|
|
pub any_state_transitions: Vec<Transition>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct State {
|
|
pub name: String,
|
|
pub position: GridPos,
|
|
pub motion: Option<MotionRef>,
|
|
pub transitions: Vec<Transition>,
|
|
}
|
|
|
|
/// References to entries in `ControllerGraph::clips` / `ControllerGraph::blend_trees`.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum MotionRef {
|
|
Clip { name: String },
|
|
BlendTree { name: String },
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct Transition {
|
|
pub to: String,
|
|
pub conditions: Vec<Condition>,
|
|
pub has_exit_time: bool,
|
|
pub exit_time: f32,
|
|
pub duration: f32,
|
|
pub ordered_interruption: bool,
|
|
pub source_interruption: bool,
|
|
pub can_transition_to_self: bool,
|
|
}
|
|
|
|
impl Transition {
|
|
/// Mirrors `AacDefaultsProvider.ConfigureTransition`, so unspecified fields match library defaults.
|
|
pub fn new(to: impl Into<String>) -> Self {
|
|
Self {
|
|
to: to.into(),
|
|
conditions: Vec::new(),
|
|
has_exit_time: false,
|
|
exit_time: 0.0,
|
|
duration: 0.0,
|
|
ordered_interruption: true,
|
|
source_interruption: false,
|
|
can_transition_to_self: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct Condition {
|
|
pub parameter: String,
|
|
pub mode: CondMode,
|
|
pub threshold: f32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum CondMode {
|
|
Greater,
|
|
Less,
|
|
Equals,
|
|
NotEqual,
|
|
If,
|
|
IfNot,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct ClipData {
|
|
pub name: String,
|
|
/// `None` for a store clip that leaves the source asset's looping setting alone.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub looping: Option<bool>,
|
|
pub curves: Vec<Curve>,
|
|
/// Set when the clip is seeded from an asset (see `AnimationStore`): the project-relative asset
|
|
/// path. Omitted for generated clips so their JSON stays unchanged.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub source: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct Curve {
|
|
pub path: String,
|
|
pub target: TargetType,
|
|
pub property: String,
|
|
pub keys: Vec<Keyframe>,
|
|
}
|
|
|
|
/// The Unity component a curve binds to. Closed set: an unknown property is an error, not a guess.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum TargetType {
|
|
GameObject,
|
|
SkinnedMeshRenderer,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
pub struct Keyframe {
|
|
pub time: f32,
|
|
pub value: f32,
|
|
pub in_tangent: f32,
|
|
pub out_tangent: f32,
|
|
}
|
|
|
|
impl Keyframe {
|
|
pub fn linear(time: f32, value: f32) -> Self {
|
|
Self {
|
|
time,
|
|
value,
|
|
in_tangent: 0.0,
|
|
out_tangent: 0.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct BlendTreeData {
|
|
pub name: String,
|
|
pub blend_type: BlendType,
|
|
pub param_x: String,
|
|
pub param_y: Option<String>,
|
|
pub children: Vec<BlendChild>,
|
|
pub use_automatic_thresholds: bool,
|
|
}
|
|
|
|
/// `rename_all = "snake_case"` would turn `SimpleDirectional2D` into `simple_directional2_d`,
|
|
/// so every variant is renamed explicitly.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
pub enum BlendType {
|
|
#[serde(rename = "simple_1d")]
|
|
Simple1D,
|
|
#[serde(rename = "simple_directional_2d")]
|
|
SimpleDirectional2D,
|
|
#[serde(rename = "freeform_directional_2d")]
|
|
FreeformDirectional2D,
|
|
#[serde(rename = "freeform_cartesian_2d")]
|
|
FreeformCartesian2D,
|
|
#[serde(rename = "direct")]
|
|
Direct,
|
|
}
|
|
|
|
impl BlendType {
|
|
pub fn is_2d(self) -> bool {
|
|
matches!(
|
|
self,
|
|
BlendType::SimpleDirectional2D
|
|
| BlendType::FreeformDirectional2D
|
|
| BlendType::FreeformCartesian2D
|
|
)
|
|
}
|
|
|
|
/// Blend trees that place children by threshold rather than by a direct blend parameter.
|
|
pub fn uses_thresholds(self) -> bool {
|
|
self != BlendType::Direct
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct BlendChild {
|
|
pub motion: MotionRef,
|
|
pub threshold: f32,
|
|
pub threshold_y: Option<f32>,
|
|
/// Only meaningful on `Direct` blend trees.
|
|
pub direct_param: Option<String>,
|
|
}
|
|
|
|
impl BlendChild {
|
|
pub fn motion(motion: MotionRef) -> Self {
|
|
Self {
|
|
motion,
|
|
threshold: 0.0,
|
|
threshold_y: None,
|
|
direct_param: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum Parameter {
|
|
Float { name: String, default: f32 },
|
|
Int { name: String, default: i32 },
|
|
Bool { name: String, default: bool },
|
|
}
|
|
|
|
impl Parameter {
|
|
pub fn name(&self) -> &str {
|
|
match self {
|
|
Parameter::Float { name, .. } | Parameter::Int { name, .. } | Parameter::Bool { name, .. } => name,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Property-name -> component-type inference. Deliberately strict; an unmapped property is an error.
|
|
pub fn infer_target(property: &str) -> Option<TargetType> {
|
|
if property.starts_with("blendShape.") {
|
|
return Some(TargetType::SkinnedMeshRenderer);
|
|
}
|
|
match property {
|
|
"m_IsActive" => Some(TargetType::GameObject),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
impl ClipData {
|
|
/// Find-or-create the curve for one (path, target, property) binding.
|
|
pub fn curve_mut(&mut self, path: &str, target: TargetType, property: &str) -> &mut Curve {
|
|
let index = match self
|
|
.curves
|
|
.iter()
|
|
.position(|c| c.path == path && c.target == target && c.property == property)
|
|
{
|
|
Some(index) => index,
|
|
None => {
|
|
self.curves.push(Curve {
|
|
path: path.to_string(),
|
|
target,
|
|
property: property.to_string(),
|
|
keys: Vec::new(),
|
|
});
|
|
self.curves.len() - 1
|
|
}
|
|
};
|
|
&mut self.curves[index]
|
|
}
|
|
}
|