diff --git a/AGENTS.md b/AGENTS.md index a57fda1..0e7e7c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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.** diff --git a/research/ragdoll-player-step2.md b/research/ragdoll-player-step2.md index b172494..928ab19 100644 --- a/research/ragdoll-player-step2.md +++ b/research/ragdoll-player-step2.md @@ -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. diff --git a/scripts/PlayerRagdoll.cs b/scripts/PlayerRagdoll.cs index 330a115..db650fb 100644 --- a/scripts/PlayerRagdoll.cs +++ b/scripts/PlayerRagdoll.cs @@ -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; } diff --git a/scripts/RagdollSpike.cs b/scripts/RagdollSpike.cs index a985cb7..c9bad63 100644 --- a/scripts/RagdollSpike.cs +++ b/scripts/RagdollSpike.cs @@ -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);