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:
1 parent
0b8f2131c1
commit
84df772f38
4 files changed
+91
-30
No files matched your search
@@ -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
|
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
|
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`.
|
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
|
**Step 2.3/2.5:** the CAMERA ALWAYS RIDES THE BODY in 3D (2 cm deadzone, hard 4 cm `MaxViewLead`; no separate
|
||||||
to avoid drift — make the body able to match its pose instead); hold-back measured at the hips; sticky legs tuck
|
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
|
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
|
strength push-only); **Box3D wrapper reads physics-material friction only at shape creation — set
|
||||||
`PhysicsMaterialOverride` before AddChild.**
|
`PhysicsMaterialOverride` before AddChild.**
|
||||||
|
|||||||
@@ -178,3 +178,30 @@ wasn't able to climb." Fixed from the same session:
|
|||||||
in place.
|
in place.
|
||||||
- The climbing spike now climbs like a person: hand moves are sequential (never release the second hand before the
|
- 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.
|
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
@@ -79,17 +79,18 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
// Play-space reconciliation.
|
// Play-space reconciliation.
|
||||||
public const float CameraRadius = 0.03f; // sphere swept from the head's eye to the camera
|
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 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 ViewDeadzone = 0.02f; // 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 ViewFollowTau = 0.04f; // s
|
||||||
public const float ClimbViewDeadzone = 0.02f; // hanging/mantling: the view rides the body tightly
|
public const float MaxViewLead = 0.04f; // hard: the head box reaches 2 cm in front of the eye
|
||||||
public const float ClimbViewFollowTau = 0.03f;
|
public const float FloorRecenterTau = 1f; // s
|
||||||
public const float MaxViewLead = 0.08f;
|
public const float HeadGuideFrequency = 5f; // Hz
|
||||||
public const float GroundFollowRate = 8f;
|
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
|
public const float ResetDistance = 1.5f; // pelvis this far from its target → snap the whole body to the pose
|
||||||
// Geometry.
|
// Geometry.
|
||||||
public const float LumbarFraction = 0.35f; // lumbar joint height above the hips, × trunk
|
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 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 HeadSize = new(0.16f, 0.21f, 0.2f);
|
||||||
public static readonly Vector3 HandSize = new(0.035f, 0.09f, 0.12f);
|
public static readonly Vector3 HandSize = new(0.035f, 0.09f, 0.12f);
|
||||||
public const float WristOffset = TrueBodySolver.WristOffset;
|
public const float WristOffset = TrueBodySolver.WristOffset;
|
||||||
@@ -503,6 +504,7 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
if (!Built)
|
if (!Built)
|
||||||
return;
|
return;
|
||||||
_dt = dt;
|
_dt = dt;
|
||||||
|
_floorYForHead = floorY;
|
||||||
ComputeTargets(s, left, right, physicalArms: true, dt);
|
ComputeTargets(s, left, right, physicalArms: true, dt);
|
||||||
foreach (RigidBody3D b in _bodies)
|
foreach (RigidBody3D b in _bodies)
|
||||||
{
|
{
|
||||||
@@ -537,6 +539,7 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
Braced |= IsBraced(a);
|
Braced |= IsBraced(a);
|
||||||
}
|
}
|
||||||
GroundControl(pelvisTarget, floorY, dt);
|
GroundControl(pelvisTarget, floorY, dt);
|
||||||
|
HeadGuide(s, dt);
|
||||||
LegsDrive(s);
|
LegsDrive(s);
|
||||||
_legVisuals.Tick(_footY, Grounded, !_legsSolid, s.YawBasis, dt);
|
_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()
|
private void ApplyStrength()
|
||||||
{
|
{
|
||||||
_appliedStrength = Strength;
|
_appliedStrength = Strength;
|
||||||
@@ -629,13 +659,14 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
j.SetParamZ(Generic6DofJoint3D.Param.LinearMotorForceLimit, cap);
|
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)
|
private static bool IsBraced(Arm a)
|
||||||
{
|
{
|
||||||
if (a.Held is StaticBody3D)
|
if (a.Held is StaticBody3D)
|
||||||
return true;
|
return true;
|
||||||
foreach (Node3D n in a.Hand.GetCollidingBodies())
|
foreach (Node3D n in a.Hand.GetCollidingBodies())
|
||||||
if (n is StaticBody3D)
|
if (n is StaticBody3D && !n.IsInGroup(PropGrabber.NoGrabGroup))
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -859,13 +890,16 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- play space
|
// ---------------------------------------------------------------- play space
|
||||||
|
|
||||||
// How the driver should move its play-space origin after the last physics step. The view RIDES
|
// How the driver should move its play-space origin after the last physics step. ONE rule, always,
|
||||||
// THE BODY: beyond a small deadzone (normal head motion leads the body a little) the play space
|
// in all directions (Boneworks/BIMOS: the rig rides the body): the camera rides the character's
|
||||||
// eases so the camera sits on the character's eye — hanging, mantling, being shoved, standing up
|
// eye. Head motion leads the body by at most ViewDeadzone (eased) / MaxViewLead (hard) — less
|
||||||
// under a table. On top, a sphere swept eye→camera puts the view straight back when geometry
|
// than the head box reaches in front of the eye, so the view stays inside the head. A sphere
|
||||||
// is in the way (no peeking through walls). The floor follows the ground under the body.
|
// swept eye→camera snaps the view back at once when geometry is in the way.
|
||||||
// Requirement for this not to drift: the body must be able to match the pose (RagdollSpike
|
// This does NOT feed back into the pose: the True Body reads the head relative to the play-space
|
||||||
// "holds poses without drift"); 2.2's "never follow while standing" left the view behind the body.
|
// 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)
|
public Vector3 OriginCorrection(Vector3 camera, float floorY, float dt)
|
||||||
{
|
{
|
||||||
if (!Built)
|
if (!Built)
|
||||||
@@ -883,25 +917,22 @@ public partial class PlayerRagdoll : Node3D
|
|||||||
if (safe < 1f)
|
if (safe < 1f)
|
||||||
correction = -lead * (1f - safe);
|
correction = -lead * (1f - safe);
|
||||||
Vector3 rest = lead + correction;
|
Vector3 rest = lead + correction;
|
||||||
if (Grounded && !Climbing)
|
if (rest.Length() > ViewDeadzone)
|
||||||
rest.Y = 0f; // standing: height belongs to the ground follow (and the blocked sweep)
|
correction -= rest * (1f - ViewDeadzone / rest.Length()) * (1f - Mathf.Exp(-dt / ViewFollowTau));
|
||||||
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).
|
|
||||||
rest = lead + correction;
|
rest = lead + correction;
|
||||||
if (Grounded && !Climbing)
|
|
||||||
rest.Y = 0f;
|
|
||||||
if (rest.Length() > MaxViewLead)
|
if (rest.Length() > MaxViewLead)
|
||||||
correction -= rest * (1f - MaxViewLead / rest.Length());
|
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)
|
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
|
// 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).
|
// must not show up in ground control's feed-forward (that loop ran away at 7 m/s).
|
||||||
_pelvisTargetPrev += correction;
|
_pelvisTargetPrev += correction;
|
||||||
|
_headGoalPrev += correction;
|
||||||
return correction;
|
return correction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ public partial class RagdollSpike : Node3D
|
|||||||
_holdOn = false;
|
_holdOn = false;
|
||||||
}
|
}
|
||||||
GD.Print($"RAGDOLL: input mode {(_realInput ? "REAL (90 Hz tracking, jitter)" : "ideal")} at {Engine.PhysicsTicksPerSecond} Hz physics");
|
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" };
|
_ragdoll = new PlayerRagdoll { Name = "Ragdoll" };
|
||||||
AddChild(_ragdoll);
|
AddChild(_ragdoll);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user