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>
This commit is contained in:
parent
e3d8a44c48
commit
71b1622293
5 changed files with 218 additions and 16 deletions
|
|
@ -0,0 +1,98 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue