Skip to main content

oxihuman_morph/
muscle_line.rs

1//! Muscle line-of-action deformer (bulge along muscle axis).
2
3#[allow(dead_code)]
4pub struct MuscleLine {
5    pub name: String,
6    pub origin: [f32; 3],
7    pub insertion: [f32; 3],
8    pub max_bulge: f32,
9    pub falloff_radius: f32,
10    pub contraction: f32,
11}
12
13#[allow(dead_code)]
14pub struct MuscleDeformation {
15    pub vertex_deltas: Vec<[f32; 3]>,
16    pub influence_weights: Vec<f32>,
17}
18
19#[allow(dead_code)]
20pub struct MuscleGroup {
21    pub name: String,
22    pub muscles: Vec<MuscleLine>,
23}
24
25#[allow(dead_code)]
26pub fn new_muscle_line(
27    name: &str,
28    origin: [f32; 3],
29    insertion: [f32; 3],
30    max_bulge: f32,
31    falloff: f32,
32) -> MuscleLine {
33    MuscleLine {
34        name: name.to_string(),
35        origin,
36        insertion,
37        max_bulge,
38        falloff_radius: falloff,
39        contraction: 0.0,
40    }
41}
42
43#[allow(dead_code)]
44pub fn muscle_direction(muscle: &MuscleLine) -> [f32; 3] {
45    let dx = muscle.insertion[0] - muscle.origin[0];
46    let dy = muscle.insertion[1] - muscle.origin[1];
47    let dz = muscle.insertion[2] - muscle.origin[2];
48    let len = (dx * dx + dy * dy + dz * dz).sqrt();
49    if len > 1e-6 {
50        [dx / len, dy / len, dz / len]
51    } else {
52        [0.0, 1.0, 0.0]
53    }
54}
55
56#[allow(dead_code)]
57pub fn muscle_length(muscle: &MuscleLine) -> f32 {
58    let dx = muscle.insertion[0] - muscle.origin[0];
59    let dy = muscle.insertion[1] - muscle.origin[1];
60    let dz = muscle.insertion[2] - muscle.origin[2];
61    (dx * dx + dy * dy + dz * dz).sqrt()
62}
63
64#[allow(dead_code)]
65pub fn point_to_line_distance(point: [f32; 3], line_start: [f32; 3], line_end: [f32; 3]) -> f32 {
66    let dx = line_end[0] - line_start[0];
67    let dy = line_end[1] - line_start[1];
68    let dz = line_end[2] - line_start[2];
69    let len_sq = dx * dx + dy * dy + dz * dz;
70
71    if len_sq < 1e-12 {
72        let ex = point[0] - line_start[0];
73        let ey = point[1] - line_start[1];
74        let ez = point[2] - line_start[2];
75        return (ex * ex + ey * ey + ez * ez).sqrt();
76    }
77
78    let t = ((point[0] - line_start[0]) * dx
79        + (point[1] - line_start[1]) * dy
80        + (point[2] - line_start[2]) * dz)
81        / len_sq;
82    let t = t.clamp(0.0, 1.0);
83
84    let proj_x = line_start[0] + t * dx - point[0];
85    let proj_y = line_start[1] + t * dy - point[1];
86    let proj_z = line_start[2] + t * dz - point[2];
87    (proj_x * proj_x + proj_y * proj_y + proj_z * proj_z).sqrt()
88}
89
90#[allow(dead_code)]
91pub fn muscle_influence_weight(muscle: &MuscleLine, pos: [f32; 3]) -> f32 {
92    let dist = point_to_line_distance(pos, muscle.origin, muscle.insertion);
93    if muscle.falloff_radius < 1e-6 {
94        return 0.0;
95    }
96    let normalized = (dist / muscle.falloff_radius).clamp(0.0, 1.0);
97    (1.0 - normalized).max(0.0)
98}
99
100#[allow(dead_code)]
101pub fn compute_muscle_deformation(
102    muscle: &MuscleLine,
103    positions: &[[f32; 3]],
104    normals: &[[f32; 3]],
105) -> MuscleDeformation {
106    let n = positions.len().min(normals.len());
107    let dir = muscle_direction(muscle);
108    let mut vertex_deltas = Vec::with_capacity(n);
109    let mut influence_weights = Vec::with_capacity(n);
110
111    for i in 0..n {
112        let pos = positions[i];
113        let weight = muscle_influence_weight(muscle, pos);
114        let influence = weight * muscle.contraction * muscle.max_bulge;
115
116        // Compute radial direction: perpendicular to muscle axis
117        let to_point = [
118            pos[0] - muscle.origin[0],
119            pos[1] - muscle.origin[1],
120            pos[2] - muscle.origin[2],
121        ];
122        let dot = to_point[0] * dir[0] + to_point[1] * dir[1] + to_point[2] * dir[2];
123        let along = [dir[0] * dot, dir[1] * dot, dir[2] * dot];
124        let radial = [
125            to_point[0] - along[0],
126            to_point[1] - along[1],
127            to_point[2] - along[2],
128        ];
129        let radial_len =
130            (radial[0] * radial[0] + radial[1] * radial[1] + radial[2] * radial[2]).sqrt();
131
132        let delta = if radial_len > 1e-6 {
133            [
134                radial[0] / radial_len * influence,
135                radial[1] / radial_len * influence,
136                radial[2] / radial_len * influence,
137            ]
138        } else {
139            let nrm = normals[i];
140            [nrm[0] * influence, nrm[1] * influence, nrm[2] * influence]
141        };
142
143        vertex_deltas.push(delta);
144        influence_weights.push(weight);
145    }
146
147    MuscleDeformation {
148        vertex_deltas,
149        influence_weights,
150    }
151}
152
153#[allow(dead_code)]
154pub fn apply_muscle_deformation(
155    positions: &mut [[f32; 3]],
156    deform: &MuscleDeformation,
157    weight: f32,
158) {
159    let n = positions.len().min(deform.vertex_deltas.len());
160    for (pos, delta) in positions[..n]
161        .iter_mut()
162        .zip(deform.vertex_deltas[..n].iter())
163    {
164        pos[0] += delta[0] * weight;
165        pos[1] += delta[1] * weight;
166        pos[2] += delta[2] * weight;
167    }
168}
169
170#[allow(dead_code)]
171pub fn contract_muscle(muscle: &mut MuscleLine, amount: f32) {
172    muscle.contraction = amount.clamp(0.0, 1.0);
173}
174
175#[allow(dead_code)]
176pub fn relax_muscle(muscle: &mut MuscleLine) {
177    muscle.contraction = 0.0;
178}
179
180#[allow(dead_code)]
181pub fn muscle_group_deformation(
182    group: &MuscleGroup,
183    positions: &[[f32; 3]],
184    normals: &[[f32; 3]],
185) -> Vec<MuscleDeformation> {
186    group
187        .muscles
188        .iter()
189        .map(|m| compute_muscle_deformation(m, positions, normals))
190        .collect()
191}
192
193#[allow(dead_code)]
194pub fn add_muscle_to_group(group: &mut MuscleGroup, muscle: MuscleLine) {
195    group.muscles.push(muscle);
196}
197
198#[allow(dead_code)]
199pub fn new_muscle_group(name: &str) -> MuscleGroup {
200    MuscleGroup {
201        name: name.to_string(),
202        muscles: Vec::new(),
203    }
204}
205
206#[allow(dead_code)]
207pub fn default_arm_muscles() -> MuscleGroup {
208    let mut group = new_muscle_group("arm");
209
210    // Bicep: shoulder to elbow front
211    let mut bicep = new_muscle_line("bicep", [0.15, 0.4, 0.0], [0.15, -0.1, 0.05], 0.02, 0.08);
212    bicep.contraction = 0.0;
213
214    // Tricep: shoulder to elbow back
215    let mut tricep = new_muscle_line(
216        "tricep",
217        [0.15, 0.35, -0.02],
218        [0.15, -0.1, -0.04],
219        0.018,
220        0.07,
221    );
222    tricep.contraction = 0.0;
223
224    // Deltoid: acromion to humerus
225    let mut deltoid = new_muscle_line("deltoid", [0.08, 0.42, 0.0], [0.18, 0.25, 0.0], 0.025, 0.1);
226    deltoid.contraction = 0.0;
227
228    group.muscles.push(bicep);
229    group.muscles.push(tricep);
230    group.muscles.push(deltoid);
231
232    group
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_muscle_direction_unit_length() {
241        let m = new_muscle_line("test", [0.0, 0.0, 0.0], [3.0, 4.0, 0.0], 0.01, 0.1);
242        let dir = muscle_direction(&m);
243        let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
244        assert!((len - 1.0).abs() < 1e-5);
245    }
246
247    #[test]
248    fn test_muscle_direction_components() {
249        let m = new_muscle_line("test", [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], 0.01, 0.1);
250        let dir = muscle_direction(&m);
251        assert!((dir[0] - 1.0).abs() < 1e-5);
252        assert!(dir[1].abs() < 1e-5);
253        assert!(dir[2].abs() < 1e-5);
254    }
255
256    #[test]
257    fn test_muscle_length() {
258        let m = new_muscle_line("test", [0.0, 0.0, 0.0], [3.0, 4.0, 0.0], 0.01, 0.1);
259        assert!((muscle_length(&m) - 5.0).abs() < 1e-5);
260    }
261
262    #[test]
263    fn test_muscle_length_zero() {
264        let m = new_muscle_line("test", [1.0, 2.0, 3.0], [1.0, 2.0, 3.0], 0.01, 0.1);
265        assert!(muscle_length(&m) < 1e-5);
266    }
267
268    #[test]
269    fn test_point_to_line_distance_on_line() {
270        // Point on the line should have distance ~0
271        let dist = point_to_line_distance([0.5, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
272        assert!(dist < 1e-5);
273    }
274
275    #[test]
276    fn test_point_to_line_distance_perpendicular() {
277        // Point 1 unit above midpoint of x-axis
278        let dist = point_to_line_distance([0.5, 1.0, 0.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
279        assert!((dist - 1.0).abs() < 1e-5);
280    }
281
282    #[test]
283    fn test_point_to_line_distance_clamped() {
284        // Point beyond the end of the segment
285        let dist = point_to_line_distance([2.0, 1.0, 0.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
286        // Closest point is [1,0,0], distance = sqrt(1+1) = sqrt(2)
287        assert!((dist - 2.0f32.sqrt()).abs() < 1e-4);
288    }
289
290    #[test]
291    fn test_muscle_influence_weight_on_axis() {
292        let m = new_muscle_line("test", [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], 0.01, 0.5);
293        // Point on the axis midpoint, distance = 0, weight = 1
294        let w = muscle_influence_weight(&m, [0.5, 0.0, 0.0]);
295        assert!((w - 1.0).abs() < 1e-5);
296    }
297
298    #[test]
299    fn test_muscle_influence_weight_far() {
300        let m = new_muscle_line("test", [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], 0.01, 0.5);
301        // Point far away should have weight ~0
302        let w = muscle_influence_weight(&m, [0.5, 10.0, 0.0]);
303        assert!(w < 1e-5);
304    }
305
306    #[test]
307    fn test_compute_muscle_deformation() {
308        let mut m = new_muscle_line("test", [0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 0.05, 0.2);
309        contract_muscle(&mut m, 1.0);
310        let positions = vec![[0.1, 0.5, 0.0], [5.0, 5.0, 5.0]];
311        let normals = vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
312        let deform = compute_muscle_deformation(&m, &positions, &normals);
313        assert_eq!(deform.vertex_deltas.len(), 2);
314        assert_eq!(deform.influence_weights.len(), 2);
315        // First vertex is close so it should have positive weight
316        assert!(deform.influence_weights[0] > 0.0);
317    }
318
319    #[test]
320    fn test_contract_relax() {
321        let mut m = new_muscle_line("test", [0.0; 3], [1.0, 0.0, 0.0], 0.01, 0.1);
322        contract_muscle(&mut m, 0.7);
323        assert!((m.contraction - 0.7).abs() < 1e-5);
324        relax_muscle(&mut m);
325        assert!(m.contraction < 1e-5);
326    }
327
328    #[test]
329    fn test_contract_clamp() {
330        let mut m = new_muscle_line("test", [0.0; 3], [1.0, 0.0, 0.0], 0.01, 0.1);
331        contract_muscle(&mut m, 2.0);
332        assert!((m.contraction - 1.0).abs() < 1e-5);
333        contract_muscle(&mut m, -1.0);
334        assert!(m.contraction < 1e-5);
335    }
336
337    #[test]
338    fn test_muscle_group() {
339        let mut group = new_muscle_group("legs");
340        assert!(group.muscles.is_empty());
341        let m = new_muscle_line("quad", [0.0; 3], [0.0, -0.5, 0.0], 0.02, 0.1);
342        add_muscle_to_group(&mut group, m);
343        assert_eq!(group.muscles.len(), 1);
344    }
345
346    #[test]
347    fn test_default_arm_muscles_has_three() {
348        let group = default_arm_muscles();
349        assert_eq!(group.muscles.len(), 3);
350    }
351
352    #[test]
353    fn test_default_arm_muscles_names() {
354        let group = default_arm_muscles();
355        let names: Vec<&str> = group.muscles.iter().map(|m| m.name.as_str()).collect();
356        assert!(names.contains(&"bicep"));
357        assert!(names.contains(&"tricep"));
358        assert!(names.contains(&"deltoid"));
359    }
360
361    #[test]
362    fn test_muscle_group_deformation() {
363        let group = default_arm_muscles();
364        let positions = vec![[0.15f32, 0.3, 0.0]];
365        let normals = vec![[0.0f32, 0.0, 1.0]];
366        let deforms = muscle_group_deformation(&group, &positions, &normals);
367        assert_eq!(deforms.len(), 3);
368    }
369
370    #[test]
371    fn test_apply_muscle_deformation() {
372        let mut m = new_muscle_line("test", [0.0; 3], [0.0, 1.0, 0.0], 0.1, 0.5);
373        contract_muscle(&mut m, 1.0);
374        let positions_orig = vec![[0.1f32, 0.5, 0.0]];
375        let normals = vec![[1.0f32, 0.0, 0.0]];
376        let deform = compute_muscle_deformation(&m, &positions_orig, &normals);
377        let mut positions = positions_orig.clone();
378        apply_muscle_deformation(&mut positions, &deform, 1.0);
379        assert_eq!(positions.len(), 1);
380    }
381}