Skip to main content

kvas_engine/
ragdoll.rs

1//! Ragdolls for humanoid rigs: when [`Ragdoll::active`] turns on, the character's body is
2//! rebuilt from physics capsules (pelvis, chest, head, upper arms, forearms, thighs, shins)
3//! joined by ball joints, starting exactly in the pose the clip was in and moving with the
4//! velocity it is given. The skeleton then follows the bodies every frame (hands and feet ride
5//! on their forearms and shins); turning it off removes the bodies and the clip takes over again.
6//!
7//! The capsules collide with the world but not with each other ([`RAGDOLL_GROUP`]). They are
8//! transient objects: not saved, not listed in the hierarchy.
9//!
10//! ```no_run
11//! # use kvas_engine::prelude::*;
12//! # fn f(ctx: &mut Ctx, monster: Entity, hit: Vec3) {
13//! ctx.ragdoll(monster, hit * 40.0); // collapse, pushed the way it was hit
14//! # }
15//! ```
16
17use crate::components::*;
18use crate::skeletal::{SkinnedModel, Trs};
19use glam::{Mat4, Quat, Vec3};
20use kvas_ecs::{Entity, World};
21use serde::{Deserialize, Serialize};
22
23/// Collision layer of ragdoll capsules: they hit everything else, not one another.
24pub const RAGDOLL_GROUP: u32 = 1 << 29;
25
26/// Physics-driven body for an animated humanoid.
27#[derive(Clone, Serialize, Deserialize)]
28#[serde(default)]
29pub struct Ragdoll {
30    /// The body is limp and simulated (on) or animated by the clip (off).
31    pub active: bool,
32    /// Total mass in kilograms, shared out over the body parts.
33    pub mass: f32,
34    /// A push given to the chest when the ragdoll starts (newton-seconds, world space).
35    pub impulse: Vec3,
36    /// Joint looseness: angular damping of the parts (higher = stiffer, slower fall).
37    pub damping: f32,
38}
39
40impl Default for Ragdoll {
41    fn default() -> Self {
42        Self { active: false, mass: 65.0, impulse: Vec3::ZERO, damping: 1.5 }
43    }
44}
45
46impl Component for Ragdoll {
47    fn fields(&mut self) -> Vec<crate::reflect::Field<'_>> {
48        use crate::reflect::Field;
49        vec![Field::bool("active", &mut self.active), Field::f32("mass", &mut self.mass), Field::vec3("impulse", &mut self.impulse), Field::f32("damping", &mut self.damping)]
50    }
51}
52
53/// One simulated part: the bone it drives and where the bone sits in the capsule.
54#[derive(Clone)]
55pub(crate) struct Part {
56    node: usize,
57    entity: Entity,
58    offset: Mat4,
59}
60
61/// Bodies of an active ragdoll and the pose it started from.
62#[derive(Clone, Default)]
63pub(crate) struct RagdollState {
64    pub(crate) parts: Vec<Part>,
65    start: Vec<Trs>,
66}
67
68impl RagdollState {
69    pub(crate) fn is_active(&self) -> bool {
70        !self.parts.is_empty()
71    }
72}
73
74fn body_name(owner: Entity, i: usize) -> String {
75    format!("ragdoll#{}#{i}", owner.to_bits())
76}
77
78/// Builds the capsules in the current pose. `world` is the owner's transform.
79pub(crate) fn activate(world_ecs: &mut World, owner: Entity, model: &SkinnedModel, pose: &[Trs], world: Mat4, rd: &Ragdoll, velocity: Vec3) -> RagdollState {
80    let Some(h) = &model.humanoid else { return RagdollState::default() };
81    let at = |n: Option<usize>| n.and_then(|i| pose.get(i)).map(|p| world.transform_point3(p.t));
82    let up = world.transform_vector3(h.up).normalize_or(Vec3::Y);
83    let (Some(hips), Some(head)) = (at(h.hips), at(h.head)) else { return RagdollState::default() };
84    let height = ((head - hips).length() * 2.4).max(0.5);
85    let chest = at(h.chest).or(at(h.spine));
86    let chest_node = h.chest.or(h.spine);
87    // (bone, from, to, radius / height, mass share, parent part)
88    let mut spec: Vec<(usize, Vec3, Vec3, f32, f32, Option<usize>)> = Vec::new();
89    let torso_top = chest.unwrap_or(head);
90    spec.push((h.hips.unwrap_or(0), hips, torso_top, 0.075, 0.26, None));
91    let mut upper = 0;
92    if let (Some(c), Some(cn)) = (chest, chest_node) {
93        spec.push((cn, c, at(h.neck).unwrap_or(head), 0.08, 0.24, Some(0)));
94        upper = 1;
95    }
96    let head_part = spec.len();
97    spec.push((h.head.unwrap_or(0), head, head + up * height * 0.13, 0.06, 0.08, Some(upper)));
98    let _ = head_part;
99    for s in 0..2 {
100        if let (Some(ua), Some(fa)) = (at(h.upper_arm[s]), at(h.forearm[s])) {
101            let p = spec.len();
102            spec.push((h.upper_arm[s].unwrap_or(0), ua, fa, 0.03, 0.03, Some(upper)));
103            let hand = at(h.hand[s]).unwrap_or(fa + (fa - ua));
104            spec.push((h.forearm[s].unwrap_or(0), fa, hand, 0.025, 0.02, Some(p)));
105        }
106        if let (Some(th), Some(sh)) = (at(h.thigh[s]), at(h.shin[s])) {
107            let p = spec.len();
108            spec.push((h.thigh[s].unwrap_or(0), th, sh, 0.045, 0.1, Some(0)));
109            let foot = at(h.foot[s]).unwrap_or(sh + (sh - th));
110            spec.push((h.shin[s].unwrap_or(0), sh, foot, 0.035, 0.05, Some(p)));
111        }
112    }
113    let mut parts = Vec::new();
114    for (i, (node, from, to, r, share, parent)) in spec.iter().enumerate() {
115        let seg = *to - *from;
116        let len = seg.length().max(0.02);
117        let radius = (r * height).clamp(0.015, len * 0.5);
118        let center = (*from + *to) * 0.5;
119        let rot = Quat::from_rotation_arc(Vec3::Y, seg / len);
120        let e = world_ecs.spawn();
121        world_ecs.insert(e, Name(body_name(owner, i)));
122        let mut t = Transform { position: center, ..Default::default() };
123        t.set_quat(rot);
124        world_ecs.insert(e, t);
125        world_ecs.insert(e, Transient);
126        world_ecs.insert(
127            e,
128            RigidBody {
129                mass: (rd.mass * share).max(0.2),
130                linear_damping: 0.15,
131                angular_damping: rd.damping,
132                velocity,
133                set_velocity: Some(velocity),
134                impulse: if i == upper { rd.impulse } else { Vec3::ZERO },
135                ..Default::default()
136            },
137        );
138        world_ecs.insert(
139            e,
140            Collider { shape: ShapeKind::Capsule, size: Vec3::new(radius * 2.0, len.max(radius * 2.0 + 0.01), radius * 2.0), friction: 0.8, restitution: 0.0, memberships: RAGDOLL_GROUP, filter: !RAGDOLL_GROUP, ..Default::default() },
141        );
142        if let Some(p) = parent {
143            world_ecs.insert(e, Joint { kind: JointKind::Ball, target: body_name(owner, *p), anchor: Vec3::new(0.0, -len * 0.5, 0.0), ..Default::default() });
144        }
145        let body = Mat4::from_rotation_translation(rot, center);
146        let bone = world * pose[*node].mat();
147        parts.push(Part { node: *node, entity: e, offset: body.inverse() * bone });
148    }
149    RagdollState { parts, start: pose.to_vec() }
150}
151
152/// The pose the bodies hold now (model space). Bones without a body keep their place
153/// relative to their parent from the moment the ragdoll started.
154pub(crate) fn pose(world_ecs: &World, model: &SkinnedModel, st: &RagdollState, world: Mat4, out: &mut Vec<Trs>) {
155    let inv = world.inverse();
156    let start: Vec<Mat4> = st.start.iter().map(Trs::mat).collect();
157    let mut now: Vec<Option<Mat4>> = vec![None; model.nodes.len()];
158    for p in &st.parts {
159        if let Some(t) = world_ecs.get::<Transform>(p.entity) {
160            now[p.node] = Some(inv * t.matrix() * p.offset);
161        }
162    }
163    // Parents first.
164    let depth = |mut i: usize| {
165        let mut d = 0;
166        while let Some(p) = model.nodes[i].parent {
167            i = p;
168            d += 1;
169            if d > 128 {
170                break;
171            }
172        }
173        d
174    };
175    let mut order: Vec<usize> = (0..model.nodes.len()).collect();
176    order.sort_by_key(|&i| depth(i));
177    for &i in &order {
178        if now[i].is_some() {
179            continue;
180        }
181        now[i] = Some(match model.nodes[i].parent {
182            Some(p) => now[p].unwrap_or(start[p]) * start[p].inverse() * start[i],
183            None => start[i],
184        });
185    }
186    out.clear();
187    out.extend(now.into_iter().map(|m| Trs::from_mat(m.unwrap_or(Mat4::IDENTITY))));
188}
189
190/// Removes the bodies of a ragdoll.
191pub(crate) fn deactivate(world_ecs: &mut World, st: &mut RagdollState) {
192    for p in st.parts.drain(..) {
193        world_ecs.despawn(p.entity);
194    }
195    st.start.clear();
196}