From 2e7361530161803d7dbc644f4de9d327a7d7e858 Mon Sep 17 00:00:00 2001 From: Lee-Orr Date: Thu, 15 Feb 2024 20:22:52 -0500 Subject: [PATCH] added comments --- src/csv_animation.rs | 12 +++++++++--- src/csv_animation_mapping.rs | 18 +++++++++++++++++- src/csv_frame.rs | 4 ++++ src/lib.rs | 14 +++++++++++--- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/csv_animation.rs b/src/csv_animation.rs index 11fa24b..46eb2c0 100644 --- a/src/csv_animation.rs +++ b/src/csv_animation.rs @@ -1,3 +1,4 @@ +/// This module contains the parser for the CSV Animation itself. use std::num::ParseFloatError; use bevy::{asset::Asset, reflect::Reflect}; @@ -67,6 +68,9 @@ impl CSVAnimation { .ok_or(CSVParseError::NoData)? .map_err(CSVParseError::InvalidCSV)?; + // The header row contains the header for the timestamp column, a column we ignore (the number of morphs) + // and then the names of every morph in order. We need to keep this information for later, when we + // handle mapping the animation to another set of morphs. let morph_names: Vec = header_row .iter() .enumerate() @@ -84,8 +88,11 @@ impl CSVAnimation { } let mut frames = Vec::new(); + // The CSV doesn't contain information about FPS, but we can guess by looking at the + // largest frame number we reach - since the frame numbers reset every second let mut fps: u8 = 0; + // here we iterate over every frame, and set up it's morph data and timestamp for result in records { let record = result.map_err(CSVParseError::InvalidCSV)?; @@ -99,7 +106,6 @@ impl CSVAnimation { let _ = iter.next().ok_or(CSVParseError::MisssingKnownHeaders)?; // similarly, we already validate that there are a single morph in the CSV, so the skipped columns shouldn't be valid CSVs. } - let morphs = iter .map(|v| { v.trim() @@ -109,7 +115,7 @@ impl CSVAnimation { .collect::, _>>()?; if fps < timestamp.frame { - fps = timestamp.frame; + fps = timestamp.frame; // this is used to guess the FPS } frames.push(CSVFrame { timestamp, morphs }); @@ -122,7 +128,7 @@ impl CSVAnimation { Ok(CSVAnimation { frames, morph_names, - fps: (fps + 1) as f32, + fps: (fps + 1) as f32, // Since frame numbers start at 0, we need to add 1 to the largest frame number to get the FPS }) } } diff --git a/src/csv_animation_mapping.rs b/src/csv_animation_mapping.rs index bb002c2..d3e259d 100644 --- a/src/csv_animation_mapping.rs +++ b/src/csv_animation_mapping.rs @@ -20,11 +20,16 @@ pub enum MappingType { Bone(BoneMapping), } +/// A Bone Mapping maps the value from the CSV to a rotation in a specific axis on a bone #[derive(Serialize, Deserialize, Debug, Clone)] pub struct BoneMapping { + /// the hierarchy path to the bone path: Vec, + /// the axis of rotation axis: Axis, + /// an offset - in case "0" in the CSV is different from "0" in the bone hierarchy offset: Option, + /// scale - to allow you to tone the animation up or down scale: Option, } @@ -39,11 +44,13 @@ pub enum Axis { impl CSVAnimation { pub fn generate_clip(self, name: &str, mapping: Option<&CSVAnimationMapping>) -> AnimationClip { + // we find the path for the root entity and create a clip let path = EntityPath { parts: vec![Name::new(name.to_owned())], }; let mut clip = AnimationClip::default(); + // we note the order and name of every morph in the CSV let csv_morph_order: HashMap<&str, usize> = self .morph_names .iter() @@ -51,8 +58,10 @@ impl CSVAnimation { .map(|(i, val)| (val.as_str(), i)) .collect(); + // we generate the animation curve for the morphs themselves clip.add_curve_to_path(path, self.generate_morph_curve(mapping, &csv_morph_order)); + // if we have a mapping file, we also generate curves for any bones we're mapped to if let Some(mapping) = mapping { self.generate_bone_curves(&mut clip, mapping, &csv_morph_order); } @@ -66,6 +75,7 @@ impl CSVAnimation { mapping: &CSVAnimationMapping, csv_morph_order: &HashMap<&str, usize>, ) { + // we group all our mappings by the name of the bone, and throw out the non-bone mappings let bones = mapping .0 .iter() @@ -78,6 +88,7 @@ impl CSVAnimation { .group_by(|(key, _)| key.clone()); for (bone, mappings) in bones.into_iter() { + // for any bone, we generate the relevant curve and add it to the clip let curve = self.generate_bone_curve(mappings.map(|(_, v)| v), csv_morph_order); let path = EntityPath { parts: bone.into_iter().map(Name::new).collect(), @@ -100,9 +111,12 @@ impl CSVAnimation { let time = (frame as f32) * seconds_per_frame; keyframe_timestamps.push(time); + // we set up a transform at rest let mut transform = Transform::from_rotation(Quat::from_euler(bevy::math::EulerRot::XYZ, 0., 0., 0.)); + // for every mapping - we grab that data from the CSV and + // rotate the transform accordingly. for (mapping, csv_key) in mappings.iter() { let result = csv_morph_order .get(csv_key.as_str()) @@ -125,7 +139,6 @@ impl CSVAnimation { transform.rotate_axis(axis, result); } - keyframes.push(transform.rotation) } @@ -140,6 +153,7 @@ impl CSVAnimation { mapping: Option<&CSVAnimationMapping>, csv_morph_order: &HashMap<&str, usize>, ) -> VariableCurve { + // we figure out how we need to re-order morphs, going from the mesh order to the csv order let (num_mesh_morphs, mesh_to_csv_morph_order) = match mapping { Some(mapping) => { let mesh_to_csv_morph_order: Vec> = mapping @@ -169,9 +183,11 @@ impl CSVAnimation { let mut keyframes = Vec::with_capacity(self.frames.len() * num_mesh_morphs); let seconds_per_frame = 1.0 / self.fps; + for (frame, data) in self.frames.iter().enumerate() { let time = (frame as f32) * seconds_per_frame; keyframe_timestamps.push(time); + // we iterate over all the morphs in the mesh, and if they have a matching morph in the CSV - we grab the data for that morph for i in 0..num_mesh_morphs { let result = mesh_to_csv_morph_order .get(i) diff --git a/src/csv_frame.rs b/src/csv_frame.rs index 030bf0f..31aa8c6 100644 --- a/src/csv_frame.rs +++ b/src/csv_frame.rs @@ -4,6 +4,9 @@ use bevy::reflect::Reflect; use serde::{Deserialize, Serialize}; +// Neither the chrono crate nor the standard library has a good way of parsing SMPT timestamps. +// As a result, we set one up here - and we're sticking to u8s since recording data at above 255 FPS +// is unlikely in this context, and none of the other values should go beyond 60 (or 24 for hours) #[derive( Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Reflect, Default, )] @@ -51,6 +54,7 @@ impl FromStr for Timestamp { return Err(TimestampParseError::TooManySegments); } + // The segments start with hours, but we want to start with frames and be robust to missing segments segments.reverse(); let Some(frame) = segments.first() else { diff --git a/src/lib.rs b/src/lib.rs index b794cff..31bb939 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,13 @@ -pub mod csv_animation; -pub mod csv_animation_mapping; -pub mod csv_frame; +/// This library converts Unreal's LiveLink CSV format to Bevy animation clips. +/// It exports the [`CSVAnimationLoader`] struct, which is an asset loader for these animations. + +/// The main module contains the asset loader itself, and a struct defining a clip info JSON file, +/// which is used to point to the CSV and contain additional information to assist with generating +/// the animation clip - such as the name of the root object in the animation clip, the FPS, and +/// the path of a mapping file +mod csv_animation; +mod csv_animation_mapping; +mod csv_frame; use std::{path::PathBuf, string::FromUtf8Error}; @@ -9,6 +16,7 @@ use bevy::{ asset::{AssetLoader, AsyncReadExt, ReadAssetBytesError}, log::info, }; + use csv_animation::*; use csv_animation_mapping::*; use csv_frame::*;