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

+644
View File
@@ -0,0 +1,644 @@
using Godot;
using System.Collections.Generic;
// Body step 2 — the player IS a muscle-driven ragdoll (Nimso's Project TX, research/nimso-project-tx.md
// §2): every bone drives ITSELF toward the True Body pose through torque-capped joint motors
// (never world-pinned), the hands are additionally pulled to the controllers by a force-capped
// guide that reacts into the chest, and "ground control" holds the pelvis up and carries the body
// after the head. Input-agnostic: a driver (PlayerRig in-game, RagdollSpike headless) solves the
// TrueBodySolver, then calls Step(); the camera stays 1:1 and the driver moves its play space by
// OriginCorrection() when the body is blocked (push-back) or stands on higher/lower ground.
//
// Bodies (boxes, origin = centre of mass): pelvis (+ the not-yet-physical legs' mass), chest,
// head (incl. neck), and per arm upper arm, forearm, hand. No light intermediate links: a 0.6 kg
// neck or 0.8 kg clavicle squeezed between two strong motors oscillates under Box3D's
// one-iteration-per-substep solver (measured: RagdollSpike, 2026-10-02).
// Joints (fork Generic6DOFJoint3D mapping, research/godot-box3d-6dof.md):
// spine, neck, wrist = BALL (cone/twist limits), spine/neck with muscle motors;
// shoulder = DRIVE chest→upper arm: linear motor holds the humeral head on the (moving) girdle
// point, angular motor = the shoulder muscle (no cone limit — Box3D's motor joint has none);
// elbow = HingeJoint3D (0..150°) with a muscle motor;
// hand guide = DRIVE chest→hand; balance = DRIVE world anchor→pelvis (angular only).
// All motors are PDs written as target velocities each tick; the force/torque limits are the strength.
public partial class PlayerRagdoll : Node3D
{
public const float BodyMass = 60f; // Nimso's character is 60 kg
// Muscles: PD gain (1/s), max commanded spin (rad/s), torque cap (N·m).
public const float SpineKp = 15f, SpineTorque = 400f;
public const float NeckKp = 20f, NeckTorque = 60f;
public const float ShoulderKp = 20f, ShoulderTorque = 45f; // ~human shoulder flexion strength
public const float ElbowKp = 20f, ElbowTorque = 45f;
public const float MuscleMaxSpin = 15f;
public const float GirdleKp = 30f, GirdleForce = 2000f; // shoulder drive, linear part
// Hand guide (chest→hand drive): pulls the hand onto the controller; capped so the arm,
// not the guide, carries real loads.
public const float HandGuideKp = 40f, HandGuideForce = 40f, HandGuideMaxSpeed = 10f;
public const float HandGuideAngularKp = 40f, HandGuideTorque = 25f;
// Ground control (world-space support on the pelvis, only while grounded).
public const float GroundFrequency = 4f; // Hz
public const float GroundDamping = 1f;
public const float LegStrength = 2f; // max support, × body weight
public const float GroundFriction = 1f; // horizontal force ≤ this × support
public const float GroundProbeExtra = 0.3f; // ground counts while within hip height + this
// Balance: an implicit torque-capped motor from a world anchor to the pelvis (explicit torques
// let the arms' reaction torques set the whole trunk swinging).
public const float BalanceKp = 10f;
public const float BalanceTorque = 400f;
public const float AirBalanceShare = 0.3f;
// Play-space reconciliation.
public const float CameraRadius = 0.03f; // sphere swept from the head's eye to the camera
public const float FreeSlack = 0.2f; // camera may lead an unobstructed (lagging/held) head by this much
public const float GroundFollowRate = 8f;
public const float ResetDistance = 1.5f; // pelvis this far from its target → snap the whole body to the pose
// Geometry.
public const float LumbarFraction = 0.35f; // lumbar joint height above the hips, × trunk
public const float PelvisLeanShare = 0.5f; // share of the crouch lean taken by the hips
public static readonly Vector3 HeadCenterFromEye = new(0f, 0.02f, 0.08f);
public static readonly Vector3 HeadSize = new(0.16f, 0.21f, 0.2f);
public static readonly Vector3 HandSize = new(0.035f, 0.09f, 0.12f);
public const float WristOffset = TrueBodySolver.WristOffset;
public const float ElbowMaxFlex = 2.6f; // rad (~150°)
// Sign of HingeJoint3D's angle/motor for a positive rotation of the child about the joint's
// +Z (measured by RagdollSpike's "hinge sign" phase).
public static float HingeSign = 1f;
// Collision layers (AGENTS.md): 3 body, 4 hands, 5 arms.
private const uint BodyLayer = 0b100, HandLayer = 0b1000, ArmLayer = 0b10000;
private const uint WorldMask = 0b1;
private sealed class Ball
{
public Generic6DofJoint3D Joint;
public RigidBody3D Parent, Child;
public Quaternion FrameA, FrameB; // joint frame relative to each body
public float Kp;
public Quaternion PrevTarget;
public bool HasPrev;
}
private sealed class Drive
{
public Generic6DofJoint3D Joint;
public Quaternion FrameA, FrameB;
public Vector3 AnchorA, AnchorB; // anchor point, parent-local / child-local
public Quaternion PrevTarget;
public bool HasPrev;
}
private sealed class Arm
{
public RigidBody3D Upper, Fore, Hand;
public Ball Wrist;
public HingeJoint3D Elbow;
public Drive Shoulder, Guide;
public float UpperLen, ForeLen;
public RigidBody3D Held;
public float PrevFlexTarget = -1f;
}
public RigidBody3D Pelvis { get; private set; }
public RigidBody3D Chest { get; private set; }
public RigidBody3D Head { get; private set; }
public bool Built { get; private set; }
public bool Grounded { get; private set; }
public float GroundY { get; private set; }
public Vector3 Eye => Head.GlobalTransform * _eyeLocal;
public BodyProfile Profile { get; private set; }
public int Snaps { get; private set; } // safety resets (NaN / too far) — should stay 0
private readonly Arm[] _arms = { new(), new() };
private readonly List<RigidBody3D> _bodies = new();
private Ball _spine, _neck;
private Vector3 _neckBaseToEye; // head frame, canonical (neck straight)
private float _trunk, _lumbar, _neckLen;
private Vector3 _eyeLocal;
private Vector3 _pelvisCenterLocal, _chestCenterLocal, _headCenterLocal; // from each body's proximal joint, in its basis
private readonly Dictionary<RigidBody3D, Transform3D> _targets = new();
private readonly Vector3[] _girdle = new Vector3[2]; // True Body shoulder offset, chest frame
private Vector3 _pelvisTargetPrev;
private bool _havePelvisPrev;
public RigidBody3D Hand(int side) => Built ? _arms[side].Hand : null;
public RigidBody3D UpperArm(int side) => Built ? _arms[side].Upper : null;
public RigidBody3D Forearm(int side) => Built ? _arms[side].Fore : null;
public IReadOnlyList<RigidBody3D> Bodies => _bodies;
// ---------------------------------------------------------------- build
public void Build(BodyProfile p, TrueBodySolver pose, Transform3D left, Transform3D right)
{
Clear();
Profile = p;
_trunk = TrueBodySolver.TrunkRatio * p.Stature;
_lumbar = LumbarFraction * _trunk;
_neckLen = Mathf.Max(0.05f, p.ShoulderDrop + TrueBodySolver.EyeToNeckPivot.Y - TrueBodySolver.NeckBaseAboveShoulders);
float m = BodyMass;
float s = p.Stature / 1.75f;
// Canonical pose (standing, level head, arms straight down-forward-out) in frame C at the
// current pelvis: joint cones/twists are centred on it and the elbow hinge's zero is a
// straight arm. The body is then moved onto the live pose.
Transform3D c = new(pose.YawBasis, pose.PelvisTarget);
Vector3 hip = Vector3.Zero;
Vector3 lumbar = new(0f, _lumbar, 0f);
Vector3 upperChest = new(0f, _trunk, 0f);
Vector3 neckBase = upperChest + new Vector3(0f, TrueBodySolver.NeckBaseAboveShoulders,
-(TrueBodySolver.ShouldersBehindEyes - TrueBodySolver.EyeToNeckPivot.Z));
Vector3 neckPivot = neckBase + new Vector3(0f, _neckLen, 0f);
_eyeLocal = -TrueBodySolver.EyeToNeckPivot; // head body origin is set to its centre below
Vector3 headCenterFromPivot = _eyeLocal + HeadCenterFromEye;
Vector3 pelvisSize = new(0.17f * p.Stature, _lumbar + 0.06f, 0.18f * s);
_pelvisCenterLocal = new Vector3(0f, _lumbar * 0.5f - 0.03f, 0f);
float chestHeight = _trunk - _lumbar + TrueBodySolver.NeckBaseAboveShoulders;
Vector3 chestSize = new(2f * p.ShoulderHalfWidth * 0.75f, chestHeight, 0.2f * s);
_chestCenterLocal = new Vector3(0f, chestHeight * 0.5f, 0f);
_headCenterLocal = headCenterFromPivot;
_eyeLocal -= headCenterFromPivot; // eye relative to the head body's centre
Color skin = new(0.85f, 0.72f, 0.6f), shirt = new(0.3f, 0.45f, 0.7f), pants = new(0.25f, 0.27f, 0.32f);
Pelvis = MakeBody("Pelvis", new BoxShape3D { Size = pelvisSize }, (0.142f + 0.139f + 0.322f) * m, BodyLayer, WorldMask, pants,
c * new Transform3D(Basis.Identity, hip + _pelvisCenterLocal));
Chest = MakeBody("Chest", new BoxShape3D { Size = chestSize }, 0.216f * m, BodyLayer, WorldMask, shirt,
c * new Transform3D(Basis.Identity, lumbar + _chestCenterLocal));
Head = MakeBody("Head", new BoxShape3D { Size = HeadSize * s }, 0.081f * m, BodyLayer, WorldMask, skin,
c * new Transform3D(Basis.Identity, neckPivot + headCenterFromPivot));
// The neck: a box hanging under the head, same body.
Vector3 neckCenter = (neckBase + neckPivot) * 0.5f - (neckPivot + headCenterFromPivot);
Head.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(0.09f * s, _neckLen, 0.09f * s) }, Position = neckCenter });
MeshInstance3D neckMesh = new() { Mesh = new BoxMesh { Size = new Vector3(0.09f * s, _neckLen, 0.09f * s) }, Position = neckCenter };
neckMesh.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = skin, Roughness = 0.6f });
Head.AddChild(neckMesh);
_neckBaseToEye = neckPivot - TrueBodySolver.EyeToNeckPivot - neckBase;
// World anchor for the balance motor (no shape, linear motor force 0 = position free).
StaticBody3D anchor = new() { Name = "BalanceAnchor", CollisionLayer = 0, CollisionMask = 0 };
AddChild(anchor);
_balance = MakeDrive(anchor, Pelvis, Pelvis.GlobalTransform, 0f, BalanceTorque);
_balanceCap = BalanceTorque;
_spine = MakeBall(Pelvis, Chest, c * lumbar, c.Basis * TwistAlong(Vector3.Up), Mathf.DegToRad(60f), Mathf.DegToRad(35f), SpineKp, SpineTorque);
_neck = MakeBall(Chest, Head, c * neckBase, c.Basis * TwistAlong(Vector3.Up), Mathf.DegToRad(75f), Mathf.DegToRad(85f), NeckKp, NeckTorque);
for (int side = 0; side < 2; side++)
{
Arm a = _arms[side];
float sign = side == 0 ? -1f : 1f;
a.UpperLen = p.UpperArm;
a.ForeLen = p.Forearm;
Vector3 shoulder = upperChest + new Vector3(sign * p.ShoulderHalfWidth, 0f, 0f);
Vector3 dir = new Vector3(sign * 0.42f, -0.74f, -0.47f).Normalized();
Vector3 reference = Vector3.Down * 2f + Vector3.Back * 0.8f + new Vector3(sign * 0.3f, 0f, 0f);
Vector3 pole = (reference - dir * reference.Dot(dir)).Normalized();
Vector3 hinge = pole.Cross(dir).Normalized();
Basis bone = BoneBasis(dir, hinge);
Vector3 elbow = shoulder + dir * a.UpperLen;
Vector3 wrist = elbow + dir * a.ForeLen;
Vector3 handX = (Vector3.Right - dir * dir.X).Normalized();
Basis handBasis = new(handX, (-dir).Cross(handX), -dir);
Vector3 handOrigin = wrist + dir * WristOffset;
string n = side == 0 ? "Left" : "Right";
a.Upper = MakeBody(n + "UpperArm", new BoxShape3D { Size = new Vector3(0.085f * s, a.UpperLen, 0.085f * s) }, 0.028f * m, ArmLayer, WorldMask, shirt,
c * new Transform3D(bone, shoulder + dir * (a.UpperLen * 0.5f)));
a.Fore = MakeBody(n + "Forearm", new BoxShape3D { Size = new Vector3(0.07f * s, a.ForeLen - 0.01f, 0.07f * s) }, 0.016f * m, ArmLayer, WorldMask, skin,
c * new Transform3D(bone, elbow + dir * (a.ForeLen * 0.5f)));
a.Hand = MakeBody(n + "Hand", new BoxShape3D { Size = HandSize }, 0.006f * m, HandLayer, WorldMask | HandLayer,
side == 0 ? new Color(0.9f, 0.3f, 0.35f) : new Color(0.3f, 0.5f, 0.9f), c * new Transform3D(handBasis, handOrigin));
a.Hand.ContinuousCd = true;
a.Hand.ContactMonitor = true;
a.Hand.MaxContactsReported = 8;
a.Shoulder = MakeDrive(Chest, a.Upper, c * new Transform3D(Basis.Identity, shoulder), GirdleForce, ShoulderTorque);
a.Elbow = MakeHinge(a.Upper, a.Fore, c * elbow, c.Basis * new Basis(dir, hinge.Cross(dir), hinge));
a.Wrist = MakeBall(a.Fore, a.Hand, c * wrist, c.Basis * TwistAlong(dir), Mathf.DegToRad(80f), Mathf.DegToRad(90f), 0f, 0f);
a.Guide = MakeDrive(Chest, a.Hand, c * new Transform3D(handBasis, handOrigin), HandGuideForce, HandGuideTorque);
}
Built = true;
_havePelvisPrev = false;
SnapToPose(pose, left, right);
GD.Print($"PlayerRagdoll: built for stature {p.Stature:F2} m (arm {p.UpperArm:F3}+{p.Forearm:F3} m, {_bodies.Count} bodies)");
}
public void Clear()
{
foreach (Node child in GetChildren())
child.QueueFree();
foreach (Node child in GetChildren())
RemoveChild(child);
_bodies.Clear();
_targets.Clear();
for (int i = 0; i < 2; i++)
_arms[i] = new Arm();
Built = false;
}
// Twist axis (joint X) along `dir`.
private static Basis TwistAlong(Vector3 dir)
{
Vector3 helper = Mathf.Abs(dir.Y) < 0.9f ? Vector3.Up : Vector3.Right;
Vector3 z = dir.Cross(helper).Normalized();
return new Basis(dir, z.Cross(dir), z);
}
// Segment basis: Y along the bone, X = elbow hinge axis.
private static Basis BoneBasis(Vector3 boneDir, Vector3 hinge)
{
Vector3 x = (hinge - boneDir * hinge.Dot(boneDir)).Normalized();
return new Basis(x, boneDir, x.Cross(boneDir));
}
private RigidBody3D MakeBody(string name, Shape3D shape, float mass, uint layer, uint mask, Color color, Transform3D at)
{
RigidBody3D body = new() { Name = name, Mass = mass, CollisionLayer = layer, CollisionMask = mask, Transform = at };
body.AddChild(new CollisionShape3D { Shape = shape });
if (shape is BoxShape3D box && layer != 0)
{
MeshInstance3D mesh = new() { Mesh = new BoxMesh { Size = box.Size } };
mesh.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = color, Roughness = 0.6f });
body.AddChild(mesh);
}
AddChild(body);
_bodies.Add(body);
return body;
}
private static Quaternion Rot(Node3D n) => n.GlobalTransform.Basis.Orthonormalized().GetRotationQuaternion();
// Configure BEFORE AddChild (the node pushes everything to the server on enter-tree).
private Ball MakeBall(RigidBody3D parent, RigidBody3D child, Vector3 at, Basis frame, float cone, float twist, float kp, float torque)
{
Generic6DofJoint3D j = new() { Transform = new Transform3D(frame, at), NodeA = parent.GetPath(), NodeB = child.GetPath(), ExcludeNodesFromCollision = true };
j.SetParamX(Generic6DofJoint3D.Param.AngularLowerLimit, -twist);
j.SetParamX(Generic6DofJoint3D.Param.AngularUpperLimit, twist);
foreach (var set in new System.Action<Generic6DofJoint3D.Param, float>[] { j.SetParamY, j.SetParamZ })
{
set(Generic6DofJoint3D.Param.AngularLowerLimit, -cone);
set(Generic6DofJoint3D.Param.AngularUpperLimit, cone);
}
if (torque > 0f)
{
j.SetFlagX(Generic6DofJoint3D.Flag.EnableMotor, true);
j.SetFlagY(Generic6DofJoint3D.Flag.EnableMotor, true);
j.SetFlagZ(Generic6DofJoint3D.Flag.EnableMotor, true);
j.SetParamX(Generic6DofJoint3D.Param.AngularMotorForceLimit, torque);
j.SetParamY(Generic6DofJoint3D.Param.AngularMotorForceLimit, torque);
j.SetParamZ(Generic6DofJoint3D.Param.AngularMotorForceLimit, torque);
}
AddChild(j);
Quaternion jq = frame.Orthonormalized().GetRotationQuaternion();
return new Ball { Joint = j, Parent = parent, Child = child, FrameA = Rot(parent).Inverse() * jq, FrameB = Rot(child).Inverse() * jq, Kp = kp };
}
private Drive MakeDrive(PhysicsBody3D parent, RigidBody3D child, Transform3D at, float force, float torque)
{
Generic6DofJoint3D j = new() { Transform = at, NodeA = parent.GetPath(), NodeB = child.GetPath(), ExcludeNodesFromCollision = true };
foreach (var flag in new System.Action<Generic6DofJoint3D.Flag, bool>[] { j.SetFlagX, j.SetFlagY, j.SetFlagZ })
{
flag(Generic6DofJoint3D.Flag.EnableLinearLimit, false);
flag(Generic6DofJoint3D.Flag.EnableAngularLimit, false);
flag(Generic6DofJoint3D.Flag.EnableLinearMotor, true);
flag(Generic6DofJoint3D.Flag.EnableMotor, true);
}
foreach (var param in new System.Action<Generic6DofJoint3D.Param, float>[] { j.SetParamX, j.SetParamY, j.SetParamZ })
{
param(Generic6DofJoint3D.Param.LinearMotorForceLimit, force);
param(Generic6DofJoint3D.Param.AngularMotorForceLimit, torque);
}
AddChild(j);
Quaternion jq = at.Basis.Orthonormalized().GetRotationQuaternion();
return new Drive
{
Joint = j,
FrameA = Rot(parent).Inverse() * jq,
FrameB = Rot(child).Inverse() * jq,
AnchorA = parent.GlobalTransform.AffineInverse() * at.Origin,
AnchorB = child.GlobalTransform.AffineInverse() * at.Origin,
};
}
private HingeJoint3D MakeHinge(RigidBody3D parent, RigidBody3D child, Vector3 at, Basis frame)
{
HingeJoint3D j = new() { Transform = new Transform3D(frame, at), NodeA = parent.GetPath(), NodeB = child.GetPath(), ExcludeNodesFromCollision = true };
float lo = HingeSign > 0f ? 0f : -ElbowMaxFlex;
float hi = HingeSign > 0f ? ElbowMaxFlex : 0f;
j.SetFlag(HingeJoint3D.Flag.UseLimit, true);
j.SetParam(HingeJoint3D.Param.LimitLower, lo);
j.SetParam(HingeJoint3D.Param.LimitUpper, hi);
j.SetFlag(HingeJoint3D.Flag.EnableMotor, true);
j.SetParam(HingeJoint3D.Param.MotorMaxImpulse, ElbowTorque);
AddChild(j);
return j;
}
// ---------------------------------------------------------------- pose targets
// World transform every body should have in the True Body pose. The trunk is rebuilt top-down
// through the physical chain (upper chest → lumbar → hips) so a leaning chest doesn't drag
// the head off the camera; the hips take part of the crouch lean (hip hinge).
private void ComputeTargets(TrueBodySolver s, Transform3D left, Transform3D right)
{
Basis chest = s.ChestBasis;
Basis pelvis = s.YawBasis * new Basis(Vector3.Right, -s.Lean * PelvisLeanShare);
Vector3 lumbar = s.UpperChest - chest.Y * (_trunk - _lumbar);
Vector3 hip = lumbar - pelvis.Y * _lumbar;
_targets[Pelvis] = new Transform3D(pelvis, hip + pelvis * _pelvisCenterLocal);
_targets[Chest] = new Transform3D(chest, lumbar + chest * _chestCenterLocal);
// Head + neck pivot at the neck base: orient like the camera, tilted by the small extra
// rotation that puts the body's eye on the camera (the True Body bends the neck at two points).
Vector3 eyeDir = s.HeadBasis * _neckBaseToEye;
Vector3 wantDir = s.Eye - s.NeckBase;
Quaternion tilt = eyeDir.LengthSquared() > 1e-8f && wantDir.LengthSquared() > 1e-8f
? new Quaternion(eyeDir.Normalized(), wantDir.Normalized()) : Quaternion.Identity;
Basis head = new Basis(tilt) * s.HeadBasis;
_targets[Head] = new Transform3D(head, s.NeckBase + head * (_neckBaseToEye - _eyeLocal));
for (int side = 0; side < 2; side++)
{
Arm a = _arms[side];
Vector3 sh = s.Shoulder[side], el = s.Elbow[side], wr = s.Wrist[side];
Vector3 armDir = wr - sh;
armDir = armDir.LengthSquared() > 1e-8f ? armDir.Normalized() : chest * Vector3.Down;
Vector3 hinge = s.Pole[side].Cross(armDir);
hinge = hinge.LengthSquared() > 1e-8f ? hinge.Normalized() : chest.X;
Vector3 up = (el - sh).Normalized();
Vector3 fore = (wr - el).Normalized();
_girdle[side] = chest.Transposed() * (sh - (s.UpperChest + chest.X * ((side == 0 ? -1f : 1f) * Profile.ShoulderHalfWidth)));
_targets[a.Upper] = new Transform3D(BoneBasis(up, hinge), sh + up * (a.UpperLen * 0.5f));
_targets[a.Fore] = new Transform3D(BoneBasis(fore, hinge), el + fore * (a.ForeLen * 0.5f));
_targets[a.Hand] = (side == 0 ? left : right).Orthonormalized();
}
}
public Transform3D Target(RigidBody3D body) => _targets.TryGetValue(body, out Transform3D t) ? t : body.GlobalTransform;
// ---------------------------------------------------------------- per tick
public void Step(TrueBodySolver s, Transform3D left, Transform3D right, Vector3 leftVelocity, Vector3 rightVelocity, float floorY, float dt)
{
if (!Built)
return;
ComputeTargets(s, left, right);
foreach (RigidBody3D b in _bodies)
{
if (BodyMath.ResetIfNonFinite(b, Target(b), "PlayerRagdoll"))
{
Snaps++;
SnapToPose(s, left, right);
return;
}
}
Transform3D pelvisTarget = _targets[Pelvis];
if (Pelvis.GlobalPosition.DistanceTo(pelvisTarget.Origin) > ResetDistance)
{
GD.Print("PlayerRagdoll: body too far from the pose, snapped to it");
Snaps++;
SnapToPose(s, left, right);
return;
}
Muscle(_spine, _targets[Pelvis].Basis, _targets[Chest].Basis, dt);
Muscle(_neck, _targets[Chest].Basis, _targets[Head].Basis, dt);
for (int side = 0; side < 2; side++)
{
Arm a = _arms[side];
ShoulderDrive(a, side, dt);
ElbowMuscle(a, dt);
HandGuide(a, _targets[a.Hand], side == 0 ? leftVelocity : rightVelocity);
}
GroundControl(pelvisTarget, floorY, dt);
}
// Muscle = PD on the joint's relative rotation toward the relative rotation of the two TARGET
// bases (a local pose, not a world pin), written as the motor's target velocity in frame A.
// Feed-forward = the target's own relative spin, so a moving pose isn't chased with a lag of
// speed/Kp.
private static void Muscle(Ball b, Basis parentTarget, Basis childTarget, float dt)
{
Quaternion current = (Rot(b.Parent) * b.FrameA).Inverse() * (Rot(b.Child) * b.FrameB);
Quaternion target = (parentTarget.Orthonormalized().GetRotationQuaternion() * b.FrameA).Inverse()
* (childTarget.Orthonormalized().GetRotationQuaternion() * b.FrameB);
Vector3 ff = b.HasPrev && dt > 0f ? (BodyMath.RotationError(target, b.PrevTarget) / dt).LimitLength(MuscleMaxSpin) : Vector3.Zero;
b.PrevTarget = target;
b.HasPrev = true;
Vector3 w = (ff + BodyMath.RotationError(target, current) * b.Kp).LimitLength(MuscleMaxSpin);
SetAngularVelocity(b.Joint, w);
}
private static void SetAngularVelocity(Generic6DofJoint3D j, Vector3 w)
{
j.SetParamX(Generic6DofJoint3D.Param.AngularMotorTargetVelocity, w.X);
j.SetParamY(Generic6DofJoint3D.Param.AngularMotorTargetVelocity, w.Y);
j.SetParamZ(Generic6DofJoint3D.Param.AngularMotorTargetVelocity, w.Z);
}
private static void SetLinearVelocity(Generic6DofJoint3D j, Vector3 v)
{
j.SetParamX(Generic6DofJoint3D.Param.LinearMotorTargetVelocity, v.X);
j.SetParamY(Generic6DofJoint3D.Param.LinearMotorTargetVelocity, v.Y);
j.SetParamZ(Generic6DofJoint3D.Param.LinearMotorTargetVelocity, v.Z);
}
// Elbow flexion (0 = straight) about the upper arm's hinge axis.
public float ElbowFlex(int side)
{
Arm a = _arms[side];
Basis u = a.Upper.GlobalTransform.Basis.Orthonormalized();
Vector3 f = a.Fore.GlobalTransform.Basis.Orthonormalized().Y;
return Mathf.Atan2(u.X.Dot(u.Y.Cross(f)), u.Y.Dot(f));
}
private void ElbowMuscle(Arm a, float dt)
{
int side = a == _arms[0] ? 0 : 1;
Vector3 up = _targets[a.Upper].Basis.Y, fore = _targets[a.Fore].Basis.Y;
float target = Mathf.Acos(Mathf.Clamp(up.Dot(fore), -1f, 1f));
float ff = a.PrevFlexTarget >= 0f && dt > 0f ? Mathf.Clamp((target - a.PrevFlexTarget) / dt, -MuscleMaxSpin, MuscleMaxSpin) : 0f;
a.PrevFlexTarget = target;
float w = Mathf.Clamp(ff + (target - ElbowFlex(side)) * ElbowKp, -MuscleMaxSpin, MuscleMaxSpin);
a.Elbow.SetParam(HingeJoint3D.Param.MotorTargetVelocity, HingeSign * w);
}
// Shoulder = one drive: its linear motor keeps the humeral head on the chest's shoulder point
// shifted by the True Body girdle offset (shrug/protraction), its angular motor is the
// shoulder muscle (same PD + feed-forward as Muscle, parent = chest).
private void ShoulderDrive(Arm a, int side, float dt)
{
Drive d = a.Shoulder;
Transform3D chest = Chest.GlobalTransform.Orthonormalized();
Vector3 desired = chest * (d.AnchorA + _girdle[side]);
Vector3 err = desired - a.Upper.GlobalTransform * d.AnchorB;
Quaternion frameA = chest.Basis.GetRotationQuaternion() * d.FrameA;
SetLinearVelocity(d.Joint, frameA.Inverse() * (err * GirdleKp));
Quaternion current = frameA.Inverse() * (Rot(a.Upper) * d.FrameB);
Quaternion target = (_targets[Chest].Basis.Orthonormalized().GetRotationQuaternion() * d.FrameA).Inverse()
* (_targets[a.Upper].Basis.Orthonormalized().GetRotationQuaternion() * d.FrameB);
Vector3 ff = d.HasPrev && dt > 0f ? (BodyMath.RotationError(target, d.PrevTarget) / dt).LimitLength(MuscleMaxSpin) : Vector3.Zero;
d.PrevTarget = target;
d.HasPrev = true;
SetAngularVelocity(d.Joint, (ff + BodyMath.RotationError(target, current) * ShoulderKp).LimitLength(MuscleMaxSpin));
}
// Force-capped pull of the hand onto the controller, reacting into the chest (arm muscles).
private void HandGuide(Arm a, Transform3D target, Vector3 targetVelocity)
{
Transform3D chest = Chest.GlobalTransform;
Vector3 anchor = chest * a.Guide.AnchorA;
Vector3 anchorVelocity = Chest.LinearVelocity + Chest.AngularVelocity.Cross(anchor - Chest.GlobalPosition);
Vector3 desired = (targetVelocity + (target.Origin - a.Hand.GlobalPosition) * HandGuideKp).LimitLength(HandGuideMaxSpeed);
Quaternion frameA = Rot(Chest) * a.Guide.FrameA;
SetLinearVelocity(a.Guide.Joint, frameA.Inverse() * (desired - anchorVelocity));
// Angular: hand rotation onto the controller rotation, as a relative spin vs the chest.
Vector3 err = BodyMath.RotationError(target.Basis.GetRotationQuaternion(), Rot(a.Hand));
Vector3 wWorld = (err * HandGuideAngularKp).LimitLength(MuscleMaxSpin * 2f) - Chest.AngularVelocity;
SetAngularVelocity(a.Guide.Joint, frameA.Inverse() * wWorld);
}
// "Leg strength": a stiff, mass-scaled support holding the pelvis at its pose height above the
// ground and carrying it after the head horizontally (friction-limited), plus an upright/yaw
// balance motor (world anchor → pelvis). No ground → no support (falls); balance weakens in the air.
private void GroundControl(Transform3D target, float floorY, float dt)
{
Vector3 pos = Pelvis.GlobalPosition;
float hipAboveFloor = target.Origin.Y - floorY;
var query = PhysicsRayQueryParameters3D.Create(pos, pos + Vector3.Down * (hipAboveFloor + GroundProbeExtra + 0.5f), WorldMask);
Godot.Collections.Dictionary hit = GetWorld3D().DirectSpaceState.IntersectRay(query);
Grounded = false;
if (hit.Count > 0)
{
Vector3 point = (Vector3)hit["position"];
Vector3 normal = (Vector3)hit["normal"];
if (pos.Y - point.Y < hipAboveFloor + GroundProbeExtra && normal.Y > 0.6f)
{
Grounded = true;
GroundY = point.Y;
}
}
Vector3 targetVelocity = _havePelvisPrev && dt > 0f ? (target.Origin - _pelvisTargetPrev) / dt : Vector3.Zero;
_pelvisTargetPrev = target.Origin;
_havePelvisPrev = true;
if (targetVelocity.Length() > 10f)
targetVelocity = Vector3.Zero;
float weight = BodyMass * 9.8f;
if (Grounded)
{
// Pose heights are relative to the tracking floor; stand on the ground actually under us.
Vector3 goal = target.Origin + Vector3.Up * (GroundY - floorY);
float w = Mathf.Tau * GroundFrequency;
Vector3 accel = (goal - pos) * (w * w) + (targetVelocity - Pelvis.LinearVelocity) * (2f * GroundDamping * w);
Vector3 force = accel * BodyMass;
float support = Mathf.Clamp(force.Y + weight, 0f, LegStrength * weight);
Vector3 horizontal = new Vector3(force.X, 0f, force.Z).LimitLength(GroundFriction * support);
Pelvis.ApplyCentralForce(horizontal + Vector3.Up * support);
}
Vector3 rotErr = BodyMath.RotationError(target.Basis.GetRotationQuaternion(), Rot(Pelvis));
Vector3 spin = (rotErr * BalanceKp).LimitLength(MuscleMaxSpin);
SetAngularVelocity(_balance.Joint, _balance.FrameA.Inverse() * spin); // anchor = world frame
float cap = BalanceTorque * (Grounded ? 1f : AirBalanceShare);
if (cap != _balanceCap)
{
_balanceCap = cap;
_balance.Joint.SetParamX(Generic6DofJoint3D.Param.AngularMotorForceLimit, cap);
_balance.Joint.SetParamY(Generic6DofJoint3D.Param.AngularMotorForceLimit, cap);
_balance.Joint.SetParamZ(Generic6DofJoint3D.Param.AngularMotorForceLimit, cap);
}
}
private Drive _balance;
private float _balanceCap;
// ---------------------------------------------------------------- play space
// How the driver should move its play-space origin after the last physics step (Bonelab-style
// push-back): a sphere swept from the head body's eye to the camera — if geometry is in the way
// the camera is somewhere the head can't follow (a wall, a bar, a table it walked into), so the
// view is put back at the last free point. With nothing in between, the head may lag or be
// held up to FreeSlack (leaning over a table with the hips blocked) before the view follows.
// Plus: the floor follows the ground under the body; airborne, the view rides the body (falls).
public Vector3 OriginCorrection(Vector3 camera, float floorY, float dt)
{
if (!Built)
return Vector3.Zero;
Vector3 eye = Eye;
Vector3 lead = camera - eye;
Vector3 correction = Vector3.Zero;
if (lead.LengthSquared() > 1e-8f)
{
_cameraQuery ??= new PhysicsShapeQueryParameters3D { Shape = new SphereShape3D { Radius = CameraRadius }, CollisionMask = WorldMask };
_cameraQuery.Transform = new Transform3D(Basis.Identity, eye);
_cameraQuery.Motion = lead;
float[] fractions = GetWorld3D().DirectSpaceState.CastMotion(_cameraQuery);
float safe = fractions.Length > 0 ? fractions[0] : 1f;
if (safe < 1f)
correction = -lead * (1f - safe);
else if (lead.Length() > FreeSlack)
correction = -lead * (1f - FreeSlack / lead.Length());
}
if (Grounded)
{
correction.Y = 0f;
correction.Y += (GroundY - floorY) * (1f - Mathf.Exp(-dt * GroundFollowRate));
}
return correction;
}
private PhysicsShapeQueryParameters3D _cameraQuery;
public void TeleportBy(Transform3D delta)
{
if (!Built)
return;
foreach (RigidBody3D b in _bodies)
{
b.GlobalTransform = delta * b.GlobalTransform;
b.LinearVelocity = delta.Basis * b.LinearVelocity;
b.AngularVelocity = delta.Basis * b.AngularVelocity;
}
_pelvisTargetPrev = delta * _pelvisTargetPrev;
}
public void SnapToPose(TrueBodySolver s, Transform3D left, Transform3D right)
{
ComputeTargets(s, left, right);
foreach (RigidBody3D b in _bodies)
{
b.GlobalTransform = Target(b);
b.LinearVelocity = Vector3.Zero;
b.AngularVelocity = Vector3.Zero;
}
// The hands sit at the wrist end of the solved arm, not at a possibly unreachable controller.
for (int side = 0; side < 2; side++)
{
Arm a = _arms[side];
Basis hb = _targets[a.Hand].Basis;
a.Hand.GlobalTransform = new Transform3D(hb, s.Wrist[side] - hb * new Vector3(0f, 0f, WristOffset));
}
_havePelvisPrev = false;
}
// Collision exceptions between a held prop and that arm's segments (its hand is excluded by
// the grab weld itself) — a forearm wrapped around a held object must not fight it.
public void SetHeld(int side, RigidBody3D prop)
{
if (!Built)
return;
Arm a = _arms[side];
if (a.Held != null && IsInstanceValid(a.Held) && a.Held != _arms[1 - side].Held)
{
a.Upper.RemoveCollisionExceptionWith(a.Held);
a.Fore.RemoveCollisionExceptionWith(a.Held);
}
a.Held = prop;
if (prop != null)
{
a.Upper.AddCollisionExceptionWith(prop);
a.Fore.AddCollisionExceptionWith(prop);
}
}
}