Body step 2: the player is a muscle-driven ragdoll (Project TX)

PlayerRagdoll: 9 bodies, spine/neck ball joints, shoulder drives (girdle + muscle),
elbow hinges, torque-capped motor PDs toward the True Body, chest->hand guides,
implicit balance motor, mass-scaled ground control. PlayerRig: XR driver with
stick move / snap turn and Bonelab-style push-back (eye->camera sphere sweep).
Replaces the chase bodies in main.tscn; RagdollSpike regression (14 phases).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FoxyzandClaude Opus 5.5 committed 2026-10-02 06:43:52 +03:00
1 parent d0b50fbc41
commit d83273dd2f
16 files changed
+1487 -28

No files matched your search

+25 -1
View File
@@ -26,8 +26,13 @@ public sealed class TrueBodySolver
public const float WristOffset = 0.06f;
public Vector3 Eye, NeckPivot, NeckBase, UpperChest, Pelvis;
public Vector3 PelvisTarget; // un-lagged pelvis (the ragdoll's ground control adds its own lag)
public Basis ChestBasis = Basis.Identity;
public Basis PelvisBasis = Basis.Identity;
public Basis YawBasis = Basis.Identity;
public Basis HeadBasis = Basis.Identity;
public float Lean { get; private set; }
public readonly Vector3[] Pole = new Vector3[2]; // per arm, see ArmSolver.Pole
public readonly Vector3[] Shoulder = new Vector3[2];
public readonly Vector3[] Elbow = new Vector3[2];
public readonly Vector3[] Wrist = new Vector3[2];
@@ -40,6 +45,18 @@ public sealed class TrueBodySolver
private Vector3 _pelvisVelocity;
private float _yaw;
// A discrete move of the whole player (teleport, snap turn): carry the lagging state along
// instead of letting it swing across the gap.
public void Shift(Transform3D delta)
{
if (!Initialized)
return;
Pelvis = delta * Pelvis;
_pelvisVelocity = delta.Basis * _pelvisVelocity;
Vector3 f = delta.Basis * new Basis(Vector3.Up, _yaw) * Vector3.Forward;
_yaw = Mathf.Atan2(-f.X, -f.Z);
}
public void Reset()
{
Initialized = false;
@@ -52,6 +69,7 @@ public sealed class TrueBodySolver
{
Eye = head.Origin;
Basis headBasis = head.Basis.Orthonormalized();
HeadBasis = headBasis;
NeckPivot = Eye + headBasis * EyeToNeckPivot;
WristTarget[0] = left * new Vector3(0f, 0f, WristOffset);
WristTarget[1] = right * new Vector3(0f, 0f, WristOffset);
@@ -73,6 +91,7 @@ public sealed class TrueBodySolver
_yaw = hmdYaw;
_yaw = Mathf.LerpAngle(_yaw, targetYaw, 1f - Mathf.Exp(-dt * YawSmoothRate));
Basis yawBasis = new Basis(Vector3.Up, _yaw);
YawBasis = yawBasis;
// Crouch lean: measured at the NECK PIVOT (nodding moves the eyes ~10 cm but not the pivot),
// below its standing height the chest pitches forward; looking down adds a little.
@@ -81,6 +100,7 @@ public sealed class TrueBodySolver
Crouch = Mathf.Clamp((standingPivot - pivotAboveFloor) / (CrouchRange * p.EyeHeight), 0f, 1f);
float headPitchDown = Mathf.Max(0f, -Mathf.Asin(Mathf.Clamp(headFwd.Y, -1f, 1f)));
float lean = Crouch * ChestLeanMax + headPitchDown * HeadPitchToChest;
Lean = lean;
ChestBasis = yawBasis * new Basis(Vector3.Right, -lean);
// Neck: from the head pivot down along the average of chest-up and head-up, sized so a
@@ -96,6 +116,7 @@ public sealed class TrueBodySolver
// Pelvis: hangs below the upper chest along the trunk; follows through a critically damped
// spring so it lags quick head motion (hip sway) instead of being rigidly attached.
Vector3 pelvisTarget = UpperChest - ChestBasis.Y * (TrunkRatio * p.Stature);
PelvisTarget = pelvisTarget;
if (!Initialized)
Pelvis = pelvisTarget;
float w = Mathf.Tau * PelvisFrequency;
@@ -138,6 +159,7 @@ public sealed class TrueBodySolver
_arms[side].Solve(Shoulder[side], WristTarget[side], ChestBasis, outward, handBasis, p.UpperArm, p.Forearm, dt);
Elbow[side] = _arms[side].Elbow;
Pole[side] = _arms[side].Pole;
Wrist[side] = _arms[side].Wrist;
}
}
@@ -157,6 +179,8 @@ public partial class TrueBody : Node3D
private readonly System.Collections.Generic.List<MeshInstance3D> _joints = new();
public TrueBodySolver Solver { get; } = new();
// Set by PlayerRig, which solves at the right point of its own tick (after moving the origin).
public bool ExternallySolved;
public bool HasPose => Solver.Initialized;
public override void _Ready()
@@ -178,7 +202,7 @@ public partial class TrueBody : Node3D
public override void _PhysicsProcess(double delta)
{
BodyProfile profile = BodyProfile.Current;
if (profile == null)
if (profile == null || ExternallySolved)
return;
Solver.Solve(profile, _camera.GlobalTransform, _left.GlobalTransform, _right.GlobalTransform, _origin.GlobalPosition.Y, (float)delta);
}