use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::assets::AssetId;
use crate::tree::NodeId;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ClipLoop {
Loop,
PingPong,
Once,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ClipDirection {
Forward,
Reverse,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Interp {
Step,
Linear,
Cubic,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SamplerKind {
Step,
Linear,
Cubic,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum TrackValue {
Vec3([f32; 3]),
Quat([f32; 4]),
Scalar(f32),
Vec2([f32; 2]),
Vec4([f32; 4]),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Keyframe {
pub value: TrackValue,
pub interp: Interp,
pub in_tangent: TrackValue,
pub out_tangent: TrackValue,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TransformProp {
Translation,
Rotation,
Scale,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum BuiltinParamKind {
BaseColor,
Metallic,
Roughness,
Emissive,
NormalScale,
OcclusionStrength,
EmissiveStrength,
AlphaCutoff,
ToonDiffuseBands,
ToonSpecularSteps,
ToonShininess,
ToonRimStrength,
ToonRimPower,
FlipbookFps,
FlipbookTimeOffset,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum TexSlot {
BaseColor,
MetallicRoughness,
Normal,
Occlusion,
Emissive,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum TexTransformProp {
Offset,
Scale,
Rotation,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum LightParamKind {
Intensity,
Color,
Range,
InnerAngle,
OuterAngle,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum CameraParamKind {
FovY,
Near,
Far,
Aperture,
FocusDistance,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "target", rename_all = "snake_case")]
pub enum TrackTarget {
Transform {
node: NodeId,
prop: TransformProp,
},
Morph {
node: NodeId,
index: usize,
},
Uniform {
material: AssetId,
name: String,
},
BuiltinParam {
node: NodeId,
param: BuiltinParamKind,
},
Light {
node: NodeId,
param: LightParamKind,
},
Camera {
node: NodeId,
param: CameraParamKind,
},
TextureTransform {
node: NodeId,
slot: TexSlot,
prop: TexTransformProp,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StoredTrack {
pub target: TrackTarget,
pub sampler: SamplerKind,
#[serde(default)]
pub mute: bool,
#[serde(default)]
pub solo: bool,
#[serde(default)]
pub expanded: bool,
#[serde(default)]
pub times: Vec<f64>,
#[serde(default)]
pub keys: Vec<Keyframe>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StoredAnimation {
pub id: AssetId,
pub name: String,
pub duration: f64,
pub loop_style: ClipLoop,
pub speed: f64,
pub direction: ClipDirection,
#[serde(default)]
pub color: String,
#[serde(default)]
pub tracks: Vec<StoredTrack>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CustomAnimationRef {
pub name: String,
pub file: PathBuf,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "snake_case")]
pub enum LayerModeDoc {
#[default]
Replace,
Additive {
#[serde(default)]
base_clip: Option<AssetId>,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StripDoc {
pub clip: AssetId,
pub start: f64,
pub len: f64,
#[serde(default = "one_f64")]
pub scale: f64,
#[serde(default)]
pub repeat: bool,
}
fn one_f64() -> f64 {
1.0
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct LayerDoc {
pub mode: LayerModeDoc,
#[serde(default = "one_f64")]
pub weight: f64,
#[serde(default)]
pub mask_nodes: Vec<NodeId>,
#[serde(default)]
pub include_descendants: bool,
#[serde(default)]
pub strips: Vec<StripDoc>,
}
impl Default for LayerDoc {
fn default() -> Self {
Self {
mode: LayerModeDoc::Replace,
weight: 1.0,
mask_nodes: Vec::new(),
include_descendants: false,
strips: Vec::new(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct MixerDoc {
#[serde(default)]
pub layers: Vec<LayerDoc>,
}
pub fn spin_keyframes(
axis: [f32; 3],
turns: f32,
duration: f64,
keys_per_turn: u32,
) -> (Vec<f64>, Vec<Keyframe>) {
use std::f32::consts::TAU;
let kpt = keys_per_turn.max(1);
let n = ((turns.abs() * kpt as f32).ceil() as usize).max(1) + 1;
let len = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt();
let ax = if len > 1.0e-6 {
[axis[0] / len, axis[1] / len, axis[2] / len]
} else {
[0.0, 1.0, 0.0] };
let zero = TrackValue::Quat([0.0, 0.0, 0.0, 0.0]);
let mut times = Vec::with_capacity(n);
let mut keys = Vec::with_capacity(n);
let mut prev: Option<[f32; 4]> = None;
for k in 0..n {
let frac = k as f32 / (n - 1) as f32;
let t = duration * frac as f64;
let half = TAU * turns * frac * 0.5;
let s = half.sin();
let mut q = [ax[0] * s, ax[1] * s, ax[2] * s, half.cos()];
if let Some(p) = prev {
let dot = p[0] * q[0] + p[1] * q[1] + p[2] * q[2] + p[3] * q[3];
if dot < 0.0 {
for c in q.iter_mut() {
*c = -*c;
}
}
}
prev = Some(q);
times.push(t);
keys.push(Keyframe {
value: TrackValue::Quat(q),
interp: Interp::Linear,
in_tangent: zero,
out_tangent: zero,
});
}
(times, keys)
}
#[cfg(test)]
mod track_value_tests {
use super::*;
#[test]
fn track_value_round_trips_json_and_toml() {
for v in [
TrackValue::Vec3([1.0, 2.0, 3.0]),
TrackValue::Quat([0.0, 0.0, 0.0, 1.0]),
TrackValue::Scalar(0.75),
TrackValue::Vec2([0.25, 0.5]),
TrackValue::Vec4([0.1, 0.2, 0.3, 0.4]),
] {
let j = serde_json::to_string(&v).expect("json ser");
let back: TrackValue = serde_json::from_str(&j).expect("json de");
assert_eq!(v, back, "json round-trip: {j}");
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Wrap {
v: TrackValue,
}
let w = Wrap { v };
let t = toml::to_string(&w).expect("toml ser");
let back: Wrap = toml::from_str(&t).expect("toml de");
assert_eq!(w, back, "toml round-trip: {t}");
}
}
#[test]
fn keyframe_round_trips() {
let k = Keyframe {
value: TrackValue::Quat([0.1, 0.2, 0.3, 0.92]),
interp: Interp::Cubic,
in_tangent: TrackValue::Quat([0.0, 0.0, 0.0, 0.0]),
out_tangent: TrackValue::Quat([0.0, 0.0, 0.0, 0.0]),
};
let j = serde_json::to_string(&k).expect("json ser");
let back: Keyframe = serde_json::from_str(&j).expect("json de");
assert_eq!(k, back);
}
fn is_identity_rotation(q: [f32; 4]) -> bool {
q[0].abs() < 1.0e-4
&& q[1].abs() < 1.0e-4
&& q[2].abs() < 1.0e-4
&& (q[3].abs() - 1.0).abs() < 1.0e-4
}
#[test]
fn spin_keyframes_one_full_turn() {
let (times, keys) = spin_keyframes([0.0, 1.0, 0.0], 1.0, 2.0, 4);
assert_eq!(times.len(), 5);
assert_eq!(keys.len(), 5);
assert!((times[0]).abs() < 1.0e-9);
assert!((times[4] - 2.0).abs() < 1.0e-6);
assert!(times.windows(2).all(|w| w[1] > w[0]));
let q0 = match keys[0].value {
TrackValue::Quat(q) => q,
_ => panic!("expected quat"),
};
assert!(is_identity_rotation(q0));
let q4 = match keys[4].value {
TrackValue::Quat(q) => q,
_ => panic!("expected quat"),
};
assert!(
is_identity_rotation(q4),
"1 turn should return to identity: {q4:?}"
);
let q2 = match keys[2].value {
TrackValue::Quat(q) => q,
_ => panic!("expected quat"),
};
assert!((q2[1].abs() - 1.0).abs() < 1.0e-4, "180deg key: {q2:?}");
assert!(keys.iter().all(|k| k.interp == Interp::Linear));
}
#[test]
fn spin_keyframes_degenerate_axis_and_quarter_turn() {
let (times, keys) = spin_keyframes([0.0, 0.0, 0.0], 0.25, 1.0, 4);
assert_eq!(keys.len(), 2);
assert_eq!(times.len(), 2);
let q1 = match keys[1].value {
TrackValue::Quat(q) => q,
_ => panic!(),
};
assert!((q1[1] - 0.70710677).abs() < 1.0e-4, "{q1:?}");
assert!((q1[3] - 0.70710677).abs() < 1.0e-4, "{q1:?}");
}
}