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

+3 -2
View File
@@ -312,8 +312,9 @@ device only; the editor has an XR simulator for dev. Trackers/action map: see `x
Rules: low-pass every feed-forward; muscles stay local to the TARGET pose (never the physical parent — rotation
feedback); no free-path view chasing while grounded; bodies whose shapes can all be disabled need explicit
inertia; Box3D shape queries return a null `collider` — use `collider_id`.
**Step 2.3:** the VIEW RIDES THE BODY (deadzone + smoothed follow + hard 8 cm `MaxViewLead`; never detach the view
to avoid drift — make the body able to match its pose instead); hold-back measured at the hips; sticky legs tuck
**Step 2.3/2.5:** the CAMERA ALWAYS RIDES THE BODY in 3D (2 cm deadzone, hard 4 cm `MaxViewLead`; no separate
floor-follow — targets are play-space-relative so there's no loop) + a HEAD GUIDE on your feet (BIMOS-style) +
slow floor re-centering; never detach the view to avoid drift — make the body able to match its pose instead; hold-back measured at the hips; sticky legs tuck
with a free-space check; strength asymmetric by direction (pull-down/flexion strong, raise weak; structural guide
strength push-only); **Box3D wrapper reads physics-material friction only at shape creation — set
`PhysicsMaterialOverride` before AddChild.**
+27
View File
@@ -178,3 +178,30 @@ wasn't able to climb." Fixed from the same session:
in place.
- The climbing spike now climbs like a person: hand moves are sequential (never release the second hand before the
first re-grips), and pulls are relative to the body/room.
## Step 2.5 — the camera always rides the body (2026-10-02)
Owner: "why doesn't the camera just follow the character when landing? … And not just when landing, but always.
And I'd prefer to exclude the moments where the camera clips out of the head as much as possible." Correct, and
it is what Boneworks/BIMOS does (the rig rides the body; the head is driven to the HMD).
- **One rule, always, 3D:** the camera rides the character's eye. Deadzone 2 cm, follow τ 0.04 s, hard
`MaxViewLead` 4 cm (the head box reaches 2 cm in front of the eye). The blocked eye→camera sweep stays.
2.3's separate standing-height "floor follow" is gone.
- **Why there is no feedback loop** (the fear behind 2.3's split): the True Body reads crouch as LOCAL head height
(camera − play-space floor), ground control re-bases its goal onto the real ground (`footY − floorY`), and so
does the new head guide. Moving the play space changes no target. 2.3's mantle "fight" came from two controllers
editing the same height.
- **Head guide** (BIMOS drives the head to the HMD): a 250 N, 5 Hz PD pulls the character's eye onto the camera
while on your feet (off while airborne, climbing or bracing on the hands). Without it, any transient body lag
(mid-squat) was converted by the tight view follow into a permanent play-space offset: 7 cm per deep squat. With
it, six held poses drift 1 cm.
- **Slow floor re-centering** (τ 1 s, grounded): eases the play-space floor onto the real ground. It moves the
camera toward the eye, the same direction as the follow, so it can't fight it. Without it, a small height offset
picked up while crawling held the head against a tunnel roof for good.
- **`Braced` excludes the floor** (group `no_grab`): hands on the floor while crawling aren't "pushing yourself up".
- **Anatomical head box:** centred 0.5 cm below the eyes (crown ~10 cm above, chin ~11 cm below; was 12.5 / 8.5).
The old box poked 2 cm into a 0.6 m tunnel's roof at a 0.42 m eye height.
Results (real-input spike): landing keeps the pelvis at standing height with eye-to-camera ≤ 6.8 cm (during the
fall); held poses drift 1 cm with the eye within 7 mm; crawl, mantle, climb, pull-up and hang all pass.
+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;
}
+3 -1
View File
@@ -73,7 +73,9 @@ public partial class RagdollSpike : Node3D
_holdOn = false;
}
GD.Print($"RAGDOLL: input mode {(_realInput ? "REAL (90 Hz tracking, jitter)" : "ideal")} at {Engine.PhysicsTicksPerSecond} Hz physics");
AddChild(MakeStatic(new Vector3(40f, 1f, 40f), new Vector3(0f, -0.5f, 0f), 29));
StaticBody3D floor = MakeStatic(new Vector3(40f, 1f, 40f), new Vector3(0f, -0.5f, 0f), 29);
floor.AddToGroup(PropGrabber.NoGrabGroup); // like main.tscn's floor
AddChild(floor);
_ragdoll = new PlayerRagdoll { Name = "Ragdoll" };
AddChild(_ragdoll);