Body step 2.5: the camera always rides the body

- one 3D view-follow rule (2 cm deadzone, hard 4 cm limit), no separate
  floor-follow; play-space-relative targets mean no feedback loop
- head guide drives the character's eye to the HMD while on your feet
- slow floor re-centering; Braced ignores the floor; anatomical head box

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FoxyzandClaude Opus 5.5 committed 2026-10-02 10:38:14 +03:00
1 parent 0b8f2131c1
commit 84df772f38
4 files changed
+91 -30

No files matched your search

+58 -27
View File
@@ -79,17 +79,18 @@ public partial class PlayerRagdoll : Node3D
// Play-space reconciliation.
public const float CameraRadius = 0.03f; // sphere swept from the head's eye to the camera
public const float FreeSlack = 0.12f; // camera leading the body's eye by more than this = body held back: stick stops pushing that way
public const float ViewDeadzone = 0.06f; // m the head may lead the body before the view is eased back onto it
public const float ViewFollowTau = 0.12f; // s
public const float ClimbViewDeadzone = 0.02f; // hanging/mantling: the view rides the body tightly
public const float ClimbViewFollowTau = 0.03f;
public const float MaxViewLead = 0.08f;
public const float GroundFollowRate = 8f;
public const float ViewDeadzone = 0.02f; // m the head may lead the body before the view is eased back onto it
public const float ViewFollowTau = 0.04f; // s
public const float MaxViewLead = 0.04f; // hard: the head box reaches 2 cm in front of the eye
public const float FloorRecenterTau = 1f; // s
public const float HeadGuideFrequency = 5f; // Hz
public const float HeadGuideMass = 15f; // kg effective (head + part of the trunk)
public const float HeadGuideForce = 250f; // N
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 HeadCenterFromEye = new(0f, -0.005f, 0.08f); // crown ~10 cm above the eyes, chin ~11 cm below (was 12.5 / 8.5)
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;
@@ -503,6 +504,7 @@ public partial class PlayerRagdoll : Node3D
if (!Built)
return;
_dt = dt;
_floorYForHead = floorY;
ComputeTargets(s, left, right, physicalArms: true, dt);
foreach (RigidBody3D b in _bodies)
{
@@ -537,6 +539,7 @@ public partial class PlayerRagdoll : Node3D
Braced |= IsBraced(a);
}
GroundControl(pelvisTarget, floorY, dt);
HeadGuide(s, dt);
LegsDrive(s);
_legVisuals.Tick(_footY, Grounded, !_legsSolid, s.YawBasis, dt);
}
@@ -599,6 +602,33 @@ public partial class PlayerRagdoll : Node3D
}
}
// Head guide (Boneworks/BIMOS drive the head to the HMD): while standing on the ground, a force-
// capped PD pulls the character's eye onto the camera, so voluntary head motion (squats, leans,
// quick looks) doesn't leave the body lagging — a lag the riding view would otherwise turn into
// a permanent play-space offset. Off while airborne/climbing (no levitating by the head) or
// bracing on the hands (mantling, pushing off).
private void HeadGuide(TrueBodySolver s, float dt)
{
Vector3 goal = s.Eye + Vector3.Up * (_footY - _floorYForHead);
Vector3 raw = _haveHeadPrev && dt > 0f ? (goal - _headGoalPrev) / dt : Vector3.Zero;
_headGoalPrev = goal;
_haveHeadPrev = true;
if (raw.Length() > 10f)
raw = Vector3.Zero;
_headGoalVelocity = _headGoalVelocity.Lerp(raw, LowPass(dt, GroundFeedForwardTau));
// Only on your feet: hands bracing (mantling, pushing yourself) or crawling move the body by
// other means, and the view simply follows it.
if (!Grounded || Climbing || Braced)
return;
float w = Mathf.Tau * HeadGuideFrequency;
Vector3 force = ((goal - Eye) * (w * w) + (_headGoalVelocity - Head.LinearVelocity) * (2f * w)) * HeadGuideMass;
Head.ApplyCentralForce(force.LimitLength(HeadGuideForce));
}
private Vector3 _headGoalPrev, _headGoalVelocity;
private bool _haveHeadPrev;
private float _floorYForHead;
private void ApplyStrength()
{
_appliedStrength = Strength;
@@ -629,13 +659,14 @@ public partial class PlayerRagdoll : Node3D
j.SetParamZ(Generic6DofJoint3D.Param.LinearMotorForceLimit, cap);
}
// A hand pressing on or holding static world (a ledge, a wall, a grabbed bar).
// A hand pressing on or holding static world (a ledge, a wall, a grabbed bar) — not the floor:
// hands on the floor while crawling don't lift the body.
private static bool IsBraced(Arm a)
{
if (a.Held is StaticBody3D)
return true;
foreach (Node3D n in a.Hand.GetCollidingBodies())
if (n is StaticBody3D)
if (n is StaticBody3D && !n.IsInGroup(PropGrabber.NoGrabGroup))
return true;
return false;
}
@@ -859,13 +890,16 @@ public partial class PlayerRagdoll : Node3D
// ---------------------------------------------------------------- play space
// How the driver should move its play-space origin after the last physics step. The view RIDES
// THE BODY: beyond a small deadzone (normal head motion leads the body a little) the play space
// eases so the camera sits on the character's eye — hanging, mantling, being shoved, standing up
// under a table. On top, a sphere swept eye→camera puts the view straight back when geometry
// is in the way (no peeking through walls). The floor follows the ground under the body.
// Requirement for this not to drift: the body must be able to match the pose (RagdollSpike
// "holds poses without drift"); 2.2's "never follow while standing" left the view behind the body.
// How the driver should move its play-space origin after the last physics step. ONE rule, always,
// in all directions (Boneworks/BIMOS: the rig rides the body): the camera rides the character's
// eye. Head motion leads the body by at most ViewDeadzone (eased) / MaxViewLead (hard) — less
// than the head box reaches in front of the eye, so the view stays inside the head. A sphere
// swept eye→camera snaps the view back at once when geometry is in the way.
// This does NOT feed back into the pose: the True Body reads the head relative to the play-space
// floor (crouch = local head height) and ground control re-bases its goal onto the real ground
// (footY − floorY), so moving the play space vertically changes neither. (2.3 split height off
// to a separate floor-follow out of fear of that loop; the two then fought over the same height,
// and landings left the view hanging above a sinking body — owner 2.4.)
public Vector3 OriginCorrection(Vector3 camera, float floorY, float dt)
{
if (!Built)
@@ -883,25 +917,22 @@ public partial class PlayerRagdoll : Node3D
if (safe < 1f)
correction = -lead * (1f - safe);
Vector3 rest = lead + correction;
if (Grounded && !Climbing)
rest.Y = 0f; // standing: height belongs to the ground follow (and the blocked sweep)
float deadzone = Climbing ? ClimbViewDeadzone : ViewDeadzone;
float tau = Climbing ? ClimbViewFollowTau : ViewFollowTau;
if (rest.Length() > deadzone)
correction -= rest * (1f - deadzone / rest.Length()) * (1f - Mathf.Exp(-dt / tau));
// Hard limit: the view is never more than MaxViewLead off the character's head (a
// smoothed follow always trails a sudden drop — letting go with one hand, a fall).
if (rest.Length() > ViewDeadzone)
correction -= rest * (1f - ViewDeadzone / rest.Length()) * (1f - Mathf.Exp(-dt / ViewFollowTau));
rest = lead + correction;
if (Grounded && !Climbing)
rest.Y = 0f;
if (rest.Length() > MaxViewLead)
correction -= rest * (1f - MaxViewLead / rest.Length());
}
// Slow re-centering of the play-space floor onto the real ground while standing: removes the
// small height offsets the riding view picks up (a transient lag, a landing). The pose and the
// head guide are re-based on the real ground, so this only brings the camera onto the eye —
// the same direction as the follow above, never against it.
if (Grounded)
correction.Y += (GroundY - floorY) * (1f - Mathf.Exp(-dt * GroundFollowRate));
correction.Y += (_footY - floorY) * (1f - Mathf.Exp(-dt / FloorRecenterTau));
// The pose target moves with the play space: a correction is not intended motion, so it
// must not show up in ground control's feed-forward (that loop ran away at 7 m/s).
_pelvisTargetPrev += correction;
_headGoalPrev += correction;
return correction;
}