1use crate::math::vec3::{add, cross, dot, length, scale, sub};
8use crate::math::{acos, atan2, sin_cos};
9use alloc::vec::Vec;
10
11use crate::gfx::skeleton::Skeleton;
12use crate::gfx::transform::{Mat4, mat4_affine_inverse, mat4_mul};
13
14type Vec3 = [f32; 3];
15type Mat3 = [[f32; 3]; 3];
16
17const EPS: f32 = 1.0e-5;
18
19fn normalize(v: Vec3) -> Option<Vec3> {
20 let len = length(v);
21 (len > EPS).then(|| scale(v, 1.0 / len))
22}
23
24fn rotate_about(axis: Vec3, angle: f32) -> Mat3 {
27 let (s, c) = sin_cos(angle);
28 let t = 1.0 - c;
29 let [x, y, z] = axis;
30 [
31 [t * x * x + c, t * x * y + s * z, t * x * z - s * y],
32 [t * x * y - s * z, t * y * y + c, t * y * z + s * x],
33 [t * x * z + s * y, t * y * z - s * x, t * z * z + c],
34 ]
35}
36
37const MAT3_IDENTITY: Mat3 = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
38
39fn from_to(a: Vec3, b: Vec3) -> Mat3 {
43 let (Some(a), Some(b)) = (normalize(a), normalize(b)) else {
44 return MAT3_IDENTITY;
45 };
46 let c = cross(a, b);
47 let angle = atan2(length(c), dot(a, b));
48 match normalize(c) {
49 Some(axis) => rotate_about(axis, angle),
50 None if dot(a, b) > 0.0 => MAT3_IDENTITY,
53 None => rotate_about(any_perpendicular(a), core::f32::consts::PI),
54 }
55}
56
57fn any_perpendicular(v: Vec3) -> Vec3 {
59 let candidate = if v[0].abs() < 0.9 {
60 cross(v, [1.0, 0.0, 0.0])
61 } else {
62 cross(v, [0.0, 1.0, 0.0])
63 };
64 normalize(candidate).unwrap_or([0.0, 0.0, 1.0])
65}
66
67fn mat3_apply(m: Mat3, v: Vec3) -> Vec3 {
68 [
69 m[0][0] * v[0] + m[1][0] * v[1] + m[2][0] * v[2],
70 m[0][1] * v[0] + m[1][1] * v[1] + m[2][1] * v[2],
71 m[0][2] * v[0] + m[1][2] * v[1] + m[2][2] * v[2],
72 ]
73}
74
75fn mat3_mul(a: Mat3, b: Mat3) -> Mat3 {
76 let mut out = [[0.0f32; 3]; 3];
77 for col in 0..3 {
78 for row in 0..3 {
79 for k in 0..3 {
80 out[col][row] += a[k][row] * b[col][k];
81 }
82 }
83 }
84 out
85}
86
87fn rotate_mat4_about(r: Mat3, pivot: Vec3, m: Mat4) -> Mat4 {
90 let mut out = m;
91 for col in 0..3 {
92 let rotated = mat3_apply(r, [m[col][0], m[col][1], m[col][2]]);
93 out[col][0] = rotated[0];
94 out[col][1] = rotated[1];
95 out[col][2] = rotated[2];
96 }
97 let p = mat3_apply(r, sub([m[3][0], m[3][1], m[3][2]], pivot));
98 out[3][0] = pivot[0] + p[0];
99 out[3][1] = pivot[1] + p[1];
100 out[3][2] = pivot[2] + p[2];
101 out
102}
103
104#[derive(Debug, Clone)]
108pub struct TwoBoneChain {
109 pub root: usize,
111 pub mid: usize,
113 pub end: usize,
115 pub pole: Vec3,
117}
118
119pub(crate) fn solve_two_bone(
126 root: Vec3,
127 mid: Vec3,
128 end: Vec3,
129 target: Vec3,
130 pole: Vec3,
131) -> Option<(Mat3, Mat3)> {
132 let upper = sub(mid, root);
133 let lower = sub(end, mid);
134 let a = length(upper);
135 let b = length(lower);
136 if a <= EPS || b <= EPS {
137 return None;
138 }
139 let to_target = sub(target, root);
140 let dist = length(to_target);
141 if dist <= EPS {
142 return None;
143 }
144 let t_dir = scale(to_target, 1.0 / dist);
145 let t = dist.clamp((a - b).abs() + 1.0e-4, a + b - 1.0e-4);
146
147 let u = normalize(sub(root, mid)).expect("a > EPS");
148 let v = normalize(lower).expect("b > EPS");
149
150 let axis = normalize(cross(upper, lower))
156 .or_else(|| normalize(cross(v, pole)))
157 .unwrap_or_else(|| any_perpendicular(v));
158
159 let desired = acos(((a * a + b * b - t * t) / (2.0 * a * b)).clamp(-1.0, 1.0));
164 let current_cos = dot(u, v).clamp(-1.0, 1.0);
165 let current_sin = dot(cross(u, v), axis);
166 let current = atan2(current_sin, current_cos);
167 let signed_desired = if current >= 0.0 { desired } else { -desired };
168 let r_mid = rotate_about(axis, signed_desired - current);
169
170 let end_bent = add(mid, mat3_apply(r_mid, lower));
173 let r_aim = from_to(sub(end_bent, root), to_target);
174
175 let mid_aimed = mat3_apply(r_aim, upper);
178 let bend_current = sub(mid_aimed, scale(t_dir, dot(mid_aimed, t_dir)));
179 let bend_pole = sub(pole, scale(t_dir, dot(pole, t_dir)));
180 let r_root = match (normalize(bend_current), normalize(bend_pole)) {
181 (Some(c), Some(p)) => {
182 let twist = atan2(dot(cross(c, p), t_dir), dot(c, p));
183 mat3_mul(rotate_about(t_dir, twist), r_aim)
184 }
185 _ => r_aim,
186 };
187 Some((r_root, r_mid))
188}
189
190pub fn apply_two_bone_ik(
198 skeleton: &Skeleton,
199 locals: &mut Vec<Mat4>,
200 chain: &TwoBoneChain,
201 target: Vec3,
202 weight: f32,
203 world: &mut Vec<Mat4>,
204) {
205 let weight = weight.clamp(0.0, 1.0);
206 let n = skeleton.len();
207 if weight <= 0.0 || chain.root >= n || chain.mid >= n || chain.end >= n {
208 return;
209 }
210 while locals.len() < n {
211 let i = locals.len();
212 locals.push(skeleton.joints()[i].bind.to_matrix());
213 }
214 skeleton.world_matrices_into(locals, world);
215 let pos = |m: &Mat4| [m[3][0], m[3][1], m[3][2]];
216 let p_root = pos(&world[chain.root]);
217 let p_mid = pos(&world[chain.mid]);
218 let p_end = pos(&world[chain.end]);
219 let effective = add(p_end, scale(sub(target, p_end), weight));
220 let Some((r_root, r_mid)) = solve_two_bone(p_root, p_mid, p_end, effective, chain.pole) else {
221 return;
222 };
223
224 let root_world = rotate_mat4_about(r_root, p_root, world[chain.root]);
227 let mid_world = rotate_mat4_about(
228 r_root,
229 p_root,
230 rotate_mat4_about(r_mid, p_mid, world[chain.mid]),
231 );
232
233 locals[chain.root] = match skeleton.joints()[chain.root].parent {
236 Some(p) => mat4_mul(mat4_affine_inverse(world[p]), root_world),
237 None => root_world,
238 };
239 locals[chain.mid] = mat4_mul(mat4_affine_inverse(root_world), mid_world);
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use crate::gfx::skeleton::{Joint, JointPose};
246 use alloc::string::ToString;
247 use alloc::vec;
248
249 fn leg() -> Skeleton {
252 let joint = |name: &str, parent: Option<usize>, ty: f32| Joint {
253 name: name.to_string(),
254 parent,
255 bind: JointPose {
256 translation: [0.0, ty, 0.0],
257 ..JointPose::default()
258 },
259 };
260 Skeleton::new(vec![
261 joint("hip", None, 2.0),
262 joint("knee", Some(0), -1.0),
263 joint("foot", Some(1), -1.0),
264 ])
265 }
266
267 fn bind_locals(skeleton: &Skeleton) -> Vec<Mat4> {
268 skeleton
269 .joints()
270 .iter()
271 .map(|j| j.bind.to_matrix())
272 .collect()
273 }
274
275 fn joint_pos(skeleton: &Skeleton, locals: &[Mat4], i: usize) -> [f32; 3] {
276 let mut worlds = Vec::new();
277 skeleton.world_matrices_into(locals, &mut worlds);
278 let w = worlds[i];
279 [w[3][0], w[3][1], w[3][2]]
280 }
281
282 fn solve(
283 skeleton: &Skeleton,
284 locals: &mut Vec<Mat4>,
285 chain: &TwoBoneChain,
286 target: [f32; 3],
287 weight: f32,
288 ) {
289 apply_two_bone_ik(skeleton, locals, chain, target, weight, &mut Vec::new());
290 }
291
292 fn assert_close(a: [f32; 3], b: [f32; 3], tol: f32) {
293 for k in 0..3 {
294 assert!((a[k] - b[k]).abs() < tol, "{a:?} vs {b:?}");
295 }
296 }
297
298 const CHAIN: TwoBoneChain = TwoBoneChain {
299 root: 0,
300 mid: 1,
301 end: 2,
302 pole: [0.0, 0.0, 1.0],
303 };
304
305 #[test]
306 fn reachable_target_lands_the_foot_and_bends_toward_the_pole() {
307 let skeleton = leg();
308 let mut locals = bind_locals(&skeleton);
309 solve(&skeleton, &mut locals, &CHAIN, [0.0, 0.5, 0.0], 1.0);
311 assert_close(joint_pos(&skeleton, &locals, 2), [0.0, 0.5, 0.0], 1e-3);
312 let knee = joint_pos(&skeleton, &locals, 1);
314 assert!(knee[2] > 0.1, "knee bends toward the pole: {knee:?}");
315 assert_close(joint_pos(&skeleton, &locals, 0), [0.0, 2.0, 0.0], 1e-4);
316 let hip = joint_pos(&skeleton, &locals, 0);
318 let foot = joint_pos(&skeleton, &locals, 2);
319 let len = |a: [f32; 3], b: [f32; 3]| length(sub(a, b));
320 assert!((len(hip, knee) - 1.0).abs() < 1e-3);
321 assert!((len(knee, foot) - 1.0).abs() < 1e-3);
322 }
323
324 #[test]
325 fn out_of_reach_target_straightens_toward_it() {
326 let skeleton = leg();
327 let mut locals = bind_locals(&skeleton);
328 solve(&skeleton, &mut locals, &CHAIN, [3.0, 2.0, 0.0], 1.0);
329 let foot = joint_pos(&skeleton, &locals, 2);
331 assert_close(foot, [2.0, 2.0, 0.0], 2e-2);
332 }
333
334 #[test]
335 fn sideways_target_respects_the_pole_plane() {
336 let skeleton = leg();
337 let mut locals = bind_locals(&skeleton);
338 solve(&skeleton, &mut locals, &CHAIN, [1.0, 0.8, 0.0], 1.0);
339 assert_close(joint_pos(&skeleton, &locals, 2), [1.0, 0.8, 0.0], 1e-3);
340 let knee = joint_pos(&skeleton, &locals, 1);
341 assert!(knee[2] > 0.05, "knee stays on the +Z pole side: {knee:?}");
342 }
343
344 #[test]
345 fn weight_blends_between_animated_and_solved() {
346 let skeleton = leg();
347 let mut half = bind_locals(&skeleton);
348 solve(&skeleton, &mut half, &CHAIN, [0.0, 0.5, 0.0], 0.5);
349 assert_close(joint_pos(&skeleton, &half, 2), [0.0, 0.25, 0.0], 1e-3);
352
353 let mut off = bind_locals(&skeleton);
354 solve(&skeleton, &mut off, &CHAIN, [0.0, 0.5, 0.0], 0.0);
355 assert_close(joint_pos(&skeleton, &off, 2), [0.0, 0.0, 0.0], 1e-6);
356 }
357
358 #[test]
359 fn degenerate_targets_leave_the_pose_untouched() {
360 let skeleton = leg();
361 let mut locals = bind_locals(&skeleton);
362 let before = locals.clone();
363 solve(&skeleton, &mut locals, &CHAIN, [0.0, 2.0, 0.0], 1.0);
365 assert_eq!(locals, before);
366 let bad = TwoBoneChain {
368 root: 0,
369 mid: 9,
370 end: 2,
371 pole: [0.0, 0.0, 1.0],
372 };
373 solve(&skeleton, &mut locals, &bad, [0.0, 0.5, 0.0], 1.0);
374 assert_eq!(locals, before);
375 }
376
377 #[test]
378 fn solve_composes_with_an_animated_pose() {
379 let skeleton = leg();
383 let mut locals = bind_locals(&skeleton);
384 locals[0] = JointPose {
385 translation: [0.0, 2.0, 0.0],
386 rotation_deg: [0.0, 0.0, 45.0],
387 ..JointPose::default()
388 }
389 .to_matrix();
390 solve(&skeleton, &mut locals, &CHAIN, [0.0, 0.2, 0.0], 1.0);
391 assert_close(joint_pos(&skeleton, &locals, 2), [0.0, 0.2, 0.0], 1e-3);
392 }
393
394 #[test]
398 fn the_shortest_arc_handles_every_degenerate_pair() {
399 let x: Vec3 = [1.0, 0.0, 0.0];
400
401 assert_eq!(from_to([0.0; 3], x), MAT3_IDENTITY, "no direction to turn");
402 assert_eq!(from_to(x, [0.0; 3]), MAT3_IDENTITY, "nowhere to turn to");
403 assert_eq!(
404 from_to(x, [2.0, 0.0, 0.0]),
405 MAT3_IDENTITY,
406 "already aligned"
407 );
408
409 let flipped = mat3_apply(from_to(x, [-1.0, 0.0, 0.0]), x);
412 assert_close(flipped, [-1.0, 0.0, 0.0], 1e-5);
413 }
414
415 #[test]
419 fn the_perpendicular_is_perpendicular_on_either_arm() {
420 for v in [
421 [0.0, 1.0, 0.0],
422 [0.0, 0.0, 1.0],
423 [1.0, 0.0, 0.0],
424 [-1.0, 0.0, 0.0],
425 ] {
426 let p = any_perpendicular(v);
427 assert!(
428 dot(v, p).abs() < 1e-5,
429 "{v:?} vs {p:?} are not perpendicular"
430 );
431 assert!((length(p) - 1.0).abs() < 1e-5, "{p:?} is not unit length");
432 }
433 }
434
435 #[test]
438 fn a_degenerate_chain_or_target_does_not_solve() {
439 let origin = [0.0, 0.0, 0.0];
440 let pole = [0.0, 0.0, 1.0];
441 assert!(
442 solve_two_bone(origin, origin, [0.0, -1.0, 0.0], [1.0, 0.0, 0.0], pole).is_none(),
443 "the upper bone has no length"
444 );
445 assert!(
446 solve_two_bone(
447 origin,
448 [0.0, -1.0, 0.0],
449 [0.0, -1.0, 0.0],
450 [1.0, 0.0, 0.0],
451 pole
452 )
453 .is_none(),
454 "the lower bone has no length"
455 );
456 assert!(
457 solve_two_bone(origin, [0.0, -1.0, 0.0], [0.0, -2.0, 0.0], origin, pole).is_none(),
458 "the target sits on the root"
459 );
460 }
461
462 #[test]
465 fn a_pole_on_the_aim_axis_leaves_the_twist_alone() {
466 let solved = solve_two_bone(
467 [0.0, 2.0, 0.0],
468 [0.0, 1.0, 0.0],
469 [0.0, 0.0, 0.0],
470 [0.0, 0.5, 0.0],
471 [0.0, 1.0, 0.0],
472 );
473 assert!(
474 solved.is_some(),
475 "the chain still solves without a bend plane"
476 );
477 }
478
479 #[test]
483 fn a_short_locals_buffer_is_filled_from_the_bind_pose() {
484 let skeleton = leg();
485 let mut locals: Vec<Mat4> = Vec::new();
486 solve(&skeleton, &mut locals, &CHAIN, [0.5, 0.5, 0.0], 1.0);
487 assert_eq!(locals.len(), skeleton.joints().len());
488 }
489}