Skip to main content

oxihuman_morph/
linear_blend_skin.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Linear blend skinning (LBS) stub.
6
7/// A skinning influence (bone index + weight).
8#[derive(Debug, Clone, Copy)]
9pub struct SkinInfluence {
10    pub bone_index: usize,
11    pub weight: f32,
12}
13
14/// LBS vertex binding — up to 4 influences per vertex.
15#[derive(Debug, Clone)]
16pub struct LbsVertex {
17    pub influences: Vec<SkinInfluence>,
18}
19
20impl LbsVertex {
21    pub fn new() -> Self {
22        LbsVertex {
23            influences: Vec::new(),
24        }
25    }
26}
27
28impl Default for LbsVertex {
29    fn default() -> Self {
30        LbsVertex::new()
31    }
32}
33
34/// Linear blend skin mesh.
35#[derive(Debug, Clone)]
36pub struct LinearBlendSkin {
37    pub vertices: Vec<LbsVertex>,
38    pub bone_count: usize,
39}
40
41impl LinearBlendSkin {
42    pub fn new(vertex_count: usize, bone_count: usize) -> Self {
43        LinearBlendSkin {
44            vertices: (0..vertex_count).map(|_| LbsVertex::default()).collect(),
45            bone_count,
46        }
47    }
48}
49
50/// Create a new LBS mesh.
51pub fn new_lbs(vertex_count: usize, bone_count: usize) -> LinearBlendSkin {
52    LinearBlendSkin::new(vertex_count, bone_count)
53}
54
55/// Add an influence to a vertex.
56pub fn lbs_add_influence(lbs: &mut LinearBlendSkin, vertex: usize, bone: usize, weight: f32) {
57    if vertex < lbs.vertices.len() && bone < lbs.bone_count {
58        lbs.vertices[vertex].influences.push(SkinInfluence {
59            bone_index: bone,
60            weight,
61        });
62    }
63}
64
65/// Normalize influence weights so they sum to 1.0 for each vertex.
66pub fn lbs_normalize(lbs: &mut LinearBlendSkin) {
67    for v in &mut lbs.vertices {
68        let sum: f32 = v.influences.iter().map(|i| i.weight).sum();
69        if sum > 1e-9 {
70            for inf in &mut v.influences {
71                inf.weight /= sum;
72            }
73        }
74    }
75}
76
77/// Return vertex count.
78pub fn lbs_vertex_count(lbs: &LinearBlendSkin) -> usize {
79    lbs.vertices.len()
80}
81
82/// Return influence count for a vertex.
83pub fn lbs_influence_count(lbs: &LinearBlendSkin, vertex: usize) -> usize {
84    if vertex < lbs.vertices.len() {
85        lbs.vertices[vertex].influences.len()
86    } else {
87        0
88    }
89}
90
91/// Return a JSON-like string.
92pub fn lbs_to_json(lbs: &LinearBlendSkin) -> String {
93    format!(
94        r#"{{"vertices":{},"bones":{}}}"#,
95        lbs.vertices.len(),
96        lbs.bone_count
97    )
98}
99
100/// Check if all vertex influence weights sum to approximately 1.0.
101pub fn lbs_is_normalized(lbs: &LinearBlendSkin) -> bool {
102    lbs.vertices
103        .iter()
104        .filter(|v| !v.influences.is_empty())
105        .all(|v| {
106            let s: f32 = v.influences.iter().map(|i| i.weight).sum();
107            (s - 1.0).abs() < 1e-4
108        })
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_new_lbs_vertex_count() {
117        let lbs = new_lbs(20, 5);
118        assert_eq!(
119            lbs_vertex_count(&lbs),
120            20, /* vertex count must match */
121        );
122    }
123
124    #[test]
125    fn test_add_influence_increases_count() {
126        let mut lbs = new_lbs(5, 3);
127        lbs_add_influence(&mut lbs, 0, 0, 0.5);
128        lbs_add_influence(&mut lbs, 0, 1, 0.5);
129        assert_eq!(
130            lbs_influence_count(&lbs, 0),
131            2, /* two influences added */
132        );
133    }
134
135    #[test]
136    fn test_normalize_makes_sum_one() {
137        let mut lbs = new_lbs(2, 3);
138        lbs_add_influence(&mut lbs, 0, 0, 2.0);
139        lbs_add_influence(&mut lbs, 0, 1, 2.0);
140        lbs_normalize(&mut lbs);
141        assert!(lbs_is_normalized(&lbs), /* normalized weights should sum to 1 */);
142    }
143
144    #[test]
145    fn test_add_out_of_bounds_ignored() {
146        let mut lbs = new_lbs(2, 3);
147        lbs_add_influence(&mut lbs, 99, 0, 1.0);
148        assert_eq!(
149            lbs_influence_count(&lbs, 0),
150            0, /* out-of-bounds vertex ignored */
151        );
152    }
153
154    #[test]
155    fn test_add_invalid_bone_ignored() {
156        let mut lbs = new_lbs(2, 3);
157        lbs_add_influence(&mut lbs, 0, 99, 1.0);
158        assert_eq!(
159            lbs_influence_count(&lbs, 0),
160            0, /* out-of-bounds bone ignored */
161        );
162    }
163
164    #[test]
165    fn test_to_json_contains_bones() {
166        let lbs = new_lbs(4, 6);
167        let j = lbs_to_json(&lbs);
168        assert!(j.contains("bones") /* JSON must contain bones */,);
169    }
170
171    #[test]
172    fn test_empty_vertices_are_normalized() {
173        let lbs = new_lbs(3, 2);
174        assert!(lbs_is_normalized(&lbs), /* empty vertices trivially normalized */);
175    }
176
177    #[test]
178    fn test_influence_count_out_of_bounds() {
179        let lbs = new_lbs(2, 3);
180        assert_eq!(
181            lbs_influence_count(&lbs, 99),
182            0, /* out-of-bounds returns 0 */
183        );
184    }
185
186    #[test]
187    fn test_bone_count_stored() {
188        let lbs = new_lbs(5, 8);
189        assert_eq!(lbs.bone_count, 8 /* bone count must match */,);
190    }
191
192    #[test]
193    fn test_single_influence_is_normalized() {
194        let mut lbs = new_lbs(1, 1);
195        lbs_add_influence(&mut lbs, 0, 0, 1.0);
196        assert!(lbs_is_normalized(&lbs), /* single influence should be normalized */);
197    }
198}