Render-clock tracking timestamps (menu toggle), physics step cap 6, docs for step 2.7

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FoxyzandClaude Opus 5.5 committed 2026-10-02 20:43:49 +03:00
1 parent 22159aa902
commit ca60df3d75
9 files changed
+111 -11

No files matched your search

+8 -1
View File
@@ -321,7 +321,7 @@ device only; the editor has an XR simulator for dev. Trackers/action map: see `x
`PhysicsMaterialOverride` before AddChild.**
**Step 2.6:** landing DIP is physics (legs touch down only within reach → leg strength stops the body below
its stance; big falls fold the legs, Boneworks-style; rise eased so gravity alone can stop it at the stance);
JUMP on right A (countermovement dip + leg push over the stroke — crouching first jumps higher); STICK CROUCH on
JUMP on right A (2.7: hold to charge, release to jump; crouching charges too); STICK CROUCH on
right stick Y (`RagdollDriver.StickCrouch` = virtual floor offset); body facing from `BodyMath.HeadFacing`
(no flip looking down past vertical); the XR origin is DRAWN interpolated like the bodies (`PlayerRig`
`_physOrigin`, restored before physics) — the camera/body "two places at once" desync. Rule learned: play-space
@@ -330,6 +330,13 @@ device only; the editor has an XR simulator for dev. Trackers/action map: see `x
**The crag** (`Crag.cs`): a 26 m box-rock cliff north of the playground with one graded route (boulders →
ladder → 8 m ledge → jug wall → traverse → overhang → 16 m ledge → chimney → summit with props); the way down
is the "Crag" teleport marker at the base. Regression `scenes/crag_spike.tscn` "CRAG: overall PASS" (layout only).
**Step 2.7 (owner 2026-10-02: "Nimso's project is the main source of inspiration/mechanics — don't reinvent
the wheel"; check `research/nimso-project-tx.md` + `research/sources/nimso-transcripts/` FIRST for any body
mechanic):** ground under the BODY (centre ray + stance-wide sphere footprint), clambering by PRESSURE (higher
ground is stood on once the hips are over it and the body presses onto it — no special mantle mechanic),
capsule hips (Nimso ~27 cm), heavy physics objects brace like world, arms always collide with held things,
SOFT GRAB (`SoftGrab.cs`: eased into the hand before the weld), hold-A-to-charge jump, render-clock-timed
tracking (`PoseFilter` sampleTime; menu toggle "Tracking: steady/low lag"), physics step cap 6.
Owner scope: avatar scaling and NPCs later.
10. **Body physics Stage 2 — PHYSICAL ARMS, implemented 2026-10-02, awaiting owner playtest.**
`PhysicsArms.cs`: per side an upper-arm + forearm RigidBody3D (2.0 / 1.2 kg, layer 5) that
+2 -2
View File
@@ -27,9 +27,9 @@ project/assembly_name="VR Messs"
[physics]
common/physics_ticks_per_second=144
common/physics_interpolation=true
common/max_physics_steps_per_frame=14
common/max_physics_steps_per_frame=6
3d/physics_engine="Box3D Physics"
common/physics_interpolation=true
[rendering]
+20
View File
@@ -221,3 +221,23 @@ fall); held poses drift 1 cm with the eye within 7 mm; crawl, mantle, climb, pul
| Menu summon on left X only | Right A is jump |
**Shoulder range (2026-10-02):** the fork's DRIVE now honours angular limits (Box3D motor joint + swing ellipse/twist, `research/godot-box3d-6dof.md`); `PlayerRagdoll.ShoulderRange` (Y −90..+100° mirrored, Z −85..+155°, twist ±80°, frame X = humerus at build). Ragdoll spike unchanged with it on — the limits only engage when something forces the arm.
## Step 2.7 — climbing family, soft grab, charge jump, render-timed tracking (2026-10-02)
Owner: Nimso's Project TX is the main source of mechanics. His clambering has "no actual mechanics — the physics
engine does all the work"; higher ground "doesn't move my body up … until I push on it with enough pressure".
| Change | Why / measured |
|---|---|
| **Ground = centre ray + sphere footprint** (r 0.12, face normal from a ray 2 cm inside the contact). Same-level ground from either; higher ground from the centre ray, or from the footprint while clambering | One ray dropped support as the hips swayed off a plank. Spike: 15 cm plank, ±9 cm hip sway → never loses the plank |
| **Pressure rule** replaces "hips well above + whole hips over": stand on higher ground when the hips are over it and pressing = airborne, hands braced, lifted, a press within 0.6 s, or legs tucked | The old rule needed a full press-up. Two hands mantle 0.9 m and chest-high 1.2 m; one hand can't do 1.2 m |
| **Bracing on physics objects** (≥ 15 kg or frozen; not what the hand carries) | Mantling the 25 kg playground box never counted as bracing (owner screenshot). Now mantles |
| **Knee up**: braced (or just pressed) + hips held back tucks the legs collider | Thigh-high box: straight arms can't press while standing |
| **Capsule hips** (Nimso) | The box pelvis's bottom edge caught ledge lips 1 cm below it |
| **Support goal ≤ straight legs** | A head above the calibrated height (tiptoes) floated the body 35 cm |
| **Arms never get collision exceptions** with held objects | Gripping one hold of a crag rock switched the arm off against the whole rock — arms "randomly" phased through the world |
| **Soft grab** (`SoftGrab.cs`): grip point = palm-normal cast (else toward the centre); light props (≤ 6 kg) are moved to the palm, otherwise the hand goes to the hold; ~0.1 s, position only, weld when touching or stalled | Welds froze hover gaps. 1 kg cube from 6 cm → 1.5 mm in 0.10 s; bar from 5 cm → 1.2 cm (contact slop) |
| **Jump: hold A to charge, release** (support goal sinks at 1.2 m/s, max stroke 0.5 m; JumpForce 2.2 g) | Owner: Boneworks-style. Tap 0.16 m, full charge 0.68 m, crouch + tap 0.66 m |
| **Render-clock tracking timestamps** (`PoseFilter.Sample(..., sampleTime)`, `PlayerRig.TrackingAge`, bible v2 recipe) | At 1 m/s the per-tick speed spread is 1.25 m/s by arrival vs 0.013 m/s render-timed — every feed-forward beat at 18 Hz. Constant ~2-tick lag instead. Toggle in the menu to compare |
| **max_physics_steps_per_frame 14 → 6** | One hitch could queue 14 ragdoll ticks in a frame (bible v2) |
| Tried and reverted: arm aim from the physical shoulder vertically while climbing/tilted | Free-hand error 6.4 → 6.2 cm only, and it weakened pulls. The "tracking off when tilted" screenshots need a recording to diagnose |
+5
View File
@@ -20,6 +20,7 @@ public partial class MainMenu : PanelContainer
public event Action Clicked;
public event Action<bool> SpectatorToggled;
public event Action<bool> SmoothTurnToggled;
public event Action<bool> LowLagTrackingToggled;
public event Action<float> StrengthChanged;
public TabContainer Tabs { get; private set; }
@@ -155,6 +156,10 @@ public partial class MainMenu : PanelContainer
SpectatorButton = MakeToggle("Spectator: off", "Spectator: on (grab-fly)", on => SpectatorToggled?.Invoke(on));
modeRow.AddChild(SpectatorButton);
player.AddChild(modeRow);
// Compare in the headset: tracking timed by the render clock (even motion) or by arrival (less lag).
HBoxContainer trackingRow = new();
trackingRow.AddChild(MakeToggle("Tracking: steady", "Tracking: low lag", on => LowLagTrackingToggled?.Invoke(on)));
player.AddChild(trackingRow);
Tabs.AddChild(player);
// World
+25 -1
View File
@@ -15,6 +15,9 @@ public partial class PlayerRig : Node
public const float MoveAcceleration = 8f; // m/s²
public const float StickDeadzone = 0.2f;
public static bool SmoothTurn = false;
// Tracking timed by the render clock (bible v2: uniform motion, constant ~2-tick lag) vs by
// arrival (less lag, 18 Hz speed beat). Menu toggle so both can be compared in the headset.
public static bool SteadyTracking = true;
public const float SnapTurnDegrees = 30f;
public const float SmoothTurnSpeed = 2.5f; // rad/s
public const float SnapTurnThreshold = 0.7f;
@@ -144,7 +147,8 @@ public partial class PlayerRig : Node
_driver.RebuildRequested |= _rebuild;
_rebuild = false;
Transform3D o = _origin.GlobalTransform;
_driver.Tick(profile, ref o, _camera.Transform, _left.Transform, _right.Transform, _moveVelocity, dt);
_driver.Tick(profile, ref o, _camera.Transform, _left.Transform, _right.Transform, _moveVelocity, dt,
SteadyTracking ? TrackingAge() : null);
_origin.GlobalTransform = o;
_lastOrigin = _origin.GlobalTransform;
_haveLastOrigin = true;
@@ -153,6 +157,24 @@ public partial class PlayerRig : Node
_interpolateOrigin = true;
}
// The tracking poses read in this tick were delivered with the last frame, predicted for its render
// time. In physics ticks: render time of that frame ≈ (physics frames + fraction) at its _process,
// plus one frame delta for the frame these ticks belong to (bible v2 recipe).
private double TrackingAge()
{
float rate = Engine.PhysicsTicksPerSecond;
ulong frame = Engine.GetProcessFrames();
if (frame != _sampleFrame)
{
_sampleFrame = frame;
_sampleTicks = _renderTicks + _renderDelta * rate;
}
return (Engine.GetPhysicsFrames() - _sampleTicks) / rate;
}
private double _renderTicks, _renderDelta, _sampleTicks;
private ulong _sampleFrame = ulong.MaxValue;
// The origin on screen is interpolated; physics must run on the real one. Anyone else who moved
// the origin since (teleport marker, menu) is respected as the new real one.
private void RestorePhysicsOrigin()
@@ -168,6 +190,8 @@ public partial class PlayerRig : Node
public override void _Process(double delta)
{
_renderTicks = Engine.GetPhysicsFrames() + Engine.GetPhysicsInterpolationFraction();
_renderDelta = delta;
if (_interpolateOrigin && !Spectator)
{
Transform3D current = _origin.GlobalTransform;
+16 -7
View File
@@ -21,19 +21,25 @@ public sealed class PoseFilter
public void Reset() => _has = false;
public Transform3D Sample(Transform3D raw, double now)
// sampleTime: when the pose was really measured (the render time of the frame that delivered it),
// in the same clock as `now`. Without it a sample is timestamped by the first tick that saw it,
// which at 144 Hz under 90 Hz frames alternates 1 and 2 ticks late: the extrapolated motion
// then beats at 18 Hz (±30% speed per frame — bible v2, measured). With it the age is exact
// and motion uniform, at a constant ~2-tick lag. May be negative (a tick before the sample).
public Transform3D Sample(Transform3D raw, double now, double? sampleTime = null)
{
double stamp = sampleTime ?? now;
if (!_has)
{
_has = true;
_last = raw;
_tLast = now;
_tLast = stamp;
Velocity = AngularVelocity = Vector3.Zero;
return raw;
}
if (raw != _last)
{
float interval = (float)(now - _tLast);
float interval = (float)(stamp - _tLast);
if (interval > 1e-4f)
{
if (raw.Origin.DistanceTo(_last.Origin) > JumpDistance)
@@ -49,9 +55,9 @@ public sealed class PoseFilter
}
}
_last = raw;
_tLast = now;
_tLast = stamp;
}
float age = Mathf.Min((float)(now - _tLast), MaxExtrapolation);
float age = Mathf.Clamp((float)(now - _tLast), -MaxExtrapolation, MaxExtrapolation);
Basis spin = AngularVelocity.LengthSquared() > 1e-10f
? new Basis(AngularVelocity.Normalized(), AngularVelocity.Length() * age) : Basis.Identity;
return new Transform3D(spin * _last.Basis.Orthonormalized(), _last.Origin + Velocity * age);
@@ -78,11 +84,14 @@ public sealed class RagdollDriver
// origin = play space (modified by the correction); raw* = origin-local tracked poses as last
// reported (repeat between tracking updates); moveVelocity = intended origin velocity (stick),
// the only origin motion that is real hand motion for the guides' feed-forward.
// trackingAge: how long ago (s) the current tracking poses were measured, if known (PlayerRig
// derives it from the render clock); null = timestamp by arrival (the spike's simulated input).
public void Tick(BodyProfile profile, ref Transform3D origin, Transform3D rawHead, Transform3D rawLeft, Transform3D rawRight,
Vector3 moveVelocity, float dt)
Vector3 moveVelocity, float dt, double? trackingAge = null)
{
_now += dt;
Transform3D head = Head.Sample(rawHead, _now), left = Left.Sample(rawLeft, _now), right = Right.Sample(rawRight, _now);
double? stamp = trackingAge.HasValue ? _now - trackingAge.Value : null;
Transform3D head = Head.Sample(rawHead, _now, stamp), left = Left.Sample(rawLeft, _now, stamp), right = Right.Sample(rawRight, _now, stamp);
if (Ragdoll.Built && !RebuildRequested)
origin.Origin += Ragdoll.OriginCorrection((origin * head).Origin, origin.Origin.Y + StickCrouch, dt);
float floorY = origin.Origin.Y + StickCrouch;
+33
View File
@@ -83,6 +83,7 @@ public partial class RagdollSpike : Node3D
AddChild(_ragdoll);
_phases.Add(("hinge sign", SetupHinge, TickHinge, 0.3f, FinishHinge));
_phases.Add(("pose filter: render-timed tracking moves evenly (no 18 Hz beat)", Rest, _ => { }, 0.05f, FinishPoseFilter));
_phases.Add(("stands still, head on the camera, hands on the controllers", Rest, _ => { }, 4f, FinishStand));
_phases.Add(("reaches forward, out and overhead", Rest, TickReach, 4.5f, FinishReach));
_phases.Add(("fast arm swings stay attached", Rest, TickSwing, 3f, FinishSwing));
@@ -233,6 +234,38 @@ public partial class RagdollSpike : Node3D
private int NewSnaps => _ragdoll.Snaps - _snapsAtStart;
// A controller moving at 1 m/s, sampled once per 90 Hz frame (predicted for that frame's render
// time), read by 144 Hz ticks. Per-tick output steps should all be 1/144 m: timestamped by
// arrival they alternate (1 or 2 ticks late), timestamped by render time they don't.
private static bool FinishPoseFilter()
{
float Spread(bool stamped)
{
PoseFilter f = new();
float min = float.MaxValue, max = 0f;
Vector3 prev = Vector3.Zero;
for (int tick = 0; tick < 300; tick++)
{
double now = tick / 144.0;
long frame = (long)Math.Floor(now * 90.0); // the last frame delivered before this tick
double render = (frame + 1) / 90.0; // its predicted display time
Transform3D raw = new(Basis.Identity, new Vector3((float)render, 0f, 0f));
Vector3 p = f.Sample(raw, now, stamped ? render : null).Origin;
if (tick > 20)
{
float step = p.X - prev.X;
min = Mathf.Min(min, step);
max = Mathf.Max(max, step);
}
prev = p;
}
return (max - min) * 144f; // spread of the per-tick speed, m/s
}
float arrival = Spread(false), stamped = Spread(true);
GD.Print($"RAGDOLL: pose filter at 1 m/s: per-tick speed spread {arrival:F3} m/s by arrival, {stamped:F3} m/s render-timed");
return stamped < 0.05f && stamped < arrival;
}
private static StaticBody3D MakeStatic(Vector3 size, Vector3 pos, uint mask = 0)
{
StaticBody3D body = new() { Position = pos, CollisionMask = mask };
+1
View File
@@ -0,0 +1 @@
uid://c4rg1kcdtsxni
+1
View File
@@ -79,6 +79,7 @@ public partial class WorldPanel : Node3D
_menu.ClickVolumeSlider.ValueChanged += v => _click.VolumeDb = Mathf.LinearToDb((float)v / 70f);
_menu.StrengthChanged += v => PlayerRagdoll.Strength = v;
_menu.SmoothTurnToggled += on => PlayerRig.SmoothTurn = on;
_menu.LowLagTrackingToggled += on => PlayerRig.SteadyTracking = !on;
_menu.SpectatorToggled += PlayerRig.SetSpectator;
SetPanelActive(false);