Skip to main content

kvas_engine/
humanoid.rs

1//! Procedural clips for humanoid rigs that come without animations: `Idle`, `Walk`, `Run`,
2//! `Talk`, `Wave`, `Sit`, `Wipe`, `Look`.
3//!
4//! Bones are recognised by name (`hips`/`pelvis`, `spine`, `chest`, `neck`, `head`,
5//! `upper_arm`, `forearm`, `hand`, `thigh`, `shin`, `foot`, with sides written as `.L`, `_left`,
6//! `Left...`). The character's own axes come from the rest pose (up, right from shoulder to
7//! shoulder, forward), so a bend is "forward" whatever the bone axes of the file are. Arms in a
8//! T- or A-pose are brought down first. Each clip is a function of time baked at 30 fps.
9
10use crate::skeletal::{Clip, SkinnedModel, Trs};
11use glam::{Mat4, Quat, Vec3};
12
13const L: usize = 0;
14const R: usize = 1;
15
16/// Bone indices (into [`SkinnedModel::nodes`]) of the recognised parts of a humanoid; sides
17/// are `[left, right]`. Used by procedural clips, IK and ragdolls.
18#[derive(Default, Clone, Debug)]
19pub struct HumanoidBones {
20    /// Hips or pelvis: the root of the legs and the spine.
21    pub hips: Option<usize>,
22    /// Lower spine.
23    pub spine: Option<usize>,
24    /// Upper spine / chest.
25    pub chest: Option<usize>,
26    /// Neck.
27    pub neck: Option<usize>,
28    /// Head.
29    pub head: Option<usize>,
30    /// Upper arms.
31    pub upper_arm: [Option<usize>; 2],
32    /// Forearms.
33    pub forearm: [Option<usize>; 2],
34    /// Hands.
35    pub hand: [Option<usize>; 2],
36    /// Thighs.
37    pub thigh: [Option<usize>; 2],
38    /// Shins.
39    pub shin: [Option<usize>; 2],
40    /// Feet.
41    pub foot: [Option<usize>; 2],
42    /// The character's up axis in model space.
43    pub up: Vec3,
44    /// The character's right (from its left shoulder to its right one).
45    pub right: Vec3,
46    /// The direction it faces.
47    pub forward: Vec3,
48}
49
50type Rig = HumanoidBones;
51
52/// Finds the humanoid bones of a model by name (see the module docs); `None` when there are no
53/// hips and two complete legs.
54pub fn find_bones(model: &SkinnedModel) -> Option<HumanoidBones> {
55    let mut rig = Rig::of(model)?;
56    let f = frame(model, &rig);
57    (rig.up, rig.right, rig.forward) = (f.up, f.right, f.forward);
58    Some(rig)
59}
60
61fn norm(s: &str) -> String {
62    let s = s.to_lowercase().replace("mixamorig", "");
63    s.chars().filter(|c| c.is_alphanumeric()).collect()
64}
65
66fn find(names: &[String], bases: &[&str]) -> Option<usize> {
67    bases.iter().find_map(|b| names.iter().position(|n| n == b))
68}
69
70fn find_side(names: &[String], bases: &[&str], side: usize) -> Option<usize> {
71    let (short, long) = if side == L { ("l", "left") } else { ("r", "right") };
72    bases.iter().find_map(|b| {
73        names.iter().position(|n| {
74            *n == format!("{b}{short}") || *n == format!("{b}{long}") || *n == format!("{long}{b}") || *n == format!("{short}{b}") || n.ends_with(&format!("{long}{b}"))
75        })
76    })
77}
78
79impl Rig {
80    fn of(model: &SkinnedModel) -> Option<Rig> {
81        let names: Vec<String> = model.nodes.iter().map(|n| norm(&n.name)).collect();
82        let side = |bases: &[&str]| [find_side(&names, bases, L), find_side(&names, bases, R)];
83        let rig = Rig {
84            up: Vec3::Y,
85            right: Vec3::NEG_X,
86            forward: Vec3::Z,
87            hips: find(&names, &["hips", "pelvis", "hip"]),
88            spine: find(&names, &["spine", "spine01", "spine1"]),
89            chest: find(&names, &["chest", "spine2", "spine02", "upperchest", "spine03"]),
90            neck: find(&names, &["neck", "neck01"]),
91            head: find(&names, &["head"]),
92            upper_arm: side(&["upperarm", "arm"]),
93            forearm: side(&["forearm", "lowerarm"]),
94            hand: side(&["hand"]),
95            thigh: side(&["thigh", "upperleg", "upleg"]),
96            shin: side(&["shin", "calf", "lowerleg", "leg"]),
97            foot: side(&["foot"]),
98        };
99        let legs = rig.thigh.iter().chain(&rig.shin).all(Option::is_some);
100        (rig.hips.is_some() && legs).then_some(rig)
101    }
102}
103
104/// The character's axes and the rest-pose fix for the arms.
105struct Frame {
106    up: Vec3,
107    right: Vec3,
108    forward: Vec3,
109    arm_down: [Quat; 2],
110}
111
112fn pos(model: &SkinnedModel, i: Option<usize>) -> Option<Vec3> {
113    Some(model.nodes.get(i?)?.rest.w_axis.truncate())
114}
115
116fn frame(model: &SkinnedModel, rig: &Rig) -> Frame {
117    let up = Vec3::Y;
118    let across = match (pos(model, rig.upper_arm[R]).zip(pos(model, rig.upper_arm[L])), pos(model, rig.thigh[R]).zip(pos(model, rig.thigh[L]))) {
119        (Some((r, l)), _) => r - l,
120        (None, Some((r, l))) => r - l,
121        _ => Vec3::NEG_X,
122    };
123    let right = (across - up * across.dot(up)).normalize_or(Vec3::NEG_X);
124    let forward = up.cross(right).normalize_or(Vec3::Z);
125    // Arms hanging a little away from the body, whatever the rest pose is.
126    let mut arm_down = [Quat::IDENTITY; 2];
127    for s in [L, R] {
128        if let (Some(a), Some(b)) = (pos(model, rig.upper_arm[s]), pos(model, rig.forearm[s])) {
129            let dir = (b - a).normalize_or_zero();
130            let out = if s == L { -right } else { right };
131            let target = (-up * 12f32.to_radians().cos() + out * 12f32.to_radians().sin() + forward * 0.05).normalize();
132            if dir != Vec3::ZERO {
133                arm_down[s] = Quat::from_rotation_arc(dir, target);
134            }
135        }
136    }
137    Frame { up, right, forward, arm_down }
138}
139
140/// Bone rotations (model space) and a hips offset for one moment of a clip.
141#[derive(Default)]
142struct Pose {
143    rot: Vec<(usize, Quat)>,
144    hips_offset: Vec3,
145}
146
147impl Pose {
148    fn set(&mut self, bone: Option<usize>, q: Quat) {
149        if let Some(b) = bone {
150            self.rot.push((b, q));
151        }
152    }
153}
154
155fn deg(axis: Vec3, d: f32) -> Quat {
156    Quat::from_axis_angle(axis, d.to_radians())
157}
158
159const TAU: f32 = std::f32::consts::TAU;
160
161/// One moment of a procedural clip.
162fn pose(name: &str, t: f32, rig: &Rig, f: &Frame) -> Pose {
163    let mut p = Pose::default();
164    let (rt, up, fw) = (f.right, f.up, f.forward);
165    // Swing about the right axis: positive moves a hanging limb forward, a torso backwards.
166    let swing = |d: f32| deg(rt, d);
167    let turn = |d: f32| deg(up, d);
168    let roll = |d: f32| deg(fw, d);
169    let breathe = (TAU * t / 4.0).sin();
170    let arms_idle = |p: &mut Pose, sway: f32| {
171        for s in [L, R] {
172            p.set(rig.upper_arm[s], swing(2.0 * sway) * f.arm_down[s]);
173            p.set(rig.forearm[s], swing(10.0));
174        }
175    };
176    match name {
177        "Idle" => {
178            p.set(rig.chest, swing(-1.5 * breathe));
179            p.set(rig.spine, roll(1.2 * (TAU * t / 8.0).sin()));
180            p.set(rig.head, turn(8.0 * (TAU * t / 8.0).sin()) * swing(-3.0 + 2.0 * (TAU * t / 5.0).sin()));
181            arms_idle(&mut p, breathe);
182            p.hips_offset = up * (-0.004 * breathe);
183        }
184        "Walk" | "Run" => {
185            let run = name == "Run";
186            let (period, k) = if run { (0.7, 1.6) } else { (1.1, 1.0) };
187            let ph = TAU * t / period;
188            let s = ph.sin();
189            let c = ph.cos();
190            p.set(rig.thigh[L], swing(24.0 * k * s));
191            p.set(rig.thigh[R], swing(-24.0 * k * s));
192            p.set(rig.shin[L], swing(-(8.0 + 32.0 * k * c.max(0.0))));
193            p.set(rig.shin[R], swing(-(8.0 + 32.0 * k * (-c).max(0.0))));
194            p.set(rig.foot[L], swing(8.0 * s));
195            p.set(rig.foot[R], swing(-8.0 * s));
196            p.set(rig.upper_arm[L], swing(-18.0 * k * s) * f.arm_down[L]);
197            p.set(rig.upper_arm[R], swing(18.0 * k * s) * f.arm_down[R]);
198            p.set(rig.forearm[L], swing(15.0 * k));
199            p.set(rig.forearm[R], swing(15.0 * k));
200            p.set(rig.hips, turn(5.0 * s));
201            p.set(rig.spine, swing(if run { -8.0 } else { -2.0 }));
202            p.set(rig.chest, turn(-7.0 * s));
203            p.set(rig.head, turn(2.0 * s));
204            p.hips_offset = up * (0.022 * k * (2.0 * ph).cos());
205        }
206        "Talk" => {
207            let g = (TAU * t * 0.9).sin();
208            p.set(rig.chest, swing(-1.5 * breathe));
209            p.set(rig.head, swing(-4.0 * (TAU * t * 1.3).sin()) * turn(6.0 * (TAU * t / 3.0).sin()));
210            p.set(rig.neck, swing(-2.0 * (TAU * t * 1.3).sin()));
211            p.set(rig.upper_arm[L], f.arm_down[L]);
212            p.set(rig.forearm[L], swing(12.0));
213            p.set(rig.upper_arm[R], swing(18.0 + 6.0 * g) * f.arm_down[R]);
214            p.set(rig.forearm[R], swing(40.0 + 18.0 * g) * turn(-10.0 * g));
215            p.set(rig.hand[R], roll(15.0 * g));
216        }
217        "Wave" => {
218            let w = (TAU * t * 1.5).sin();
219            p.set(rig.upper_arm[L], f.arm_down[L]);
220            p.set(rig.forearm[L], swing(10.0));
221            // Right arm up to the side, the forearm waving.
222            p.set(rig.upper_arm[R], roll(-150.0) * f.arm_down[R]);
223            p.set(rig.forearm[R], roll(25.0 * w));
224            p.set(rig.head, turn(-6.0));
225            p.set(rig.chest, swing(-1.0 * breathe));
226        }
227        "Sit" => {
228            for s in [L, R] {
229                p.set(rig.thigh[s], swing(88.0));
230                p.set(rig.shin[s], swing(-88.0));
231                p.set(rig.upper_arm[s], swing(32.0) * f.arm_down[s]);
232                p.set(rig.forearm[s], swing(48.0));
233            }
234            p.set(rig.spine, swing(-4.0));
235            p.set(rig.chest, swing(-1.5 * breathe));
236            p.set(rig.head, swing(-6.0) * turn(5.0 * (TAU * t / 6.0).sin()));
237        }
238        "Wipe" => {
239            let w = TAU * t / 1.2;
240            p.set(rig.spine, swing(-14.0));
241            p.set(rig.chest, swing(-6.0) * turn(4.0 * w.sin()));
242            p.set(rig.head, swing(-16.0));
243            p.set(rig.upper_arm[R], turn(16.0 * w.sin()) * swing(50.0 + 8.0 * w.cos()) * f.arm_down[R]);
244            p.set(rig.forearm[R], swing(55.0 + 10.0 * w.sin()));
245            p.set(rig.upper_arm[L], swing(35.0) * f.arm_down[L]);
246            p.set(rig.forearm[L], swing(60.0));
247        }
248        "Look" => {
249            let a = (TAU * t / 6.0).sin();
250            p.set(rig.chest, turn(10.0 * a) * swing(-1.5 * breathe));
251            p.set(rig.head, turn(35.0 * a) * swing(-2.0));
252            arms_idle(&mut p, breathe);
253        }
254        _ => {}
255    }
256    p
257}
258
259/// Forward kinematics: the rest pose with model-space rotations applied at each bone.
260fn evaluate(model: &SkinnedModel, order: &[usize], rest_local: &[Mat4], rig: &Rig, p: &Pose) -> Vec<Trs> {
261    let n = model.nodes.len();
262    let mut delta = vec![Quat::IDENTITY; n];
263    for (b, q) in &p.rot {
264        delta[*b] = *q * delta[*b];
265    }
266    let mut world = vec![Mat4::IDENTITY; n];
267    for &i in order {
268        let rest_rot = Trs::from_mat(model.nodes[i].rest).r;
269        let local = Mat4::from_quat(rest_rot.inverse() * delta[i] * rest_rot);
270        let parent = model.nodes[i].parent.map_or(Mat4::IDENTITY, |p| world[p]);
271        world[i] = parent * rest_local[i] * local;
272        if Some(i) == rig.hips {
273            world[i] = Mat4::from_translation(p.hips_offset) * world[i];
274        }
275    }
276    world.into_iter().map(Trs::from_mat).collect()
277}
278
279/// Adds the procedural clips a humanoid rig lacks.
280pub fn add_clips(model: &mut SkinnedModel) {
281    let Some(rig) = Rig::of(model) else { return };
282    let f = frame(model, &rig);
283    // Parents before children.
284    let depth = |mut i: usize| {
285        let mut d = 0;
286        while let Some(p) = model.nodes[i].parent {
287            i = p;
288            d += 1;
289            if d > 128 {
290                break;
291            }
292        }
293        d
294    };
295    let mut order: Vec<usize> = (0..model.nodes.len()).collect();
296    order.sort_by_key(|&i| depth(i));
297    let rest_local: Vec<Mat4> = model.nodes.iter().map(|n| n.parent.map_or(n.rest, |p| model.nodes[p].rest.inverse() * n.rest)).collect();
298    let fps = 30.0;
299    for (name, duration) in [("Idle", 8.0), ("Walk", 1.1), ("Run", 0.7), ("Talk", 6.0), ("Wave", 2.0), ("Sit", 6.0), ("Wipe", 2.4), ("Look", 6.0)] {
300        if model.clip(name).is_some() {
301            continue;
302        }
303        let count = (duration * fps) as usize + 1;
304        let frames = (0..count).map(|k| evaluate(model, &order, &rest_local, &rig, &pose(name, k as f32 / fps, &rig, &f))).collect();
305        model.clips.push(Clip { name: name.to_string(), duration, fps, frames });
306    }
307}