1use std::collections::HashMap;
12
13#[allow(dead_code)]
17fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
18 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
19}
20
21#[allow(dead_code)]
23fn vec3_add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
24 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
25}
26
27#[allow(dead_code)]
29fn vec3_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
30 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
31}
32
33#[allow(dead_code)]
35fn vec3_scale(v: [f64; 3], s: f64) -> [f64; 3] {
36 [v[0] * s, v[1] * s, v[2] * s]
37}
38
39#[allow(dead_code)]
41fn vec3_lerp(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
42 [
43 a[0] + (b[0] - a[0]) * t,
44 a[1] + (b[1] - a[1]) * t,
45 a[2] + (b[2] - a[2]) * t,
46 ]
47}
48
49#[allow(dead_code)]
51fn quat_normalize(q: [f64; 4]) -> [f64; 4] {
52 let len2 = q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3];
53 if len2 < 1e-30 {
54 return [0.0, 0.0, 0.0, 1.0];
55 }
56 let inv = 1.0 / len2.sqrt();
57 [q[0] * inv, q[1] * inv, q[2] * inv, q[3] * inv]
58}
59
60#[allow(dead_code)]
62fn quat_dot(a: [f64; 4], b: [f64; 4]) -> f64 {
63 a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]
64}
65
66#[allow(dead_code)]
68fn quat_mul(p: [f64; 4], q: [f64; 4]) -> [f64; 4] {
69 let [px, py, pz, pw] = p;
70 let [qx, qy, qz, qw] = q;
71 [
72 pw * qx + px * qw + py * qz - pz * qy,
73 pw * qy - px * qz + py * qw + pz * qx,
74 pw * qz + px * qy - py * qx + pz * qw,
75 pw * qw - px * qx - py * qy - pz * qz,
76 ]
77}
78
79#[allow(dead_code)]
81fn quat_conjugate(q: [f64; 4]) -> [f64; 4] {
82 [-q[0], -q[1], -q[2], q[3]]
83}
84
85#[allow(dead_code)]
87fn euler_xyz_to_quat(euler_deg: [f64; 3]) -> [f64; 4] {
88 let to_rad = std::f64::consts::PI / 180.0;
89 let (hx, hy, hz) = (
90 euler_deg[0] * to_rad * 0.5,
91 euler_deg[1] * to_rad * 0.5,
92 euler_deg[2] * to_rad * 0.5,
93 );
94 let (sx, cx) = (hx.sin(), hx.cos());
95 let (sy, cy) = (hy.sin(), hy.cos());
96 let (sz, cz) = (hz.sin(), hz.cos());
97 quat_normalize([
98 sx * cy * cz + cx * sy * sz,
99 cx * sy * cz - sx * cy * sz,
100 cx * cy * sz + sx * sy * cz,
101 cx * cy * cz - sx * sy * sz,
102 ])
103}
104
105#[allow(dead_code)]
107fn quat_to_euler_xyz(q: [f64; 4]) -> [f64; 3] {
108 let [x, y, z, w] = q;
109 let to_deg = 180.0 / std::f64::consts::PI;
110 let sinr_cosp = 2.0 * (w * x + y * z);
111 let cosr_cosp = 1.0 - 2.0 * (x * x + y * y);
112 let rx = sinr_cosp.atan2(cosr_cosp);
113 let sinp = 2.0 * (w * y - z * x);
114 let ry = if sinp.abs() >= 1.0 {
115 std::f64::consts::FRAC_PI_2.copysign(sinp)
116 } else {
117 sinp.asin()
118 };
119 let siny_cosp = 2.0 * (w * z + x * y);
120 let cosy_cosp = 1.0 - 2.0 * (y * y + z * z);
121 let rz = siny_cosp.atan2(cosy_cosp);
122 [rx * to_deg, ry * to_deg, rz * to_deg]
123}
124
125#[allow(dead_code)]
134pub fn quat_slerp(a: [f64; 4], b: [f64; 4], t: f64) -> [f64; 4] {
135 let mut dot = quat_dot(a, b);
136 let b = if dot < 0.0 {
137 dot = -dot;
138 [-b[0], -b[1], -b[2], -b[3]]
139 } else {
140 b
141 };
142 if dot > 0.9995 {
143 return quat_normalize([
145 a[0] + (b[0] - a[0]) * t,
146 a[1] + (b[1] - a[1]) * t,
147 a[2] + (b[2] - a[2]) * t,
148 a[3] + (b[3] - a[3]) * t,
149 ]);
150 }
151 let theta = dot.clamp(-1.0, 1.0).acos();
152 let sin_theta = theta.sin();
153 if sin_theta.abs() < 1e-15 {
154 return a;
155 }
156 let wa = ((1.0 - t) * theta).sin() / sin_theta;
157 let wb = (t * theta).sin() / sin_theta;
158 quat_normalize([
159 wa * a[0] + wb * b[0],
160 wa * a[1] + wb * b[1],
161 wa * a[2] + wb * b[2],
162 wa * a[3] + wb * b[3],
163 ])
164}
165
166#[derive(Debug, Clone)]
172pub struct Joint {
173 pub name: String,
175 pub parent: Option<usize>,
177 pub offset: [f64; 3],
179 pub channels: Vec<String>,
181}
182
183#[derive(Debug, Clone)]
185pub struct Skeleton {
186 pub joints: Vec<Joint>,
188 pub name_to_index: HashMap<String, usize>,
190}
191
192impl Skeleton {
193 #[allow(dead_code)]
195 pub fn new() -> Self {
196 Self {
197 joints: Vec::new(),
198 name_to_index: HashMap::new(),
199 }
200 }
201
202 #[allow(dead_code)]
204 pub fn add_joint(&mut self, name: &str, parent: Option<usize>, offset: [f64; 3]) -> usize {
205 let idx = self.joints.len();
206 self.name_to_index.insert(name.to_string(), idx);
207 self.joints.push(Joint {
208 name: name.to_string(),
209 parent,
210 offset,
211 channels: Vec::new(),
212 });
213 idx
214 }
215
216 #[allow(dead_code)]
218 pub fn set_channels(&mut self, joint_idx: usize, channels: Vec<String>) {
219 if let Some(j) = self.joints.get_mut(joint_idx) {
220 j.channels = channels;
221 }
222 }
223
224 #[allow(dead_code)]
226 pub fn num_joints(&self) -> usize {
227 self.joints.len()
228 }
229
230 #[allow(dead_code)]
232 pub fn children(&self, joint_idx: usize) -> Vec<usize> {
233 self.joints
234 .iter()
235 .enumerate()
236 .filter(|(_, j)| j.parent == Some(joint_idx))
237 .map(|(i, _)| i)
238 .collect()
239 }
240
241 #[allow(dead_code)]
243 pub fn find_joint(&self, name: &str) -> Option<usize> {
244 self.name_to_index.get(name).copied()
245 }
246
247 #[allow(dead_code)]
249 pub fn total_channels(&self) -> usize {
250 self.joints.iter().map(|j| j.channels.len()).sum()
251 }
252}
253
254impl Default for Skeleton {
255 fn default() -> Self {
256 Self::new()
257 }
258}
259
260#[derive(Debug, Clone, Copy)]
266pub struct JointPose {
267 pub translation: [f64; 3],
269 pub rotation: [f64; 4],
271 pub scale: [f64; 3],
273}
274
275impl JointPose {
276 #[allow(dead_code)]
278 pub fn identity() -> Self {
279 Self {
280 translation: [0.0; 3],
281 rotation: [0.0, 0.0, 0.0, 1.0],
282 scale: [1.0, 1.0, 1.0],
283 }
284 }
285
286 #[allow(dead_code)]
288 pub fn lerp(&self, other: &JointPose, t: f64) -> JointPose {
289 JointPose {
290 translation: vec3_lerp(self.translation, other.translation, t),
291 rotation: quat_slerp(self.rotation, other.rotation, t),
292 scale: vec3_lerp(self.scale, other.scale, t),
293 }
294 }
295}
296
297#[derive(Debug, Clone)]
299pub struct SkeletonPose {
300 pub joint_poses: Vec<JointPose>,
302}
303
304impl SkeletonPose {
305 #[allow(dead_code)]
307 pub fn default_for(skeleton: &Skeleton) -> Self {
308 let n = skeleton.num_joints();
309 Self {
310 joint_poses: vec![JointPose::identity(); n],
311 }
312 }
313
314 #[allow(dead_code)]
316 pub fn lerp(&self, other: &SkeletonPose, t: f64) -> SkeletonPose {
317 let poses: Vec<JointPose> = self
318 .joint_poses
319 .iter()
320 .zip(&other.joint_poses)
321 .map(|(a, b)| a.lerp(b, t))
322 .collect();
323 SkeletonPose { joint_poses: poses }
324 }
325}
326
327#[derive(Debug, Clone)]
329pub struct AnimationClip {
330 pub name: String,
332 pub fps: f64,
334 pub times: Vec<f64>,
336 pub frames: Vec<SkeletonPose>,
338}
339
340impl AnimationClip {
341 #[allow(dead_code)]
343 pub fn new(name: &str, fps: f64) -> Self {
344 Self {
345 name: name.to_string(),
346 fps,
347 times: Vec::new(),
348 frames: Vec::new(),
349 }
350 }
351
352 #[allow(dead_code)]
354 pub fn add_frame(&mut self, time: f64, pose: SkeletonPose) {
355 self.times.push(time);
356 self.frames.push(pose);
357 }
358
359 #[allow(dead_code)]
361 pub fn duration(&self) -> f64 {
362 if self.times.is_empty() {
363 return 0.0;
364 }
365 self.times[self.times.len() - 1] - self.times[0]
366 }
367
368 #[allow(dead_code)]
370 pub fn num_frames(&self) -> usize {
371 self.frames.len()
372 }
373
374 #[allow(dead_code)]
377 pub fn sample(&self, time: f64) -> Option<SkeletonPose> {
378 if self.frames.is_empty() {
379 return None;
380 }
381 if self.frames.len() == 1 || time <= self.times[0] {
382 return Some(self.frames[0].clone());
383 }
384 let last = self.times.len() - 1;
385 if time >= self.times[last] {
386 return Some(self.frames[last].clone());
387 }
388 let idx = match self
390 .times
391 .binary_search_by(|t| t.partial_cmp(&time).unwrap_or(std::cmp::Ordering::Equal))
392 {
393 Ok(i) => return Some(self.frames[i].clone()),
394 Err(i) => i,
395 };
396 let i0 = idx - 1;
397 let i1 = idx;
398 let dt = self.times[i1] - self.times[i0];
399 let t = if dt > 1e-15 {
400 (time - self.times[i0]) / dt
401 } else {
402 0.0
403 };
404 Some(self.frames[i0].lerp(&self.frames[i1], t))
405 }
406}
407
408#[derive(Debug, Clone)]
414pub struct BvhData {
415 pub skeleton: Skeleton,
417 pub frame_time: f64,
419 pub motion: Vec<Vec<f64>>,
421}
422
423#[allow(dead_code)]
425pub fn parse_bvh(input: &str) -> Result<BvhData, String> {
426 let mut skeleton = Skeleton::new();
427 let lines = input.lines().map(|l| l.trim()).peekable();
428 let mut parent_stack: Vec<Option<usize>> = vec![None];
429 let mut frame_time = 1.0 / 30.0;
430 let mut motion: Vec<Vec<f64>> = Vec::new();
431 let mut in_motion = false;
432 let mut _num_frames: usize = 0;
433
434 for line in lines {
435 if line.is_empty() {
436 continue;
437 }
438 if in_motion {
439 let vals: Result<Vec<f64>, _> =
440 line.split_whitespace().map(|s| s.parse::<f64>()).collect();
441 match vals {
442 Ok(v) if !v.is_empty() => motion.push(v),
443 _ => {}
444 }
445 continue;
446 }
447
448 let tokens: Vec<&str> = line.split_whitespace().collect();
449 if tokens.is_empty() {
450 continue;
451 }
452
453 match tokens[0] {
454 "HIERARCHY" => {}
455 "ROOT" | "JOINT" => {
456 let name = if tokens.len() > 1 {
457 tokens[1]
458 } else {
459 "unnamed"
460 };
461 let parent = *parent_stack.last().unwrap_or(&None);
462 let idx = skeleton.add_joint(name, parent, [0.0; 3]);
463 parent_stack.push(Some(idx));
464 }
465 "End" => {
466 let parent = *parent_stack.last().unwrap_or(&None);
468 let name = format!("EndSite_{}", skeleton.num_joints());
469 let idx = skeleton.add_joint(&name, parent, [0.0; 3]);
470 parent_stack.push(Some(idx));
471 }
472 "OFFSET"
473 if tokens.len() >= 4 => {
474 let ox: f64 = tokens[1].parse().unwrap_or(0.0);
475 let oy: f64 = tokens[2].parse().unwrap_or(0.0);
476 let oz: f64 = tokens[3].parse().unwrap_or(0.0);
477 if let Some(&Some(idx)) = parent_stack.last()
478 && let Some(j) = skeleton.joints.get_mut(idx) {
479 j.offset = [ox, oy, oz];
480 }
481 }
482 "CHANNELS"
483 if tokens.len() >= 2 => {
484 let _n: usize = tokens[1].parse().unwrap_or(0);
485 let channels: Vec<String> = tokens[2..].iter().map(|s| s.to_string()).collect();
486 if let Some(&Some(idx)) = parent_stack.last() {
487 skeleton.set_channels(idx, channels);
488 }
489 }
490 "{" => {}
491 "}" => {
492 parent_stack.pop();
493 }
494 "MOTION" => {
495 in_motion = true;
496 }
497 "Frames:"
498 if tokens.len() >= 2 => {
499 _num_frames = tokens[1].parse().unwrap_or(0);
500 }
501 "Frame"
502 if tokens.len() >= 3 => {
504 frame_time = tokens[2].parse().unwrap_or(1.0 / 30.0);
505 }
506 _ => {}
507 }
508 }
509
510 Ok(BvhData {
511 skeleton,
512 frame_time,
513 motion,
514 })
515}
516
517#[allow(dead_code)]
519pub fn bvh_frame_to_pose(skeleton: &Skeleton, channels: &[f64]) -> SkeletonPose {
520 let mut pose = SkeletonPose::default_for(skeleton);
521 let mut ch_idx = 0;
522
523 for (j_idx, joint) in skeleton.joints.iter().enumerate() {
524 let mut trans = joint.offset;
525 let mut euler = [0.0; 3];
526
527 for ch_name in &joint.channels {
528 if ch_idx >= channels.len() {
529 break;
530 }
531 let val = channels[ch_idx];
532 ch_idx += 1;
533 match ch_name.as_str() {
534 "Xposition" => trans[0] += val,
535 "Yposition" => trans[1] += val,
536 "Zposition" => trans[2] += val,
537 "Xrotation" => euler[0] = val,
538 "Yrotation" => euler[1] = val,
539 "Zrotation" => euler[2] = val,
540 _ => {}
541 }
542 }
543
544 let rot = euler_xyz_to_quat(euler);
545 pose.joint_poses[j_idx] = JointPose {
546 translation: trans,
547 rotation: rot,
548 scale: [1.0, 1.0, 1.0],
549 };
550 }
551
552 pose
553}
554
555#[allow(dead_code)]
557pub fn bvh_to_clip(bvh: &BvhData) -> AnimationClip {
558 let fps = if bvh.frame_time > 1e-15 {
559 1.0 / bvh.frame_time
560 } else {
561 30.0
562 };
563 let mut clip = AnimationClip::new("bvh_clip", fps);
564 for (i, frame_data) in bvh.motion.iter().enumerate() {
565 let time = i as f64 * bvh.frame_time;
566 let pose = bvh_frame_to_pose(&bvh.skeleton, frame_data);
567 clip.add_frame(time, pose);
568 }
569 clip
570}
571
572#[allow(dead_code)]
574pub fn export_bvh(skeleton: &Skeleton, clip: &AnimationClip) -> String {
575 let mut out = String::new();
576 out.push_str("HIERARCHY\n");
577
578 fn write_joint(out: &mut String, skeleton: &Skeleton, idx: usize, indent: usize) {
579 let joint = &skeleton.joints[idx];
580 let prefix = " ".repeat(indent);
581 let keyword = if joint.parent.is_none() {
582 "ROOT"
583 } else {
584 "JOINT"
585 };
586 out.push_str(&format!("{}{} {}\n", prefix, keyword, joint.name));
587 out.push_str(&format!("{}{{\n", prefix));
588 out.push_str(&format!(
589 "{} OFFSET {:.6} {:.6} {:.6}\n",
590 prefix, joint.offset[0], joint.offset[1], joint.offset[2]
591 ));
592 if !joint.channels.is_empty() {
593 out.push_str(&format!(
594 "{} CHANNELS {} {}\n",
595 prefix,
596 joint.channels.len(),
597 joint.channels.join(" ")
598 ));
599 }
600 let children = skeleton.children(idx);
601 if children.is_empty() {
602 out.push_str(&format!("{} End Site\n", prefix));
603 out.push_str(&format!("{} {{\n", prefix));
604 out.push_str(&format!(
605 "{} OFFSET 0.000000 0.000000 0.000000\n",
606 prefix
607 ));
608 out.push_str(&format!("{} }}\n", prefix));
609 } else {
610 for &child in &children {
611 write_joint(out, skeleton, child, indent + 2);
612 }
613 }
614 out.push_str(&format!("{}}}\n", prefix));
615 }
616
617 for (i, j) in skeleton.joints.iter().enumerate() {
619 if j.parent.is_none() {
620 write_joint(&mut out, skeleton, i, 0);
621 }
622 }
623
624 out.push_str("MOTION\n");
625 out.push_str(&format!("Frames: {}\n", clip.num_frames()));
626 let frame_time = if clip.fps > 0.0 {
627 1.0 / clip.fps
628 } else {
629 1.0 / 30.0
630 };
631 out.push_str(&format!("Frame Time: {:.6}\n", frame_time));
632
633 for frame in &clip.frames {
634 let mut vals = Vec::new();
635 for (j_idx, joint) in skeleton.joints.iter().enumerate() {
636 let pose = &frame.joint_poses[j_idx];
637 for ch in &joint.channels {
638 match ch.as_str() {
639 "Xposition" => vals.push(pose.translation[0]),
640 "Yposition" => vals.push(pose.translation[1]),
641 "Zposition" => vals.push(pose.translation[2]),
642 "Xrotation" | "Yrotation" | "Zrotation" => {
643 let euler = quat_to_euler_xyz(pose.rotation);
644 match ch.as_str() {
645 "Xrotation" => vals.push(euler[0]),
646 "Yrotation" => vals.push(euler[1]),
647 _ => vals.push(euler[2]),
648 }
649 }
650 _ => vals.push(0.0),
651 }
652 }
653 }
654 let line: Vec<String> = vals.iter().map(|v| format!("{:.6}", v)).collect();
655 out.push_str(&line.join(" "));
656 out.push('\n');
657 }
658
659 out
660}
661
662#[derive(Debug, Clone)]
668pub struct FbxNode {
669 pub name: String,
671 pub properties: Vec<String>,
673 pub children: Vec<FbxNode>,
675}
676
677impl FbxNode {
678 #[allow(dead_code)]
680 pub fn new(name: &str) -> Self {
681 Self {
682 name: name.to_string(),
683 properties: Vec::new(),
684 children: Vec::new(),
685 }
686 }
687
688 #[allow(dead_code)]
690 pub fn add_property(&mut self, value: &str) {
691 self.properties.push(value.to_string());
692 }
693
694 #[allow(dead_code)]
696 pub fn add_child(&mut self, child: FbxNode) {
697 self.children.push(child);
698 }
699
700 #[allow(dead_code)]
702 pub fn to_ascii(&self, indent: usize) -> String {
703 let prefix = " ".repeat(indent);
704 let mut out = format!("{}{}: ", prefix, self.name);
705 out.push_str(&self.properties.join(", "));
706 if self.children.is_empty() {
707 out.push_str(" {\n");
708 out.push_str(&format!("{}}}\n", prefix));
709 } else {
710 out.push_str(" {\n");
711 for child in &self.children {
712 out.push_str(&child.to_ascii(indent + 1));
713 }
714 out.push_str(&format!("{}}}\n", prefix));
715 }
716 out
717 }
718
719 #[allow(dead_code)]
721 pub fn find_child(&self, name: &str) -> Option<&FbxNode> {
722 self.children.iter().find(|c| c.name == name)
723 }
724}
725
726#[allow(dead_code)]
732pub fn parse_fbx_ascii(input: &str) -> Result<Vec<FbxNode>, String> {
733 let mut nodes = Vec::new();
734 let mut stack: Vec<FbxNode> = Vec::new();
735
736 for line in input.lines() {
737 let trimmed = line.trim();
738 if trimmed.is_empty() || trimmed.starts_with(';') {
739 continue;
740 }
741 if trimmed == "}" {
742 if let Some(node) = stack.pop() {
743 if let Some(parent) = stack.last_mut() {
744 parent.add_child(node);
745 } else {
746 nodes.push(node);
747 }
748 }
749 continue;
750 }
751 if let Some(colon_pos) = trimmed.find(':') {
752 let name = trimmed[..colon_pos].trim();
753 let rest = trimmed[colon_pos + 1..].trim();
754 let mut node = FbxNode::new(name);
755 let props_str = if let Some(brace) = rest.find('{') {
757 rest[..brace].trim()
758 } else {
759 rest
760 };
761 if !props_str.is_empty() {
762 for prop in props_str.split(',') {
763 let p = prop.trim().trim_matches('"');
764 if !p.is_empty() {
765 node.add_property(p);
766 }
767 }
768 }
769 if rest.contains('{') {
770 stack.push(node);
771 } else {
772 if let Some(parent) = stack.last_mut() {
773 parent.add_child(node);
774 } else {
775 nodes.push(node);
776 }
777 }
778 }
779 }
780
781 while let Some(node) = stack.pop() {
783 if let Some(parent) = stack.last_mut() {
784 parent.add_child(node);
785 } else {
786 nodes.push(node);
787 }
788 }
789
790 Ok(nodes)
791}
792
793#[allow(dead_code)]
795pub fn export_fbx_ascii_skeleton(skeleton: &Skeleton) -> String {
796 let mut root = FbxNode::new("FBXHeaderExtension");
797 let mut version = FbxNode::new("FBXVersion");
798 version.add_property("7400");
799 root.add_child(version);
800
801 let mut objects = FbxNode::new("Objects");
802 for (i, joint) in skeleton.joints.iter().enumerate() {
803 let mut model = FbxNode::new("Model");
804 model.add_property(&format!("{}", i));
805 model.add_property(&format!("\"Model::{}\"", joint.name));
806 model.add_property("\"LimbNode\"");
807
808 let mut props = FbxNode::new("Properties70");
809 let mut lct = FbxNode::new("P");
810 lct.add_property("\"Lcl Translation\"");
811 lct.add_property(&format!("{:.6}", joint.offset[0]));
812 lct.add_property(&format!("{:.6}", joint.offset[1]));
813 lct.add_property(&format!("{:.6}", joint.offset[2]));
814 props.add_child(lct);
815 model.add_child(props);
816 objects.add_child(model);
817 }
818
819 let mut out = root.to_ascii(0);
820 out.push_str(&objects.to_ascii(0));
821 out
822}
823
824#[allow(dead_code)]
830pub fn export_usda(skeleton: &Skeleton, clip: &AnimationClip, stage_name: &str) -> String {
831 let mut out = String::new();
832 out.push_str("#usda 1.0\n");
833 out.push_str(&format!("(\n defaultPrim = \"{}\"\n)\n\n", stage_name));
834
835 out.push_str(&format!("def Xform \"{}\" {{\n", stage_name));
837 out.push_str(" def Skeleton \"Skeleton\" {\n");
838
839 let joint_paths: Vec<String> = skeleton
841 .joints
842 .iter()
843 .map(|j| format!("\"{}\"", j.name))
844 .collect();
845 out.push_str(&format!(
846 " uniform token[] joints = [{}]\n",
847 joint_paths.join(", ")
848 ));
849
850 out.push_str(" uniform matrix4d[] bindTransforms = [\n");
852 for joint in &skeleton.joints {
853 out.push_str(&format!(
854 " (({:.6}, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), ({:.6}, {:.6}, {:.6}, 1)),\n",
855 1.0, joint.offset[0], joint.offset[1], joint.offset[2]
856 ));
857 }
858 out.push_str(" ]\n");
859 out.push_str(" }\n");
860
861 if !clip.frames.is_empty() {
863 out.push_str(" def SkelAnimation \"Animation\" {\n");
864 out.push_str(&format!(
865 " uniform token[] joints = [{}]\n",
866 joint_paths.join(", ")
867 ));
868
869 for (f_idx, frame) in clip.frames.iter().enumerate() {
871 let time = if f_idx < clip.times.len() {
872 clip.times[f_idx]
873 } else {
874 f_idx as f64 / clip.fps
875 };
876 out.push_str(&format!(
877 " float3[] translations.timeSamples[{:.6}] = [\n",
878 time
879 ));
880 for pose in &frame.joint_poses {
881 out.push_str(&format!(
882 " ({:.6}, {:.6}, {:.6}),\n",
883 pose.translation[0], pose.translation[1], pose.translation[2]
884 ));
885 }
886 out.push_str(" ]\n");
887
888 out.push_str(&format!(
889 " quatf[] rotations.timeSamples[{:.6}] = [\n",
890 time
891 ));
892 for pose in &frame.joint_poses {
893 let [x, y, z, w] = pose.rotation;
894 out.push_str(&format!(
895 " ({:.6}, {:.6}, {:.6}, {:.6}),\n",
896 w, x, y, z
897 ));
898 }
899 out.push_str(" ]\n");
900 }
901 out.push_str(" }\n");
902 }
903
904 out.push_str("}\n");
905 out
906}
907
908#[derive(Debug, Clone)]
914pub struct BlendShape {
915 pub name: String,
917 pub deltas: Vec<(usize, [f64; 3])>,
919}
920
921impl BlendShape {
922 #[allow(dead_code)]
924 pub fn new(name: &str) -> Self {
925 Self {
926 name: name.to_string(),
927 deltas: Vec::new(),
928 }
929 }
930
931 #[allow(dead_code)]
933 pub fn add_delta(&mut self, vertex_idx: usize, delta: [f64; 3]) {
934 self.deltas.push((vertex_idx, delta));
935 }
936}
937
938#[derive(Debug, Clone)]
940pub struct BlendShapeSet {
941 pub shapes: Vec<BlendShape>,
943 pub weights: Vec<f64>,
945}
946
947impl BlendShapeSet {
948 #[allow(dead_code)]
950 pub fn new() -> Self {
951 Self {
952 shapes: Vec::new(),
953 weights: Vec::new(),
954 }
955 }
956
957 #[allow(dead_code)]
959 pub fn add_shape(&mut self, shape: BlendShape) -> usize {
960 let idx = self.shapes.len();
961 self.shapes.push(shape);
962 self.weights.push(0.0);
963 idx
964 }
965
966 #[allow(dead_code)]
968 pub fn set_weight(&mut self, idx: usize, weight: f64) {
969 if idx < self.weights.len() {
970 self.weights[idx] = weight;
971 }
972 }
973
974 #[allow(dead_code)]
978 pub fn evaluate(&self) -> HashMap<usize, [f64; 3]> {
979 let mut result: HashMap<usize, [f64; 3]> = HashMap::new();
980 for (shape, &weight) in self.shapes.iter().zip(&self.weights) {
981 if weight.abs() < 1e-15 {
982 continue;
983 }
984 for &(vi, delta) in &shape.deltas {
985 let entry = result.entry(vi).or_insert([0.0; 3]);
986 *entry = vec3_add(*entry, vec3_scale(delta, weight));
987 }
988 }
989 result
990 }
991
992 #[allow(dead_code)]
994 pub fn interpolate_weights(a: &[f64], b: &[f64], t: f64) -> Vec<f64> {
995 a.iter()
996 .zip(b.iter())
997 .map(|(&wa, &wb)| wa + (wb - wa) * t)
998 .collect()
999 }
1000}
1001
1002impl Default for BlendShapeSet {
1003 fn default() -> Self {
1004 Self::new()
1005 }
1006}
1007
1008#[derive(Debug, Clone)]
1014pub struct TimelineEvent {
1015 pub time: f64,
1017 pub name: String,
1019 pub payload: String,
1021}
1022
1023#[derive(Debug, Clone)]
1025pub struct TimelineTrack {
1026 pub name: String,
1028 pub start: f64,
1030 pub end: f64,
1032 pub events: Vec<TimelineEvent>,
1034 pub clip_name: Option<String>,
1036}
1037
1038impl TimelineTrack {
1039 #[allow(dead_code)]
1041 pub fn new(name: &str, start: f64, end: f64) -> Self {
1042 Self {
1043 name: name.to_string(),
1044 start,
1045 end,
1046 events: Vec::new(),
1047 clip_name: None,
1048 }
1049 }
1050
1051 #[allow(dead_code)]
1053 pub fn duration(&self) -> f64 {
1054 self.end - self.start
1055 }
1056
1057 #[allow(dead_code)]
1059 pub fn add_event(&mut self, time: f64, name: &str, payload: &str) {
1060 self.events.push(TimelineEvent {
1061 time,
1062 name: name.to_string(),
1063 payload: payload.to_string(),
1064 });
1065 }
1066
1067 #[allow(dead_code)]
1069 pub fn events_at(&self, time: f64, tolerance: f64) -> Vec<&TimelineEvent> {
1070 self.events
1071 .iter()
1072 .filter(|e| (e.time - time).abs() < tolerance)
1073 .collect()
1074 }
1075}
1076
1077#[derive(Debug, Clone)]
1079pub struct Timeline {
1080 pub tracks: Vec<TimelineTrack>,
1082 pub start: f64,
1084 pub end: f64,
1086 pub fps: f64,
1088}
1089
1090impl Timeline {
1091 #[allow(dead_code)]
1093 pub fn new(start: f64, end: f64, fps: f64) -> Self {
1094 Self {
1095 tracks: Vec::new(),
1096 start,
1097 end,
1098 fps,
1099 }
1100 }
1101
1102 #[allow(dead_code)]
1104 pub fn add_track(&mut self, track: TimelineTrack) {
1105 self.tracks.push(track);
1106 }
1107
1108 #[allow(dead_code)]
1110 pub fn duration(&self) -> f64 {
1111 self.end - self.start
1112 }
1113
1114 #[allow(dead_code)]
1116 pub fn total_frames(&self) -> usize {
1117 ((self.end - self.start) * self.fps).ceil() as usize
1118 }
1119
1120 #[allow(dead_code)]
1122 pub fn frame_time(&self, frame: usize) -> f64 {
1123 self.start + (frame as f64) / self.fps
1124 }
1125}
1126
1127#[derive(Debug, Clone)]
1133pub struct RetargetMapping {
1134 pub joint_map: HashMap<usize, usize>,
1136 pub scale: f64,
1138 pub rotation_offsets: HashMap<usize, [f64; 4]>,
1140}
1141
1142impl RetargetMapping {
1143 #[allow(dead_code)]
1145 pub fn new(scale: f64) -> Self {
1146 Self {
1147 joint_map: HashMap::new(),
1148 scale,
1149 rotation_offsets: HashMap::new(),
1150 }
1151 }
1152
1153 #[allow(dead_code)]
1155 pub fn map_joint(&mut self, source_idx: usize, target_idx: usize) {
1156 self.joint_map.insert(source_idx, target_idx);
1157 }
1158
1159 #[allow(dead_code)]
1161 pub fn set_rotation_offset(&mut self, target_idx: usize, offset: [f64; 4]) {
1162 self.rotation_offsets.insert(target_idx, offset);
1163 }
1164
1165 #[allow(dead_code)]
1167 pub fn from_name_mapping(
1168 source: &Skeleton,
1169 target: &Skeleton,
1170 name_map: &HashMap<String, String>,
1171 scale: f64,
1172 ) -> Self {
1173 let mut mapping = Self::new(scale);
1174 for (src_name, tgt_name) in name_map {
1175 if let (Some(&si), Some(&ti)) = (
1176 source.name_to_index.get(src_name),
1177 target.name_to_index.get(tgt_name),
1178 ) {
1179 mapping.map_joint(si, ti);
1180 }
1181 }
1182 mapping
1183 }
1184}
1185
1186#[allow(dead_code)]
1188pub fn retarget_pose(
1189 source_pose: &SkeletonPose,
1190 target_skeleton: &Skeleton,
1191 mapping: &RetargetMapping,
1192) -> SkeletonPose {
1193 let mut target_pose = SkeletonPose::default_for(target_skeleton);
1194
1195 for (&src_idx, &tgt_idx) in &mapping.joint_map {
1196 if src_idx >= source_pose.joint_poses.len() || tgt_idx >= target_pose.joint_poses.len() {
1197 continue;
1198 }
1199 let src = &source_pose.joint_poses[src_idx];
1200 let mut tgt = JointPose {
1201 translation: vec3_scale(src.translation, mapping.scale),
1202 rotation: src.rotation,
1203 scale: src.scale,
1204 };
1205 if let Some(&offset) = mapping.rotation_offsets.get(&tgt_idx) {
1207 tgt.rotation = quat_mul(offset, tgt.rotation);
1208 tgt.rotation = quat_normalize(tgt.rotation);
1209 }
1210 target_pose.joint_poses[tgt_idx] = tgt;
1211 }
1212
1213 target_pose
1214}
1215
1216#[allow(dead_code)]
1218pub fn retarget_clip(
1219 source_clip: &AnimationClip,
1220 target_skeleton: &Skeleton,
1221 mapping: &RetargetMapping,
1222) -> AnimationClip {
1223 let mut target_clip = AnimationClip::new(&source_clip.name, source_clip.fps);
1224 for (i, frame) in source_clip.frames.iter().enumerate() {
1225 let time = if i < source_clip.times.len() {
1226 source_clip.times[i]
1227 } else {
1228 i as f64 / source_clip.fps
1229 };
1230 let retargeted = retarget_pose(frame, target_skeleton, mapping);
1231 target_clip.add_frame(time, retargeted);
1232 }
1233 target_clip
1234}
1235
1236#[derive(Debug, Clone, Copy, PartialEq)]
1242pub enum InterpolationMode {
1243 Step,
1245 Linear,
1247 CubicHermite,
1249}
1250
1251#[allow(dead_code)]
1253pub fn step_interpolate(a: f64, _b: f64, _t: f64) -> f64 {
1254 a
1255}
1256
1257#[allow(dead_code)]
1259pub fn linear_interpolate(a: f64, b: f64, t: f64) -> f64 {
1260 a + (b - a) * t
1261}
1262
1263#[allow(dead_code)]
1265pub fn cubic_hermite_interpolate(p0: f64, m0: f64, p1: f64, m1: f64, t: f64) -> f64 {
1266 let t2 = t * t;
1267 let t3 = t2 * t;
1268 let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
1269 let h10 = t3 - 2.0 * t2 + t;
1270 let h01 = -2.0 * t3 + 3.0 * t2;
1271 let h11 = t3 - t2;
1272 h00 * p0 + h10 * m0 + h01 * p1 + h11 * m1
1273}
1274
1275#[allow(dead_code)]
1277pub fn catmull_rom_tangent(p_prev: f64, p_next: f64, dt: f64) -> f64 {
1278 if dt.abs() < 1e-15 {
1279 return 0.0;
1280 }
1281 (p_next - p_prev) / (2.0 * dt)
1282}
1283
1284#[allow(dead_code)]
1287pub fn resample_clip(
1288 clip: &AnimationClip,
1289 target_fps: f64,
1290 _mode: InterpolationMode,
1291) -> AnimationClip {
1292 let duration = clip.duration();
1293 if duration <= 0.0 || clip.frames.is_empty() {
1294 return clip.clone();
1295 }
1296 let num_frames = (duration * target_fps).ceil() as usize + 1;
1297 let mut resampled = AnimationClip::new(&clip.name, target_fps);
1298
1299 for i in 0..num_frames {
1300 let time = clip.times[0] + (i as f64) / target_fps;
1301 if let Some(pose) = clip.sample(time) {
1302 resampled.add_frame(time, pose);
1303 }
1304 }
1305
1306 resampled
1307}
1308
1309#[allow(dead_code)]
1315pub fn blend_poses(a: &SkeletonPose, b: &SkeletonPose, t: f64) -> SkeletonPose {
1316 a.lerp(b, t)
1317}
1318
1319#[allow(dead_code)]
1322pub fn additive_blend(
1323 base: &SkeletonPose,
1324 reference: &SkeletonPose,
1325 additive: &SkeletonPose,
1326 weight: f64,
1327) -> SkeletonPose {
1328 let n = base.joint_poses.len();
1329 let mut result = base.clone();
1330
1331 for i in 0..n {
1332 if i >= reference.joint_poses.len() || i >= additive.joint_poses.len() {
1333 continue;
1334 }
1335 let ref_pose = &reference.joint_poses[i];
1336 let add_pose = &additive.joint_poses[i];
1337 let base_pose = &base.joint_poses[i];
1338
1339 let t_delta = vec3_sub(add_pose.translation, ref_pose.translation);
1341 result.joint_poses[i].translation =
1342 vec3_add(base_pose.translation, vec3_scale(t_delta, weight));
1343
1344 let ref_inv = quat_conjugate(ref_pose.rotation);
1346 let r_delta = quat_mul(add_pose.rotation, ref_inv);
1347 let r_delta_scaled = quat_slerp([0.0, 0.0, 0.0, 1.0], r_delta, weight);
1348 result.joint_poses[i].rotation =
1349 quat_normalize(quat_mul(r_delta_scaled, base_pose.rotation));
1350 }
1351
1352 result
1353}
1354
1355#[allow(dead_code)]
1363pub fn export_animation_csv(clip: &AnimationClip) -> String {
1364 let mut out = String::from("time,joint,tx,ty,tz,rx,ry,rz,rw\n");
1365 for (f_idx, frame) in clip.frames.iter().enumerate() {
1366 let time = if f_idx < clip.times.len() {
1367 clip.times[f_idx]
1368 } else {
1369 f_idx as f64 / clip.fps
1370 };
1371 for (j_idx, pose) in frame.joint_poses.iter().enumerate() {
1372 out.push_str(&format!(
1373 "{:.6},{},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6}\n",
1374 time,
1375 j_idx,
1376 pose.translation[0],
1377 pose.translation[1],
1378 pose.translation[2],
1379 pose.rotation[0],
1380 pose.rotation[1],
1381 pose.rotation[2],
1382 pose.rotation[3],
1383 ));
1384 }
1385 }
1386 out
1387}
1388
1389#[allow(dead_code)]
1394pub fn parse_animation_csv(input: &str, num_joints: usize) -> Result<AnimationClip, String> {
1395 let mut times_and_poses: Vec<(f64, usize, JointPose)> = Vec::new();
1396
1397 for (line_no, line) in input.lines().enumerate() {
1398 if line_no == 0 || line.trim().is_empty() {
1399 continue; }
1401 let parts: Vec<&str> = line.split(',').collect();
1402 if parts.len() < 9 {
1403 continue;
1404 }
1405 let time: f64 = parts[0].parse().map_err(|_| "Invalid time".to_string())?;
1406 let joint: usize = parts[1].parse().map_err(|_| "Invalid joint".to_string())?;
1407 let tx: f64 = parts[2].parse().unwrap_or(0.0);
1408 let ty: f64 = parts[3].parse().unwrap_or(0.0);
1409 let tz: f64 = parts[4].parse().unwrap_or(0.0);
1410 let rx: f64 = parts[5].parse().unwrap_or(0.0);
1411 let ry: f64 = parts[6].parse().unwrap_or(0.0);
1412 let rz: f64 = parts[7].parse().unwrap_or(0.0);
1413 let rw: f64 = parts[8].parse().unwrap_or(1.0);
1414
1415 times_and_poses.push((
1416 time,
1417 joint,
1418 JointPose {
1419 translation: [tx, ty, tz],
1420 rotation: [rx, ry, rz, rw],
1421 scale: [1.0, 1.0, 1.0],
1422 },
1423 ));
1424 }
1425
1426 let mut frame_map: HashMap<u64, (f64, Vec<(usize, JointPose)>)> = HashMap::new();
1428 for (time, joint, pose) in ×_and_poses {
1429 let key = (*time * 1_000_000.0) as u64;
1430 let entry = frame_map.entry(key).or_insert((*time, Vec::new()));
1431 entry.1.push((*joint, *pose));
1432 }
1433
1434 let mut sorted_frames: Vec<(f64, Vec<(usize, JointPose)>)> = frame_map.into_values().collect();
1435 sorted_frames.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1436
1437 let fps = if sorted_frames.len() >= 2 {
1438 let dt = sorted_frames[1].0 - sorted_frames[0].0;
1439 if dt > 1e-15 { 1.0 / dt } else { 30.0 }
1440 } else {
1441 30.0
1442 };
1443
1444 let mut clip = AnimationClip::new("csv_clip", fps);
1445 for (time, joint_poses) in &sorted_frames {
1446 let mut pose = SkeletonPose {
1447 joint_poses: vec![JointPose::identity(); num_joints],
1448 };
1449 for (ji, jp) in joint_poses {
1450 if *ji < num_joints {
1451 pose.joint_poses[*ji] = *jp;
1452 }
1453 }
1454 clip.add_frame(*time, pose);
1455 }
1456
1457 Ok(clip)
1458}
1459
1460#[cfg(test)]
1465mod tests {
1466 use super::*;
1467
1468 #[test]
1469 fn test_quat_slerp_identity() {
1470 let id = [0.0, 0.0, 0.0, 1.0];
1471 let result = quat_slerp(id, id, 0.5);
1472 assert!((result[3] - 1.0).abs() < 1e-10, "w = {:.6}", result[3]);
1473 }
1474
1475 #[test]
1476 fn test_quat_slerp_endpoints() {
1477 let a = [0.0, 0.0, 0.0, 1.0];
1478 let angle = 0.5_f64;
1479 let b = quat_normalize([0.0, angle.sin(), 0.0, angle.cos()]);
1480 let r0 = quat_slerp(a, b, 0.0);
1481 let r1 = quat_slerp(a, b, 1.0);
1482 for i in 0..4 {
1483 assert!((r0[i] - a[i]).abs() < 1e-10, "r0[{i}] = {:.6}", r0[i]);
1484 assert!((r1[i] - b[i]).abs() < 1e-10, "r1[{i}] = {:.6}", r1[i]);
1485 }
1486 }
1487
1488 #[test]
1489 fn test_quat_slerp_midpoint() {
1490 let a = [0.0, 0.0, 0.0, 1.0];
1491 let angle = std::f64::consts::FRAC_PI_2;
1492 let b = quat_normalize([0.0, (angle / 2.0).sin(), 0.0, (angle / 2.0).cos()]);
1493 let mid = quat_slerp(a, b, 0.5);
1494 let dot_val = quat_dot(mid, a);
1496 assert!(
1497 dot_val > 0.9,
1498 "Midpoint should be close to a, dot = {:.6}",
1499 dot_val
1500 );
1501 }
1502
1503 #[test]
1504 fn test_skeleton_hierarchy() {
1505 let mut skel = Skeleton::new();
1506 let root = skel.add_joint("Hips", None, [0.0, 1.0, 0.0]);
1507 let spine = skel.add_joint("Spine", Some(root), [0.0, 0.2, 0.0]);
1508 let _head = skel.add_joint("Head", Some(spine), [0.0, 0.3, 0.0]);
1509 assert_eq!(skel.num_joints(), 3);
1510 assert_eq!(skel.children(root), vec![1]);
1511 assert_eq!(skel.find_joint("Spine"), Some(1));
1512 }
1513
1514 #[test]
1515 fn test_joint_pose_identity() {
1516 let pose = JointPose::identity();
1517 assert!((pose.rotation[3] - 1.0).abs() < 1e-10);
1518 assert!((pose.scale[0] - 1.0).abs() < 1e-10);
1519 }
1520
1521 #[test]
1522 fn test_joint_pose_lerp() {
1523 let a = JointPose {
1524 translation: [0.0, 0.0, 0.0],
1525 rotation: [0.0, 0.0, 0.0, 1.0],
1526 scale: [1.0, 1.0, 1.0],
1527 };
1528 let b = JointPose {
1529 translation: [2.0, 4.0, 6.0],
1530 rotation: [0.0, 0.0, 0.0, 1.0],
1531 scale: [2.0, 2.0, 2.0],
1532 };
1533 let mid = a.lerp(&b, 0.5);
1534 assert!((mid.translation[0] - 1.0).abs() < 1e-10);
1535 assert!((mid.scale[0] - 1.5).abs() < 1e-10);
1536 }
1537
1538 #[test]
1539 fn test_animation_clip_sample() {
1540 let mut skel = Skeleton::new();
1541 skel.add_joint("Root", None, [0.0; 3]);
1542 let mut clip = AnimationClip::new("test", 30.0);
1543 let mut pose0 = SkeletonPose::default_for(&skel);
1544 pose0.joint_poses[0].translation = [0.0, 0.0, 0.0];
1545 let mut pose1 = SkeletonPose::default_for(&skel);
1546 pose1.joint_poses[0].translation = [10.0, 0.0, 0.0];
1547 clip.add_frame(0.0, pose0);
1548 clip.add_frame(1.0, pose1);
1549 let sampled = clip.sample(0.5).unwrap();
1550 assert!(
1551 (sampled.joint_poses[0].translation[0] - 5.0).abs() < 1e-10,
1552 "tx = {:.6}",
1553 sampled.joint_poses[0].translation[0]
1554 );
1555 }
1556
1557 #[test]
1558 fn test_animation_clip_duration() {
1559 let mut clip = AnimationClip::new("test", 30.0);
1560 let pose = SkeletonPose {
1561 joint_poses: vec![],
1562 };
1563 clip.add_frame(0.0, pose.clone());
1564 clip.add_frame(2.0, pose);
1565 assert!((clip.duration() - 2.0).abs() < 1e-10);
1566 }
1567
1568 #[test]
1569 fn test_parse_bvh_basic() {
1570 let bvh_text = "\
1571HIERARCHY
1572ROOT Hips
1573{
1574 OFFSET 0.0 0.0 0.0
1575 CHANNELS 6 Xposition Yposition Zposition Xrotation Yrotation Zrotation
1576 JOINT Spine
1577 {
1578 OFFSET 0.0 5.0 0.0
1579 CHANNELS 3 Xrotation Yrotation Zrotation
1580 End Site
1581 {
1582 OFFSET 0.0 3.0 0.0
1583 }
1584 }
1585}
1586MOTION
1587Frames: 2
1588Frame Time: 0.033333
15890.0 0.0 0.0 0.0 0.0 0.0 10.0 20.0 30.0
15901.0 2.0 3.0 5.0 10.0 15.0 20.0 25.0 30.0";
1591
1592 let bvh = parse_bvh(bvh_text).unwrap();
1593 assert!(bvh.skeleton.num_joints() >= 2);
1594 assert_eq!(bvh.motion.len(), 2);
1595 assert!((bvh.frame_time - 0.033333).abs() < 1e-4);
1596 }
1597
1598 #[test]
1599 fn test_bvh_to_clip() {
1600 let bvh_text = "\
1601HIERARCHY
1602ROOT Root
1603{
1604 OFFSET 0.0 0.0 0.0
1605 CHANNELS 3 Xposition Yposition Zposition
1606}
1607MOTION
1608Frames: 2
1609Frame Time: 0.033333
16101.0 2.0 3.0
16114.0 5.0 6.0";
1612
1613 let bvh = parse_bvh(bvh_text).unwrap();
1614 let clip = bvh_to_clip(&bvh);
1615 assert_eq!(clip.num_frames(), 2);
1616 }
1617
1618 #[test]
1619 fn test_export_bvh_roundtrip() {
1620 let mut skel = Skeleton::new();
1621 let root = skel.add_joint("Root", None, [0.0; 3]);
1622 skel.set_channels(
1623 root,
1624 vec!["Xposition".into(), "Yposition".into(), "Zposition".into()],
1625 );
1626
1627 let mut clip = AnimationClip::new("test", 30.0);
1628 let mut pose = SkeletonPose::default_for(&skel);
1629 pose.joint_poses[0].translation = [1.0, 2.0, 3.0];
1630 clip.add_frame(0.0, pose);
1631
1632 let exported = export_bvh(&skel, &clip);
1633 assert!(exported.contains("ROOT Root"));
1634 assert!(exported.contains("MOTION"));
1635 }
1636
1637 #[test]
1638 fn test_fbx_node_ascii() {
1639 let mut node = FbxNode::new("Objects");
1640 node.add_property("\"Test\"");
1641 let child = FbxNode::new("Child");
1642 node.add_child(child);
1643 let text = node.to_ascii(0);
1644 assert!(text.contains("Objects:"));
1645 assert!(text.contains("Child:"));
1646 }
1647
1648 #[test]
1649 fn test_parse_fbx_ascii_basic() {
1650 let input = "\
1651; FBX test
1652Objects: {
1653 Model: 1, \"Model::Hips\", \"LimbNode\" {
1654 Properties70: {
1655 }
1656 }
1657}";
1658 let nodes = parse_fbx_ascii(input).unwrap();
1659 assert!(!nodes.is_empty());
1660 assert_eq!(nodes[0].name, "Objects");
1661 }
1662
1663 #[test]
1664 fn test_export_usda() {
1665 let mut skel = Skeleton::new();
1666 skel.add_joint("Root", None, [0.0, 1.0, 0.0]);
1667 let mut clip = AnimationClip::new("walk", 30.0);
1668 let pose = SkeletonPose::default_for(&skel);
1669 clip.add_frame(0.0, pose);
1670
1671 let usda = export_usda(&skel, &clip, "Character");
1672 assert!(usda.contains("#usda 1.0"));
1673 assert!(usda.contains("Skeleton"));
1674 }
1675
1676 #[test]
1677 fn test_blend_shape() {
1678 let mut bs = BlendShape::new("smile");
1679 bs.add_delta(0, [0.1, 0.2, 0.0]);
1680 bs.add_delta(1, [0.0, 0.1, 0.0]);
1681
1682 let mut set = BlendShapeSet::new();
1683 let idx = set.add_shape(bs);
1684 set.set_weight(idx, 0.5);
1685
1686 let result = set.evaluate();
1687 assert!((result[&0][0] - 0.05).abs() < 1e-10);
1688 assert!((result[&0][1] - 0.1).abs() < 1e-10);
1689 }
1690
1691 #[test]
1692 fn test_blend_shape_interpolate_weights() {
1693 let a = vec![0.0, 1.0];
1694 let b = vec![1.0, 0.0];
1695 let mid = BlendShapeSet::interpolate_weights(&a, &b, 0.5);
1696 assert!((mid[0] - 0.5).abs() < 1e-10);
1697 assert!((mid[1] - 0.5).abs() < 1e-10);
1698 }
1699
1700 #[test]
1701 fn test_timeline() {
1702 let mut tl = Timeline::new(0.0, 10.0, 30.0);
1703 let mut track = TimelineTrack::new("main", 0.0, 10.0);
1704 track.add_event(5.0, "footstep", "left");
1705 tl.add_track(track);
1706 assert!((tl.duration() - 10.0).abs() < 1e-10);
1707 assert_eq!(tl.total_frames(), 300);
1708 let events = tl.tracks[0].events_at(5.0, 0.01);
1709 assert_eq!(events.len(), 1);
1710 }
1711
1712 #[test]
1713 fn test_retarget_mapping() {
1714 let mut source = Skeleton::new();
1715 source.add_joint("Hips", None, [0.0; 3]);
1716 source.add_joint("Spine", Some(0), [0.0, 1.0, 0.0]);
1717
1718 let mut target = Skeleton::new();
1719 target.add_joint("pelvis", None, [0.0; 3]);
1720 target.add_joint("spine_01", Some(0), [0.0, 0.5, 0.0]);
1721
1722 let mut name_map = HashMap::new();
1723 name_map.insert("Hips".to_string(), "pelvis".to_string());
1724 name_map.insert("Spine".to_string(), "spine_01".to_string());
1725
1726 let mapping = RetargetMapping::from_name_mapping(&source, &target, &name_map, 0.5);
1727 assert_eq!(mapping.joint_map.len(), 2);
1728 }
1729
1730 #[test]
1731 fn test_retarget_pose() {
1732 let mut source_skel = Skeleton::new();
1733 source_skel.add_joint("A", None, [0.0; 3]);
1734
1735 let mut target_skel = Skeleton::new();
1736 target_skel.add_joint("B", None, [0.0; 3]);
1737
1738 let mut mapping = RetargetMapping::new(2.0);
1739 mapping.map_joint(0, 0);
1740
1741 let mut source_pose = SkeletonPose::default_for(&source_skel);
1742 source_pose.joint_poses[0].translation = [1.0, 2.0, 3.0];
1743
1744 let result = retarget_pose(&source_pose, &target_skel, &mapping);
1745 assert!((result.joint_poses[0].translation[0] - 2.0).abs() < 1e-10);
1746 assert!((result.joint_poses[0].translation[1] - 4.0).abs() < 1e-10);
1747 }
1748
1749 #[test]
1750 fn test_cubic_hermite() {
1751 let v0 = cubic_hermite_interpolate(0.0, 1.0, 1.0, 1.0, 0.0);
1753 let v1 = cubic_hermite_interpolate(0.0, 1.0, 1.0, 1.0, 1.0);
1754 assert!((v0).abs() < 1e-10);
1755 assert!((v1 - 1.0).abs() < 1e-10);
1756 }
1757
1758 #[test]
1759 fn test_step_interpolate() {
1760 assert!((step_interpolate(5.0, 10.0, 0.5) - 5.0).abs() < 1e-10);
1761 }
1762
1763 #[test]
1764 fn test_linear_interpolate() {
1765 assert!((linear_interpolate(0.0, 10.0, 0.3) - 3.0).abs() < 1e-10);
1766 }
1767
1768 #[test]
1769 fn test_resample_clip() {
1770 let mut skel = Skeleton::new();
1771 skel.add_joint("Root", None, [0.0; 3]);
1772 let mut clip = AnimationClip::new("test", 10.0);
1773 let mut p0 = SkeletonPose::default_for(&skel);
1774 p0.joint_poses[0].translation = [0.0, 0.0, 0.0];
1775 let mut p1 = SkeletonPose::default_for(&skel);
1776 p1.joint_poses[0].translation = [10.0, 0.0, 0.0];
1777 clip.add_frame(0.0, p0);
1778 clip.add_frame(1.0, p1);
1779 let resampled = resample_clip(&clip, 20.0, InterpolationMode::Linear);
1780 assert!(resampled.num_frames() > clip.num_frames());
1781 }
1782
1783 #[test]
1784 fn test_export_animation_csv() {
1785 let mut skel = Skeleton::new();
1786 skel.add_joint("Root", None, [0.0; 3]);
1787 let mut clip = AnimationClip::new("test", 30.0);
1788 let pose = SkeletonPose::default_for(&skel);
1789 clip.add_frame(0.0, pose);
1790 let csv = export_animation_csv(&clip);
1791 assert!(csv.contains("time,joint,tx,ty,tz,rx,ry,rz,rw"));
1792 }
1793
1794 #[test]
1795 fn test_parse_animation_csv_roundtrip() {
1796 let mut skel = Skeleton::new();
1797 skel.add_joint("Root", None, [0.0; 3]);
1798 let mut clip = AnimationClip::new("test", 30.0);
1799 let mut pose = SkeletonPose::default_for(&skel);
1800 pose.joint_poses[0].translation = [1.0, 2.0, 3.0];
1801 clip.add_frame(0.0, pose.clone());
1802 clip.add_frame(0.033333, pose);
1803 let csv = export_animation_csv(&clip);
1804 let parsed = parse_animation_csv(&csv, 1).unwrap();
1805 assert_eq!(parsed.num_frames(), 2);
1806 }
1807
1808 #[test]
1809 fn test_additive_blend() {
1810 let mut skel = Skeleton::new();
1811 skel.add_joint("Root", None, [0.0; 3]);
1812
1813 let mut base = SkeletonPose::default_for(&skel);
1814 base.joint_poses[0].translation = [5.0, 0.0, 0.0];
1815
1816 let reference = SkeletonPose::default_for(&skel);
1817 let mut additive = SkeletonPose::default_for(&skel);
1818 additive.joint_poses[0].translation = [1.0, 0.0, 0.0];
1819
1820 let result = additive_blend(&base, &reference, &additive, 1.0);
1821 assert!(
1823 (result.joint_poses[0].translation[0] - 6.0).abs() < 1e-10,
1824 "tx = {:.6}",
1825 result.joint_poses[0].translation[0]
1826 );
1827 }
1828
1829 #[test]
1830 fn test_euler_quat_roundtrip() {
1831 let euler = [15.0, 0.0, 0.0];
1833 let q = euler_xyz_to_quat(euler);
1834 let euler_back = quat_to_euler_xyz(q);
1835 assert!(
1836 (euler[0] - euler_back[0]).abs() < 0.1,
1837 "euler[0] = {:.6}, got {:.6}",
1838 euler[0],
1839 euler_back[0]
1840 );
1841 let euler_y = [0.0, 25.0, 0.0];
1843 let qy = euler_xyz_to_quat(euler_y);
1844 let ey_back = quat_to_euler_xyz(qy);
1845 assert!(
1846 (euler_y[1] - ey_back[1]).abs() < 0.1,
1847 "euler_y[1] = {:.6}, got {:.6}",
1848 euler_y[1],
1849 ey_back[1]
1850 );
1851 }
1852
1853 #[test]
1854 fn test_catmull_rom_tangent() {
1855 let t = catmull_rom_tangent(0.0, 4.0, 1.0);
1856 assert!((t - 2.0).abs() < 1e-10, "tangent = {:.6}", t);
1857 }
1858
1859 #[test]
1860 fn test_export_fbx_skeleton() {
1861 let mut skel = Skeleton::new();
1862 skel.add_joint("Root", None, [0.0, 1.0, 0.0]);
1863 skel.add_joint("Spine", Some(0), [0.0, 0.5, 0.0]);
1864 let fbx = export_fbx_ascii_skeleton(&skel);
1865 assert!(fbx.contains("FBXVersion"));
1866 assert!(fbx.contains("Model"));
1867 }
1868}