acdream/docs/research/2026-05-06-issue-38-render-interp-pseudocode.md
Erik 71b1622293 fix(camera): #38 render-interpolate player motion
Keep local physics authoritative at the retail 30 Hz MinQuantum, but expose a render-only position that lerps between completed physics ticks for the player mesh and chase-camera target. Network outbound continues to use the discrete physics position.

Also make the visually confirmed #47 humanoid close-detail DIDDegrade path default-on, with ACDREAM_RETAIL_CLOSE_DEGRADES=0 left as a diagnostic opt-out.

Verification: dotnet build AcDream.slnx -c Debug; focused #38 interpolation tests passed; visual confirmed smooth 2026-05-06. Full dotnet test AcDream.slnx -c Debug --no-build still has the known 8 AcDream.Core.Tests baseline failures.

Co-authored-by: Codex <codex@openai.com>
2026-05-06 17:53:34 +02:00

98 lines
3.4 KiB
Markdown

# Issue #38 render interpolation pseudocode
## Problem
Phase L.5 correctly restored retail's `CPhysicsObj::update_object`
physics gate: the body only integrates when accumulated frame time reaches
`PhysicsBody.MinQuantum` (1/30 second). That keeps collision behavior aligned
with retail, but the renderer currently reads `_body.Position` directly.
At 60+ FPS that means several rendered frames can show the same physics
position, then jump to the next 30 Hz position. Chase camera makes the stepping
obvious because both the player mesh and camera target follow that discrete
physics sample.
## Evidence
- Named retail: `CPhysicsObj::update_object` at `0x00515d10`
(`docs/research/named-retail/acclient_2013_pseudo_c.txt:283950`) skips or
consumes time outside the valid quantum window and calls
`CPhysicsObj::UpdateObjectInternal` only for accepted physics quanta.
- ACE mirrors the same constants in `PhysicsGlobals`: `MinQuantum = 1/30`,
`MaxQuantum = 0.1`, `HugeQuantum = 2.0`.
- ACE's `InterpolationManager` / `PositionManager` prove client-style
smoothing is a known AC-family concept, but their queue chases network target
positions. Issue #38 is narrower: local render-only interpolation between the
last two authoritative local physics ticks.
- Glenn Fiedler's "Fix Your Timestep!" describes the canonical fixed-timestep
accumulator plus render interpolation pattern:
https://gafferongames.com/post/fix_your_timestep/
## Intentional divergence
Retail did not need a separate high-FPS render interpolation layer because the
2013 client effectively presented near the same cadence as its physics gate.
acdream renders at modern display rates, so we preserve retail's 30 Hz physics
truth but draw a blended visual position between the last two physics truths.
## Pseudocode
State stored by `PlayerMovementController`:
```text
physicsAccum: leftover render time not yet consumed by physics
prevPhysicsPos: body position at the start of the most recently completed tick
currPhysicsPos: body position at the end of the most recently completed tick
```
On spawn, teleport, or any authoritative SetPosition:
```text
body.Position = newPosition
prevPhysicsPos = newPosition
currPhysicsPos = newPosition
physicsAccum = 0
```
Each Update(dt):
```text
physicsAccum += dt
if physicsAccum > HugeQuantum:
physicsAccum = 0
prevPhysicsPos = body.Position
currPhysicsPos = body.Position
return/render body.Position
if physicsAccum >= MinQuantum:
oldTickEnd = currPhysicsPos
preIntegratePos = body.Position
tickDt = min(physicsAccum, MaxQuantum)
calc_acceleration()
UpdatePhysicsInternal(tickDt)
postIntegratePos = body.Position
resolve collision from preIntegratePos to postIntegratePos
body.Position = resolved physics position
update contact/cell/velocity exactly as before
prevPhysicsPos = oldTickEnd
currPhysicsPos = body.Position
physicsAccum -= tickDt
alpha = clamp(physicsAccum / MinQuantum, 0, 1)
renderPosition = lerp(prevPhysicsPos, currPhysicsPos, alpha)
```
Important constraints:
- Never write `renderPosition` back to `_body.Position`.
- `MovementResult.Position` remains the authoritative physics/collision/network
position.
- Add `MovementResult.RenderPosition` for mesh and camera drawing only.
- Outbound `MoveToState` / `AutonomousPosition` keep using
`MovementResult.Position`.
- If no physics tick has completed yet, `prevPhysicsPos == currPhysicsPos`, so
interpolation is a no-op.