1use alloc::vec::Vec;
7
8use crate::components::JointProportion;
9
10use crate::gfx::skeleton::Skeleton;
11use crate::gfx::transform::Mat4;
12
13#[derive(Debug, Clone, PartialEq)]
15struct ChildOffset {
16 child: usize,
17 offset: [f32; 3],
18}
19
20#[derive(Debug, Clone, PartialEq)]
21struct Entry {
22 joint: usize,
23 scale: f32,
24 offsets: Vec<ChildOffset>,
25}
26
27#[derive(Debug, Clone, Default, PartialEq)]
30pub struct ProportionLayer {
31 entries: Vec<Entry>,
32}
33
34impl ProportionLayer {
35 pub fn resolve(skeleton: &Skeleton, proportions: &[JointProportion]) -> Self {
39 let joints = skeleton.joints();
40 let entries = proportions
41 .iter()
42 .filter(|p| p.scale != 1.0 || p.length != 0.0)
43 .filter_map(|p| {
44 let joint = skeleton.joint_index(&p.joint)?;
45 let offsets = joints
46 .iter()
47 .enumerate()
48 .filter(|(_, j)| j.parent == Some(joint))
49 .filter_map(|(child, j)| {
50 let dir = normalize(j.bind.translation)?;
51 Some(ChildOffset {
52 child,
53 offset: crate::math::vec3::scale(dir, p.length),
54 })
55 })
56 .collect();
57 Some(Entry {
58 joint,
59 scale: p.scale,
60 offsets,
61 })
62 })
63 .collect();
64 Self { entries }
65 }
66
67 pub fn is_empty(&self) -> bool {
69 self.entries.is_empty()
70 }
71
72 pub fn apply(&self, locals: &mut [Mat4]) {
77 for e in &self.entries {
78 if let Some(m) = locals.get_mut(e.joint) {
79 for col in m.iter_mut().take(3) {
80 col[0] *= e.scale;
81 col[1] *= e.scale;
82 col[2] *= e.scale;
83 }
84 }
85 for o in &e.offsets {
86 if let Some(m) = locals.get_mut(o.child) {
87 m[3][0] += o.offset[0];
88 m[3][1] += o.offset[1];
89 m[3][2] += o.offset[2];
90 }
91 }
92 }
93 }
94
95 pub fn root_scale(&self, skeleton: &Skeleton) -> f32 {
98 let root = skeleton.joints().iter().position(|j| j.parent.is_none());
99 self.entries
100 .iter()
101 .find(|e| Some(e.joint) == root)
102 .map_or(1.0, |e| e.scale)
103 }
104
105 pub fn height_ratio(&self, skeleton: &Skeleton) -> f32 {
109 let mut world = Vec::new();
110 skeleton.world_matrices_into(skeleton.bind_locals(), &mut world);
111 let bind_top = top(&world);
112 let mut locals: Vec<Mat4> = skeleton.bind_locals().to_vec();
113 self.apply(&mut locals);
114 skeleton.world_matrices_into(&locals, &mut world);
115 if bind_top <= 1e-6 {
116 1.0
117 } else {
118 top(&world) / bind_top
119 }
120 }
121}
122
123fn top(world: &[Mat4]) -> f32 {
124 world.iter().map(|m| m[3][1]).fold(0.0, f32::max)
125}
126
127fn normalize(v: [f32; 3]) -> Option<[f32; 3]> {
128 let len = crate::math::vec3::length(v);
129 (len > 1e-6).then(|| crate::math::vec3::scale(v, 1.0 / len))
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::gfx::skeleton::{Joint, JointPose};
136 use alloc::string::String;
137 use alloc::vec;
138
139 fn chain() -> Skeleton {
141 let joint = |name: &str, parent: Option<usize>, y: f32| Joint {
142 name: String::from(name),
143 parent,
144 bind: JointPose {
145 translation: [0.0, y, 0.0],
146 ..Default::default()
147 },
148 };
149 Skeleton::new(vec![
150 joint("root", None, 0.0),
151 joint("mid", Some(0), 1.0),
152 joint("tip", Some(1), 1.0),
153 ])
154 }
155
156 fn proportion(joint: &str, scale: f32, length: f32) -> JointProportion {
157 JointProportion {
158 joint: String::from(joint),
159 scale,
160 length,
161 }
162 }
163
164 fn positions(skeleton: &Skeleton, layer: &ProportionLayer) -> Vec<[f32; 3]> {
165 let mut locals = skeleton.bind_locals().to_vec();
166 layer.apply(&mut locals);
167 let mut world = Vec::new();
168 skeleton.world_matrices_into(&locals, &mut world);
169 world.iter().map(|m| [m[3][0], m[3][1], m[3][2]]).collect()
170 }
171
172 fn close(a: [f32; 3], b: [f32; 3]) -> bool {
173 (0..3).all(|i| (a[i] - b[i]).abs() < 1e-5)
174 }
175
176 #[test]
177 fn length_pushes_only_the_children_along_the_bone() {
178 let s = chain();
179 let layer = ProportionLayer::resolve(&s, &[proportion("mid", 1.0, 0.5)]);
180 let p = positions(&s, &layer);
181 assert!(close(p[1], [0.0, 1.0, 0.0]), "{p:?}");
184 assert!(close(p[2], [0.0, 2.5, 0.0]), "{p:?}");
185 assert!((layer.height_ratio(&s) - 1.25).abs() < 1e-5);
186 }
187
188 #[test]
189 fn scale_propagates_to_descendants() {
190 let s = chain();
191 let layer = ProportionLayer::resolve(&s, &[proportion("mid", 2.0, 0.0)]);
192 let p = positions(&s, &layer);
193 assert!(close(p[2], [0.0, 3.0, 0.0]), "{p:?}");
196 let layer = ProportionLayer::resolve(&s, &[proportion("root", 2.0, 0.0)]);
198 let p = positions(&s, &layer);
199 assert!(close(p[2], [0.0, 4.0, 0.0]), "{p:?}");
200 assert_eq!(layer.root_scale(&s), 2.0);
201 assert!((layer.height_ratio(&s) - 2.0).abs() < 1e-5);
202 }
203
204 #[test]
205 fn unknown_and_identity_entries_are_dropped() {
206 let s = chain();
207 let layer = ProportionLayer::resolve(
208 &s,
209 &[proportion("tail", 2.0, 1.0), proportion("mid", 1.0, 0.0)],
210 );
211 assert!(layer.is_empty());
212 assert_eq!(layer.root_scale(&s), 1.0);
213 assert_eq!(layer.height_ratio(&s), 1.0);
214 let mut locals = s.bind_locals().to_vec();
215 layer.apply(&mut locals);
216 assert_eq!(locals, s.bind_locals());
217 }
218
219 #[test]
220 fn a_short_local_buffer_is_applied_in_range() {
221 let s = chain();
222 let layer = ProportionLayer::resolve(&s, &[proportion("mid", 2.0, 0.5)]);
223 let mut locals = s.bind_locals()[..2].to_vec();
224 layer.apply(&mut locals);
225 assert_eq!(locals[1][0][0], 2.0);
226 }
227}