Skip to main content

oxihuman_morph/
facial_rig.rs

1//! Facial rigging with control bones and corrective shapes.
2
3#[allow(dead_code)]
4pub struct FacialBone {
5    pub name: String,
6    pub position: [f32; 3],
7    pub rotation: [f32; 4], // quaternion
8    pub parent: Option<usize>,
9    pub children: Vec<usize>,
10    pub weight: f32,
11    pub influence_radius: f32,
12}
13
14#[allow(dead_code)]
15pub struct CorrectiveShape {
16    pub name: String,
17    pub trigger_bone: String,
18    pub trigger_angle: f32, // radians
19    pub deltas: Vec<[f32; 3]>,
20    pub weight: f32,
21}
22
23#[allow(dead_code)]
24pub struct FacialRig {
25    pub bones: Vec<FacialBone>,
26    pub correctives: Vec<CorrectiveShape>,
27    pub version: u32,
28}
29
30#[allow(dead_code)]
31pub struct FacialPose {
32    pub bone_rotations: Vec<[f32; 4]>, // per-bone quaternions
33    pub blend_weights: Vec<f32>,       // per-corrective weights
34}
35
36#[allow(dead_code)]
37pub fn new_facial_rig() -> FacialRig {
38    FacialRig {
39        bones: Vec::new(),
40        correctives: Vec::new(),
41        version: 1,
42    }
43}
44
45#[allow(dead_code)]
46pub fn add_bone(rig: &mut FacialRig, name: &str, pos: [f32; 3], parent: Option<usize>) -> usize {
47    let idx = rig.bones.len();
48    if let Some(p) = parent {
49        if p < rig.bones.len() {
50            rig.bones[p].children.push(idx);
51        }
52    }
53    rig.bones.push(FacialBone {
54        name: name.to_string(),
55        position: pos,
56        rotation: [0.0, 0.0, 0.0, 1.0],
57        parent,
58        children: Vec::new(),
59        weight: 1.0,
60        influence_radius: 0.1,
61    });
62    idx
63}
64
65#[allow(dead_code)]
66pub fn add_corrective(rig: &mut FacialRig, c: CorrectiveShape) {
67    rig.correctives.push(c);
68}
69
70#[allow(dead_code)]
71pub fn default_facial_rig() -> FacialRig {
72    let mut rig = new_facial_rig();
73    // Root jaw
74    let jaw = add_bone(&mut rig, "jaw", [0.0, -0.05, 0.0], None);
75    // Eyes
76    let _eye_l = add_bone(&mut rig, "eye_l", [-0.03, 0.02, 0.04], None);
77    let _eye_r = add_bone(&mut rig, "eye_r", [0.03, 0.02, 0.04], None);
78    // Brows
79    let _brow_l = add_bone(&mut rig, "brow_l", [-0.03, 0.05, 0.03], None);
80    let _brow_l_inner = add_bone(&mut rig, "brow_l_inner", [-0.015, 0.05, 0.03], None);
81    let _brow_r = add_bone(&mut rig, "brow_r", [0.03, 0.05, 0.03], None);
82    let _brow_r_inner = add_bone(&mut rig, "brow_r_inner", [0.015, 0.05, 0.03], None);
83    // Cheeks
84    let _cheek_l = add_bone(&mut rig, "cheek_l", [-0.04, 0.0, 0.03], None);
85    let _cheek_r = add_bone(&mut rig, "cheek_r", [0.04, 0.0, 0.03], None);
86    // Lips (children of jaw)
87    let _lip_upper = add_bone(&mut rig, "lip_upper", [0.0, -0.01, 0.05], Some(jaw));
88    let _lip_lower = add_bone(&mut rig, "lip_lower", [0.0, -0.03, 0.05], Some(jaw));
89    let _lip_corner_l = add_bone(&mut rig, "lip_corner_l", [-0.02, -0.02, 0.05], Some(jaw));
90    let _lip_corner_r = add_bone(&mut rig, "lip_corner_r", [0.02, -0.02, 0.05], Some(jaw));
91    // Nose
92    let _nose = add_bone(&mut rig, "nose", [0.0, 0.01, 0.06], None);
93    rig
94}
95
96#[allow(dead_code)]
97pub fn get_bone<'a>(rig: &'a FacialRig, name: &str) -> Option<&'a FacialBone> {
98    rig.bones.iter().find(|b| b.name == name)
99}
100
101#[allow(dead_code)]
102pub fn set_bone_rotation(rig: &mut FacialRig, name: &str, rot: [f32; 4]) -> bool {
103    if let Some(bone) = rig.bones.iter_mut().find(|b| b.name == name) {
104        bone.rotation = rot;
105        true
106    } else {
107        false
108    }
109}
110
111/// Compute corrective weights from bone angles.
112#[allow(dead_code)]
113pub fn evaluate_correctives(rig: &FacialRig) -> Vec<f32> {
114    rig.correctives
115        .iter()
116        .map(|c| {
117            // Find trigger bone rotation angle
118            if let Some(bone) = rig.bones.iter().find(|b| b.name == c.trigger_bone) {
119                let angle = quat_angle_from_identity(bone.rotation);
120                let diff = (angle - c.trigger_angle).abs();
121                let w = (1.0 - diff / std::f32::consts::PI).max(0.0);
122                w * c.weight
123            } else {
124                0.0
125            }
126        })
127        .collect()
128}
129
130fn quat_angle_from_identity(q: [f32; 4]) -> f32 {
131    // w component of normalized quaternion → angle = 2 * acos(w)
132    let w = q[3].clamp(-1.0, 1.0);
133    2.0 * w.acos()
134}
135
136#[allow(dead_code)]
137pub fn apply_facial_pose(
138    rig: &FacialRig,
139    pose: &FacialPose,
140    base_positions: &[[f32; 3]],
141) -> Vec<[f32; 3]> {
142    let mut result: Vec<[f32; 3]> = base_positions.to_vec();
143
144    // Apply corrective shape deltas
145    for (i, &w) in pose.blend_weights.iter().enumerate() {
146        if i >= rig.correctives.len() {
147            break;
148        }
149        if w.abs() < 1e-6 {
150            continue;
151        }
152        let corr = &rig.correctives[i];
153        for (j, pos) in result.iter_mut().enumerate() {
154            if j < corr.deltas.len() {
155                pos[0] += corr.deltas[j][0] * w;
156                pos[1] += corr.deltas[j][1] * w;
157                pos[2] += corr.deltas[j][2] * w;
158            }
159        }
160    }
161    result
162}
163
164#[allow(dead_code)]
165pub fn bone_count(rig: &FacialRig) -> usize {
166    rig.bones.len()
167}
168
169#[allow(dead_code)]
170pub fn corrective_count(rig: &FacialRig) -> usize {
171    rig.correctives.len()
172}
173
174#[allow(dead_code)]
175pub fn facial_rig_to_json(rig: &FacialRig) -> String {
176    let mut s = String::from("{");
177    s.push_str(&format!(
178        r#""version":{},"bone_count":{},"corrective_count":{}"#,
179        rig.version,
180        rig.bones.len(),
181        rig.correctives.len()
182    ));
183    s.push('}');
184    s
185}
186
187#[allow(dead_code)]
188pub fn identity_pose(rig: &FacialRig) -> FacialPose {
189    FacialPose {
190        bone_rotations: vec![[0.0, 0.0, 0.0, 1.0]; rig.bones.len()],
191        blend_weights: vec![0.0; rig.correctives.len()],
192    }
193}
194
195#[allow(dead_code)]
196pub fn blend_facial_poses(a: &FacialPose, b: &FacialPose, t: f32) -> FacialPose {
197    let len = a.bone_rotations.len().min(b.bone_rotations.len());
198    let bone_rotations = (0..len)
199        .map(|i| {
200            let qa = a.bone_rotations[i];
201            let qb = b.bone_rotations[i];
202            slerp_quat(qa, qb, t)
203        })
204        .collect();
205
206    let wlen = a.blend_weights.len().min(b.blend_weights.len());
207    let blend_weights = (0..wlen)
208        .map(|i| a.blend_weights[i] * (1.0 - t) + b.blend_weights[i] * t)
209        .collect();
210
211    FacialPose {
212        bone_rotations,
213        blend_weights,
214    }
215}
216
217fn slerp_quat(a: [f32; 4], b: [f32; 4], t: f32) -> [f32; 4] {
218    let mut dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
219    let bq = if dot < 0.0 {
220        dot = -dot;
221        [-b[0], -b[1], -b[2], -b[3]]
222    } else {
223        b
224    };
225
226    if dot > 0.9995 {
227        let r = [
228            a[0] + t * (bq[0] - a[0]),
229            a[1] + t * (bq[1] - a[1]),
230            a[2] + t * (bq[2] - a[2]),
231            a[3] + t * (bq[3] - a[3]),
232        ];
233        normalize_q(r)
234    } else {
235        let theta_0 = dot.acos();
236        let theta = theta_0 * t;
237        let sin_theta = theta.sin();
238        let sin_theta_0 = theta_0.sin();
239        let s0 = (theta_0 * (1.0 - t)).sin() / sin_theta_0;
240        let s1 = sin_theta / sin_theta_0;
241        [
242            s0 * a[0] + s1 * bq[0],
243            s0 * a[1] + s1 * bq[1],
244            s0 * a[2] + s1 * bq[2],
245            s0 * a[3] + s1 * bq[3],
246        ]
247    }
248}
249
250fn normalize_q(q: [f32; 4]) -> [f32; 4] {
251    let len = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
252    if len < 1e-9 {
253        [0.0, 0.0, 0.0, 1.0]
254    } else {
255        [q[0] / len, q[1] / len, q[2] / len, q[3] / len]
256    }
257}
258
259#[allow(dead_code)]
260pub fn quat_angle_between(q1: [f32; 4], q2: [f32; 4]) -> f32 {
261    // angle between two quaternions: 2 * acos(|q1 . q2|)
262    let dot = (q1[0] * q2[0] + q1[1] * q2[1] + q1[2] * q2[2] + q1[3] * q2[3])
263        .abs()
264        .clamp(0.0, 1.0);
265    2.0 * dot.acos()
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use std::f32::consts::PI;
272
273    #[test]
274    fn test_new_facial_rig_empty() {
275        let rig = new_facial_rig();
276        assert_eq!(rig.bones.len(), 0);
277        assert_eq!(rig.correctives.len(), 0);
278        assert_eq!(rig.version, 1);
279    }
280
281    #[test]
282    fn test_add_bone_returns_index() {
283        let mut rig = new_facial_rig();
284        let idx = add_bone(&mut rig, "jaw", [0.0, 0.0, 0.0], None);
285        assert_eq!(idx, 0);
286        assert_eq!(rig.bones.len(), 1);
287        assert_eq!(rig.bones[0].name, "jaw");
288    }
289
290    #[test]
291    fn test_add_bone_parent_child_link() {
292        let mut rig = new_facial_rig();
293        let p = add_bone(&mut rig, "root", [0.0, 0.0, 0.0], None);
294        let c = add_bone(&mut rig, "child", [1.0, 0.0, 0.0], Some(p));
295        assert!(rig.bones[p].children.contains(&c));
296        assert_eq!(rig.bones[c].parent, Some(p));
297    }
298
299    #[test]
300    fn test_add_corrective() {
301        let mut rig = new_facial_rig();
302        add_bone(&mut rig, "jaw", [0.0, 0.0, 0.0], None);
303        let cs = CorrectiveShape {
304            name: "jaw_open".to_string(),
305            trigger_bone: "jaw".to_string(),
306            trigger_angle: PI / 4.0,
307            deltas: vec![[0.0, -0.01, 0.0]; 3],
308            weight: 1.0,
309        };
310        add_corrective(&mut rig, cs);
311        assert_eq!(rig.correctives.len(), 1);
312    }
313
314    #[test]
315    fn test_default_facial_rig_non_empty() {
316        let rig = default_facial_rig();
317        assert!(rig.bones.len() >= 14);
318    }
319
320    #[test]
321    fn test_get_bone_found() {
322        let rig = default_facial_rig();
323        let bone = get_bone(&rig, "jaw");
324        assert!(bone.is_some());
325        assert_eq!(bone.expect("should succeed").name, "jaw");
326    }
327
328    #[test]
329    fn test_get_bone_not_found() {
330        let rig = default_facial_rig();
331        assert!(get_bone(&rig, "nonexistent").is_none());
332    }
333
334    #[test]
335    fn test_bone_count() {
336        let rig = default_facial_rig();
337        assert_eq!(bone_count(&rig), rig.bones.len());
338    }
339
340    #[test]
341    fn test_corrective_count_zero() {
342        let rig = default_facial_rig();
343        assert_eq!(corrective_count(&rig), 0);
344    }
345
346    #[test]
347    fn test_set_bone_rotation_success() {
348        let mut rig = default_facial_rig();
349        let rot = [0.0, 0.0, 0.707, 0.707];
350        let ok = set_bone_rotation(&mut rig, "jaw", rot);
351        assert!(ok);
352        let bone = get_bone(&rig, "jaw").expect("should succeed");
353        assert_eq!(bone.rotation, rot);
354    }
355
356    #[test]
357    fn test_set_bone_rotation_fail() {
358        let mut rig = default_facial_rig();
359        let ok = set_bone_rotation(&mut rig, "ghost_bone", [0.0, 0.0, 0.0, 1.0]);
360        assert!(!ok);
361    }
362
363    #[test]
364    fn test_identity_pose() {
365        let rig = default_facial_rig();
366        let pose = identity_pose(&rig);
367        assert_eq!(pose.bone_rotations.len(), rig.bones.len());
368        for rot in &pose.bone_rotations {
369            assert_eq!(*rot, [0.0, 0.0, 0.0, 1.0]);
370        }
371        for w in &pose.blend_weights {
372            assert_eq!(*w, 0.0);
373        }
374    }
375
376    #[test]
377    fn test_blend_facial_poses() {
378        let rig = default_facial_rig();
379        let a = identity_pose(&rig);
380        let mut b = identity_pose(&rig);
381        if !b.bone_rotations.is_empty() {
382            b.bone_rotations[0] = [0.0, 0.0, 0.707, 0.707];
383        }
384        let blended = blend_facial_poses(&a, &b, 0.5);
385        assert_eq!(blended.bone_rotations.len(), a.bone_rotations.len());
386    }
387
388    #[test]
389    fn test_evaluate_correctives_empty() {
390        let rig = default_facial_rig();
391        let weights = evaluate_correctives(&rig);
392        assert!(weights.is_empty());
393    }
394
395    #[test]
396    fn test_apply_facial_pose_no_correctives() {
397        let rig = default_facial_rig();
398        let pose = identity_pose(&rig);
399        let base = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
400        let result = apply_facial_pose(&rig, &pose, &base);
401        assert_eq!(result.len(), base.len());
402        for (r, b) in result.iter().zip(base.iter()) {
403            assert!((r[0] - b[0]).abs() < 1e-6);
404        }
405    }
406
407    #[test]
408    fn test_quat_angle_between_identity() {
409        let q = [0.0, 0.0, 0.0, 1.0];
410        let angle = quat_angle_between(q, q);
411        assert!(angle < 1e-5);
412    }
413
414    #[test]
415    fn test_facial_rig_to_json_contains_version() {
416        let rig = default_facial_rig();
417        let json = facial_rig_to_json(&rig);
418        assert!(json.contains("version"));
419        assert!(json.contains("bone_count"));
420    }
421}