1
2use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
12pub struct Bone {
13 pub name: String,
14 pub parent: Option<usize>,
15 pub bind_pose_local: Mat4,
16 pub inverse_bind_pose: Mat4,
17 pub length: f32,
18 pub flags: BoneFlags,
19}
20
21#[derive(Debug, Clone, Copy, Default)]
22pub struct BoneFlags {
23 pub is_root: bool,
24 pub is_leaf: bool,
25 pub is_twist: bool,
26 pub is_ik_target: bool,
27 pub has_physics: bool,
28 pub no_export: bool,
29}
30
31#[derive(Debug, Clone)]
32pub struct Skeleton {
33 pub name: String,
34 pub bones: Vec<Bone>,
35 pub root_indices: Vec<usize>,
36}
37
38impl Skeleton {
39 pub fn new(name: impl Into<String>) -> Self {
40 Self { name: name.into(), bones: Vec::new(), root_indices: Vec::new() }
41 }
42
43 pub fn add_bone(&mut self, bone: Bone) -> usize {
44 let i = self.bones.len();
45 if bone.parent.is_none() {
46 self.root_indices.push(i);
47 }
48 self.bones.push(bone);
49 i
50 }
51
52 pub fn find_bone(&self, name: &str) -> Option<usize> {
53 self.bones.iter().position(|b| b.name == name)
54 }
55
56 pub fn bone_count(&self) -> usize {
57 self.bones.len()
58 }
59
60 pub fn children_of(&self, idx: usize) -> Vec<usize> {
61 self.bones.iter().enumerate()
62 .filter(|(_, b)| b.parent == Some(idx))
63 .map(|(i, _)| i)
64 .collect()
65 }
66
67 pub fn chain_to_root(&self, start: usize) -> Vec<usize> {
68 let mut chain = Vec::new();
69 let mut current = Some(start);
70 while let Some(i) = current {
71 chain.push(i);
72 current = self.bones[i].parent;
73 }
74 chain
75 }
76
77 pub fn world_transform(&self, idx: usize, local_poses: &[Mat4]) -> Mat4 {
78 let chain = self.chain_to_root(idx);
79 let mut mat = Mat4::IDENTITY;
80 for &i in chain.iter().rev() {
81 mat = mat * local_poses[i];
82 }
83 mat
84 }
85
86 pub fn build_biped() -> Self {
88 let mut skel = Skeleton::new("Biped");
89 let root = skel.add_bone(Bone {
90 name: "Root".into(), parent: None,
91 bind_pose_local: Mat4::IDENTITY, inverse_bind_pose: Mat4::IDENTITY,
92 length: 0.1, flags: BoneFlags { is_root: true, ..Default::default() },
93 });
94 let hips = skel.add_bone(Bone {
95 name: "Hips".into(), parent: Some(root),
96 bind_pose_local: Mat4::from_translation(Vec3::new(0.0, 1.0, 0.0)),
97 inverse_bind_pose: Mat4::from_translation(Vec3::new(0.0, -1.0, 0.0)),
98 length: 0.2, flags: Default::default(),
99 });
100 let spine = skel.add_bone(Bone {
101 name: "Spine".into(), parent: Some(hips),
102 bind_pose_local: Mat4::from_translation(Vec3::new(0.0, 0.25, 0.0)),
103 inverse_bind_pose: Mat4::from_translation(Vec3::new(0.0, -0.25, 0.0)),
104 length: 0.25, flags: Default::default(),
105 });
106 let chest = skel.add_bone(Bone {
107 name: "Chest".into(), parent: Some(spine),
108 bind_pose_local: Mat4::from_translation(Vec3::new(0.0, 0.25, 0.0)),
109 inverse_bind_pose: Mat4::from_translation(Vec3::new(0.0, -0.25, 0.0)),
110 length: 0.25, flags: Default::default(),
111 });
112 let neck = skel.add_bone(Bone {
113 name: "Neck".into(), parent: Some(chest),
114 bind_pose_local: Mat4::from_translation(Vec3::new(0.0, 0.25, 0.0)),
115 inverse_bind_pose: Mat4::from_translation(Vec3::new(0.0, -0.25, 0.0)),
116 length: 0.15, flags: Default::default(),
117 });
118 let _head = skel.add_bone(Bone {
119 name: "Head".into(), parent: Some(neck),
120 bind_pose_local: Mat4::from_translation(Vec3::new(0.0, 0.15, 0.0)),
121 inverse_bind_pose: Mat4::from_translation(Vec3::new(0.0, -0.15, 0.0)),
122 length: 0.2, flags: BoneFlags { is_leaf: false, ..Default::default() },
123 });
124 for (side, x) in [("L", 0.2_f32), ("R", -0.2)] {
126 let shoulder = skel.add_bone(Bone {
127 name: format!("{}_Shoulder", side), parent: Some(chest),
128 bind_pose_local: Mat4::from_translation(Vec3::new(x, 0.2, 0.0)),
129 inverse_bind_pose: Mat4::from_translation(Vec3::new(-x, -0.2, 0.0)),
130 length: 0.15, flags: Default::default(),
131 });
132 let upper_arm = skel.add_bone(Bone {
133 name: format!("{}_UpperArm", side), parent: Some(shoulder),
134 bind_pose_local: Mat4::from_translation(Vec3::new(x * 1.5, 0.0, 0.0)),
135 inverse_bind_pose: Mat4::from_translation(Vec3::new(-x * 1.5, 0.0, 0.0)),
136 length: 0.3, flags: Default::default(),
137 });
138 let lower_arm = skel.add_bone(Bone {
139 name: format!("{}_LowerArm", side), parent: Some(upper_arm),
140 bind_pose_local: Mat4::from_translation(Vec3::new(x * 1.5, 0.0, 0.0)),
141 inverse_bind_pose: Mat4::from_translation(Vec3::new(-x * 1.5, 0.0, 0.0)),
142 length: 0.28, flags: Default::default(),
143 });
144 let _hand = skel.add_bone(Bone {
145 name: format!("{}_Hand", side), parent: Some(lower_arm),
146 bind_pose_local: Mat4::from_translation(Vec3::new(x * 1.2, 0.0, 0.0)),
147 inverse_bind_pose: Mat4::from_translation(Vec3::new(-x * 1.2, 0.0, 0.0)),
148 length: 0.1, flags: BoneFlags { is_ik_target: true, ..Default::default() },
149 });
150 }
151 for (side, x) in [("L", 0.1_f32), ("R", -0.1)] {
153 let upper_leg = skel.add_bone(Bone {
154 name: format!("{}_UpperLeg", side), parent: Some(hips),
155 bind_pose_local: Mat4::from_translation(Vec3::new(x, -0.1, 0.0)),
156 inverse_bind_pose: Mat4::from_translation(Vec3::new(-x, 0.1, 0.0)),
157 length: 0.45, flags: Default::default(),
158 });
159 let lower_leg = skel.add_bone(Bone {
160 name: format!("{}_LowerLeg", side), parent: Some(upper_leg),
161 bind_pose_local: Mat4::from_translation(Vec3::new(0.0, -0.45, 0.0)),
162 inverse_bind_pose: Mat4::from_translation(Vec3::new(0.0, 0.45, 0.0)),
163 length: 0.42, flags: Default::default(),
164 });
165 let _foot = skel.add_bone(Bone {
166 name: format!("{}_Foot", side), parent: Some(lower_leg),
167 bind_pose_local: Mat4::from_translation(Vec3::new(0.0, -0.42, 0.0)),
168 inverse_bind_pose: Mat4::from_translation(Vec3::new(0.0, 0.42, 0.0)),
169 length: 0.15, flags: BoneFlags { is_ik_target: true, ..Default::default() },
170 });
171 }
172 skel
173 }
174}
175
176#[derive(Debug, Clone)]
181pub struct Pose {
182 pub local_transforms: Vec<Mat4>,
183 pub bone_count: usize,
184}
185
186impl Pose {
187 pub fn bind_pose(skel: &Skeleton) -> Self {
188 Self {
189 local_transforms: skel.bones.iter().map(|b| b.bind_pose_local).collect(),
190 bone_count: skel.bones.len(),
191 }
192 }
193
194 pub fn identity(bone_count: usize) -> Self {
195 Self {
196 local_transforms: vec![Mat4::IDENTITY; bone_count],
197 bone_count,
198 }
199 }
200
201 pub fn lerp(&self, other: &Pose, t: f32) -> Pose {
202 assert_eq!(self.bone_count, other.bone_count);
203 let local_transforms = self.local_transforms.iter().zip(other.local_transforms.iter())
204 .map(|(a, b)| {
205 let (sa, qa, ta) = decompose_mat4(*a);
206 let (sb, qb, tb) = decompose_mat4(*b);
207 let s = sa.lerp(sb, t);
208 let q = qa.slerp(qb, t);
209 let tr = ta.lerp(tb, t);
210 Mat4::from_scale_rotation_translation(s, q, tr)
211 })
212 .collect();
213 Pose { local_transforms, bone_count: self.bone_count }
214 }
215}
216
217fn decompose_mat4(m: Mat4) -> (Vec3, Quat, Vec3) {
218 let translation = m.w_axis.truncate();
219 let sx = m.x_axis.truncate().length();
220 let sy = m.y_axis.truncate().length();
221 let sz = m.z_axis.truncate().length();
222 let scale = Vec3::new(sx, sy, sz);
223 let rot_mat = Mat4::from_cols(
224 (m.x_axis.truncate() / sx).extend(0.0),
225 (m.y_axis.truncate() / sy).extend(0.0),
226 (m.z_axis.truncate() / sz).extend(0.0),
227 Vec4::W,
228 );
229 let rotation = Quat::from_mat4(&rot_mat);
230 (scale, rotation, translation)
231}
232
233#[derive(Debug, Clone, Copy, PartialEq)]
238pub enum MappingMode {
239 Exact, HumanoidRig, Manual, AutoLearned, }
244
245#[derive(Debug, Clone, Copy, PartialEq)]
246pub enum HumanoidSlot {
247 Hips, Spine, Chest, UpperChest, Neck, Head,
248 LeftShoulder, LeftUpperArm, LeftLowerArm, LeftHand,
249 RightShoulder, RightUpperArm, RightLowerArm, RightHand,
250 LeftUpperLeg, LeftLowerLeg, LeftFoot, LeftToes,
251 RightUpperLeg, RightLowerLeg, RightFoot, RightToes,
252 LeftThumbProximal, LeftThumbIntermediate, LeftThumbDistal,
253 RightThumbProximal, RightThumbIntermediate, RightThumbDistal,
254}
255
256impl HumanoidSlot {
257 pub fn label(self) -> &'static str {
258 match self {
259 HumanoidSlot::Hips => "Hips",
260 HumanoidSlot::Spine => "Spine",
261 HumanoidSlot::Chest => "Chest",
262 HumanoidSlot::UpperChest => "Upper Chest",
263 HumanoidSlot::Neck => "Neck",
264 HumanoidSlot::Head => "Head",
265 HumanoidSlot::LeftUpperArm => "Left Upper Arm",
266 HumanoidSlot::LeftLowerArm => "Left Lower Arm",
267 HumanoidSlot::LeftHand => "Left Hand",
268 HumanoidSlot::RightUpperArm => "Right Upper Arm",
269 HumanoidSlot::RightLowerArm => "Right Lower Arm",
270 HumanoidSlot::RightHand => "Right Hand",
271 HumanoidSlot::LeftUpperLeg => "Left Upper Leg",
272 HumanoidSlot::LeftLowerLeg => "Left Lower Leg",
273 HumanoidSlot::LeftFoot => "Left Foot",
274 HumanoidSlot::RightUpperLeg => "Right Upper Leg",
275 HumanoidSlot::RightLowerLeg => "Right Lower Leg",
276 HumanoidSlot::RightFoot => "Right Foot",
277 _ => "Other",
278 }
279 }
280}
281
282#[derive(Debug, Clone)]
283pub struct BoneMapping {
284 pub source_bone: String,
285 pub target_bone: String,
286 pub rotation_offset: Quat,
287 pub scale_factor: f32,
288 pub position_correction: Vec3,
289 pub mirror: bool,
290 pub invert_axes: [bool; 3],
291}
292
293impl BoneMapping {
294 pub fn direct(source: impl Into<String>, target: impl Into<String>) -> Self {
295 Self {
296 source_bone: source.into(),
297 target_bone: target.into(),
298 rotation_offset: Quat::IDENTITY,
299 scale_factor: 1.0,
300 position_correction: Vec3::ZERO,
301 mirror: false,
302 invert_axes: [false; 3],
303 }
304 }
305}
306
307#[derive(Debug, Clone, Copy, PartialEq)]
312pub enum HeightMatchMode {
313 None, ScaleUniform, ScaleLegs, IKAdapt,
314}
315
316#[derive(Debug, Clone)]
317pub struct RetargetingSettings {
318 pub mode: MappingMode,
319 pub height_match: HeightMatchMode,
320 pub preserve_foot_contact: bool,
321 pub preserve_hand_contact: bool,
322 pub copy_root_motion: bool,
323 pub root_motion_scale: f32,
324 pub compensate_hip_position: bool,
325 pub ik_solve_hands: bool,
326 pub ik_solve_feet: bool,
327 pub ik_iterations: u32,
328 pub ik_tolerance: f32,
329 pub global_scale: f32,
330}
331
332impl Default for RetargetingSettings {
333 fn default() -> Self {
334 Self {
335 mode: MappingMode::HumanoidRig,
336 height_match: HeightMatchMode::ScaleLegs,
337 preserve_foot_contact: true,
338 preserve_hand_contact: false,
339 copy_root_motion: true,
340 root_motion_scale: 1.0,
341 compensate_hip_position: true,
342 ik_solve_hands: false,
343 ik_solve_feet: true,
344 ik_iterations: 20,
345 ik_tolerance: 0.001,
346 global_scale: 1.0,
347 }
348 }
349}
350
351#[derive(Debug, Clone)]
356pub struct IkChain {
357 pub bone_indices: Vec<usize>,
358 pub positions: Vec<Vec3>,
359 pub lengths: Vec<f32>,
360 pub target: Vec3,
361 pub pole_target: Option<Vec3>,
362 pub constraints: Vec<IkConstraint>,
363}
364
365#[derive(Debug, Clone, Copy)]
366pub struct IkConstraint {
367 pub min_angle: f32,
368 pub max_angle: f32,
369 pub twist_min: f32,
370 pub twist_max: f32,
371}
372
373impl IkChain {
374 pub fn new(bone_indices: Vec<usize>, positions: Vec<Vec3>, lengths: Vec<f32>) -> Self {
375 let target = *positions.last().unwrap_or(&Vec3::ZERO);
376 Self {
377 bone_indices, positions, lengths, target,
378 pole_target: None, constraints: Vec::new(),
379 }
380 }
381
382 pub fn solve_fabrik(&mut self, max_iter: u32, tolerance: f32) {
384 let n = self.positions.len();
385 if n < 2 { return; }
386 let root = self.positions[0];
387 let total_length: f32 = self.lengths.iter().sum();
388 let dist = self.target.distance(root);
389
390 if dist >= total_length {
392 for i in 1..n {
393 let d = (self.target - self.positions[i-1]).normalize();
394 self.positions[i] = self.positions[i-1] + d * self.lengths[i-1];
395 }
396 return;
397 }
398
399 for _ in 0..max_iter {
400 self.positions[n-1] = self.target;
402 for i in (0..n-1).rev() {
403 let d = (self.positions[i] - self.positions[i+1]).normalize();
404 self.positions[i] = self.positions[i+1] + d * self.lengths[i];
405 }
406 self.positions[0] = root;
408 for i in 0..n-1 {
409 let d = (self.positions[i+1] - self.positions[i]).normalize();
410 self.positions[i+1] = self.positions[i] + d * self.lengths[i];
411 }
412 if self.positions[n-1].distance(self.target) < tolerance {
414 break;
415 }
416 }
417 }
418
419 pub fn compute_rotations(&self, rest_positions: &[Vec3]) -> Vec<Quat> {
421 let n = self.positions.len() - 1;
422 (0..n).map(|i| {
423 let rest_dir = (rest_positions[i+1] - rest_positions[i]).normalize();
424 let solved_dir = (self.positions[i+1] - self.positions[i]).normalize();
425 if rest_dir.dot(solved_dir) > 0.9999 {
426 Quat::IDENTITY
427 } else {
428 Quat::from_rotation_arc(rest_dir, solved_dir)
429 }
430 }).collect()
431 }
432}
433
434#[derive(Debug, Clone)]
439pub struct AnimationRetargeter {
440 pub source_skeleton: Skeleton,
441 pub target_skeleton: Skeleton,
442 pub mappings: Vec<BoneMapping>,
443 pub settings: RetargetingSettings,
444 pub humanoid_source: HashMap<String, String>,
445 pub humanoid_target: HashMap<String, String>,
446 pub source_height: f32,
447 pub target_height: f32,
448}
449
450#[derive(Debug, Clone)]
452pub struct AnimRetargeter {
453 pub source_skeleton: Skeleton,
454 pub target_skeleton: Skeleton,
455 pub mappings: Vec<BoneMapping>,
456 pub settings: RetargetingSettings,
457 pub source_height: f32,
458 pub target_height: f32,
459 pub scale_factor: f32,
460}
461
462impl AnimRetargeter {
463 pub fn new(source: Skeleton, target: Skeleton) -> Self {
464 let src_h = 1.8_f32;
465 let tgt_h = 1.8_f32;
466 Self {
467 source_skeleton: source,
468 target_skeleton: target,
469 mappings: Vec::new(),
470 settings: RetargetingSettings::default(),
471 source_height: src_h,
472 target_height: tgt_h,
473 scale_factor: tgt_h / src_h,
474 }
475 }
476
477 pub fn auto_map_by_name(&mut self) {
478 self.mappings.clear();
479 for src_bone in &self.source_skeleton.bones {
480 if let Some(_) = self.target_skeleton.find_bone(&src_bone.name) {
481 self.mappings.push(BoneMapping::direct(&src_bone.name, &src_bone.name));
482 }
483 }
484 }
485
486 pub fn add_mapping(&mut self, mapping: BoneMapping) {
487 self.mappings.retain(|m| m.source_bone != mapping.source_bone);
488 self.mappings.push(mapping);
489 }
490
491 pub fn retarget_pose(&self, source_pose: &Pose) -> Pose {
492 let n = self.target_skeleton.bone_count();
493 let mut result = Pose::bind_pose(&self.target_skeleton);
494
495 for mapping in &self.mappings {
496 let src_idx = self.source_skeleton.find_bone(&mapping.source_bone);
497 let tgt_idx = self.target_skeleton.find_bone(&mapping.target_bone);
498 if let (Some(si), Some(ti)) = (src_idx, tgt_idx) {
499 let src_mat = source_pose.local_transforms[si];
500 let (scale, rot, trans) = decompose_mat4(src_mat);
501 let rot = rot * mapping.rotation_offset;
503 let trans = (trans + mapping.position_correction) * mapping.scale_factor * self.scale_factor;
504 result.local_transforms[ti] = Mat4::from_scale_rotation_translation(scale, rot, trans);
505 }
506 }
507 result
508 }
509
510 pub fn unmapped_source_bones(&self) -> Vec<&str> {
511 self.source_skeleton.bones.iter()
512 .filter(|b| !self.mappings.iter().any(|m| m.source_bone == b.name))
513 .map(|b| b.name.as_str())
514 .collect()
515 }
516
517 pub fn unmapped_target_bones(&self) -> Vec<&str> {
518 self.target_skeleton.bones.iter()
519 .filter(|b| !self.mappings.iter().any(|m| m.target_bone == b.name))
520 .map(|b| b.name.as_str())
521 .collect()
522 }
523}
524
525#[derive(Debug, Clone)]
530pub struct AnimationTrack {
531 pub bone_name: String,
532 pub times: Vec<f32>,
533 pub translations: Vec<Vec3>,
534 pub rotations: Vec<Quat>,
535 pub scales: Vec<Vec3>,
536}
537
538impl AnimationTrack {
539 pub fn new(bone_name: impl Into<String>) -> Self {
540 Self {
541 bone_name: bone_name.into(),
542 times: Vec::new(),
543 translations: Vec::new(),
544 rotations: Vec::new(),
545 scales: Vec::new(),
546 }
547 }
548
549 pub fn add_keyframe(&mut self, time: f32, translation: Vec3, rotation: Quat, scale: Vec3) {
550 let i = self.times.partition_point(|&t| t <= time);
551 self.times.insert(i, time);
552 self.translations.insert(i, translation);
553 self.rotations.insert(i, rotation);
554 self.scales.insert(i, scale);
555 }
556
557 pub fn sample(&self, time: f32) -> Mat4 {
558 if self.times.is_empty() { return Mat4::IDENTITY; }
559 if self.times.len() == 1 {
560 return Mat4::from_scale_rotation_translation(self.scales[0], self.rotations[0], self.translations[0]);
561 }
562 let i = self.times.partition_point(|&t| t <= time);
563 if i == 0 {
564 return Mat4::from_scale_rotation_translation(self.scales[0], self.rotations[0], self.translations[0]);
565 }
566 if i >= self.times.len() {
567 let last = self.times.len() - 1;
568 return Mat4::from_scale_rotation_translation(self.scales[last], self.rotations[last], self.translations[last]);
569 }
570 let t0 = self.times[i-1];
571 let t1 = self.times[i];
572 let u = ((time - t0) / (t1 - t0)).clamp(0.0, 1.0);
573 let translation = self.translations[i-1].lerp(self.translations[i], u);
574 let rotation = self.rotations[i-1].slerp(self.rotations[i], u);
575 let scale = self.scales[i-1].lerp(self.scales[i], u);
576 Mat4::from_scale_rotation_translation(scale, rotation, translation)
577 }
578
579 pub fn duration(&self) -> f32 {
580 self.times.last().copied().unwrap_or(0.0)
581 }
582}
583
584#[derive(Debug, Clone)]
585pub struct AnimationClip {
586 pub name: String,
587 pub duration: f32,
588 pub frame_rate: f32,
589 pub tracks: Vec<AnimationTrack>,
590 pub loop_mode: LoopMode,
591 pub root_motion: bool,
592}
593
594#[derive(Debug, Clone, Copy, PartialEq)]
595pub enum LoopMode { Once, Loop, PingPong, ClampForever }
596
597impl AnimationClip {
598 pub fn new(name: impl Into<String>, duration: f32, fps: f32) -> Self {
599 Self {
600 name: name.into(),
601 duration,
602 frame_rate: fps,
603 tracks: Vec::new(),
604 loop_mode: LoopMode::Loop,
605 root_motion: false,
606 }
607 }
608
609 pub fn sample_pose(&self, time: f32, skeleton: &Skeleton) -> Pose {
610 let mut pose = Pose::bind_pose(skeleton);
611 for track in &self.tracks {
612 if let Some(idx) = skeleton.find_bone(&track.bone_name) {
613 pose.local_transforms[idx] = track.sample(time);
614 }
615 }
616 pose
617 }
618
619 pub fn frame_count(&self) -> u32 {
620 (self.duration * self.frame_rate) as u32
621 }
622
623 pub fn normalized_time(&self, time: f32) -> f32 {
624 match self.loop_mode {
625 LoopMode::Once => (time / self.duration).min(1.0),
626 LoopMode::Loop => (time / self.duration).fract(),
627 LoopMode::PingPong => {
628 let t = time / self.duration;
629 let n = t.floor() as i32;
630 if n % 2 == 0 { t.fract() } else { 1.0 - t.fract() }
631 }
632 LoopMode::ClampForever => (time / self.duration).clamp(0.0, 1.0),
633 }
634 }
635}
636
637#[derive(Debug, Clone, Copy, PartialEq)]
642pub enum RetargetEditorTab {
643 SkeletonMapping,
644 PreviewComparison,
645 IkSettings,
646 ClipLibrary,
647}
648
649#[derive(Debug, Clone)]
650pub struct RetargetEditor {
651 pub retargeter: Option<AnimRetargeter>,
652 pub active_tab: RetargetEditorTab,
653 pub source_clips: Vec<AnimationClip>,
654 pub selected_clip: Option<usize>,
655 pub preview_time: f32,
656 pub preview_playing: bool,
657 pub show_bind_pose: bool,
658 pub show_ik_debug: bool,
659 pub selected_bone_src: Option<String>,
660 pub selected_bone_tgt: Option<String>,
661 pub filter_unmapped: bool,
662 pub search_query: String,
663}
664
665impl RetargetEditor {
666 pub fn new() -> Self {
667 let src = Skeleton::build_biped();
668 let tgt = Skeleton::build_biped();
669 let mut retargeter = AnimRetargeter::new(src, tgt);
670 retargeter.auto_map_by_name();
671 Self {
672 retargeter: Some(retargeter),
673 active_tab: RetargetEditorTab::SkeletonMapping,
674 source_clips: Vec::new(),
675 selected_clip: None,
676 preview_time: 0.0,
677 preview_playing: false,
678 show_bind_pose: false,
679 show_ik_debug: false,
680 selected_bone_src: None,
681 selected_bone_tgt: None,
682 filter_unmapped: false,
683 search_query: String::new(),
684 }
685 }
686
687 pub fn load_demo_clip(&mut self) {
688 let mut clip = AnimationClip::new("Walk_Cycle", 1.0, 30.0);
689 if let Some(ret) = &self.retargeter {
690 for bone in &ret.source_skeleton.bones {
691 let mut track = AnimationTrack::new(&bone.name);
692 for f in 0..30 {
694 let t = f as f32 / 30.0;
695 let offset = Vec3::new(0.0, (t * std::f32::consts::TAU).sin() * 0.02, 0.0);
696 track.add_keyframe(t, offset, Quat::IDENTITY, Vec3::ONE);
697 }
698 clip.tracks.push(track);
699 }
700 }
701 self.source_clips.push(clip);
702 self.selected_clip = Some(0);
703 }
704
705 pub fn update(&mut self, dt: f32) {
706 if self.preview_playing {
707 self.preview_time += dt;
708 if let Some(idx) = self.selected_clip {
709 let dur = self.source_clips.get(idx).map(|c| c.duration).unwrap_or(1.0);
710 self.preview_time = self.preview_time % dur;
711 }
712 }
713 }
714
715 pub fn mapping_count(&self) -> usize {
716 self.retargeter.as_ref().map(|r| r.mappings.len()).unwrap_or(0)
717 }
718}
719
720#[cfg(test)]
724mod tests {
725 use super::*;
726
727 #[test]
728 fn test_biped_skeleton() {
729 let skel = Skeleton::build_biped();
730 assert!(skel.bone_count() > 10);
731 assert!(skel.find_bone("Hips").is_some());
732 assert!(skel.find_bone("L_Hand").is_some());
733 }
734
735 #[test]
736 fn test_retargeter_auto_map() {
737 let src = Skeleton::build_biped();
738 let tgt = Skeleton::build_biped();
739 let mut ret = AnimRetargeter::new(src, tgt);
740 ret.auto_map_by_name();
741 assert!(ret.mappings.len() > 0);
742 }
743
744 #[test]
745 fn test_fabrik() {
746 let positions = vec![Vec3::ZERO, Vec3::new(0.0, 1.0, 0.0), Vec3::new(0.0, 2.0, 0.0)];
747 let lengths = vec![1.0, 1.0];
748 let mut chain = IkChain::new(vec![0, 1, 2], positions, lengths);
749 chain.target = Vec3::new(1.0, 1.0, 0.0);
750 chain.solve_fabrik(20, 0.001);
751 assert!(chain.positions[2].distance(chain.target) < 0.01);
752 }
753
754 #[test]
755 fn test_animation_track() {
756 let mut track = AnimationTrack::new("Hips");
757 track.add_keyframe(0.0, Vec3::ZERO, Quat::IDENTITY, Vec3::ONE);
758 track.add_keyframe(1.0, Vec3::Y, Quat::IDENTITY, Vec3::ONE);
759 let mat = track.sample(0.5);
760 let (_, _, t) = decompose_mat4(mat);
761 assert!((t.y - 0.5).abs() < 0.01);
762 }
763
764 #[test]
765 fn test_pose_lerp() {
766 let skel = Skeleton::build_biped();
767 let a = Pose::identity(skel.bone_count());
768 let b = Pose::identity(skel.bone_count());
769 let _ = a.lerp(&b, 0.5);
770 }
771}