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:
1 parent
22159aa902
commit
ca60df3d75
9 files changed
+111
-11
No files matched your search
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://c4rg1kcdtsxni
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user