Skip to main content

oxihuman_morph/
character_rig.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4#![allow(dead_code)]
5
6use std::collections::HashMap;
7
8// ---------------------------------------------------------------------------
9// RigJoint
10// ---------------------------------------------------------------------------
11
12/// A joint definition in the rig.
13#[derive(Clone, Debug)]
14pub struct RigJoint {
15    pub name: String,
16    pub parent: Option<String>,
17    /// Bind pose position (world space).
18    pub position: [f32; 3],
19    /// Bind pose rotation (axis-angle: \[ax, ay, az, angle\]).
20    pub rotation: [f32; 4],
21    /// Scale.
22    pub scale: [f32; 3],
23}
24
25impl RigJoint {
26    /// Create a new joint with default identity pose.
27    pub fn new(name: impl Into<String>) -> Self {
28        RigJoint {
29            name: name.into(),
30            parent: None,
31            position: [0.0, 0.0, 0.0],
32            rotation: [0.0, 1.0, 0.0, 0.0],
33            scale: [1.0, 1.0, 1.0],
34        }
35    }
36
37    /// Set the parent joint name.
38    pub fn with_parent(mut self, parent: impl Into<String>) -> Self {
39        self.parent = Some(parent.into());
40        self
41    }
42
43    /// Set the bind-pose position.
44    pub fn with_position(mut self, pos: [f32; 3]) -> Self {
45        self.position = pos;
46        self
47    }
48
49    /// Return references to all joints in `joints` whose parent is `parent`.
50    pub fn children_of<'a>(joints: &'a [RigJoint], parent: &str) -> Vec<&'a RigJoint> {
51        joints
52            .iter()
53            .filter(|j| j.parent.as_deref() == Some(parent))
54            .collect()
55    }
56}
57
58// ---------------------------------------------------------------------------
59// MorphBinding
60// ---------------------------------------------------------------------------
61
62/// Binding between a morph target and its driving parameters.
63#[derive(Clone, Debug)]
64pub struct MorphBinding {
65    pub morph_name: String,
66    /// Which parameter drives it.
67    pub param_name: String,
68    /// Linear map: weight = (param - input_min) / (input_max - input_min).
69    pub input_min: f32,
70    pub input_max: f32,
71    /// Optional joint that also contributes to this morph.
72    pub joint_driver: Option<String>,
73}
74
75impl MorphBinding {
76    /// Create a new binding with default range [0.0, 1.0].
77    pub fn new(morph: impl Into<String>, param: impl Into<String>) -> Self {
78        MorphBinding {
79            morph_name: morph.into(),
80            param_name: param.into(),
81            input_min: 0.0,
82            input_max: 1.0,
83            joint_driver: None,
84        }
85    }
86
87    /// Compute the morph weight for a given parameter value.
88    pub fn compute_weight(&self, param_value: f32) -> f32 {
89        ((param_value - self.input_min) / (self.input_max - self.input_min)).clamp(0.0, 1.0)
90    }
91}
92
93// ---------------------------------------------------------------------------
94// RigBodyRegion
95// ---------------------------------------------------------------------------
96
97/// A named region of the body (e.g., "upper_arm", "torso").
98#[derive(Clone, Debug)]
99pub struct RigBodyRegion {
100    pub name: String,
101    /// Joints that belong to this region.
102    pub joints: Vec<String>,
103    /// Morph targets relevant to this region.
104    pub morph_targets: Vec<String>,
105    /// Vertex group name.
106    pub vertex_group: Option<String>,
107}
108
109// ---------------------------------------------------------------------------
110// CharacterRig
111// ---------------------------------------------------------------------------
112
113/// The complete character rig.
114pub struct CharacterRig {
115    pub name: String,
116    pub joints: Vec<RigJoint>,
117    pub morph_bindings: Vec<MorphBinding>,
118    pub regions: Vec<RigBodyRegion>,
119    pub metadata: HashMap<String, String>,
120}
121
122impl CharacterRig {
123    /// Create a new, empty rig.
124    pub fn new(name: impl Into<String>) -> Self {
125        CharacterRig {
126            name: name.into(),
127            joints: Vec::new(),
128            morph_bindings: Vec::new(),
129            regions: Vec::new(),
130            metadata: HashMap::new(),
131        }
132    }
133
134    /// Add a joint to the rig.
135    pub fn add_joint(&mut self, joint: RigJoint) {
136        self.joints.push(joint);
137    }
138
139    /// Add a morph binding.
140    pub fn add_morph_binding(&mut self, binding: MorphBinding) {
141        self.morph_bindings.push(binding);
142    }
143
144    /// Add a body region.
145    pub fn add_region(&mut self, region: RigBodyRegion) {
146        self.regions.push(region);
147    }
148
149    /// Number of joints in the rig.
150    pub fn joint_count(&self) -> usize {
151        self.joints.len()
152    }
153
154    /// Number of morph bindings.
155    pub fn morph_binding_count(&self) -> usize {
156        self.morph_bindings.len()
157    }
158
159    /// Look up a joint by name.
160    pub fn get_joint(&self, name: &str) -> Option<&RigJoint> {
161        self.joints.iter().find(|j| j.name == name)
162    }
163
164    /// Return all joints that have no parent (i.e. root joints).
165    pub fn root_joints(&self) -> Vec<&RigJoint> {
166        self.joints.iter().filter(|j| j.parent.is_none()).collect()
167    }
168
169    /// Return all joints whose parent is `parent`.
170    pub fn children_of(&self, parent: &str) -> Vec<&RigJoint> {
171        RigJoint::children_of(&self.joints, parent)
172    }
173
174    /// Return the depth of `name` in the joint hierarchy (root = 0).
175    pub fn joint_depth(&self, name: &str) -> usize {
176        let mut depth = 0;
177        let mut current = name.to_string();
178        loop {
179            match self.get_joint(&current) {
180                None => break,
181                Some(j) => match &j.parent {
182                    None => break,
183                    Some(p) => {
184                        depth += 1;
185                        current = p.clone();
186                    }
187                },
188            }
189        }
190        depth
191    }
192
193    /// Return all bindings driven by `param`.
194    pub fn bindings_for_param(&self, param: &str) -> Vec<&MorphBinding> {
195        self.morph_bindings
196            .iter()
197            .filter(|b| b.param_name == param)
198            .collect()
199    }
200
201    /// Return all bindings that have `joint` as their joint driver.
202    pub fn bindings_for_joint(&self, joint: &str) -> Vec<&MorphBinding> {
203        self.morph_bindings
204            .iter()
205            .filter(|b| b.joint_driver.as_deref() == Some(joint))
206            .collect()
207    }
208
209    /// Evaluate all morph weights given the current parameter state.
210    pub fn evaluate_morphs(&self, params: &HashMap<String, f32>) -> HashMap<String, f32> {
211        let mut result = HashMap::new();
212        for binding in &self.morph_bindings {
213            let param_value = params.get(&binding.param_name).copied().unwrap_or(0.0);
214            let weight = binding.compute_weight(param_value);
215            result.insert(binding.morph_name.clone(), weight);
216        }
217        result
218    }
219
220    /// Serialize the rig to a JSON string.
221    pub fn to_json(&self) -> String {
222        let joints_json: Vec<serde_json::Value> = self
223            .joints
224            .iter()
225            .map(|j| {
226                serde_json::json!({
227                    "name": j.name,
228                    "parent": j.parent,
229                    "position": j.position,
230                    "rotation": j.rotation,
231                    "scale": j.scale,
232                })
233            })
234            .collect();
235
236        let bindings_json: Vec<serde_json::Value> = self
237            .morph_bindings
238            .iter()
239            .map(|b| {
240                serde_json::json!({
241                    "morph_name": b.morph_name,
242                    "param_name": b.param_name,
243                    "input_min": b.input_min,
244                    "input_max": b.input_max,
245                    "joint_driver": b.joint_driver,
246                })
247            })
248            .collect();
249
250        let regions_json: Vec<serde_json::Value> = self
251            .regions
252            .iter()
253            .map(|r| {
254                serde_json::json!({
255                    "name": r.name,
256                    "joints": r.joints,
257                    "morph_targets": r.morph_targets,
258                    "vertex_group": r.vertex_group,
259                })
260            })
261            .collect();
262
263        let obj = serde_json::json!({
264            "name": self.name,
265            "joints": joints_json,
266            "morph_bindings": bindings_json,
267            "regions": regions_json,
268            "metadata": self.metadata,
269        });
270
271        obj.to_string()
272    }
273}
274
275// ---------------------------------------------------------------------------
276// standard_human_rig  (~22 joints, hm08 skeleton approximation)
277// ---------------------------------------------------------------------------
278
279/// Build a standard human rig (hm08 skeleton approximation, ~22 joints).
280pub fn standard_human_rig() -> CharacterRig {
281    let mut rig = CharacterRig::new("standard_human");
282    rig.metadata
283        .insert("source".to_string(), "hm08_approximation".to_string());
284    rig.metadata
285        .insert("version".to_string(), "1.0".to_string());
286
287    // Root
288    rig.add_joint(RigJoint::new("hips").with_position([0.0, 1.0, 0.0]));
289
290    // Spine chain
291    rig.add_joint(
292        RigJoint::new("spine1")
293            .with_parent("hips")
294            .with_position([0.0, 1.1, 0.0]),
295    );
296    rig.add_joint(
297        RigJoint::new("spine2")
298            .with_parent("spine1")
299            .with_position([0.0, 1.3, 0.0]),
300    );
301    rig.add_joint(
302        RigJoint::new("spine3")
303            .with_parent("spine2")
304            .with_position([0.0, 1.5, 0.0]),
305    );
306
307    // Neck and head
308    rig.add_joint(
309        RigJoint::new("neck")
310            .with_parent("spine3")
311            .with_position([0.0, 1.6, 0.0]),
312    );
313    rig.add_joint(
314        RigJoint::new("head")
315            .with_parent("neck")
316            .with_position([0.0, 1.75, 0.0]),
317    );
318
319    // Left arm chain
320    rig.add_joint(
321        RigJoint::new("l_shoulder")
322            .with_parent("spine3")
323            .with_position([-0.2, 1.5, 0.0]),
324    );
325    rig.add_joint(
326        RigJoint::new("l_elbow")
327            .with_parent("l_shoulder")
328            .with_position([-0.45, 1.2, 0.0]),
329    );
330    rig.add_joint(
331        RigJoint::new("l_wrist")
332            .with_parent("l_elbow")
333            .with_position([-0.65, 0.95, 0.0]),
334    );
335
336    // Right arm chain
337    rig.add_joint(
338        RigJoint::new("r_shoulder")
339            .with_parent("spine3")
340            .with_position([0.2, 1.5, 0.0]),
341    );
342    rig.add_joint(
343        RigJoint::new("r_elbow")
344            .with_parent("r_shoulder")
345            .with_position([0.45, 1.2, 0.0]),
346    );
347    rig.add_joint(
348        RigJoint::new("r_wrist")
349            .with_parent("r_elbow")
350            .with_position([0.65, 0.95, 0.0]),
351    );
352
353    // Left leg chain
354    rig.add_joint(
355        RigJoint::new("l_hip")
356            .with_parent("hips")
357            .with_position([-0.1, 0.9, 0.0]),
358    );
359    rig.add_joint(
360        RigJoint::new("l_knee")
361            .with_parent("l_hip")
362            .with_position([-0.1, 0.55, 0.0]),
363    );
364    rig.add_joint(
365        RigJoint::new("l_ankle")
366            .with_parent("l_knee")
367            .with_position([-0.1, 0.1, 0.0]),
368    );
369
370    // Right leg chain
371    rig.add_joint(
372        RigJoint::new("r_hip")
373            .with_parent("hips")
374            .with_position([0.1, 0.9, 0.0]),
375    );
376    rig.add_joint(
377        RigJoint::new("r_knee")
378            .with_parent("r_hip")
379            .with_position([0.1, 0.55, 0.0]),
380    );
381    rig.add_joint(
382        RigJoint::new("r_ankle")
383            .with_parent("r_knee")
384            .with_position([0.1, 0.1, 0.0]),
385    );
386
387    // Left hand fingers (representative)
388    rig.add_joint(
389        RigJoint::new("l_hand")
390            .with_parent("l_wrist")
391            .with_position([-0.7, 0.85, 0.0]),
392    );
393    // Right hand fingers (representative)
394    rig.add_joint(
395        RigJoint::new("r_hand")
396            .with_parent("r_wrist")
397            .with_position([0.7, 0.85, 0.0]),
398    );
399
400    // Left foot
401    rig.add_joint(
402        RigJoint::new("l_foot")
403            .with_parent("l_ankle")
404            .with_position([-0.1, 0.0, 0.1]),
405    );
406    // Right foot
407    rig.add_joint(
408        RigJoint::new("r_foot")
409            .with_parent("r_ankle")
410            .with_position([0.1, 0.0, 0.1]),
411    );
412
413    // Sample morph bindings
414    rig.add_morph_binding(MorphBinding::new("head_size", "head_scale"));
415    rig.add_morph_binding(MorphBinding::new("body_weight", "weight"));
416    rig.add_morph_binding(MorphBinding::new("muscle_tone", "muscle"));
417
418    // Body regions
419    rig.add_region(RigBodyRegion {
420        name: "head_region".to_string(),
421        joints: vec!["neck".to_string(), "head".to_string()],
422        morph_targets: vec!["head_size".to_string()],
423        vertex_group: Some("head_vg".to_string()),
424    });
425    rig.add_region(RigBodyRegion {
426        name: "torso_region".to_string(),
427        joints: vec![
428            "spine1".to_string(),
429            "spine2".to_string(),
430            "spine3".to_string(),
431        ],
432        morph_targets: vec!["body_weight".to_string(), "muscle_tone".to_string()],
433        vertex_group: Some("torso_vg".to_string()),
434    });
435
436    rig
437}
438
439// ---------------------------------------------------------------------------
440// minimal_human_rig  (16 joints)
441// ---------------------------------------------------------------------------
442
443/// Build a simplified 16-joint rig (head, spine, limbs).
444pub fn minimal_human_rig() -> CharacterRig {
445    let mut rig = CharacterRig::new("minimal_human");
446    rig.metadata
447        .insert("source".to_string(), "minimal_16".to_string());
448
449    // Root
450    rig.add_joint(RigJoint::new("pelvis").with_position([0.0, 1.0, 0.0]));
451
452    // Spine chain
453    rig.add_joint(
454        RigJoint::new("spine")
455            .with_parent("pelvis")
456            .with_position([0.0, 1.2, 0.0]),
457    );
458    rig.add_joint(
459        RigJoint::new("chest")
460            .with_parent("spine")
461            .with_position([0.0, 1.45, 0.0]),
462    );
463    rig.add_joint(
464        RigJoint::new("head")
465            .with_parent("chest")
466            .with_position([0.0, 1.75, 0.0]),
467    );
468
469    // Left arm
470    rig.add_joint(
471        RigJoint::new("l_upper_arm")
472            .with_parent("chest")
473            .with_position([-0.2, 1.45, 0.0]),
474    );
475    rig.add_joint(
476        RigJoint::new("l_forearm")
477            .with_parent("l_upper_arm")
478            .with_position([-0.4, 1.2, 0.0]),
479    );
480    rig.add_joint(
481        RigJoint::new("l_hand")
482            .with_parent("l_forearm")
483            .with_position([-0.6, 0.95, 0.0]),
484    );
485
486    // Right arm
487    rig.add_joint(
488        RigJoint::new("r_upper_arm")
489            .with_parent("chest")
490            .with_position([0.2, 1.45, 0.0]),
491    );
492    rig.add_joint(
493        RigJoint::new("r_forearm")
494            .with_parent("r_upper_arm")
495            .with_position([0.4, 1.2, 0.0]),
496    );
497    rig.add_joint(
498        RigJoint::new("r_hand")
499            .with_parent("r_forearm")
500            .with_position([0.6, 0.95, 0.0]),
501    );
502
503    // Left leg
504    rig.add_joint(
505        RigJoint::new("l_thigh")
506            .with_parent("pelvis")
507            .with_position([-0.1, 0.9, 0.0]),
508    );
509    rig.add_joint(
510        RigJoint::new("l_shin")
511            .with_parent("l_thigh")
512            .with_position([-0.1, 0.5, 0.0]),
513    );
514    rig.add_joint(
515        RigJoint::new("l_foot")
516            .with_parent("l_shin")
517            .with_position([-0.1, 0.05, 0.0]),
518    );
519
520    // Right leg
521    rig.add_joint(
522        RigJoint::new("r_thigh")
523            .with_parent("pelvis")
524            .with_position([0.1, 0.9, 0.0]),
525    );
526    rig.add_joint(
527        RigJoint::new("r_shin")
528            .with_parent("r_thigh")
529            .with_position([0.1, 0.5, 0.0]),
530    );
531    rig.add_joint(
532        RigJoint::new("r_foot")
533            .with_parent("r_shin")
534            .with_position([0.1, 0.05, 0.0]),
535    );
536
537    // Sample morph bindings
538    rig.add_morph_binding(MorphBinding::new("head_size", "head_scale"));
539    rig.add_morph_binding(MorphBinding::new("body_weight", "weight"));
540
541    rig
542}
543
544// ---------------------------------------------------------------------------
545// Tests
546// ---------------------------------------------------------------------------
547
548#[cfg(test)]
549mod tests {
550    use super::*;
551
552    #[test]
553    fn test_rig_joint_new() {
554        let j = RigJoint::new("hips");
555        assert_eq!(j.name, "hips");
556        assert!(j.parent.is_none());
557        assert_eq!(j.position, [0.0, 0.0, 0.0]);
558        assert_eq!(j.rotation, [0.0, 1.0, 0.0, 0.0]);
559        assert_eq!(j.scale, [1.0, 1.0, 1.0]);
560    }
561
562    #[test]
563    fn test_rig_joint_with_parent() {
564        let j = RigJoint::new("spine").with_parent("hips");
565        assert_eq!(j.parent.as_deref(), Some("hips"));
566    }
567
568    #[test]
569    fn test_morph_binding_compute_weight() {
570        let b = MorphBinding::new("fat_belly", "weight");
571        // At mid-range (0.5 with default [0, 1])
572        let w = b.compute_weight(0.5);
573        assert!((w - 0.5).abs() < 1e-6);
574        // At input_max
575        assert!((b.compute_weight(1.0) - 1.0).abs() < 1e-6);
576        // At input_min
577        assert!((b.compute_weight(0.0) - 0.0).abs() < 1e-6);
578    }
579
580    #[test]
581    fn test_morph_binding_clamped() {
582        let b = MorphBinding::new("fat_belly", "weight");
583        // Beyond max → clamped to 1.0
584        assert!((b.compute_weight(2.0) - 1.0).abs() < 1e-6);
585        // Below min → clamped to 0.0
586        assert!((b.compute_weight(-1.0) - 0.0).abs() < 1e-6);
587    }
588
589    #[test]
590    fn test_character_rig_new() {
591        let rig = CharacterRig::new("test_rig");
592        assert_eq!(rig.name, "test_rig");
593        assert_eq!(rig.joint_count(), 0);
594        assert_eq!(rig.morph_binding_count(), 0);
595        assert!(rig.regions.is_empty());
596        assert!(rig.metadata.is_empty());
597    }
598
599    #[test]
600    fn test_add_joint() {
601        let mut rig = CharacterRig::new("rig");
602        rig.add_joint(RigJoint::new("hips"));
603        rig.add_joint(RigJoint::new("spine").with_parent("hips"));
604        assert_eq!(rig.joint_count(), 2);
605    }
606
607    #[test]
608    fn test_get_joint() {
609        let mut rig = CharacterRig::new("rig");
610        rig.add_joint(RigJoint::new("hips"));
611        assert!(rig.get_joint("hips").is_some());
612        assert!(rig.get_joint("nonexistent").is_none());
613    }
614
615    #[test]
616    fn test_root_joints() {
617        let mut rig = CharacterRig::new("rig");
618        rig.add_joint(RigJoint::new("hips"));
619        rig.add_joint(RigJoint::new("spine").with_parent("hips"));
620        rig.add_joint(RigJoint::new("chest").with_parent("spine"));
621        let roots = rig.root_joints();
622        assert_eq!(roots.len(), 1);
623        assert_eq!(roots[0].name, "hips");
624    }
625
626    #[test]
627    fn test_children_of() {
628        let mut rig = CharacterRig::new("rig");
629        rig.add_joint(RigJoint::new("hips"));
630        rig.add_joint(RigJoint::new("l_hip").with_parent("hips"));
631        rig.add_joint(RigJoint::new("r_hip").with_parent("hips"));
632        rig.add_joint(RigJoint::new("spine").with_parent("hips"));
633        let children = rig.children_of("hips");
634        assert_eq!(children.len(), 3);
635    }
636
637    #[test]
638    fn test_joint_depth() {
639        let mut rig = CharacterRig::new("rig");
640        rig.add_joint(RigJoint::new("hips"));
641        rig.add_joint(RigJoint::new("spine").with_parent("hips"));
642        rig.add_joint(RigJoint::new("chest").with_parent("spine"));
643        rig.add_joint(RigJoint::new("neck").with_parent("chest"));
644        rig.add_joint(RigJoint::new("head").with_parent("neck"));
645
646        assert_eq!(rig.joint_depth("hips"), 0);
647        assert_eq!(rig.joint_depth("spine"), 1);
648        assert_eq!(rig.joint_depth("chest"), 2);
649        assert_eq!(rig.joint_depth("neck"), 3);
650        assert_eq!(rig.joint_depth("head"), 4);
651    }
652
653    #[test]
654    fn test_bindings_for_param() {
655        let mut rig = CharacterRig::new("rig");
656        rig.add_morph_binding(MorphBinding::new("fat_belly", "weight"));
657        rig.add_morph_binding(MorphBinding::new("fat_arms", "weight"));
658        rig.add_morph_binding(MorphBinding::new("muscle", "muscle"));
659        let by_weight = rig.bindings_for_param("weight");
660        assert_eq!(by_weight.len(), 2);
661        let by_muscle = rig.bindings_for_param("muscle");
662        assert_eq!(by_muscle.len(), 1);
663    }
664
665    #[test]
666    fn test_evaluate_morphs() {
667        let mut rig = CharacterRig::new("rig");
668        rig.add_morph_binding(MorphBinding::new("fat_belly", "weight"));
669        rig.add_morph_binding(MorphBinding::new("muscle", "muscle"));
670
671        let mut params = HashMap::new();
672        params.insert("weight".to_string(), 0.75_f32);
673        // "muscle" not in params → defaults to 0.0
674
675        let weights = rig.evaluate_morphs(&params);
676        assert!((weights["fat_belly"] - 0.75).abs() < 1e-6);
677        assert!((weights["muscle"] - 0.0).abs() < 1e-6);
678    }
679
680    #[test]
681    fn test_to_json() {
682        let mut rig = CharacterRig::new("test_rig");
683        rig.add_joint(RigJoint::new("hips"));
684        rig.add_morph_binding(MorphBinding::new("fat_belly", "weight"));
685        let json = rig.to_json();
686        assert!(json.contains("test_rig"));
687        assert!(json.contains("hips"));
688        assert!(json.contains("fat_belly"));
689        assert!(json.contains("weight"));
690    }
691
692    #[test]
693    fn test_standard_human_rig() {
694        let rig = standard_human_rig();
695        assert_eq!(rig.name, "standard_human");
696        // Should have ~22 joints
697        assert!(rig.joint_count() >= 20);
698        assert!(rig.morph_binding_count() > 0);
699        // hips should be root
700        let roots = rig.root_joints();
701        assert_eq!(roots.len(), 1);
702        assert_eq!(roots[0].name, "hips");
703        // verify depth of head
704        assert!(rig.joint_depth("head") >= 2);
705    }
706
707    #[test]
708    fn test_minimal_human_rig() {
709        let rig = minimal_human_rig();
710        assert_eq!(rig.name, "minimal_human");
711        assert_eq!(rig.joint_count(), 16);
712        // pelvis is root
713        let roots = rig.root_joints();
714        assert_eq!(roots.len(), 1);
715        assert_eq!(roots[0].name, "pelvis");
716        // head depth: pelvis -> spine -> chest -> head = 3
717        assert_eq!(rig.joint_depth("head"), 3);
718        // bindings exist
719        assert!(rig.morph_binding_count() >= 2);
720    }
721}