Skip to main content

oxihuman_morph/
fast_lbs.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Fast LBS (linear blend skinning) approximation stub.
6
7/// A compact skinning record for one vertex: up to 4 bone weights.
8#[derive(Debug, Clone, Copy)]
9pub struct FastLbsRecord {
10    pub bones: [u8; 4],
11    pub weights: [f32; 4],
12}
13
14impl Default for FastLbsRecord {
15    fn default() -> Self {
16        FastLbsRecord {
17            bones: [0; 4],
18            weights: [0.0; 4],
19        }
20    }
21}
22
23/// Fast LBS mesh.
24#[derive(Debug, Clone)]
25pub struct FastLbs {
26    pub records: Vec<FastLbsRecord>,
27    pub bone_count: usize,
28}
29
30impl FastLbs {
31    pub fn new(vertex_count: usize, bone_count: usize) -> Self {
32        FastLbs {
33            records: vec![FastLbsRecord::default(); vertex_count],
34            bone_count,
35        }
36    }
37}
38
39/// Create a new fast LBS mesh.
40pub fn new_fast_lbs(vertex_count: usize, bone_count: usize) -> FastLbs {
41    FastLbs::new(vertex_count, bone_count)
42}
43
44/// Set the skinning record for a vertex.
45pub fn fast_lbs_set(lbs: &mut FastLbs, vertex: usize, record: FastLbsRecord) {
46    if vertex < lbs.records.len() {
47        lbs.records[vertex] = record;
48    }
49}
50
51/// Normalize weights for a vertex record so they sum to 1.0.
52pub fn fast_lbs_normalize(record: &mut FastLbsRecord) {
53    let sum: f32 = record.weights.iter().sum();
54    if sum > 1e-9 {
55        for w in &mut record.weights {
56            *w /= sum;
57        }
58    }
59}
60
61/// Compute the blended position for a vertex (stub: just returns source position).
62pub fn fast_lbs_transform(
63    lbs: &FastLbs,
64    vertex: usize,
65    source: [f32; 3],
66    _bone_matrices: &[[[f32; 4]; 4]],
67) -> [f32; 3] {
68    /* Stub: returns source position unchanged for now */
69    if vertex < lbs.records.len() {
70        source
71    } else {
72        [0.0; 3]
73    }
74}
75
76/// Return vertex count.
77pub fn fast_lbs_vertex_count(lbs: &FastLbs) -> usize {
78    lbs.records.len()
79}
80
81/// Return a JSON-like string.
82pub fn fast_lbs_to_json(lbs: &FastLbs) -> String {
83    format!(
84        r#"{{"vertices":{},"bones":{}}}"#,
85        lbs.records.len(),
86        lbs.bone_count
87    )
88}
89
90/// Check all records have weights that sum to ~1 (ignoring zero-weight vertices).
91pub fn fast_lbs_is_valid(lbs: &FastLbs) -> bool {
92    lbs.records.iter().all(|r| {
93        let s: f32 = r.weights.iter().sum();
94        s < 1e-9 || (s - 1.0).abs() < 1e-4
95    })
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_new_fast_lbs_vertex_count() {
104        let lbs = new_fast_lbs(16, 4);
105        assert_eq!(
106            fast_lbs_vertex_count(&lbs),
107            16, /* vertex count must match */
108        );
109    }
110
111    #[test]
112    fn test_initial_records_zero_weights() {
113        let lbs = new_fast_lbs(3, 2);
114        for r in &lbs.records {
115            let s: f32 = r.weights.iter().sum();
116            assert!((s).abs() < 1e-6 /* initial weights should be zero */,);
117        }
118    }
119
120    #[test]
121    fn test_initial_valid() {
122        let lbs = new_fast_lbs(4, 2);
123        assert!(fast_lbs_is_valid(&lbs), /* zero-weight records are trivially valid */);
124    }
125
126    #[test]
127    fn test_set_record_updates() {
128        let mut lbs = new_fast_lbs(4, 2);
129        let r = FastLbsRecord {
130            bones: [1, 2, 0, 0],
131            weights: [0.5, 0.5, 0.0, 0.0],
132        };
133        fast_lbs_set(&mut lbs, 0, r);
134        assert!((lbs.records[0].weights[0] - 0.5).abs() < 1e-5, /* weight must match */);
135    }
136
137    #[test]
138    fn test_set_out_of_bounds_ignored() {
139        let mut lbs = new_fast_lbs(2, 2);
140        fast_lbs_set(&mut lbs, 99, FastLbsRecord::default());
141        assert_eq!(
142            fast_lbs_vertex_count(&lbs),
143            2, /* vertex count unchanged */
144        );
145    }
146
147    #[test]
148    fn test_normalize_record() {
149        let mut r = FastLbsRecord {
150            bones: [0; 4],
151            weights: [2.0, 2.0, 0.0, 0.0],
152        };
153        fast_lbs_normalize(&mut r);
154        let s: f32 = r.weights.iter().sum();
155        assert!((s - 1.0).abs() < 1e-5, /* normalized weights should sum to 1 */);
156    }
157
158    #[test]
159    fn test_normalize_zero_weights_unchanged() {
160        let mut r = FastLbsRecord::default();
161        fast_lbs_normalize(&mut r);
162        let s: f32 = r.weights.iter().sum();
163        assert!((s).abs() < 1e-6 /* zero-weight record stays zero */,);
164    }
165
166    #[test]
167    fn test_transform_returns_source() {
168        let lbs = new_fast_lbs(2, 2);
169        let pos = fast_lbs_transform(&lbs, 0, [1.0, 2.0, 3.0], &[]);
170        assert!((pos[0] - 1.0).abs() < 1e-5, /* stub returns source position */);
171    }
172
173    #[test]
174    fn test_to_json_contains_vertices() {
175        let lbs = new_fast_lbs(5, 3);
176        let j = fast_lbs_to_json(&lbs);
177        assert!(j.contains("vertices") /* JSON must contain vertices */,);
178    }
179
180    #[test]
181    fn test_bone_count_stored() {
182        let lbs = new_fast_lbs(4, 7);
183        assert_eq!(lbs.bone_count, 7 /* bone count must match */,);
184    }
185}