Body step 2.2: fix the 2.1 headset instabilities

Spike now runs real input (90 Hz sample-and-hold tracking + jitter under 144 Hz
physics), which reproduced the playtest. Root causes fixed:
- PoseFilter/RagdollDriver: timestamped extrapolation of tracking, one tick
  pipeline shared by PlayerRig and the spike
- low-pass all feed-forwards (ground control amplified jitter by body mass)
- arm muscles back to local targets (physical-frame aim fed chest rotation back)
- structural hand strength only when braced or holding
- no free-path view chasing while grounded (8 m crouch slide); stick hold-back
- explicit inertia for the Legs body (shape-less tuck tumbled the pelvis)
- world grab: Box3D shape queries return a null collider, use collider_id
- new crouch-hold phases; world-grab detection asserted

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FoxyzandClaude Opus 5.5 committed 2026-10-02 08:37:56 +03:00
1 parent 2aff2b61da
commit 6006c5de98
8 files changed
+352 -81

No files matched your search

+62 -23
View File
@@ -29,6 +29,10 @@ public partial class PlayerRagdoll : Node3D
public const float ShoulderKp = 20f, ShoulderTorque = 45f; // ~human shoulder flexion strength
public const float ElbowKp = 20f, ElbowTorque = 45f;
public const float MuscleMaxSpin = 15f;
// Feed-forwards are derivatives of tracking: low-pass them or tracking jitter (±1–2 mm per
// sample) becomes hundreds of newtons of buzz (RagdollSpike REAL input mode).
public const float MuscleFeedForwardTau = 0.05f; // s
public const float GroundFeedForwardTau = 0.12f; // s
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.
@@ -67,7 +71,7 @@ public partial class PlayerRagdoll : Node3D
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 FreeSlack = 0.2f; // camera leading the body's eye by more than this = body held back: stick stops pushing that way
public const float AirSlack = 0.05f; // climbing/hanging (braced): the view rides the body tightly
public const float FallSlack = 0.25f; // free airborne: brief ground loss (sliding off a steep slope) mustn't sink the view
public const float GroundFollowRate = 8f;
@@ -96,6 +100,7 @@ public partial class PlayerRagdoll : Node3D
public float Kp;
public Quaternion PrevTarget;
public bool HasPrev;
public Vector3 Ff;
}
private sealed class Drive
@@ -105,6 +110,7 @@ public partial class PlayerRagdoll : Node3D
public Vector3 AnchorA, AnchorB; // anchor point, parent-local / child-local
public Quaternion PrevTarget;
public bool HasPrev;
public Vector3 Ff;
}
private sealed class Arm
@@ -115,6 +121,7 @@ public partial class PlayerRagdoll : Node3D
public Drive Shoulder, Guide;
public float UpperLen, ForeLen;
public float PrevFlexTarget = -1f;
public float FlexFf;
public float GuideCap = HandGuideForce;
public PhysicsBody3D Held;
}
@@ -126,6 +133,9 @@ public partial class PlayerRagdoll : Node3D
public bool Grounded { get; private set; }
public float GroundY => _footY;
public bool Braced { get; private set; } // a hand touches or holds static world
// Horizontal direction in which the body is held back from the camera (zero if not): the stick
// must not keep moving the play space that way.
public Vector3 HeldBack { get; private set; }
// Really climbing: a hand holds static world, or the arms are lifting the body (mantle).
public bool Climbing => Built && (_arms[0].Held is StaticBody3D || _arms[1].Held is StaticBody3D || _lifted);
public RigidBody3D Legs { get; private set; }
@@ -144,6 +154,9 @@ public partial class PlayerRagdoll : Node3D
private readonly Vector3[] _girdle = new Vector3[2]; // True Body shoulder offset, chest frame
private float _footY;
private float _supportGoalY;
private Vector3 _goalVelocity;
private static float LowPass(float dt, float tau) => 1f - Mathf.Exp(-dt / tau);
private bool _lifted;
private float _legsHeight;
private Drive _legsDrive;
@@ -198,6 +211,10 @@ public partial class PlayerRagdoll : Node3D
_legsHeight = Mathf.Max(0.2f, standingHip - MaxStepHeight);
Legs = MakeBody("Legs", new BoxShape3D { Size = new Vector3(0.28f * s, _legsHeight, 0.2f * s) }, LegsMass, BodyLayer, WorldMask, pants,
c * new Transform3D(Basis.Identity, hip - new Vector3(0f, _legsHeight * 0.5f, 0f)), visible: false);
// Explicit inertia: the collider is switched off when tucked, and a body without an
// enabled shape has no inertia — its 400 N·m drive then tumbled the whole pelvis
// (owner 2.1 "body spinning" when crouched).
Legs.Inertia = new Vector3(_legsHeight * _legsHeight + 0.04f, 0.28f * 0.28f + 0.04f, _legsHeight * _legsHeight + 0.08f) * (LegsMass / 12f);
_footY = pose.Eye.Y - p.EyeHeight;
Chest = MakeBody("Chest", new BoxShape3D { Size = chestSize }, 0.216f * m, BodyLayer, WorldMask, shirt,
c * new Transform3D(Basis.Identity, lumbar + _chestCenterLocal));
@@ -428,18 +445,23 @@ public partial class PlayerRagdoll : Node3D
// real hand. Vertically a sagging body keeps its arm pose (the aim point moves with
// the shoulder), so straight real arms stay straight virtual arms — pillars that hold
// you up when crawling or press you up a ledge (the hand guide adds the rest).
// Only a horizontal TRANSLATION of the reach: solved in the chest TARGET frame (the
// muscles stay local). Aiming in the physical chest frame made the shoulder muscles
// feed chest rotation back into the chest — a slow twist that tracking jitter
// excited into "chest slipping out of control" (owner playtest 2.1).
Transform3D chestPhys = Chest.GlobalTransform.Orthonormalized();
Vector3 shPhys = chestPhys * (a.Shoulder.AnchorA + _girdle[side]);
Vector3 held = shPhys - sh;
held.Y = 0f;
Transform3D ctrl = _targets[a.Hand];
Vector3 aim = ctrl * new Vector3(0f, 0f, WristOffset) + Vector3.Up * (shPhys.Y - sh.Y);
ArmSolver solver = _armSolvers[side];
solver.Solve(shPhys, aim, chestPhys.Basis, chestPhys.Basis.X * (side == 0 ? -1f : 1f),
solver.Solve(sh, ctrl * new Vector3(0f, 0f, WristOffset) - held, chest, chest.X * (side == 0 ? -1f : 1f),
ctrl.Basis, a.UpperLen, a.ForeLen, dt);
Vector3 d = solver.Wrist - shPhys;
Vector3 h = d.LengthSquared() > 1e-8f ? solver.Pole.Cross(d.Normalized()) : chestPhys.Basis.X;
h = h.LengthSquared() > 1e-8f ? h.Normalized() : chestPhys.Basis.X;
Vector3 u = (solver.Elbow - shPhys).Normalized(), f = (solver.Wrist - solver.Elbow).Normalized();
_targets[a.Upper] = new Transform3D(BoneBasis(u, h), shPhys + u * (a.UpperLen * 0.5f));
Vector3 d = solver.Wrist - sh;
Vector3 h = d.LengthSquared() > 1e-8f ? solver.Pole.Cross(d.Normalized()) : chest.X;
h = h.LengthSquared() > 1e-8f ? h.Normalized() : chest.X;
Vector3 u = (solver.Elbow - sh).Normalized(), f = (solver.Wrist - solver.Elbow).Normalized();
_targets[a.Upper] = new Transform3D(BoneBasis(u, h), sh + u * (a.UpperLen * 0.5f));
_targets[a.Fore] = new Transform3D(BoneBasis(f, h), solver.Elbow + f * (a.ForeLen * 0.5f));
}
}
@@ -502,10 +524,11 @@ public partial class PlayerRagdoll : Node3D
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;
Vector3 raw = b.HasPrev && dt > 0f ? (BodyMath.RotationError(target, b.PrevTarget) / dt).LimitLength(MuscleMaxSpin) : Vector3.Zero;
b.Ff = b.Ff.Lerp(raw, LowPass(dt, MuscleFeedForwardTau));
b.PrevTarget = target;
b.HasPrev = true;
Vector3 w = (ff + BodyMath.RotationError(target, current) * b.Kp).LimitLength(MuscleMaxSpin);
Vector3 w = (b.Ff + BodyMath.RotationError(target, current) * b.Kp).LimitLength(MuscleMaxSpin);
SetAngularVelocity(b.Joint, w);
}
@@ -537,9 +560,10 @@ public partial class PlayerRagdoll : Node3D
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;
float raw = a.PrevFlexTarget >= 0f && dt > 0f ? Mathf.Clamp((target - a.PrevFlexTarget) / dt, -MuscleMaxSpin, MuscleMaxSpin) : 0f;
a.FlexFf = Mathf.Lerp(a.FlexFf, raw, LowPass(dt, MuscleFeedForwardTau));
a.PrevFlexTarget = target;
float w = Mathf.Clamp(ff + (target - ElbowFlex(side)) * ElbowKp, -MuscleMaxSpin, MuscleMaxSpin);
float w = Mathf.Clamp(a.FlexFf + (target - ElbowFlex(side)) * ElbowKp, -MuscleMaxSpin, MuscleMaxSpin);
a.Elbow.SetParam(HingeJoint3D.Param.MotorTargetVelocity, HingeSign * w);
}
@@ -622,13 +646,15 @@ public partial class PlayerRagdoll : Node3D
SetLinearVelocity(d.Joint, frameA.Inverse() * (err * GirdleKp));
Quaternion current = frameA.Inverse() * (Rot(a.Upper) * d.FrameB);
// Arm targets are solved from the physical chest, so the physical chest is the parent frame.
Quaternion target = (chest.Basis.GetRotationQuaternion() * d.FrameA).Inverse()
// Local muscle: relative to the chest's TARGET pose (never to the physical chest — that
// closes a rotation feedback loop through the arms).
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;
Vector3 raw = d.HasPrev && dt > 0f ? (BodyMath.RotationError(target, d.PrevTarget) / dt).LimitLength(MuscleMaxSpin) : Vector3.Zero;
d.Ff = d.Ff.Lerp(raw, LowPass(dt, MuscleFeedForwardTau));
d.PrevTarget = target;
d.HasPrev = true;
SetAngularVelocity(d.Joint, (ff + BodyMath.RotationError(target, current) * ShoulderKp).LimitLength(MuscleMaxSpin));
SetAngularVelocity(d.Joint, (d.Ff + BodyMath.RotationError(target, current) * ShoulderKp).LimitLength(MuscleMaxSpin));
}
// Force-capped pull of the hand onto the controller, reacting into the chest (arm muscles).
@@ -645,6 +671,10 @@ public partial class PlayerRagdoll : Node3D
// Axial share of the error, with a 3 cm floor so a nearly-reached target's noisy error
// direction can't flip a held load into "structural".
float align = axis.LengthSquared() > 1e-8f ? Mathf.Abs(err.Dot(axis.Normalized())) / (err.Length() + StructuralDeadband) : 0f;
// Structure only helps against something: a hand in free air reaching past its arm's
// length must not haul the whole body after it.
if (a.Held == null && a.Hand.GetContactCount() == 0)
align = 0f;
float cap = Mathf.Lerp(HandGuideForce, HandStructuralForce, align) * Mathf.Max(0.05f, Strength);
if (Mathf.Abs(cap - a.GuideCap) > 1f)
{
@@ -701,11 +731,13 @@ public partial class PlayerRagdoll : Node3D
// ground, the pose target and the ground offset move by the same amount in opposite
// directions — differentiating the pose alone would launch the body.
Vector3 goalNow = target.Origin + Vector3.Up * (_footY - floorY);
Vector3 targetVelocity = _havePelvisPrev && dt > 0f ? (goalNow - _pelvisTargetPrev) / dt : Vector3.Zero;
Vector3 rawVelocity = _havePelvisPrev && dt > 0f ? (goalNow - _pelvisTargetPrev) / dt : Vector3.Zero;
_pelvisTargetPrev = goalNow;
_havePelvisPrev = true;
if (targetVelocity.Length() > 10f)
targetVelocity = Vector3.Zero;
if (rawVelocity.Length() > 10f)
rawVelocity = Vector3.Zero;
_goalVelocity = _goalVelocity.Lerp(rawVelocity, LowPass(dt, GroundFeedForwardTau));
Vector3 targetVelocity = _goalVelocity;
float weight = BodyMass * 9.8f;
if (Grounded)
@@ -778,13 +810,20 @@ public partial class PlayerRagdoll : Node3D
if (!Climbing)
correction.Y = 0f;
}
else
else if (!Grounded)
{
float slack = Grounded ? FreeSlack : Climbing ? AirSlack : FallSlack;
if (lead.Length() > slack)
correction = -lead * (1f - slack / lead.Length());
// Free path: the view never chases the body while standing — any pose mismatch
// (a crouch the body can't match exactly) would otherwise walk the play space away
// forever (body → view → pose target → body; owner 2.1 "sliding around").
// Airborne it rides the body: vertically in a fall, fully while climbing.
Vector3 follow = Climbing ? lead : new Vector3(0f, lead.Y, 0f);
float slack = Climbing ? AirSlack : FallSlack;
if (follow.Length() > slack)
correction = -follow * (1f - slack / follow.Length());
}
}
Vector3 flatLead = new(lead.X, 0f, lead.Z);
HeldBack = flatLead.Length() > FreeSlack ? flatLead.Normalized() : Vector3.Zero;
if (Grounded)
{
correction.Y = 0f;