Skip to main content

avatar_anim/
lib.rs

1use binrw::io::{Read, Seek, Write};
2use binrw::{BinRead, BinResult, BinWrite, Endian, binrw};
3use glam::{EulerRot, Quat, Vec3};
4use llsd_rs::Llsd;
5use std::collections::HashSet;
6use thiserror::Error;
7
8pub mod io;
9pub mod skeleton;
10
11use crate::io::{
12    AnimReadContext, AnimVersion, KEYFRAME_MOTION_SUBVERSION, KEYFRAME_MOTION_VERSION,
13    classify_anim_version, read_fixed_length_string, read_null_terminated_string, read_pos_vec3,
14    read_position_keys, read_rot_quat, read_rotation_keys, write_fixed_length_string,
15    write_null_terminated_string, write_pos_vec3, write_rot_quat,
16};
17
18pub use AnimError as Error;
19pub use skeleton::{SkeletonBone, SkeletonDefinition};
20pub type Result<T> = std::result::Result<T, AnimError>;
21
22#[derive(Debug, Error)]
23pub enum AnimError {
24    #[error("I/O error: {0}")]
25    Io(#[from] std::io::Error),
26    #[error("Binary parsing error: {0}")]
27    BinRw(#[from] binrw::Error),
28    #[error("Invalid structure: {0}")]
29    InvalidStructure(String),
30    #[error("LLSD parse error: {0}")]
31    Llsd(String),
32}
33
34#[binrw]
35#[brw(little)]
36#[derive(Clone, Debug, PartialEq)]
37pub struct AnimationHeader {
38    pub version: u16,
39    pub sub_version: u16,
40    pub base_priority: i32,
41    pub duration: f32,
42    #[br(parse_with = read_null_terminated_string)]
43    #[bw(write_with = write_null_terminated_string)]
44    pub emote_name: String,
45    pub loop_in_point: f32,
46    pub loop_out_point: f32,
47    pub looped: i32,
48    pub ease_in_duration: f32,
49    pub ease_out_duration: f32,
50    pub hand_pose: u32,
51}
52
53impl Default for AnimationHeader {
54    fn default() -> Self {
55        Self {
56            version: 1,
57            sub_version: 0,
58            base_priority: 6,
59            duration: 0.017,
60            emote_name: String::new(),
61            loop_in_point: 0.0,
62            loop_out_point: 0.017,
63            looped: 1,
64            ease_in_duration: 1.0,
65            ease_out_duration: 1.0,
66            hand_pose: 0,
67        }
68    }
69}
70
71#[binrw]
72#[brw(little)]
73#[derive(Clone, Debug, Default, PartialEq)]
74pub struct RotationKey {
75    pub time: u16,
76    #[br(parse_with = read_rot_quat)]
77    #[bw(write_with = write_rot_quat)]
78    pub rot: Quat,
79}
80
81impl From<Quat> for RotationKey {
82    fn from(rot: Quat) -> Self {
83        Self { time: 0, rot }
84    }
85}
86
87#[binrw]
88#[brw(little)]
89#[derive(Clone, Debug, Default, PartialEq)]
90pub struct PositionKey {
91    pub time: u16,
92    #[br(parse_with = read_pos_vec3)]
93    #[bw(write_with = write_pos_vec3)]
94    pub pos: Vec3,
95}
96
97impl From<Vec3> for PositionKey {
98    fn from(pos: Vec3) -> Self {
99        Self { time: 0, pos }
100    }
101}
102
103#[derive(Clone, Debug, Default, PartialEq)]
104pub struct JointData {
105    pub name: String,
106    pub priority: i32,
107    pub rotation_keys: Vec<RotationKey>,
108    pub position_keys: Vec<PositionKey>,
109}
110
111impl BinRead for JointData {
112    type Args<'a> = AnimReadContext;
113
114    fn read_options<R: Read + Seek>(
115        reader: &mut R,
116        endian: Endian,
117        ctx: Self::Args<'_>,
118    ) -> BinResult<Self> {
119        let name = read_null_terminated_string(reader, endian, ())?;
120        let priority = i32::read_options(reader, endian, ())?;
121
122        let num_rot_keys = i32::read_options(reader, endian, ())?;
123        let rotation_keys = read_rotation_keys(reader, endian, num_rot_keys, ctx)?;
124
125        let num_pos_keys = i32::read_options(reader, endian, ())?;
126        let position_keys = read_position_keys(reader, endian, num_pos_keys, ctx)?;
127
128        Ok(Self {
129            name,
130            priority,
131            rotation_keys,
132            position_keys,
133        })
134    }
135}
136
137impl BinWrite for JointData {
138    type Args<'a> = ();
139
140    fn write_options<W: Write + Seek>(
141        &self,
142        writer: &mut W,
143        endian: Endian,
144        _args: Self::Args<'_>,
145    ) -> BinResult<()> {
146        write_null_terminated_string(&self.name, writer, endian, ())?;
147        self.priority.write_options(writer, endian, ())?;
148        (self.rotation_keys.len() as i32).write_options(writer, endian, ())?;
149        for key in &self.rotation_keys {
150            key.write_options(writer, endian, ())?;
151        }
152
153        (self.position_keys.len() as i32).write_options(writer, endian, ())?;
154        for key in &self.position_keys {
155            key.write_options(writer, endian, ())?;
156        }
157        Ok(())
158    }
159}
160
161#[binrw]
162#[brw(little)]
163#[derive(Clone, Debug, Default, PartialEq)]
164pub struct Constraint {
165    pub chain_length: u8,
166    pub constraint_type: u8,
167
168    #[br(parse_with = read_fixed_length_string, args(16usize))]
169    #[bw(write_with = write_fixed_length_string, args(16usize))]
170    pub source_volume: String,
171
172    pub source_offset: [f32; 3],
173
174    #[br(parse_with = read_fixed_length_string, args(16usize))]
175    #[bw(write_with = write_fixed_length_string, args(16usize))]
176    pub target_volume: String,
177
178    pub target_offset: [f32; 3],
179    pub target_dir: [f32; 3],
180    pub ease_in_start: f32,
181    pub ease_in_stop: f32,
182    pub ease_out_start: f32,
183    pub ease_out_stop: f32,
184}
185
186#[derive(Clone, Debug, Default, PartialEq)]
187pub struct Animation {
188    pub header: AnimationHeader,
189    pub joints: Vec<JointData>,
190    pub constraints: Vec<Constraint>,
191}
192
193impl BinRead for Animation {
194    type Args<'a> = ();
195
196    fn read_options<R: Read + Seek>(
197        reader: &mut R,
198        endian: Endian,
199        _args: Self::Args<'_>,
200    ) -> BinResult<Self> {
201        let mut header = AnimationHeader::read_options(reader, endian, ())?;
202        let version = classify_anim_version(header.version, header.sub_version)?;
203        if version == AnimVersion::Old {
204            header.version = KEYFRAME_MOTION_VERSION;
205            header.sub_version = KEYFRAME_MOTION_SUBVERSION;
206        }
207        let ctx = AnimReadContext {
208            version,
209            duration: header.duration,
210        };
211
212        let num_joints = u32::read_options(reader, endian, ())?;
213        let mut joints = Vec::with_capacity(num_joints as usize);
214        for _ in 0..num_joints {
215            joints.push(JointData::read_options(reader, endian, ctx)?);
216        }
217
218        let num_constraints = i32::read_options(reader, endian, ())?;
219        if num_constraints < 0 {
220            return Err(binrw::Error::AssertFail {
221                pos: 0,
222                message: "num_constraints must be non-negative".into(),
223            });
224        }
225        let mut constraints = Vec::with_capacity(num_constraints as usize);
226        for _ in 0..num_constraints {
227            constraints.push(Constraint::read_options(reader, endian, ())?);
228        }
229
230        Ok(Self {
231            header,
232            joints,
233            constraints,
234        })
235    }
236}
237
238impl BinWrite for Animation {
239    type Args<'a> = ();
240
241    fn write_options<W: Write + Seek>(
242        &self,
243        writer: &mut W,
244        endian: Endian,
245        _args: Self::Args<'_>,
246    ) -> BinResult<()> {
247        let header = AnimationHeader {
248            version: KEYFRAME_MOTION_VERSION,
249            sub_version: KEYFRAME_MOTION_SUBVERSION,
250            ..self.header.clone()
251        };
252        header.write_options(writer, endian, ())?;
253        (self.joints.len() as u32).write_options(writer, endian, ())?;
254        for joint in &self.joints {
255            joint.write_options(writer, endian, ())?;
256        }
257
258        (self.constraints.len() as i32).write_options(writer, endian, ())?;
259        for constraint in &self.constraints {
260            constraint.write_options(writer, endian, ())?;
261        }
262        Ok(())
263    }
264}
265
266/// Strategy for handling duplicate keyframe times when cleaning up keys.
267#[derive(Copy, Clone, Debug, Eq, PartialEq)]
268pub enum DuplicateKeyStrategy {
269    /// Keep the first encountered key (time-stable).
270    KeepFirst,
271    /// Keep the last encountered key.
272    KeepLast,
273    /// Average all keys with the same timestamp (rotation via progressive slerp, position via arithmetic mean).
274    Average,
275}
276
277fn group_average_rot(keys: &[RotationKey]) -> Vec<RotationKey> {
278    if keys.is_empty() {
279        return Vec::new();
280    }
281    let mut out = Vec::new();
282    let mut i = 0usize;
283    while i < keys.len() {
284        let t = keys[i].time;
285        let mut acc = glam::Quat::IDENTITY;
286        let mut count = 0f32;
287        let mut j = i;
288        while j < keys.len() && keys[j].time == t {
289            acc = if count == 0.0 {
290                keys[j].rot
291            } else {
292                acc.slerp(keys[j].rot, 1.0 / (count + 1.0))
293            };
294            count += 1.0;
295            j += 1;
296        }
297        out.push(RotationKey {
298            time: t,
299            rot: acc.normalize(),
300        });
301        i = j;
302    }
303    out
304}
305
306fn group_average_pos(keys: &[PositionKey]) -> Vec<PositionKey> {
307    if keys.is_empty() {
308        return Vec::new();
309    }
310    let mut out = Vec::new();
311    let mut i = 0usize;
312    while i < keys.len() {
313        let t = keys[i].time;
314        let mut acc = glam::Vec3::ZERO;
315        let mut count = 0.0f32;
316        let mut j = i;
317        while j < keys.len() && keys[j].time == t {
318            acc += keys[j].pos;
319            count += 1.0;
320            j += 1;
321        }
322        out.push(PositionKey {
323            time: t,
324            pos: acc / count,
325        });
326        i = j;
327    }
328    out
329}
330
331impl Animation {
332    pub fn new() -> Self {
333        Self::default()
334    }
335
336    pub fn set_priority(&mut self, priority: i32) -> &mut Self {
337        self.header.base_priority = priority;
338        for joint in &mut self.joints {
339            joint.priority = priority;
340        }
341        self
342    }
343
344    pub fn set_duration(&mut self, duration: f32) -> &mut Self {
345        let duration = duration.max(0.0);
346        self.header.duration = duration;
347        self.header.loop_in_point = 0.0;
348        self.header.loop_out_point = duration;
349        self
350    }
351
352    pub fn set_joint_priority(&mut self, priority: i32) -> &mut Self {
353        for joint in &mut self.joints {
354            joint.priority = priority;
355        }
356        self
357    }
358
359    pub fn drop_empty_joints(&mut self) -> &mut Self {
360        self.joints
361            .retain(|joint| !joint.position_keys.is_empty() || !joint.rotation_keys.is_empty());
362        self
363    }
364
365    pub fn drop_position_keys(&mut self) -> &mut Self {
366        for joint in &mut self.joints {
367            joint.position_keys.clear();
368        }
369        self
370    }
371
372    pub fn drop_position(&mut self, joints: impl Fn(&JointData) -> bool) -> &mut Self {
373        for joint in &mut self.joints {
374            if joints(joint) {
375                joint.position_keys.clear();
376            }
377        }
378        self
379    }
380
381    pub fn drop_zero_position_keys(&mut self, epsilon: f32) -> &mut Self {
382        let epsilon_sq = epsilon.max(0.0) * epsilon.max(0.0);
383        for joint in &mut self.joints {
384            joint
385                .position_keys
386                .retain(|key| key.pos.length_squared() > epsilon_sq);
387        }
388        self
389    }
390
391    pub fn drop_rotation_keys(&mut self) -> &mut Self {
392        for joint in &mut self.joints {
393            joint.rotation_keys.clear();
394        }
395        self
396    }
397
398    pub fn drop_rotation(&mut self, joints: impl Fn(&JointData) -> bool) -> &mut Self {
399        for joint in &mut self.joints {
400            if joints(joint) {
401                joint.rotation_keys.clear();
402            }
403        }
404        self
405    }
406
407    pub fn cleanup_keys(&mut self) -> &mut Self {
408        for joint in &mut self.joints {
409            let mut seen_times = HashSet::new();
410            joint.rotation_keys.reverse();
411            joint
412                .rotation_keys
413                .retain(|key| seen_times.insert(key.time));
414            joint.rotation_keys.sort_by_key(|key| key.time);
415            seen_times.clear();
416            joint.position_keys.reverse();
417            joint
418                .position_keys
419                .retain(|key| seen_times.insert(key.time));
420            joint.position_keys.sort_by_key(|key| key.time);
421        }
422        self
423    }
424
425    /// Cleanup duplicate keyframe times with a customizable strategy.
426    pub fn cleanup_keys_with(&mut self, strategy: DuplicateKeyStrategy) -> &mut Self {
427        for joint in &mut self.joints {
428            match strategy {
429                DuplicateKeyStrategy::KeepFirst => {
430                    let mut seen = HashSet::new();
431                    joint.rotation_keys.retain(|k| seen.insert(k.time));
432                    seen.clear();
433                    joint.position_keys.retain(|k| seen.insert(k.time));
434                }
435                DuplicateKeyStrategy::KeepLast => {
436                    // Retain last: iterate reverse, keep first occurrence in reverse order.
437                    let mut seen = HashSet::new();
438                    joint.rotation_keys.reverse();
439                    joint.rotation_keys.retain(|k| seen.insert(k.time));
440                    joint.rotation_keys.reverse();
441                    seen.clear();
442                    joint.position_keys.reverse();
443                    joint.position_keys.retain(|k| seen.insert(k.time));
444                    joint.position_keys.reverse();
445                }
446                DuplicateKeyStrategy::Average => {
447                    // Group by time then average.
448                    joint.rotation_keys.sort_by_key(|k| k.time);
449                    joint.position_keys.sort_by_key(|k| k.time);
450                    joint.rotation_keys = group_average_rot(&joint.rotation_keys);
451                    joint.position_keys = group_average_pos(&joint.position_keys);
452                }
453            }
454            joint.rotation_keys.sort_by_key(|k| k.time);
455            joint.position_keys.sort_by_key(|k| k.time);
456        }
457        self
458    }
459
460    pub fn joint(&self, name: &str) -> Option<&JointData> {
461        self.joints.iter().find(|joint| joint.name == name)
462    }
463
464    pub fn joint_mut(&mut self, name: &str) -> Option<&mut JointData> {
465        self.joints.iter_mut().find(|joint| joint.name == name)
466    }
467
468    pub fn position_reset_from_skeleton<'a>(
469        skeleton: &SkeletonDefinition,
470        joint_names: impl IntoIterator<Item = &'a str>,
471        priority: i32,
472    ) -> Result<Self> {
473        let mut bones = Vec::new();
474        for name in joint_names {
475            let bone = skeleton.bone(name).ok_or_else(|| {
476                AnimError::InvalidStructure(format!("Skeleton is missing required bone '{name}'"))
477            })?;
478            bones.push(bone);
479        }
480        Self::position_reset_from_bones(bones, priority)
481    }
482
483    pub fn position_reset_from_bones<'a>(
484        bones: impl IntoIterator<Item = &'a SkeletonBone>,
485        priority: i32,
486    ) -> Result<Self> {
487        let priority = priority.clamp(0, 7);
488        let mut animation = Self::default();
489        animation.header.base_priority = priority;
490
491        for bone in bones {
492            animation.joints.push(JointData {
493                name: bone.name.clone(),
494                priority,
495                rotation_keys: Vec::new(),
496                position_keys: vec![
497                    PositionKey {
498                        time: 0,
499                        pos: bone.pos,
500                    },
501                    PositionKey {
502                        time: u16::MAX,
503                        pos: bone.pos,
504                    },
505                ],
506            });
507        }
508
509        Ok(animation)
510    }
511
512    pub fn add_skeleton_positions(&mut self, skeleton: &SkeletonDefinition) -> Result<&mut Self> {
513        for joint in &mut self.joints {
514            if joint.position_keys.is_empty() {
515                continue;
516            }
517            let base_pos = skeleton.position(&joint.name).ok_or_else(|| {
518                AnimError::InvalidStructure(format!(
519                    "Skeleton is missing position for joint '{}'",
520                    joint.name
521                ))
522            })?;
523            for key in &mut joint.position_keys {
524                key.pos += base_pos;
525            }
526        }
527        Ok(self)
528    }
529
530    /// Creates an animation from LLSD data, typically from Firestorm poser files.
531    ///
532    /// This function parses LLSD-XML data exported by Firestorm's poser system and converts
533    /// it into an animation structure. Firestorm stores pose data in LLSD-XML format in
534    /// the user's configuration directory.
535    ///
536    /// # Arguments
537    ///
538    /// * `llsd` - The parsed LLSD data containing joint poses
539    /// * `check_enabled` - If true, only includes joints where the "enabled" field is true
540    ///
541    /// # File Locations
542    ///
543    /// Firestorm poser files are typically found at:
544    /// * **Linux**: `~/.firestorm_x64/user_settings/poses/`
545    /// * **Windows**: `%APPDATA%/Firestorm_x64/user_settings/poses/`
546    /// * **macOS**: `~/Library/Application Support/Firestorm_x64/user_settings/poses/`
547    ///
548    /// # Example
549    ///
550    /// ```rust,no_run
551    /// use std::fs::File;
552    /// use std::io::BufReader;
553    /// use avatar_anim::Animation;
554    ///
555    /// # fn main() -> avatar_anim::Result<()> {
556    /// // Load LLSD-XML file from Firestorm poses directory
557    /// let file = BufReader::new(File::open("my_pose.xml")?);
558    /// let llsd = llsd_rs::xml::from_reader(file).map_err(|e| avatar_anim::AnimError::Llsd(e.to_string()))?;
559    ///
560    /// // Convert to animation, including only enabled joints
561    /// let animation = Animation::from_llsd(&llsd, true)?;
562    /// # Ok(())
563    /// # }
564    /// ```
565    pub fn from_llsd(llsd: &Llsd, check_enabled: bool) -> Result<Self> {
566        let Some(joints) = llsd.as_map() else {
567            return Err(AnimError::InvalidStructure("LLSD must be a map".into()));
568        };
569        let mut animation = Self::default();
570        for (key, value) in joints {
571            let Some(inner) = value.as_map() else {
572                continue;
573            };
574            if check_enabled
575                && inner
576                    .get("enabled")
577                    .is_none_or(|e| e.as_boolean() != Some(&true))
578            {
579                continue;
580            }
581            let extract_key = |key: &str| -> Option<(f32, f32, f32)> {
582                inner.get(key).and_then(|e| e.as_array()).map(|arr| {
583                    (
584                        *arr.first().and_then(|e| e.as_real()).unwrap_or(&0.0f64) as f32,
585                        *arr.get(1).and_then(|e| e.as_real()).unwrap_or(&0.0f64) as f32,
586                        *arr.get(2).and_then(|e| e.as_real()).unwrap_or(&0.0f64) as f32,
587                    )
588                })
589            };
590            let rotation = extract_key("rotation").map(|(roll, pitch, yaw)| RotationKey {
591                time: u16::MAX,
592                rot: Quat::from_euler(EulerRot::XYZ, roll, pitch, yaw).normalize(),
593            });
594            let position = extract_key("position").map(|(x, y, z)| PositionKey {
595                time: u16::MAX,
596                pos: Vec3::new(x, y, z),
597            });
598            animation.joints.push(JointData {
599                name: key.clone(),
600                rotation_keys: rotation.into_iter().collect(),
601                position_keys: position.into_iter().collect(),
602                ..Default::default()
603            });
604        }
605        Ok(animation)
606    }
607
608    /// Load an animation from a .anim file
609    ///
610    /// # Example
611    ///
612    /// ```rust,no_run
613    /// use avatar_anim::Animation;
614    ///
615    /// # fn main() -> avatar_anim::Result<()> {
616    /// let animation = Animation::from_file("my_animation.anim")?;
617    /// # Ok(())
618    /// # }
619    /// ```
620    pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
621        use binrw::BinRead;
622        use std::fs::File;
623        use std::io::BufReader;
624        let file = File::open(path).map_err(AnimError::Io)?;
625        let mut reader = BufReader::new(file);
626        Self::read_options(&mut reader, Endian::Little, ()).map_err(AnimError::BinRw)
627    }
628
629    /// Save an animation to a .anim file
630    ///
631    /// # Example
632    ///
633    /// ```rust,no_run
634    /// use avatar_anim::Animation;
635    ///
636    /// # fn main() -> avatar_anim::Result<()> {
637    /// let animation = Animation::default();
638    /// animation.to_file("output.anim")?;
639    /// # Ok(())
640    /// # }
641    /// ```
642    pub fn to_file<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
643        use binrw::BinWrite;
644        use std::fs::File;
645        use std::io::BufWriter;
646        let file = File::create(path).map_err(AnimError::Io)?;
647        let mut writer = BufWriter::new(file);
648        self.write_options(&mut writer, Endian::Little, ())
649            .map_err(AnimError::BinRw)
650    }
651
652    /// Load LLSD-XML data from a Firestorm pose file
653    ///
654    /// # Example
655    ///
656    /// ```rust,no_run
657    /// use avatar_anim::Animation;
658    ///
659    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
660    /// let animation = Animation::from_llsd_file("pose.xml", true)?;
661    /// # Ok(())
662    /// # }
663    /// ```
664    pub fn from_llsd_file<P: AsRef<std::path::Path>>(path: P, check_enabled: bool) -> Result<Self> {
665        use std::fs::File;
666        use std::io::BufReader;
667        let file = File::open(path).map_err(AnimError::Io)?;
668        let reader = BufReader::new(file);
669        let llsd = llsd_rs::xml::from_reader(reader).map_err(|e| AnimError::Llsd(e.to_string()))?;
670        Self::from_llsd(&llsd, check_enabled)
671    }
672}