Skip to main content

oxihuman_export/
skeleton_export.rs

1//! Skeleton/rig export (JSON and BVH stub).
2
3#[allow(dead_code)]
4pub struct ExportBone {
5    pub id: u32,
6    pub name: String,
7    pub parent_id: Option<u32>,
8    pub head: [f32; 3],
9    pub tail: [f32; 3],
10    pub rotation: [f32; 4],
11    pub length: f32,
12}
13
14#[allow(dead_code)]
15pub struct SkeletonExport {
16    pub name: String,
17    pub bones: Vec<ExportBone>,
18    pub frame_rate: f32,
19    pub frames: Vec<Vec<([f32; 3], [f32; 4])>>,
20}
21
22#[allow(dead_code)]
23pub fn new_skeleton_export(name: &str) -> SkeletonExport {
24    SkeletonExport {
25        name: name.to_string(),
26        bones: Vec::new(),
27        frame_rate: 30.0,
28        frames: Vec::new(),
29    }
30}
31
32#[allow(dead_code)]
33pub fn add_export_bone(skel: &mut SkeletonExport, bone: ExportBone) {
34    skel.bones.push(bone);
35}
36
37#[allow(dead_code)]
38pub fn add_skeleton_frame(skel: &mut SkeletonExport, poses: Vec<([f32; 3], [f32; 4])>) {
39    skel.frames.push(poses);
40}
41
42#[allow(dead_code)]
43pub fn skeleton_to_json(skel: &SkeletonExport) -> String {
44    let bone_strs: Vec<String> = skel
45        .bones
46        .iter()
47        .map(|b| {
48            let parent = match b.parent_id {
49                Some(p) => format!("{p}"),
50                None => "null".to_string(),
51            };
52            format!(
53                r#"{{"id":{},"name":"{}","parent_id":{},"head":[{},{},{}],"tail":[{},{},{}],"rotation":[{},{},{},{}],"length":{}}}"#,
54                b.id,
55                b.name,
56                parent,
57                b.head[0], b.head[1], b.head[2],
58                b.tail[0], b.tail[1], b.tail[2],
59                b.rotation[0], b.rotation[1], b.rotation[2], b.rotation[3],
60                b.length
61            )
62        })
63        .collect();
64
65    format!(
66        r#"{{"name":"{}","frame_rate":{},"bones":[{}]}}"#,
67        skel.name,
68        skel.frame_rate,
69        bone_strs.join(",")
70    )
71}
72
73#[allow(dead_code)]
74pub fn skeleton_to_bvh_stub(skel: &SkeletonExport) -> String {
75    let mut out = String::new();
76    out.push_str("HIERARCHY\n");
77    out.push_str("ROOT\n");
78    out.push_str("{\n");
79    for bone in &skel.bones {
80        out.push_str(&format!(
81            "  JOINT {}\n  {{\n    OFFSET {} {} {}\n  }}\n",
82            bone.name, bone.head[0], bone.head[1], bone.head[2]
83        ));
84    }
85    out.push_str("}\n");
86    out.push_str("MOTION\n");
87    out.push_str(&format!("Frames: {}\n", skel.frames.len()));
88    out.push_str(&format!("Frame Time: {}\n", 1.0 / skel.frame_rate.max(1.0)));
89    for frame in &skel.frames {
90        let values: Vec<String> = frame
91            .iter()
92            .flat_map(|(pos, rot)| {
93                vec![
94                    format!("{}", pos[0]),
95                    format!("{}", pos[1]),
96                    format!("{}", pos[2]),
97                    format!("{}", rot[0]),
98                    format!("{}", rot[1]),
99                    format!("{}", rot[2]),
100                    format!("{}", rot[3]),
101                ]
102            })
103            .collect();
104        out.push_str(&values.join(" "));
105        out.push('\n');
106    }
107    out
108}
109
110#[allow(dead_code)]
111pub fn bone_count_export(skel: &SkeletonExport) -> usize {
112    skel.bones.len()
113}
114
115#[allow(dead_code)]
116pub fn frame_count(skel: &SkeletonExport) -> usize {
117    skel.frames.len()
118}
119
120#[allow(dead_code)]
121#[allow(clippy::needless_lifetimes)]
122pub fn get_export_bone<'a>(skel: &'a SkeletonExport, name: &str) -> Option<&'a ExportBone> {
123    skel.bones.iter().find(|b| b.name == name)
124}
125
126#[allow(dead_code)]
127pub fn root_bones(skel: &SkeletonExport) -> Vec<&ExportBone> {
128    skel.bones
129        .iter()
130        .filter(|b| b.parent_id.is_none())
131        .collect()
132}
133
134#[allow(dead_code)]
135pub fn child_bones(skel: &SkeletonExport, parent_id: u32) -> Vec<&ExportBone> {
136    skel.bones
137        .iter()
138        .filter(|b| b.parent_id == Some(parent_id))
139        .collect()
140}
141
142#[allow(dead_code)]
143pub fn bone_world_matrix(bone: &ExportBone) -> [[f32; 4]; 4] {
144    let [qx, qy, qz, qw] = bone.rotation;
145    // Build rotation matrix from quaternion
146    let r00 = 1.0 - 2.0 * (qy * qy + qz * qz);
147    let r01 = 2.0 * (qx * qy - qz * qw);
148    let r02 = 2.0 * (qx * qz + qy * qw);
149    let r10 = 2.0 * (qx * qy + qz * qw);
150    let r11 = 1.0 - 2.0 * (qx * qx + qz * qz);
151    let r12 = 2.0 * (qy * qz - qx * qw);
152    let r20 = 2.0 * (qx * qz - qy * qw);
153    let r21 = 2.0 * (qy * qz + qx * qw);
154    let r22 = 1.0 - 2.0 * (qx * qx + qy * qy);
155    let [tx, ty, tz] = bone.head;
156    [
157        [r00, r01, r02, 0.0],
158        [r10, r11, r12, 0.0],
159        [r20, r21, r22, 0.0],
160        [tx, ty, tz, 1.0],
161    ]
162}
163
164#[allow(dead_code)]
165pub fn skeleton_duration(skel: &SkeletonExport) -> f32 {
166    frame_count(skel) as f32 / skel.frame_rate.max(f32::EPSILON)
167}
168
169#[allow(dead_code)]
170pub fn bind_pose_snapshot(skel: &SkeletonExport) -> Vec<([f32; 3], [f32; 4])> {
171    skel.bones.iter().map(|b| (b.head, b.rotation)).collect()
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    fn make_bone(id: u32, name: &str, parent: Option<u32>) -> ExportBone {
179        ExportBone {
180            id,
181            name: name.to_string(),
182            parent_id: parent,
183            head: [0.0, 0.0, 0.0],
184            tail: [0.0, 1.0, 0.0],
185            rotation: [0.0, 0.0, 0.0, 1.0],
186            length: 1.0,
187        }
188    }
189
190    #[test]
191    fn test_new_skeleton_export() {
192        let skel = new_skeleton_export("human");
193        assert_eq!(skel.name, "human");
194        assert!(skel.bones.is_empty());
195        assert!(skel.frames.is_empty());
196    }
197
198    #[test]
199    fn test_add_export_bone() {
200        let mut skel = new_skeleton_export("s");
201        add_export_bone(&mut skel, make_bone(0, "root", None));
202        assert_eq!(bone_count_export(&skel), 1);
203    }
204
205    #[test]
206    fn test_add_skeleton_frame() {
207        let mut skel = new_skeleton_export("s");
208        add_export_bone(&mut skel, make_bone(0, "root", None));
209        let poses = vec![([0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0])];
210        add_skeleton_frame(&mut skel, poses);
211        assert_eq!(frame_count(&skel), 1);
212    }
213
214    #[test]
215    fn test_bone_count_export() {
216        let mut skel = new_skeleton_export("s");
217        assert_eq!(bone_count_export(&skel), 0);
218        add_export_bone(&mut skel, make_bone(0, "root", None));
219        add_export_bone(&mut skel, make_bone(1, "spine", Some(0)));
220        assert_eq!(bone_count_export(&skel), 2);
221    }
222
223    #[test]
224    fn test_frame_count() {
225        let mut skel = new_skeleton_export("s");
226        assert_eq!(frame_count(&skel), 0);
227        add_skeleton_frame(&mut skel, vec![]);
228        add_skeleton_frame(&mut skel, vec![]);
229        assert_eq!(frame_count(&skel), 2);
230    }
231
232    #[test]
233    fn test_get_export_bone_by_name() {
234        let mut skel = new_skeleton_export("s");
235        add_export_bone(&mut skel, make_bone(0, "hips", None));
236        add_export_bone(&mut skel, make_bone(1, "spine", Some(0)));
237        let bone = get_export_bone(&skel, "spine");
238        assert!(bone.is_some());
239        assert_eq!(bone.expect("should succeed").id, 1);
240    }
241
242    #[test]
243    fn test_get_export_bone_not_found() {
244        let skel = new_skeleton_export("s");
245        assert!(get_export_bone(&skel, "missing").is_none());
246    }
247
248    #[test]
249    fn test_root_bones() {
250        let mut skel = new_skeleton_export("s");
251        add_export_bone(&mut skel, make_bone(0, "root", None));
252        add_export_bone(&mut skel, make_bone(1, "child", Some(0)));
253        add_export_bone(&mut skel, make_bone(2, "root2", None));
254        let roots = root_bones(&skel);
255        assert_eq!(roots.len(), 2);
256    }
257
258    #[test]
259    fn test_child_bones() {
260        let mut skel = new_skeleton_export("s");
261        add_export_bone(&mut skel, make_bone(0, "root", None));
262        add_export_bone(&mut skel, make_bone(1, "child1", Some(0)));
263        add_export_bone(&mut skel, make_bone(2, "child2", Some(0)));
264        add_export_bone(&mut skel, make_bone(3, "grandchild", Some(1)));
265        let children = child_bones(&skel, 0);
266        assert_eq!(children.len(), 2);
267    }
268
269    #[test]
270    fn test_skeleton_to_json_nonempty() {
271        let mut skel = new_skeleton_export("test_rig");
272        add_export_bone(&mut skel, make_bone(0, "root", None));
273        let json = skeleton_to_json(&skel);
274        assert!(!json.is_empty());
275        assert!(json.contains("test_rig"));
276        assert!(json.contains("root"));
277    }
278
279    #[test]
280    fn test_bvh_stub_contains_hierarchy() {
281        let skel = new_skeleton_export("s");
282        let bvh = skeleton_to_bvh_stub(&skel);
283        assert!(bvh.contains("HIERARCHY"));
284        assert!(bvh.contains("MOTION"));
285    }
286
287    #[test]
288    fn test_skeleton_duration() {
289        let mut skel = new_skeleton_export("s");
290        skel.frame_rate = 24.0;
291        add_skeleton_frame(&mut skel, vec![]);
292        add_skeleton_frame(&mut skel, vec![]);
293        add_skeleton_frame(&mut skel, vec![]);
294        let dur = skeleton_duration(&skel);
295        assert!((dur - 3.0 / 24.0).abs() < 1e-5);
296    }
297
298    #[test]
299    fn test_bind_pose_snapshot() {
300        let mut skel = new_skeleton_export("s");
301        add_export_bone(&mut skel, make_bone(0, "root", None));
302        add_export_bone(&mut skel, make_bone(1, "spine", Some(0)));
303        let snapshot = bind_pose_snapshot(&skel);
304        assert_eq!(snapshot.len(), 2);
305    }
306
307    #[test]
308    fn test_bone_world_matrix_identity() {
309        let bone = ExportBone {
310            id: 0,
311            name: "b".to_string(),
312            parent_id: None,
313            head: [0.0, 0.0, 0.0],
314            tail: [0.0, 1.0, 0.0],
315            rotation: [0.0, 0.0, 0.0, 1.0],
316            length: 1.0,
317        };
318        let m = bone_world_matrix(&bone);
319        assert!((m[0][0] - 1.0).abs() < 1e-5);
320        assert!((m[1][1] - 1.0).abs() < 1e-5);
321        assert!((m[2][2] - 1.0).abs() < 1e-5);
322    }
323
324    #[test]
325    fn test_skeleton_duration_zero_frames() {
326        let skel = new_skeleton_export("s");
327        let dur = skeleton_duration(&skel);
328        assert!((dur - 0.0).abs() < 1e-5);
329    }
330}