use crate::joint::{BallJoint, HingeJoint, Joint};
use crate::math::{Fix128, Vec3Fix};
use crate::solver::{PhysicsWorld, RigidBody};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Bone {
Pelvis,
Torso,
Head,
LeftUpperArm,
LeftForearm,
LeftHand,
RightUpperArm,
RightForearm,
RightHand,
LeftThigh,
LeftShin,
LeftFoot,
RightThigh,
RightShin,
RightFoot,
}
impl Bone {
#[must_use]
pub const fn index(self) -> usize {
self as usize
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RagdollProportions {
pub height_m: Fix128,
pub mass_kg: Fix128,
pub mass_pelvis: Fix128,
pub mass_torso: Fix128,
pub mass_head: Fix128,
pub mass_upper_arm: Fix128,
pub mass_forearm: Fix128,
pub mass_hand: Fix128,
pub mass_thigh: Fix128,
pub mass_shin: Fix128,
pub mass_foot: Fix128,
}
impl RagdollProportions {
#[must_use]
pub fn human_male() -> Self {
Self {
height_m: Fix128::from_ratio(175, 100),
mass_kg: Fix128::from_int(75),
mass_pelvis: Fix128::from_ratio(142, 1000),
mass_torso: Fix128::from_ratio(355, 1000),
mass_head: Fix128::from_ratio(81, 1000),
mass_upper_arm: Fix128::from_ratio(28, 1000),
mass_forearm: Fix128::from_ratio(16, 1000),
mass_hand: Fix128::from_ratio(6, 1000),
mass_thigh: Fix128::from_ratio(100, 1000),
mass_shin: Fix128::from_ratio(465, 10_000),
mass_foot: Fix128::from_ratio(145, 10_000),
}
}
#[must_use]
pub fn human_female() -> Self {
Self {
height_m: Fix128::from_ratio(162, 100),
mass_kg: Fix128::from_int(62),
..Self::human_male()
}
}
#[must_use]
pub fn child() -> Self {
Self {
height_m: Fix128::from_ratio(130, 100),
mass_kg: Fix128::from_int(30),
..Self::human_male()
}
}
}
#[derive(Debug, Clone)]
pub struct RagdollHandle {
pub bones: [usize; 15],
pub joints: [usize; 14],
}
impl RagdollHandle {
#[must_use]
pub fn body(&self, bone: Bone) -> usize {
self.bones[bone.index()]
}
}
struct SegmentLengths {
pelvis: Fix128,
torso: Fix128,
head: Fix128,
upper_arm: Fix128,
forearm: Fix128,
hand: Fix128,
thigh: Fix128,
shin: Fix128,
foot: Fix128,
shoulder_x: Fix128,
hip_x: Fix128,
}
impl SegmentLengths {
fn from_height(height: Fix128) -> Self {
Self {
pelvis: height * Fix128::from_ratio(8, 100),
torso: height * Fix128::from_ratio(30, 100),
head: height * Fix128::from_ratio(13, 100),
upper_arm: height * Fix128::from_ratio(18, 100),
forearm: height * Fix128::from_ratio(15, 100),
hand: height * Fix128::from_ratio(11, 100),
thigh: height * Fix128::from_ratio(24, 100),
shin: height * Fix128::from_ratio(23, 100),
foot: height * Fix128::from_ratio(5, 100),
shoulder_x: height * Fix128::from_ratio(12, 100),
hip_x: height * Fix128::from_ratio(7, 100),
}
}
}
pub struct RagdollBuilder;
impl RagdollBuilder {
#[must_use]
pub fn build(
world: &mut PhysicsWorld,
proportions: RagdollProportions,
pelvis_position: Vec3Fix,
) -> RagdollHandle {
let lengths = SegmentLengths::from_height(proportions.height_m);
let total_mass = proportions.mass_kg;
let y_pelvis = pelvis_position.y;
let y_torso = y_pelvis + (lengths.pelvis + lengths.torso) * Fix128::from_ratio(1, 2);
let y_head = y_torso + (lengths.torso + lengths.head) * Fix128::from_ratio(1, 2);
let y_shoulder = y_torso + lengths.torso * Fix128::from_ratio(35, 100);
let y_upper_arm_mid = y_shoulder - lengths.upper_arm * Fix128::from_ratio(1, 2);
let y_forearm_mid =
y_shoulder - lengths.upper_arm - lengths.forearm * Fix128::from_ratio(1, 2);
let y_hand_mid = y_shoulder
- lengths.upper_arm
- lengths.forearm
- lengths.hand * Fix128::from_ratio(1, 2);
let y_hip = y_pelvis - lengths.pelvis * Fix128::from_ratio(1, 2);
let y_thigh_mid = y_hip - lengths.thigh * Fix128::from_ratio(1, 2);
let y_shin_mid = y_hip - lengths.thigh - lengths.shin * Fix128::from_ratio(1, 2);
let y_foot_mid =
y_hip - lengths.thigh - lengths.shin - lengths.foot * Fix128::from_ratio(1, 2);
let px = pelvis_position.x;
let pz = pelvis_position.z;
let mut bones = [0_usize; 15];
bones[Bone::Pelvis.index()] = world.add_body(RigidBody::new(
Vec3Fix::new(px, y_pelvis, pz),
total_mass * proportions.mass_pelvis,
));
bones[Bone::Torso.index()] = world.add_body(RigidBody::new(
Vec3Fix::new(px, y_torso, pz),
total_mass * proportions.mass_torso,
));
bones[Bone::Head.index()] = world.add_body(RigidBody::new(
Vec3Fix::new(px, y_head, pz),
total_mass * proportions.mass_head,
));
bones[Bone::LeftUpperArm.index()] = world.add_body(RigidBody::new(
Vec3Fix::new(px - lengths.shoulder_x, y_upper_arm_mid, pz),
total_mass * proportions.mass_upper_arm,
));
bones[Bone::LeftForearm.index()] = world.add_body(RigidBody::new(
Vec3Fix::new(px - lengths.shoulder_x, y_forearm_mid, pz),
total_mass * proportions.mass_forearm,
));
bones[Bone::LeftHand.index()] = world.add_body(RigidBody::new(
Vec3Fix::new(px - lengths.shoulder_x, y_hand_mid, pz),
total_mass * proportions.mass_hand,
));
bones[Bone::RightUpperArm.index()] = world.add_body(RigidBody::new(
Vec3Fix::new(px + lengths.shoulder_x, y_upper_arm_mid, pz),
total_mass * proportions.mass_upper_arm,
));
bones[Bone::RightForearm.index()] = world.add_body(RigidBody::new(
Vec3Fix::new(px + lengths.shoulder_x, y_forearm_mid, pz),
total_mass * proportions.mass_forearm,
));
bones[Bone::RightHand.index()] = world.add_body(RigidBody::new(
Vec3Fix::new(px + lengths.shoulder_x, y_hand_mid, pz),
total_mass * proportions.mass_hand,
));
bones[Bone::LeftThigh.index()] = world.add_body(RigidBody::new(
Vec3Fix::new(px - lengths.hip_x, y_thigh_mid, pz),
total_mass * proportions.mass_thigh,
));
bones[Bone::LeftShin.index()] = world.add_body(RigidBody::new(
Vec3Fix::new(px - lengths.hip_x, y_shin_mid, pz),
total_mass * proportions.mass_shin,
));
bones[Bone::LeftFoot.index()] = world.add_body(RigidBody::new(
Vec3Fix::new(px - lengths.hip_x, y_foot_mid, pz),
total_mass * proportions.mass_foot,
));
bones[Bone::RightThigh.index()] = world.add_body(RigidBody::new(
Vec3Fix::new(px + lengths.hip_x, y_thigh_mid, pz),
total_mass * proportions.mass_thigh,
));
bones[Bone::RightShin.index()] = world.add_body(RigidBody::new(
Vec3Fix::new(px + lengths.hip_x, y_shin_mid, pz),
total_mass * proportions.mass_shin,
));
bones[Bone::RightFoot.index()] = world.add_body(RigidBody::new(
Vec3Fix::new(px + lengths.hip_x, y_foot_mid, pz),
total_mass * proportions.mass_foot,
));
let pos: [Vec3Fix; 15] = core::array::from_fn(|i| world.bodies[bones[i]].position);
let half = Fix128::from_ratio(1, 2);
let waist = Vec3Fix::new(px, y_pelvis + lengths.pelvis * half, pz);
let neck = Vec3Fix::new(px, y_torso + lengths.torso * half, pz);
let shoulder_l = Vec3Fix::new(px - lengths.shoulder_x, y_shoulder, pz);
let shoulder_r = Vec3Fix::new(px + lengths.shoulder_x, y_shoulder, pz);
let y_elbow = y_shoulder - lengths.upper_arm;
let y_wrist = y_elbow - lengths.forearm;
let hip_l = Vec3Fix::new(px - lengths.hip_x, y_hip, pz);
let hip_r = Vec3Fix::new(px + lengths.hip_x, y_hip, pz);
let y_knee = y_hip - lengths.thigh;
let y_ankle = y_knee - lengths.shin;
let at = |p: Vec3Fix, y: Fix128| Vec3Fix::new(p.x, y, p.z);
let ball = |a: Bone, b: Bone, j: Vec3Fix| {
Joint::Ball(BallJoint::new(
bones[a.index()],
bones[b.index()],
j - pos[a.index()],
j - pos[b.index()],
))
};
let hinge_axis = Vec3Fix::new(Fix128::ONE, Fix128::ZERO, Fix128::ZERO);
let hinge = |a: Bone, b: Bone, j: Vec3Fix| {
Joint::Hinge(HingeJoint::new(
bones[a.index()],
bones[b.index()],
j - pos[a.index()],
j - pos[b.index()],
hinge_axis,
hinge_axis,
))
};
let mut joints = [0_usize; 14];
let table = [
ball(Bone::Pelvis, Bone::Torso, waist),
ball(Bone::Torso, Bone::Head, neck),
ball(Bone::Torso, Bone::LeftUpperArm, shoulder_l),
hinge(
Bone::LeftUpperArm,
Bone::LeftForearm,
at(pos[Bone::LeftUpperArm.index()], y_elbow),
),
ball(
Bone::LeftForearm,
Bone::LeftHand,
at(pos[Bone::LeftForearm.index()], y_wrist),
),
ball(Bone::Torso, Bone::RightUpperArm, shoulder_r),
hinge(
Bone::RightUpperArm,
Bone::RightForearm,
at(pos[Bone::RightUpperArm.index()], y_elbow),
),
ball(
Bone::RightForearm,
Bone::RightHand,
at(pos[Bone::RightForearm.index()], y_wrist),
),
ball(Bone::Pelvis, Bone::LeftThigh, hip_l),
hinge(
Bone::LeftThigh,
Bone::LeftShin,
at(pos[Bone::LeftThigh.index()], y_knee),
),
ball(
Bone::LeftShin,
Bone::LeftFoot,
at(pos[Bone::LeftShin.index()], y_ankle),
),
ball(Bone::Pelvis, Bone::RightThigh, hip_r),
hinge(
Bone::RightThigh,
Bone::RightShin,
at(pos[Bone::RightThigh.index()], y_knee),
),
ball(
Bone::RightShin,
Bone::RightFoot,
at(pos[Bone::RightShin.index()], y_ankle),
),
];
for (slot, joint) in joints.iter_mut().zip(table) {
*slot = world.add_joint(joint);
}
RagdollHandle { bones, joints }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::solver::SolverConfig;
#[cfg(not(feature = "std"))]
use alloc::collections::BTreeSet;
#[cfg(feature = "std")]
use std::collections::BTreeSet;
fn world() -> PhysicsWorld {
PhysicsWorld::new(SolverConfig::default())
}
#[test]
fn human_male_totals_add_up_to_one() {
let p = RagdollProportions::human_male();
let sum = p.mass_pelvis
+ p.mass_torso
+ p.mass_head
+ Fix128::from_int(2) * (p.mass_upper_arm + p.mass_forearm + p.mass_hand)
+ Fix128::from_int(2) * (p.mass_thigh + p.mass_shin + p.mass_foot);
let diff = sum - Fix128::ONE;
let magnitude = if diff < Fix128::ZERO { -diff } else { diff };
assert!(magnitude < Fix128::from_ratio(1, 1_000_000_000));
}
#[test]
fn build_registers_15_bones() {
let mut w = world();
let handle = RagdollBuilder::build(
&mut w,
RagdollProportions::human_male(),
Vec3Fix::new(Fix128::ZERO, Fix128::from_int(1), Fix128::ZERO),
);
let mut seen = BTreeSet::new();
for &b in &handle.bones {
assert!(seen.insert(b), "duplicate bone index {b}");
}
}
#[test]
fn build_registers_14_joints() {
let mut w = world();
let handle = RagdollBuilder::build(&mut w, RagdollProportions::human_male(), Vec3Fix::ZERO);
let mut seen = BTreeSet::new();
for &j in &handle.joints {
assert!(seen.insert(j), "duplicate joint index {j}");
}
}
#[test]
fn bone_helper_returns_correct_index() {
let mut w = world();
let handle = RagdollBuilder::build(&mut w, RagdollProportions::human_male(), Vec3Fix::ZERO);
assert_eq!(handle.body(Bone::Pelvis), handle.bones[0]);
assert_eq!(handle.body(Bone::RightFoot), handle.bones[14]);
}
#[test]
fn female_preset_reduces_mass() {
let male = RagdollProportions::human_male();
let female = RagdollProportions::human_female();
assert!(female.mass_kg < male.mass_kg);
assert!(female.height_m < male.height_m);
}
#[test]
fn child_preset_smaller_than_adult() {
let adult = RagdollProportions::human_male();
let child = RagdollProportions::child();
assert!(child.mass_kg < adult.mass_kg);
assert!(child.height_m < adult.height_m);
}
#[test]
fn pelvis_placed_at_requested_position() {
let mut w = world();
let target = Vec3Fix::new(
Fix128::from_int(3),
Fix128::from_int(2),
Fix128::from_int(1),
);
let handle = RagdollBuilder::build(&mut w, RagdollProportions::human_male(), target);
let pelvis_idx = handle.body(Bone::Pelvis);
let body = w.bodies[pelvis_idx];
assert_eq!(body.position, target);
}
}