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:
1 parent
2aff2b61da
commit
6006c5de98
8 files changed
+352
-81
No files matched your search
+62
-23
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user