use slotmap::new_key_type;
use crate::cameras::CameraKey;
use crate::lights::LightKey;
use crate::materials::MaterialKey;
use crate::transforms::TransformKey;
use super::{
animations::AnimationMorphKey,
data::AnimationData,
player::{AnimationPlayDirection, AnimationState},
sampler::AnimationSampler,
};
pub use super::player::AnimationLoopStyle;
new_key_type! {
pub struct AnimationClipKey;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AnimationTarget {
Transform(TransformKey),
Morph(AnimationMorphKey),
Uniform {
material: MaterialKey,
slot: usize,
},
BuiltinParam {
material: MaterialKey,
param: BuiltinMaterialParam,
},
Light {
light: LightKey,
param: LightParam,
},
Camera {
camera: CameraKey,
param: CameraParam,
},
TextureUv {
material: MaterialKey,
slot: TexSlot,
prop: TexTransformProp,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TexSlot {
BaseColor,
MetallicRoughness,
Normal,
Occlusion,
Emissive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TexTransformProp {
Offset,
Scale,
Rotation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LightParam {
Intensity,
Color,
Range,
InnerAngle,
OuterAngle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BuiltinMaterialParam {
BaseColor,
Metallic,
Roughness,
Emissive,
NormalScale,
OcclusionStrength,
EmissiveStrength,
AlphaCutoff,
ToonDiffuseBands,
ToonSpecularSteps,
ToonShininess,
ToonRimStrength,
ToonRimPower,
FlipbookFps,
FlipbookTimeOffset,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CameraParam {
FovY,
Near,
Far,
Aperture,
FocusDistance,
}
#[derive(Debug, Clone)]
pub struct AnimationChannel {
pub target: AnimationTarget,
pub sampler: AnimationSampler,
}
impl AnimationChannel {
pub fn new(target: AnimationTarget, sampler: AnimationSampler) -> Self {
Self { target, sampler }
}
pub fn sample(&self, time: f64) -> AnimationData {
self.sampler.sample(time)
}
}
#[derive(Debug, Clone)]
pub struct AnimationClipGroup {
pub name: String,
pub duration: f64,
pub loop_style: Option<AnimationLoopStyle>,
pub speed: f64,
pub play_direction: AnimationPlayDirection,
pub channels: Vec<AnimationChannel>,
local_time: f64,
state: AnimationState,
}
impl AnimationClipGroup {
pub fn new(name: impl Into<String>, duration: f64, channels: Vec<AnimationChannel>) -> Self {
Self {
name: name.into(),
duration,
loop_style: Some(AnimationLoopStyle::Loop),
speed: 1.0,
play_direction: AnimationPlayDirection::Forward,
channels,
local_time: 0.0,
state: AnimationState::Playing,
}
}
pub fn local_time(&self) -> f64 {
self.local_time
}
pub fn set_local_time(&mut self, time: f64) {
self.local_time = time.clamp(0.0, self.duration.max(0.0));
}
pub fn state(&self) -> &AnimationState {
&self.state
}
pub fn set_state(&mut self, state: AnimationState) {
self.state = state;
}
pub fn reset(&mut self) {
self.local_time = 0.0;
self.state = AnimationState::Playing;
self.play_direction = AnimationPlayDirection::Forward;
}
pub fn update(&mut self, dt_seconds: f64) {
if self.state != AnimationState::Playing {
return;
}
let local_time_delta = dt_seconds * self.speed;
match self.play_direction {
AnimationPlayDirection::Forward => {
self.local_time += local_time_delta;
if self.local_time >= self.duration {
match self.loop_style {
Some(AnimationLoopStyle::Loop) => {
self.local_time = self.local_time.rem_euclid(self.duration);
}
Some(AnimationLoopStyle::PingPong) => {
self.play_direction = AnimationPlayDirection::Backward;
self.local_time = self.duration;
}
None => {
self.local_time = self.duration;
self.state = AnimationState::Ended;
}
}
}
}
AnimationPlayDirection::Backward => {
self.local_time -= local_time_delta;
if self.local_time <= 0.0 {
match self.loop_style {
Some(AnimationLoopStyle::Loop) => {
self.local_time =
self.duration - self.local_time.rem_euclid(self.duration);
}
Some(AnimationLoopStyle::PingPong) => {
self.play_direction = AnimationPlayDirection::Forward;
self.local_time = 0.0;
}
None => {
self.local_time = 0.0;
self.state = AnimationState::Ended;
}
}
}
}
}
}
pub fn for_each_sample(&self, mut f: impl FnMut(AnimationTarget, AnimationData)) {
for channel in &self.channels {
f(channel.target, channel.sample(self.local_time));
}
}
pub fn for_each_sample_at(&self, time: f64, mut f: impl FnMut(AnimationTarget, AnimationData)) {
for channel in &self.channels {
f(channel.target, channel.sample(time));
}
}
pub fn sample_all(&self) -> Vec<(AnimationTarget, AnimationData)> {
let mut out = Vec::with_capacity(self.channels.len());
self.for_each_sample(|t, v| out.push((t, v)));
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::animation::{clip::AnimationClip, player::AnimationPlayer};
use crate::transforms::TransformKey;
fn f32_channel() -> AnimationChannel {
AnimationChannel::new(
AnimationTarget::Transform(TransformKey::default()),
AnimationSampler::new_linear(
vec![0.0, 1.0],
vec![AnimationData::F32(0.0), AnimationData::F32(10.0)],
),
)
}
fn group(loop_style: Option<AnimationLoopStyle>) -> AnimationClipGroup {
let mut g = AnimationClipGroup::new("test", 1.0, vec![f32_channel()]);
g.loop_style = loop_style;
g
}
fn as_f32(d: &AnimationData) -> f32 {
match d {
AnimationData::F32(v) => *v,
other => panic!("expected F32, got {other:?}"),
}
}
#[test]
fn clock_loop_wraps() {
let mut g = group(Some(AnimationLoopStyle::Loop));
g.update(0.5);
assert!((g.local_time() - 0.5).abs() < 1e-9);
g.update(0.6); assert!(
(g.local_time() - 0.1).abs() < 1e-9,
"got {}",
g.local_time()
);
assert_eq!(g.state(), &AnimationState::Playing);
}
#[test]
fn clock_pingpong_reverses() {
let mut g = group(Some(AnimationLoopStyle::PingPong));
g.update(1.2); assert!((g.local_time() - 1.0).abs() < 1e-9);
assert_eq!(g.play_direction, AnimationPlayDirection::Backward);
g.update(0.3); assert!(
(g.local_time() - 0.7).abs() < 1e-9,
"got {}",
g.local_time()
);
}
#[test]
fn clock_once_ends_and_clamps() {
let mut g = group(None);
g.update(1.5);
assert!((g.local_time() - 1.0).abs() < 1e-9);
assert_eq!(g.state(), &AnimationState::Ended);
g.update(1.0);
assert!((g.local_time() - 1.0).abs() < 1e-9);
}
#[test]
fn seek_clamps() {
let mut g = group(Some(AnimationLoopStyle::Loop));
g.set_local_time(2.0);
assert!((g.local_time() - 1.0).abs() < 1e-9);
g.set_local_time(-1.0);
assert!(g.local_time().abs() < 1e-9);
}
#[test]
fn sample_all_interpolates() {
let mut g = group(Some(AnimationLoopStyle::Loop));
g.set_local_time(0.5);
let samples = g.sample_all();
assert_eq!(samples.len(), 1);
assert!((as_f32(&samples[0].1) - 5.0).abs() < 1e-6);
}
#[test]
fn clock_matches_player() {
let clip = AnimationClip::new(
Some("p".to_string()),
1.0,
AnimationSampler::new_linear(
vec![0.0, 1.0],
vec![AnimationData::F32(0.0), AnimationData::F32(10.0)],
),
);
let mut player = AnimationPlayer::new(clip);
let mut g = group(Some(AnimationLoopStyle::Loop));
for &dt in &[0.3, 0.3, 0.3, 0.3, 0.3, 0.7, 0.9] {
player.update(dt);
g.update(dt);
assert!(
(player.local_time() - g.local_time()).abs() < 1e-9,
"player {} != group {}",
player.local_time(),
g.local_time()
);
}
}
}