use super::*;
use crate::{JointAxis3D, JointMotion3D, Mat4, Skeleton};
#[derive(Clone, Debug, PartialEq)]
pub struct RagdollBone {
pub joint: String,
pub radius: f32,
pub swing: f32,
pub twist: f32,
pub density: f32,
}
impl RagdollBone {
#[must_use]
pub fn new(joint: &str, radius: f32) -> Self {
Self {
joint: joint.into(),
radius,
swing: 0.7,
twist: 0.3,
density: 1000.0,
}
}
#[must_use]
pub fn swing(self, radians: f32) -> Self {
Self { swing: radians, ..self }
}
#[must_use]
pub fn twist(self, radians: f32) -> Self {
Self { twist: radians, ..self }
}
}
#[derive(Clone, Debug)]
struct Bone {
joint: usize,
body: Entity,
align: Quat,
half: f32,
root: bool,
}
#[derive(Clone, Debug)]
pub struct Ragdoll3D {
bones: Vec<Bone>,
joints: Vec<Entity>,
blend: Option<(Vec<Transform3D>, f32, f32)>,
poses: Vec<(Vec3, Quat)>,
world: Vec<Mat4>,
}
impl Ragdoll3D {
pub fn body(&self, joint: usize) -> Option<Entity> {
self.bones.iter().find(|b| b.joint == joint).map(|b| b.body)
}
pub fn bodies(&self) -> impl Iterator<Item = (usize, Entity)> + '_ {
self.bones.iter().map(|b| (b.joint, b.body))
}
pub fn is_blending(&self) -> bool {
self.blend.is_some()
}
}
fn decompose(m: Mat4) -> (Vec3, Quat, Vec3) {
let (scale, rotation, position) = m.to_scale_rotation_translation();
(position, rotation.normalize(), scale)
}
impl Ctx {
pub fn start_ragdoll(&mut self, entity: Entity, bones: &[RagdollBone]) -> Result<(), String> {
let model = self
.world
.get::<Transform3D>(entity)
.ok_or("start_ragdoll: the entity has no Transform3D")?
.matrix();
let skeleton = self
.world
.get::<Skeleton>(entity)
.ok_or("start_ragdoll: the entity has no Skeleton")?
.clone();
let skeleton = &skeleton;
let joints = skeleton.joints();
let mut chosen: Vec<(usize, &RagdollBone)> = Vec::with_capacity(bones.len());
for b in bones {
let j = skeleton
.find(&b.joint)
.ok_or_else(|| format!("start_ragdoll: no joint named {:?}", b.joint))?;
if !(b.radius > 0.0 && b.radius.is_finite()) || !b.density.is_finite() || b.density <= 0.0 {
return Err(format!(
"start_ragdoll: {:?} needs a positive radius and density",
b.joint
));
}
if chosen.iter().any(|&(c, _)| c == j) {
return Err(format!("start_ragdoll: {:?} is chosen twice", b.joint));
}
chosen.push((j, b));
}
self.stop_ragdoll(entity, 0.0);
chosen.sort_by_key(|&(j, _)| j);
let is_chosen = |j: usize| chosen.binary_search_by_key(&j, |&(c, _)| c).ok();
let world: Vec<Mat4> = (0..joints.len()).map(|j| model * skeleton.joint_matrix(j)).collect();
let local = |j: usize| skeleton.pose.get(j).copied().unwrap_or(joints[j].rest);
let parent_bone = |mut j: usize| {
while let Some(p) = joints[j].parent {
if let Some(i) = is_chosen(p) {
return Some(i);
}
j = p;
}
None
};
let mut planned = Vec::with_capacity(chosen.len());
for &(j, b) in &chosen {
let children = || (0..joints.len()).filter(move |&c| joints[c].parent == Some(j));
let child = children()
.find(|&c| is_chosen(c).is_some())
.or_else(|| children().next());
let (p, q, _) = decompose(world[j]);
let (dir, length) = match child {
Some(c) => {
let to = decompose(world[c]).0 - p;
(local(c).position.try_normalize().unwrap_or(Vec3::Y), to.length())
}
None => (Vec3::Y, 2.0 * b.radius),
};
let align = Quat::from_rotation_arc(Vec3::Y, dir);
let rotation = (q * align).normalize();
let half = length * 0.5;
let height = (length - 2.0 * b.radius).max(0.0);
let collider = if height > 0.0 {
Collider3D::capsule(height, b.radius)
} else {
Collider3D::sphere(b.radius)
};
planned.push((j, b, p + rotation * Vec3::Y * half, rotation, align, half, collider));
}
let mut made = Vec::with_capacity(planned.len());
for &(j, b, position, rotation, align, half, collider) in &planned {
let t = Transform3D {
position,
rotation,
..Default::default()
};
let (rigid, collider) = (RigidBody3D::dynamic(), collider.density(b.density));
let body = self.world.spawn((t, rigid, collider));
let step = self.physics3d.step;
let kind = super::LinkKind::Body(rigid, collider);
super::sync_one(&mut self.physics3d, body, &t, kind, None, step);
made.push(Bone {
joint: j,
body,
align,
half,
root: false,
});
}
let mut links = Vec::new();
for i in 0..made.len() {
let Some(parent) = parent_bone(made[i].joint) else {
made[i].root = true;
continue;
};
let (a, b) = (&planned[parent], &planned[i]);
let anchor = decompose(world[made[i].joint]).0;
let frame = Quat::from_rotation_arc(Vec3::X, b.3 * Vec3::Y);
let in_body = |position: Vec3, rotation: Quat| Transform3D {
position: rotation.inverse() * (anchor - position),
rotation: (rotation.inverse() * frame).normalize(),
..Default::default()
};
let mut joint = Joint3D::spherical(made[parent].body, made[i].body)
.motion(JointAxis3D::AngularX, JointMotion3D::Limited(-b.1.twist, b.1.twist))
.swing_cone(b.1.swing);
joint.frame_a = in_body(a.2, a.3);
joint.frame_b = in_body(b.2, b.3);
links.push(self.world.spawn((joint,)));
}
let count = joints.len();
self.world.insert(
entity,
Ragdoll3D {
bones: made,
joints: links,
blend: None,
poses: Vec::with_capacity(chosen.len()),
world: Vec::with_capacity(count),
},
);
Ok(())
}
pub fn stop_ragdoll(&mut self, entity: Entity, seconds: f32) {
let Some(ragdoll) = self.world.get_mut::<Ragdoll3D>(entity) else {
return;
};
let doomed: Vec<Entity> = ragdoll
.joints
.drain(..)
.chain(ragdoll.bones.drain(..).map(|b| b.body))
.collect();
for e in doomed {
self.world.despawn(e);
}
let seconds = if seconds.is_finite() { seconds.max(0.0) } else { 0.0 };
let pose = self.world.get::<Skeleton>(entity).map(|s| s.pose.clone());
match pose {
Some(pose) if seconds > 0.0 => {
if let Some(r) = self.world.get_mut::<Ragdoll3D>(entity) {
r.blend = Some((pose, 0.0, seconds));
}
}
_ => {
self.world.remove::<Ragdoll3D>(entity);
}
}
}
}
pub(crate) fn pose_ragdolls(world: &mut World, scratch: &mut Vec<Entity>) {
scratch.clear();
scratch.extend(
world
.query::<(Entity, &Ragdoll3D)>()
.iter()
.filter(|(_, r)| r.blend.is_none())
.map(|(e, _)| e),
);
for &e in scratch.iter() {
let Some(mut ragdoll) = world.get_mut::<Ragdoll3D>(e).map(|r| std::mem::replace(r, empty())) else {
continue;
};
ragdoll.poses.clear();
for b in &ragdoll.bones {
let t = world.get::<Transform3D>(b.body).copied().unwrap_or_default();
ragdoll.poses.push((t.position, t.rotation));
}
let model = world.get::<Transform3D>(e).map(|t| t.matrix());
if let (Some(model), Some(skeleton)) = (model, world.get_mut::<Skeleton>(e)) {
pose_from_bodies(&mut ragdoll, model.inverse(), skeleton);
}
if let Some(r) = world.get_mut::<Ragdoll3D>(e) {
*r = ragdoll;
}
}
}
fn empty() -> Ragdoll3D {
Ragdoll3D {
bones: Vec::new(),
joints: Vec::new(),
blend: None,
poses: Vec::new(),
world: Vec::new(),
}
}
fn pose_from_bodies(ragdoll: &mut Ragdoll3D, to_model: Mat4, skeleton: &mut Skeleton) {
let count = skeleton.joints().len();
ragdoll.world.clear();
let mut next = 0;
for j in 0..count {
let joint = &skeleton.joints()[j];
let parent = joint.parent.map_or(Mat4::IDENTITY, |p| ragdoll.world[p]);
let mut local = skeleton.pose.get(j).copied().unwrap_or(joint.rest);
if let Some(b) = ragdoll.bones.get(next).filter(|b| b.joint == j) {
let (body_position, body_rotation) = ragdoll.poses[next];
let rotation = body_rotation * b.align.inverse();
let position = body_position - body_rotation * Vec3::Y * b.half;
let in_model = to_model * Mat4::from_rotation_translation(rotation, position);
let (p, q, _) = decompose(parent.inverse() * in_model);
if q.is_finite() {
local.rotation = q;
}
if b.root && p.is_finite() {
local.position = p;
}
if let Some(slot) = skeleton.pose.get_mut(j) {
*slot = local;
}
next += 1;
}
ragdoll.world.push(parent * local.matrix());
}
}
pub(crate) fn blend_back(ragdoll: &mut Ragdoll3D, skeleton: &mut Skeleton, dt: f32) -> bool {
let Some((from, elapsed, duration)) = &mut ragdoll.blend else {
return false;
};
*elapsed += dt;
let t = (*elapsed / *duration).clamp(0.0, 1.0);
let w = t * t * (3.0 - 2.0 * t);
for (to, from) in skeleton.pose.iter_mut().zip(from.iter()) {
to.position = from.position.lerp(to.position, w);
to.rotation = from.rotation.slerp(to.rotation, w);
to.scale = from.scale.lerp(to.scale, w);
}
t >= 1.0
}