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

+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);