Skip to main content

oxihuman_morph/
dual_quaternion_skin.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Dual quaternion skinning (DQS) stub.
6
7/// A dual quaternion: real part q0, dual part qe.
8#[derive(Debug, Clone, Copy)]
9pub struct DualQuat {
10    pub q0: [f32; 4],
11    pub qe: [f32; 4],
12}
13
14impl DualQuat {
15    pub fn identity() -> Self {
16        DualQuat {
17            q0: [0.0, 0.0, 0.0, 1.0],
18            qe: [0.0; 4],
19        }
20    }
21}
22
23/// DQS vertex binding.
24#[derive(Debug, Clone)]
25pub struct DqsVertex {
26    pub bone_indices: [usize; 4],
27    pub weights: [f32; 4],
28}
29
30impl Default for DqsVertex {
31    fn default() -> Self {
32        DqsVertex {
33            bone_indices: [0; 4],
34            weights: [0.0; 4],
35        }
36    }
37}
38
39/// Dual quaternion skinning mesh.
40#[derive(Debug, Clone)]
41pub struct DualQuaternionSkin {
42    pub vertices: Vec<DqsVertex>,
43    pub bone_dqs: Vec<DualQuat>,
44}
45
46impl DualQuaternionSkin {
47    pub fn new(vertex_count: usize, bone_count: usize) -> Self {
48        DualQuaternionSkin {
49            vertices: (0..vertex_count).map(|_| DqsVertex::default()).collect(),
50            bone_dqs: (0..bone_count).map(|_| DualQuat::identity()).collect(),
51        }
52    }
53}
54
55/// Create a new DQS mesh.
56pub fn new_dqs(vertex_count: usize, bone_count: usize) -> DualQuaternionSkin {
57    DualQuaternionSkin::new(vertex_count, bone_count)
58}
59
60/// Set bone influences for a vertex.
61#[allow(clippy::too_many_arguments)]
62pub fn dqs_set_vertex(
63    dqs: &mut DualQuaternionSkin,
64    vertex: usize,
65    b0: usize,
66    w0: f32,
67    b1: usize,
68    w1: f32,
69    b2: usize,
70    w2: f32,
71) {
72    if vertex < dqs.vertices.len() {
73        dqs.vertices[vertex] = DqsVertex {
74            bone_indices: [b0, b1, b2, 0],
75            weights: [w0, w1, w2, 0.0],
76        };
77    }
78}
79
80/// Set a bone's dual quaternion transform.
81pub fn dqs_set_bone(dqs: &mut DualQuaternionSkin, bone: usize, dq: DualQuat) {
82    if bone < dqs.bone_dqs.len() {
83        dqs.bone_dqs[bone] = dq;
84    }
85}
86
87/// Normalize a dual quaternion.
88pub fn dqs_normalize(dq: &DualQuat) -> DualQuat {
89    let len =
90        (dq.q0[0] * dq.q0[0] + dq.q0[1] * dq.q0[1] + dq.q0[2] * dq.q0[2] + dq.q0[3] * dq.q0[3])
91            .sqrt();
92    if len < 1e-9 {
93        return DualQuat::identity();
94    }
95    DualQuat {
96        q0: [
97            dq.q0[0] / len,
98            dq.q0[1] / len,
99            dq.q0[2] / len,
100            dq.q0[3] / len,
101        ],
102        qe: [
103            dq.qe[0] / len,
104            dq.qe[1] / len,
105            dq.qe[2] / len,
106            dq.qe[3] / len,
107        ],
108    }
109}
110
111/// Return vertex count.
112pub fn dqs_vertex_count(dqs: &DualQuaternionSkin) -> usize {
113    dqs.vertices.len()
114}
115
116/// Return bone count.
117pub fn dqs_bone_count(dqs: &DualQuaternionSkin) -> usize {
118    dqs.bone_dqs.len()
119}
120
121/// Return a JSON-like string.
122pub fn dqs_to_json(dqs: &DualQuaternionSkin) -> String {
123    format!(
124        r#"{{"vertices":{},"bones":{}}}"#,
125        dqs.vertices.len(),
126        dqs.bone_dqs.len()
127    )
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_new_dqs_vertex_count() {
136        let d = new_dqs(15, 4);
137        assert_eq!(dqs_vertex_count(&d), 15 /* vertex count must match */,);
138    }
139
140    #[test]
141    fn test_new_dqs_bone_count() {
142        let d = new_dqs(5, 8);
143        assert_eq!(dqs_bone_count(&d), 8 /* bone count must match */,);
144    }
145
146    #[test]
147    fn test_identity_dq_real_part() {
148        let dq = DualQuat::identity();
149        assert!((dq.q0[3] - 1.0).abs() < 1e-5, /* identity w component should be 1 */);
150    }
151
152    #[test]
153    fn test_normalize_identity_stays_identity() {
154        let dq = DualQuat::identity();
155        let n = dqs_normalize(&dq);
156        assert!((n.q0[3] - 1.0).abs() < 1e-5, /* normalized identity w should still be 1 */);
157    }
158
159    #[test]
160    fn test_set_bone_updates() {
161        let mut d = new_dqs(2, 2);
162        let dq = DualQuat {
163            q0: [0.0, 0.0, 0.707, 0.707],
164            qe: [0.0; 4],
165        };
166        dqs_set_bone(&mut d, 0, dq);
167        assert!((d.bone_dqs[0].q0[2] - 0.707).abs() < 1e-3, /* bone DQ z component must match */);
168    }
169
170    #[test]
171    fn test_set_bone_out_of_bounds_ignored() {
172        let mut d = new_dqs(2, 2);
173        dqs_set_bone(&mut d, 99, DualQuat::identity());
174        assert_eq!(dqs_bone_count(&d), 2 /* bone count unchanged */,);
175    }
176
177    #[test]
178    fn test_set_vertex_out_of_bounds_ignored() {
179        let mut d = new_dqs(2, 2);
180        dqs_set_vertex(&mut d, 99, 0, 1.0, 0, 0.0, 0, 0.0);
181        assert_eq!(dqs_vertex_count(&d), 2 /* vertex count unchanged */,);
182    }
183
184    #[test]
185    fn test_to_json_contains_vertices() {
186        let d = new_dqs(6, 3);
187        let j = dqs_to_json(&d);
188        assert!(j.contains("vertices") /* JSON must contain vertices */,);
189    }
190
191    #[test]
192    fn test_identity_dual_part_zero() {
193        let dq = DualQuat::identity();
194        for &v in &dq.qe {
195            assert!((v).abs() < 1e-6, /* identity dual part should be zero */);
196        }
197    }
198
199    #[test]
200    fn test_normalize_zero_length_returns_identity() {
201        let dq = DualQuat {
202            q0: [0.0; 4],
203            qe: [0.0; 4],
204        };
205        let n = dqs_normalize(&dq);
206        assert!((n.q0[3] - 1.0).abs() < 1e-5, /* zero DQ normalizes to identity */);
207    }
208}