@woosh/meep-engine 2.146.0 → 2.147.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +0 -11
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +8 -6
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +552 -551
- package/src/engine/control/first-person/abilities/LedgeGrab.d.ts +8 -3
- package/src/engine/control/first-person/abilities/LedgeGrab.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/LedgeGrab.js +213 -199
- package/src/engine/control/first-person/abilities/Mantle.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/Mantle.js +195 -188
- package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/WallRun.js +183 -175
- package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts +9 -0
- package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts.map +1 -1
- package/src/engine/control/first-person/sensors/FirstPersonSensors.js +87 -77
- package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts +8 -0
- package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts.map +1 -1
- package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js +229 -196
- package/src/engine/ecs/EntityManager.d.ts +34 -11
- package/src/engine/ecs/EntityManager.d.ts.map +1 -1
- package/src/engine/ecs/EntityManager.js +71 -42
- package/src/engine/interpolation/BinaryInterpolationAdapter.d.ts.map +1 -0
- package/src/engine/interpolation/Interpoland.d.ts +48 -0
- package/src/engine/interpolation/Interpoland.d.ts.map +1 -0
- package/src/engine/interpolation/Interpoland.js +49 -0
- package/src/engine/interpolation/Interpolated.d.ts +101 -0
- package/src/engine/interpolation/Interpolated.d.ts.map +1 -0
- package/src/engine/interpolation/Interpolated.js +149 -0
- package/src/engine/{network/sim → interpolation}/InterpolationLog.d.ts +1 -1
- package/src/engine/interpolation/InterpolationLog.d.ts.map +1 -0
- package/src/engine/{network/sim → interpolation}/InterpolationLog.js +2 -2
- package/src/engine/interpolation/InterpolationSystem.d.ts +116 -0
- package/src/engine/interpolation/InterpolationSystem.d.ts.map +1 -0
- package/src/engine/interpolation/InterpolationSystem.js +233 -0
- package/src/engine/interpolation/PoseInterpolationAdapter.d.ts +17 -0
- package/src/engine/interpolation/PoseInterpolationAdapter.d.ts.map +1 -0
- package/src/engine/interpolation/PoseInterpolationAdapter.js +61 -0
- package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts +35 -0
- package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts.map +1 -0
- package/src/engine/interpolation/TransformPoseSerializationAdapter.js +57 -0
- package/src/engine/interpolation/pose_interpoland.d.ts +18 -0
- package/src/engine/interpolation/pose_interpoland.d.ts.map +1 -0
- package/src/engine/interpolation/pose_interpoland.js +27 -0
- package/src/engine/network/NetworkSession.d.ts +2 -2
- package/src/engine/network/NetworkSession.d.ts.map +1 -1
- package/src/engine/network/NetworkSession.js +2 -2
- package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts +1 -1
- package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts.map +1 -1
- package/src/engine/network/adapters/QuaternionInterpolationAdapter.js +1 -1
- package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts +1 -1
- package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts.map +1 -1
- package/src/engine/network/adapters/TransformInterpolationAdapter.js +1 -1
- package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts +1 -1
- package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts.map +1 -1
- package/src/engine/network/adapters/Vector3InterpolationAdapter.js +1 -1
- package/src/engine/physics/INTEPOLATION_SYSTEM_PLAN.md +287 -0
- package/src/engine/physics/PLAN.md +10 -9
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +64 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +168 -0
- package/src/engine/physics/narrowphase/box_triangle_contact.js +15 -12
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +28 -1
- package/src/engine/network/sim/BinaryInterpolationAdapter.d.ts.map +0 -1
- package/src/engine/network/sim/InterpolationLog.d.ts.map +0 -1
- /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.d.ts +0 -0
- /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.js +0 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# Interpolation system — build plan
|
|
2
|
+
|
|
3
|
+
Cross-cutting plan for fixed-step → render interpolation. Spans **render**,
|
|
4
|
+
**physics** (a producer), and **network** (a producer + the existing primitives
|
|
5
|
+
this builds on). Referenced from `engine/physics/PLAN.md`.
|
|
6
|
+
|
|
7
|
+
> Filename note: created as `INTEPOLATION_SYSTEM_PLAN.md` per the request; rename
|
|
8
|
+
> to `INTERPOLATION_SYSTEM_PLAN.md` if the spelling was unintended.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Goal
|
|
13
|
+
|
|
14
|
+
The simulation advances on a fixed step (`EntityManager.fixedUpdateStepSize`,
|
|
15
|
+
~60 Hz). The renderer runs at a variable, usually higher, display rate on its own
|
|
16
|
+
`requestAnimationFrame` loop. Between two fixed steps the rendered pose is frozen
|
|
17
|
+
at the last step, so motion stutters / temporally aliases — worst at low fixed
|
|
18
|
+
rates or high refresh. This system smooths the discrete per-tick poses across the
|
|
19
|
+
render rate.
|
|
20
|
+
|
|
21
|
+
## Hard constraints (these shaped every decision)
|
|
22
|
+
|
|
23
|
+
1. **Write `Transform`, not the renderable.** Scene culling, mesh BVHs, particle
|
|
24
|
+
systems, trails, and attachment hierarchies are all driven off the ECS
|
|
25
|
+
`Transform`. Writing an interpolated pose into a three.js object directly
|
|
26
|
+
would smooth *some* visuals and desync the rest. The interpolated pose must
|
|
27
|
+
land in `Transform` so the whole engine sees it uniformly. (Bonus: attachment
|
|
28
|
+
children of an interpolated physics parent then track it **for free**, because
|
|
29
|
+
the attachment system is push-based on the parent's `Transform.onChanged`.)
|
|
30
|
+
2. **Only interpolate what moved.** Per-frame cost must be O(moving bodies),
|
|
31
|
+
never O(total). The engine targets massive worlds — tens of thousands of
|
|
32
|
+
dynamic bodies with only a tiny awake/moving fraction at any moment.
|
|
33
|
+
3. **Don't fight physics.** Physics must never integrate from an interpolated
|
|
34
|
+
pose. The simulation reads authoritative state; interpolation is a downstream
|
|
35
|
+
render channel.
|
|
36
|
+
4. **Co-designed with network.** The network package can change (not yet shipped).
|
|
37
|
+
Interpolation must share machinery with replication, not duplicate it.
|
|
38
|
+
5. **Rewind-safe.** The network can rewind and re-simulate. If a rewind+resim
|
|
39
|
+
produces the same state, the observer must see the **same** interpolated
|
|
40
|
+
result — no visual drift from re-running history.
|
|
41
|
+
6. **Agnostic to what it interpolates.** Translation+rotation is one case;
|
|
42
|
+
network may interpolate other replicated quantities. The mechanism blends
|
|
43
|
+
*encoded snapshots of any registered component type*.
|
|
44
|
+
|
|
45
|
+
## Key realization — reuse, don't rebuild
|
|
46
|
+
|
|
47
|
+
The network package already contains the agnostic interpolation engine. Physics
|
|
48
|
+
becomes a *producer* into it; we do not build a parallel system.
|
|
49
|
+
|
|
50
|
+
- `network/sim/BinaryInterpolationAdapter.js` —
|
|
51
|
+
`interpolate(out, source, offA, offB, t)`: type-agnostic blend of two encoded
|
|
52
|
+
snapshots. Concrete `Transform` / `Vector3` / `Quaternion` adapters exist.
|
|
53
|
+
**This is constraint 6, already solved.**
|
|
54
|
+
- `network/sim/InterpolationLog.js` — a **tick-indexed ring** (`Map<tick, …>`)
|
|
55
|
+
of per-component snapshots, recording **only components that changed that tick**
|
|
56
|
+
(the package's O(mutations)-not-O(entities) bet == constraint 2). Keyed by a
|
|
57
|
+
plain integer; `interpolate(out, key, type_id, tickA, tickB, t, adapter)`
|
|
58
|
+
blends two ticks into an out-buffer that is then deserialized into the live
|
|
59
|
+
component.
|
|
60
|
+
- `network/sim/SimActionExecutor.js` — `before_execute` hook, JSDoc:
|
|
61
|
+
*"NetworkSession undoes render-time interpolation on remote-owned components"*
|
|
62
|
+
before reading authoritative state. **The architecture already committed to
|
|
63
|
+
interpolation writing the live component and the sim restoring authoritative
|
|
64
|
+
before it reads** (constraints 1 + 3).
|
|
65
|
+
- `network/sim/ActionLog.js` + `network/sim/RewindEngine.js` — rewind restores
|
|
66
|
+
prior authoritative state; resim replays forward via `onReplay`
|
|
67
|
+
(constraint 5).
|
|
68
|
+
|
|
69
|
+
## Locked decisions
|
|
70
|
+
|
|
71
|
+
1. **Neutral shared primitive.** Lift `InterpolationLog`,
|
|
72
|
+
`BinaryInterpolationAdapter`, and the blend adapters out of `network/` into a
|
|
73
|
+
neutral `engine/interpolation/`. Physics and network both produce into it; one
|
|
74
|
+
render-time pass blends for both. Replication + rewind layer on top **only**
|
|
75
|
+
for networked entities — single-player physics pays no action-log cost.
|
|
76
|
+
2. **Restore-from-log at step start.** Before each physics tick, reset each moving
|
|
77
|
+
body's live `Transform` to its latest authoritative tick snapshot (a *quiet*
|
|
78
|
+
write that bypasses `onChanged`). The solver / narrowphase / broadphase /
|
|
79
|
+
integrators keep reading & writing `Transform` exactly as today — **no rewiring
|
|
80
|
+
of the physics hot paths.** The authoritative "cache" is the log's newest tick.
|
|
81
|
+
|
|
82
|
+
## Glossary (load-bearing terms)
|
|
83
|
+
|
|
84
|
+
- **Tick** — one fixed simulation step, identified by a monotonic integer *tick
|
|
85
|
+
number*. The orchestrator's frame number when networked; an internal counter
|
|
86
|
+
otherwise.
|
|
87
|
+
- **Authoritative pose / endpoint** — the exact pose the simulation produced at
|
|
88
|
+
the *end* of a tick. The thing physics owns and rewind restores.
|
|
89
|
+
- **Snapshot** — an authoritative pose encoded into the log under
|
|
90
|
+
`(log key, type_id, tick)` via a serialization adapter.
|
|
91
|
+
- **Log key** — the integer identity under which an entity's snapshots are
|
|
92
|
+
recorded and looked up in the shared log. `network_id` when the entity is
|
|
93
|
+
networked (so a server's recording and a client's receipt line up under one
|
|
94
|
+
identity); a locally-allocated interp id otherwise. Carried on the
|
|
95
|
+
`Interpolated` component. (See §"Q&A".)
|
|
96
|
+
- **alpha** — `accumulator / fixedUpdateStepSize ∈ [0, 1)`: the fractional
|
|
97
|
+
progress between the two most-recent *completed* ticks. The blend parameter `t`.
|
|
98
|
+
We render **between** the last two ticks (one step in the past), so both
|
|
99
|
+
endpoints always exist — no extrapolation. (See §"Q&A".)
|
|
100
|
+
- **`InterpolationSystem`** — the system that owns the render-time blend pass. It
|
|
101
|
+
runs in `System.update(dt)` (the variable-rate, rAF-driven cycle), NOT ad-hoc in
|
|
102
|
+
a `preRender` callback. `update` can fire on ticks where no render happens —
|
|
103
|
+
harmless (a blend with no following draw is just discarded). If display-exact
|
|
104
|
+
timing is ever needed, the system can additionally hook `preRender` from its
|
|
105
|
+
`startup`; `update` is the default.
|
|
106
|
+
- **`Interpolated` component** — opt-in marker physics & render cooperate around.
|
|
107
|
+
Holds the **log key** (defaults to `-1`; `InterpolationSystem.link` assigns it
|
|
108
|
+
`entity_id` when still `-1`, so the common case needs no manual setup — the
|
|
109
|
+
network system can instead set it to the `network_id` directly for replicated
|
|
110
|
+
entities), the blend type/adapter id, and a `snap` flag. A body without it keeps
|
|
111
|
+
today's direct-write behavior (this is how physics "sees" who to cooperate
|
|
112
|
+
with).
|
|
113
|
+
|
|
114
|
+
## Architecture — one engine, two producers
|
|
115
|
+
|
|
116
|
+
**Per fixed tick, for bodies carrying `Interpolated`:**
|
|
117
|
+
|
|
118
|
+
1. **Restore (step start):** reset the live `Transform` to the log's latest
|
|
119
|
+
authoritative tick — a quiet write (no `onChanged`). This is `before_execute`
|
|
120
|
+
/ "undo render interpolation" applied to physics. Physics then reads truth.
|
|
121
|
+
2. **Step:** physics integrates as today → `Transform` = new authoritative pose.
|
|
122
|
+
3. **Record (step end):** if it actually moved (`|Δ| > ε`), snapshot the pose into
|
|
123
|
+
the current tick. Sparse by construction → O(moving).
|
|
124
|
+
|
|
125
|
+
**Per `InterpolationSystem.update(dt)` (render/rAF cadence):** for each
|
|
126
|
+
`Interpolated` entity, pick `(tickA, tickB, t)` and blend → deserialize into the
|
|
127
|
+
live `Transform` (this write *does* fire `onChanged` → culling/particles/trails/
|
|
128
|
+
attachments update). O(moving). `t` source is per-producer: local =
|
|
129
|
+
`EntityManager.getFixedStepAlpha(physicsSystem)`; remote = the network's existing
|
|
130
|
+
render-delay/jitter playout.
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
simulate() [fixed-step catch-up loop] InterpolationSystem.update(dt) [rAF cadence]
|
|
134
|
+
for awake+Interpolated: for Interpolated e:
|
|
135
|
+
restore Transform ← log.latest (quiet) (a,b,t) = window(e) // t = getFixedStepAlpha(physics)
|
|
136
|
+
...physics step → Transform = authoritative blend(a,b,t) → Transform (onChanged)
|
|
137
|
+
for moved+Interpolated: → later draw passes read Transform
|
|
138
|
+
log.record(key, tick, Transform)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Why each constraint holds
|
|
142
|
+
|
|
143
|
+
1. **Writes Transform** — blend deserializes into the live component; every
|
|
144
|
+
Transform-linked system works, hierarchy included.
|
|
145
|
+
2. **Only moving** — produce & blend only for moved bodies; the log is sparse.
|
|
146
|
+
3. **Doesn't fight physics** — restore-at-step-start; physics never sees the
|
|
147
|
+
interpolated pose. Provable by an interp-on vs interp-off sim-state bit-identity
|
|
148
|
+
test.
|
|
149
|
+
4. **Co-designed with network** — same log, same adapters, same `before_execute`;
|
|
150
|
+
physics is a new producer into the existing pipeline.
|
|
151
|
+
5. **Rewind-safe** — the log is tick-keyed; resim re-runs the deterministic sim,
|
|
152
|
+
which re-records the *same* tick numbers with identical bytes → identical
|
|
153
|
+
blend output. Requirement: the replay path records snapshots like the live path
|
|
154
|
+
(it runs the same systems), and the log supports re-recording rewound ticks.
|
|
155
|
+
6. **Agnostic** — `BinaryInterpolationAdapter`; pose is one type; any component
|
|
156
|
+
with a (serialization adapter + interpolation adapter) pair interpolates the
|
|
157
|
+
same way.
|
|
158
|
+
|
|
159
|
+
## Invariants
|
|
160
|
+
|
|
161
|
+
- **Interpolation never feeds back into the sim.** Guarded by restore-at-step-start
|
|
162
|
+
and an interp-on/off sim bit-identity test.
|
|
163
|
+
- **The log's latest tick is the single authoritative cache** between steps;
|
|
164
|
+
`Transform` is a derived render channel for interpolated bodies.
|
|
165
|
+
- **Sparse by motion** — O(moving), never O(N).
|
|
166
|
+
|
|
167
|
+
## Precision note (massive worlds)
|
|
168
|
+
|
|
169
|
+
The network *wire* Transform adapter uses Float32 position + smallest-three
|
|
170
|
+
quaternion for bandwidth. That quantizes badly far from the origin. The shared
|
|
171
|
+
**interp log stores full-precision snapshots** (Float64 position + full
|
|
172
|
+
quaternion) for both local and remote (remote is re-serialized at full precision
|
|
173
|
+
on apply); wire compression stays on the wire only.
|
|
174
|
+
|
|
175
|
+
## Contract change (interpolated bodies only)
|
|
176
|
+
|
|
177
|
+
For a body carrying `Interpolated`, raw external `Transform` writes are no longer
|
|
178
|
+
authoritative — restore-at-step-start would clobber them. External authoritative
|
|
179
|
+
changes (teleport, kinematic drive) go through a `setPose` / `teleport` API that
|
|
180
|
+
updates the log's latest tick **and** sets `snap` (so the next render does not
|
|
181
|
+
slide across the jump). Bodies *without* `Interpolated` keep today's raw-write
|
|
182
|
+
teleport contract.
|
|
183
|
+
|
|
184
|
+
## Phases
|
|
185
|
+
|
|
186
|
+
Each phase: implement → spec → run from `H:/git/moh` → commit. Black-box tests,
|
|
187
|
+
headless where possible (the log, adapters, and physics producer are
|
|
188
|
+
headless-testable; the render hook is thin).
|
|
189
|
+
|
|
190
|
+
> **Status (Phases 0–2 landed).** Key deltas from the original sketch, decided
|
|
191
|
+
> during build: the fixed-step **tick lives on `EntityManager`**
|
|
192
|
+
> (`fixedStepTick`), not a per-system clock, and `getFixedStepAlpha()` is the one
|
|
193
|
+
> global sim-clock alpha (no system arg). To make the per-step tick exact, the
|
|
194
|
+
> scheduler was changed to a **global lock-step loop** on a single accumulator
|
|
195
|
+
> (commit `0fd41f3`) — all systems' `fixedUpdate` advance together, tick++ per
|
|
196
|
+
> step. The physics producer is **agnostic** (iterates the component's
|
|
197
|
+
> interpolands, uses their adapters) and reads its log via an injected
|
|
198
|
+
> `PhysicsSystem.interpolationLog` (null → skipped). Restore reads `tick-1`,
|
|
199
|
+
> record writes `tick`; lock-step makes both exact with no per-body tick field.
|
|
200
|
+
|
|
201
|
+
- [x] **Phase 0 — Neutralize the primitive** (`0980e45`). `InterpolationLog` +
|
|
202
|
+
`BinaryInterpolationAdapter` (+ spec) moved to `engine/interpolation/`; network
|
|
203
|
+
importers repointed. `EntityManager.getFixedStepAlpha`. (Log key generalization
|
|
204
|
+
is cosmetic — it was already a plain integer.)
|
|
205
|
+
- [x] **Phase 1 — `Interpolated` + `InterpolationSystem`** (`fc3c3aa`).
|
|
206
|
+
`Interpoland` descriptor; `Interpolated` carries a **list** of interpolands
|
|
207
|
+
(one component-per-class → many smoothed quantities); `link` auto-assigns
|
|
208
|
+
`key = entity` when `-1`; `update` blends each interpoland `(tick-1, tick)` at
|
|
209
|
+
`getFixedStepAlpha()` into the live component. Full-precision (Float64) pose
|
|
210
|
+
adapters + `POSE_INTERPOLAND`. Tested: blend / snap / untouched-when-empty /
|
|
211
|
+
key auto-assign / multi-interpoland.
|
|
212
|
+
- [x] **Phase 2 — Physics as producer** (`a05db76` tick + `dade4d6` producer;
|
|
213
|
+
scheduler `0fd41f3`). `__interp_restore` (top of `fixedUpdate`, undo render
|
|
214
|
+
interpolation) + `__interp_record` (after solve/CCD, before sleep test).
|
|
215
|
+
**Determinism guard passes**: interp ON (live Transform polluted every awake
|
|
216
|
+
step) vs OFF → bit-identical pose AND velocity every step through fall →
|
|
217
|
+
contact → settle → sleep.
|
|
218
|
+
- [x] *`setPose` teleport API* (`691f9a0`). `PhysicsSystem.setPose(rb, pos, rot)`
|
|
219
|
+
writes the Transform, wakes, and sets `snap`. `snap` semantics finalized:
|
|
220
|
+
the consumer **leaves the live pose untouched** for that frame (a teleport's
|
|
221
|
+
new pose isn't in the log yet) and the producer's restore **skips** a snapped
|
|
222
|
+
body. *Known edge:* clean for the common ~1:1 sim:render rate; when render
|
|
223
|
+
rate ≫ sim rate a teleport could show a brief flicker (the latest tick isn't
|
|
224
|
+
retroactively overwritten) — fixable later with an in-place latest-tick
|
|
225
|
+
overwrite if it ever matters.
|
|
226
|
+
- [x] *End-to-end wiring verified* (`691f9a0`,
|
|
227
|
+
`InterpolationSystem.integration.spec`). A real `EntityManager` driving
|
|
228
|
+
physics + `InterpolationSystem` with `physics.interpolationLog = interp.log`
|
|
229
|
+
interpolates a body's `Transform` every render frame between fixed steps
|
|
230
|
+
(control with interp off is frozen between steps). *App wiring* (registering
|
|
231
|
+
the systems + connecting the log in the actual game world) is still the app
|
|
232
|
+
layer's job — the recipe is the integration spec.
|
|
233
|
+
- **Phase 3 — Network unification.** *Architecture + coherence landed; live
|
|
234
|
+
NetworkSession migration is the remaining integration.*
|
|
235
|
+
- **Finding:** the network package *already* does remote interpolation on the
|
|
236
|
+
shared primitive — `NetworkSession` owns an `InterpolationLog`, records
|
|
237
|
+
remote-owned components on `onFrameApplied` (`#on_frame_applied`), undoes
|
|
238
|
+
render interpolation via `normalize_if_dirty` (the `before_execute` hook),
|
|
239
|
+
and blends each remote entity through the SAME `log.interpolate → deserialize`
|
|
240
|
+
path as the local system, on an `AdaptiveRenderDelay`-smoothed playout frame
|
|
241
|
+
(`#render_interpolated_entities`). So the *primitive* was unified by Phase 0;
|
|
242
|
+
what was missing was a single *consumer* spanning both timelines.
|
|
243
|
+
- [x] **Unified consumer.** Component stays pure data: `Interpolated.sourceId`
|
|
244
|
+
(an int; `INTERPOLATION_SOURCE_LOCAL = 0` default = local fixed-step clock +
|
|
245
|
+
the system's own log). The timelines' logs + window providers live on the
|
|
246
|
+
SYSTEM, registered via `InterpolationSystem.registerSource(id, log, sample)`
|
|
247
|
+
(the network owns its playout window). One `update` blends local and
|
|
248
|
+
source-driven entities, each source sampled once per frame. *Test:* a local
|
|
249
|
+
sim-clock entity and a remote playout-source entity both interpolate
|
|
250
|
+
correctly in one `update` (local + remote coherent in one scene).
|
|
251
|
+
- [ ] **Live migration (remaining):** point `NetworkSession`'s remote entities
|
|
252
|
+
at a `NetworkInterpolationSource` (wrapping its playout window + log), mark
|
|
253
|
+
them with `Interpolated`, and drop `#render_interpolated_entities` so the
|
|
254
|
+
`InterpolationSystem` is the sole render consumer. Deferred to avoid
|
|
255
|
+
destabilizing the tested netcode; needs exposing the session's playout window
|
|
256
|
+
as a side-effect-free `sample()`.
|
|
257
|
+
- **Phase 4 — Rewind/resim correctness.** Interp log supports resim re-recording:
|
|
258
|
+
on rewind to target, truncate ticks ≥ target; resim re-records forward.
|
|
259
|
+
*Test:* sim to T, capture interp-log bytes; rewind to T−k, resim to T with
|
|
260
|
+
identical inputs; assert interp-log bytes over (T−k, T] are bit-identical →
|
|
261
|
+
observer-identical when nothing changed.
|
|
262
|
+
- **Phase 5 — Camera + edges.** Camera following an interpolated body reads the
|
|
263
|
+
interpolated source pose. Spawn/despawn & key reuse via generation; verify a
|
|
264
|
+
visual child of an interpolated physics parent tracks it (hierarchy-for-free);
|
|
265
|
+
kinematic/parked filtered by the moved-ε test.
|
|
266
|
+
|
|
267
|
+
## Open items (flagged, not yet decided)
|
|
268
|
+
|
|
269
|
+
- **Local-vs-remote render timeline.** Local predicted-present vs remote
|
|
270
|
+
delayed-playout is the classic netcode coherence question. v1 can render both
|
|
271
|
+
at local-present; refine when the predict/reconcile client lands.
|
|
272
|
+
- **`onChanged` suppression at sim time.** Interpolated bodies could suppress
|
|
273
|
+
`integrate_position`'s `onChanged` (the render blend will set the final pose).
|
|
274
|
+
Keep it for now — it keeps culling current between slow render frames.
|
|
275
|
+
|
|
276
|
+
## Source touchpoints (verify at implementation)
|
|
277
|
+
|
|
278
|
+
- `engine/physics/integration/integrate_position.js` — writes `Transform` via
|
|
279
|
+
`.set()` (fires `onChanged`); restore must be a quiet sibling write.
|
|
280
|
+
- `engine/physics/body/BodyStorage.js` — awake set (`awake_at`/`awake_count`) =
|
|
281
|
+
moving candidates; generation = spawn/despawn/reuse detection.
|
|
282
|
+
- `engine/ecs/EntityManager.js` — `fixedUpdateStepSize`, the per-system
|
|
283
|
+
accumulator `systemAccumulatedFixedStepTime`, the fixed-step catch-up loop.
|
|
284
|
+
- `engine/graphics/GraphicsEngine.js` — `on.preRender` (the blend hook, fired
|
|
285
|
+
before the draw passes).
|
|
286
|
+
- `engine/network/sim/{InterpolationLog,BinaryInterpolationAdapter,
|
|
287
|
+
SimActionExecutor,RewindEngine}.js` — the primitives to neutralize/extend.
|
|
@@ -777,15 +777,16 @@ scaffolding is in place.
|
|
|
777
777
|
each body's pose straight into the ECS `Transform` once per fixed step
|
|
778
778
|
(`EntityManager.fixedUpdateStepSize`); with a render rate that doesn't match
|
|
779
779
|
the fixed rate, the rendered motion aliases (stutter / temporal aliasing,
|
|
780
|
-
worst at low fixed rates).
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
780
|
+
worst at low fixed rates). Designed as a cross-cutting system (render +
|
|
781
|
+
physics-as-producer + network-as-producer), reusing the network package's
|
|
782
|
+
agnostic interpolation primitives rather than a physics-local mechanism.
|
|
783
|
+
Full design + phasing: **see
|
|
784
|
+
[`INTEPOLATION_SYSTEM_PLAN.md`](./INTEPOLATION_SYSTEM_PLAN.md)**. Locked
|
|
785
|
+
decisions: a neutral shared interpolation log/adapters (lifted out of
|
|
786
|
+
`network/`); physics restores authoritative pose from the log at step start
|
|
787
|
+
(no solver rewiring) and records moved bodies' snapshots at step end;
|
|
788
|
+
blend → `Transform` at `preRender`; O(moving) not O(N); rewind-safe via the
|
|
789
|
+
tick-keyed log.
|
|
789
790
|
|
|
790
791
|
### API polish
|
|
791
792
|
- [x] **`overlap(shape, position, rotation, output, output_offset,
|
|
@@ -218,6 +218,23 @@ export class PhysicsSystem extends System<any, any, any, any, any> {
|
|
|
218
218
|
* @private
|
|
219
219
|
*/
|
|
220
220
|
private __pair_filter_bound;
|
|
221
|
+
/**
|
|
222
|
+
* Optional shared interpolation log to produce per-step pose snapshots
|
|
223
|
+
* into (the {@link InterpolationSystem}'s log). When set — and only then —
|
|
224
|
+
* each fixedUpdate restores every awake {@link Interpolated} body's live
|
|
225
|
+
* components from the previous tick (undoing render-time interpolation so
|
|
226
|
+
* the sim reads authoritative state), then records the post-step state
|
|
227
|
+
* under the current `entityManager.fixedStepTick`. Null on a headless /
|
|
228
|
+
* non-rendering world, where the producer work is skipped entirely.
|
|
229
|
+
* @type {InterpolationLog|null}
|
|
230
|
+
*/
|
|
231
|
+
interpolationLog: InterpolationLog | null;
|
|
232
|
+
/**
|
|
233
|
+
* Reusable decode buffer for restoring interpolated snapshots.
|
|
234
|
+
* @private
|
|
235
|
+
* @type {BinaryBuffer}
|
|
236
|
+
*/
|
|
237
|
+
private __interp_scratch;
|
|
221
238
|
/**
|
|
222
239
|
* Symmetric layer/mask check + optional user callback. Called per
|
|
223
240
|
* candidate pair during broadphase. Returns `true` to accept the pair.
|
|
@@ -468,6 +485,32 @@ export class PhysicsSystem extends System<any, any, any, any, any> {
|
|
|
468
485
|
y: number;
|
|
469
486
|
z: number;
|
|
470
487
|
}): void;
|
|
488
|
+
/**
|
|
489
|
+
* Teleport a body to a new pose, bypassing integration: writes the body's
|
|
490
|
+
* Transform directly and wakes it. For an interpolated body this also flags a
|
|
491
|
+
* render `snap` on its {@link Interpolated} component, so the producer keeps
|
|
492
|
+
* this pose (rather than restoring the previous tick over it) and the
|
|
493
|
+
* renderer shows the new pose without sliding across the jump.
|
|
494
|
+
*
|
|
495
|
+
* This is the authoritative way to reposition an interpolated body — a raw
|
|
496
|
+
* `Transform` write would be undone by the per-step restore. Velocity is left
|
|
497
|
+
* as-is; zero it via {@link setLinearVelocity} if the teleport should also
|
|
498
|
+
* stop the body.
|
|
499
|
+
*
|
|
500
|
+
* @param {RigidBody} rigidBody
|
|
501
|
+
* @param {Vector3|{x:number,y:number,z:number}} position world position
|
|
502
|
+
* @param {Quaternion|{x:number,y:number,z:number,w:number}} rotation world unit-quaternion rotation
|
|
503
|
+
*/
|
|
504
|
+
setPose(rigidBody: RigidBody, position: Vector3 | {
|
|
505
|
+
x: number;
|
|
506
|
+
y: number;
|
|
507
|
+
z: number;
|
|
508
|
+
}, rotation: Quaternion | {
|
|
509
|
+
x: number;
|
|
510
|
+
y: number;
|
|
511
|
+
z: number;
|
|
512
|
+
w: number;
|
|
513
|
+
}): void;
|
|
471
514
|
/**
|
|
472
515
|
* Force the body awake. Static bodies are ignored.
|
|
473
516
|
* @param {RigidBody} rigidBody
|
|
@@ -674,6 +717,27 @@ export class PhysicsSystem extends System<any, any, any, any, any> {
|
|
|
674
717
|
* @private
|
|
675
718
|
*/
|
|
676
719
|
private __dispatch_contact_events;
|
|
720
|
+
/**
|
|
721
|
+
* Producer — restore pass. At the top of a fixed step, reset every awake
|
|
722
|
+
* {@link Interpolated} body's live components to their authoritative state
|
|
723
|
+
* from the previous tick's snapshot, undoing any render-time interpolation
|
|
724
|
+
* the {@link InterpolationSystem} wrote between frames so the sim integrates
|
|
725
|
+
* from truth, not an interpolated pose. A body with no previous snapshot
|
|
726
|
+
* (first step ever, or just woken) is left as-is — its live state is already
|
|
727
|
+
* authoritative. No-op unless {@link interpolationLog} is wired.
|
|
728
|
+
* @private
|
|
729
|
+
*/
|
|
730
|
+
private __interp_restore;
|
|
731
|
+
/**
|
|
732
|
+
* Producer — record pass. At the end of a fixed step, snapshot every awake
|
|
733
|
+
* {@link Interpolated} body's live components into the shared log under the
|
|
734
|
+
* current `entityManager.fixedStepTick`. The render-time
|
|
735
|
+
* {@link InterpolationSystem} blends consecutive ticks from these snapshots.
|
|
736
|
+
* Only awake (moving) bodies are recorded, so the log stays sparse. No-op
|
|
737
|
+
* unless {@link interpolationLog} is wired.
|
|
738
|
+
* @private
|
|
739
|
+
*/
|
|
740
|
+
private __interp_record;
|
|
677
741
|
fixedUpdate(dt: any): void;
|
|
678
742
|
/**
|
|
679
743
|
* @readonly
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PhysicsSystem.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ecs/PhysicsSystem.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PhysicsSystem.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ecs/PhysicsSystem.js"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assert } from "../../../core/assert.js";
|
|
2
|
+
import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
|
|
2
3
|
import { BVH } from "../../../core/bvh2/bvh3/BVH.js";
|
|
3
4
|
import Signal from "../../../core/events/signal/Signal.js";
|
|
4
5
|
import Vector3 from "../../../core/geom/Vector3.js";
|
|
@@ -47,6 +48,7 @@ import { Collider, COLLIDER_UNBOUND } from "./Collider.js";
|
|
|
47
48
|
import { RIGID_BODY_UNALLOCATED, RigidBody } from "./RigidBody.js";
|
|
48
49
|
import { RigidBodyFlags } from "./RigidBodyFlags.js";
|
|
49
50
|
import { SleepState } from "./SleepState.js";
|
|
51
|
+
import { Interpolated } from "../../interpolation/Interpolated.js";
|
|
50
52
|
|
|
51
53
|
/**
|
|
52
54
|
* Reusable scratch buffer for world-AABB construction so the link path is
|
|
@@ -319,6 +321,26 @@ export class PhysicsSystem extends System {
|
|
|
319
321
|
* @private
|
|
320
322
|
*/
|
|
321
323
|
this.__pair_filter_bound = (idA, idB) => this.__pair_filter(idA, idB);
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Optional shared interpolation log to produce per-step pose snapshots
|
|
327
|
+
* into (the {@link InterpolationSystem}'s log). When set — and only then —
|
|
328
|
+
* each fixedUpdate restores every awake {@link Interpolated} body's live
|
|
329
|
+
* components from the previous tick (undoing render-time interpolation so
|
|
330
|
+
* the sim reads authoritative state), then records the post-step state
|
|
331
|
+
* under the current `entityManager.fixedStepTick`. Null on a headless /
|
|
332
|
+
* non-rendering world, where the producer work is skipped entirely.
|
|
333
|
+
* @type {InterpolationLog|null}
|
|
334
|
+
*/
|
|
335
|
+
this.interpolationLog = null;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Reusable decode buffer for restoring interpolated snapshots.
|
|
339
|
+
* @private
|
|
340
|
+
* @type {BinaryBuffer}
|
|
341
|
+
*/
|
|
342
|
+
this.__interp_scratch = new BinaryBuffer();
|
|
343
|
+
this.__interp_scratch.fromArrayBuffer(new ArrayBuffer(256));
|
|
322
344
|
}
|
|
323
345
|
|
|
324
346
|
/**
|
|
@@ -829,6 +851,50 @@ export class PhysicsSystem extends System {
|
|
|
829
851
|
}
|
|
830
852
|
}
|
|
831
853
|
|
|
854
|
+
/**
|
|
855
|
+
* Teleport a body to a new pose, bypassing integration: writes the body's
|
|
856
|
+
* Transform directly and wakes it. For an interpolated body this also flags a
|
|
857
|
+
* render `snap` on its {@link Interpolated} component, so the producer keeps
|
|
858
|
+
* this pose (rather than restoring the previous tick over it) and the
|
|
859
|
+
* renderer shows the new pose without sliding across the jump.
|
|
860
|
+
*
|
|
861
|
+
* This is the authoritative way to reposition an interpolated body — a raw
|
|
862
|
+
* `Transform` write would be undone by the per-step restore. Velocity is left
|
|
863
|
+
* as-is; zero it via {@link setLinearVelocity} if the teleport should also
|
|
864
|
+
* stop the body.
|
|
865
|
+
*
|
|
866
|
+
* @param {RigidBody} rigidBody
|
|
867
|
+
* @param {Vector3|{x:number,y:number,z:number}} position world position
|
|
868
|
+
* @param {Quaternion|{x:number,y:number,z:number,w:number}} rotation world unit-quaternion rotation
|
|
869
|
+
*/
|
|
870
|
+
setPose(rigidBody, position, rotation) {
|
|
871
|
+
const idx = body_id_index(rigidBody._bodyId);
|
|
872
|
+
const transform = this.__transforms[idx];
|
|
873
|
+
if (transform === undefined) {
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
transform.position.set(position.x, position.y, position.z);
|
|
878
|
+
transform.rotation.set(rotation.x, rotation.y, rotation.z, rotation.w);
|
|
879
|
+
|
|
880
|
+
this.__wake_body(rigidBody);
|
|
881
|
+
|
|
882
|
+
// Flag a snap on the body's Interpolated component, if any, so the
|
|
883
|
+
// interpolation producer (restore) and consumer (blend) treat this pose
|
|
884
|
+
// as authoritative this frame instead of interpolating across the jump.
|
|
885
|
+
const em = this.entityManager;
|
|
886
|
+
if (em !== null && em !== undefined) {
|
|
887
|
+
const dataset = em.dataset;
|
|
888
|
+
if (dataset !== null && dataset !== undefined) {
|
|
889
|
+
const entity = this.storage.entity_at(idx);
|
|
890
|
+
const interpolated = dataset.getComponent(entity, Interpolated);
|
|
891
|
+
if (interpolated !== undefined && interpolated !== null) {
|
|
892
|
+
interpolated.snap = true;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
832
898
|
/**
|
|
833
899
|
* Force the body awake. Static bodies are ignored.
|
|
834
900
|
* @param {RigidBody} rigidBody
|
|
@@ -1273,7 +1339,103 @@ export class PhysicsSystem extends System {
|
|
|
1273
1339
|
}
|
|
1274
1340
|
}
|
|
1275
1341
|
|
|
1342
|
+
/**
|
|
1343
|
+
* Producer — restore pass. At the top of a fixed step, reset every awake
|
|
1344
|
+
* {@link Interpolated} body's live components to their authoritative state
|
|
1345
|
+
* from the previous tick's snapshot, undoing any render-time interpolation
|
|
1346
|
+
* the {@link InterpolationSystem} wrote between frames so the sim integrates
|
|
1347
|
+
* from truth, not an interpolated pose. A body with no previous snapshot
|
|
1348
|
+
* (first step ever, or just woken) is left as-is — its live state is already
|
|
1349
|
+
* authoritative. No-op unless {@link interpolationLog} is wired.
|
|
1350
|
+
* @private
|
|
1351
|
+
*/
|
|
1352
|
+
__interp_restore() {
|
|
1353
|
+
const log = this.interpolationLog;
|
|
1354
|
+
const em = this.entityManager;
|
|
1355
|
+
if (log === null || em === null || em === undefined) return;
|
|
1356
|
+
const dataset = em.dataset;
|
|
1357
|
+
if (dataset === null || dataset === undefined) return;
|
|
1358
|
+
|
|
1359
|
+
const prev_tick = em.fixedStepTick - 1;
|
|
1360
|
+
if (prev_tick < 0) return;
|
|
1361
|
+
|
|
1362
|
+
const storage = this.storage;
|
|
1363
|
+
const count = storage.awake_count;
|
|
1364
|
+
const scratch = this.__interp_scratch;
|
|
1365
|
+
|
|
1366
|
+
for (let i = 0; i < count; i++) {
|
|
1367
|
+
const idx = storage.awake_at(i);
|
|
1368
|
+
const entity = storage.entity_at(idx);
|
|
1369
|
+
const interpolated = dataset.getComponent(entity, Interpolated);
|
|
1370
|
+
if (interpolated === undefined || interpolated === null) continue;
|
|
1371
|
+
// A teleported body (snap set, e.g. via setPose) keeps its live pose
|
|
1372
|
+
// this step — restoring the previous tick would undo the teleport.
|
|
1373
|
+
if (interpolated.snap) continue;
|
|
1374
|
+
const key = interpolated.key;
|
|
1375
|
+
if (key < 0) continue;
|
|
1376
|
+
|
|
1377
|
+
const interpolands = interpolated.interpolands;
|
|
1378
|
+
for (let k = 0; k < interpolands.length; k++) {
|
|
1379
|
+
const ip = interpolands[k];
|
|
1380
|
+
scratch.position = 0;
|
|
1381
|
+
// Snap to the previous tick's snapshot (both offsets equal → t irrelevant).
|
|
1382
|
+
const ok = log.interpolate(scratch, key, ip.type_id, prev_tick, prev_tick, 0, ip.interpolation_adapter);
|
|
1383
|
+
if (!ok) continue;
|
|
1384
|
+
const target = dataset.getComponent(entity, ip.component_class);
|
|
1385
|
+
if (target === undefined || target === null) continue;
|
|
1386
|
+
scratch.position = 0;
|
|
1387
|
+
ip.serialization_adapter.deserialize(scratch, target);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Producer — record pass. At the end of a fixed step, snapshot every awake
|
|
1394
|
+
* {@link Interpolated} body's live components into the shared log under the
|
|
1395
|
+
* current `entityManager.fixedStepTick`. The render-time
|
|
1396
|
+
* {@link InterpolationSystem} blends consecutive ticks from these snapshots.
|
|
1397
|
+
* Only awake (moving) bodies are recorded, so the log stays sparse. No-op
|
|
1398
|
+
* unless {@link interpolationLog} is wired.
|
|
1399
|
+
* @private
|
|
1400
|
+
*/
|
|
1401
|
+
__interp_record() {
|
|
1402
|
+
const log = this.interpolationLog;
|
|
1403
|
+
const em = this.entityManager;
|
|
1404
|
+
if (log === null || em === null || em === undefined) return;
|
|
1405
|
+
const dataset = em.dataset;
|
|
1406
|
+
if (dataset === null || dataset === undefined) return;
|
|
1407
|
+
|
|
1408
|
+
const tick = em.fixedStepTick;
|
|
1409
|
+
const storage = this.storage;
|
|
1410
|
+
const count = storage.awake_count;
|
|
1411
|
+
|
|
1412
|
+
log.begin_tick(tick);
|
|
1413
|
+
for (let i = 0; i < count; i++) {
|
|
1414
|
+
const idx = storage.awake_at(i);
|
|
1415
|
+
const entity = storage.entity_at(idx);
|
|
1416
|
+
const interpolated = dataset.getComponent(entity, Interpolated);
|
|
1417
|
+
if (interpolated === undefined || interpolated === null) continue;
|
|
1418
|
+
const key = interpolated.key;
|
|
1419
|
+
if (key < 0) continue;
|
|
1420
|
+
|
|
1421
|
+
const interpolands = interpolated.interpolands;
|
|
1422
|
+
for (let k = 0; k < interpolands.length; k++) {
|
|
1423
|
+
const ip = interpolands[k];
|
|
1424
|
+
const target = dataset.getComponent(entity, ip.component_class);
|
|
1425
|
+
if (target === undefined || target === null) continue;
|
|
1426
|
+
const buf = log.begin_record(key, ip.type_id);
|
|
1427
|
+
ip.serialization_adapter.serialize(buf, target);
|
|
1428
|
+
log.end_record();
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
log.end_tick();
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1276
1434
|
fixedUpdate(dt) {
|
|
1435
|
+
// Producer: restore authoritative pose (undo render interpolation)
|
|
1436
|
+
// before the sim reads any Transform this step.
|
|
1437
|
+
this.__interp_restore();
|
|
1438
|
+
|
|
1277
1439
|
const gx = this.gravity.x;
|
|
1278
1440
|
const gy = this.gravity.y;
|
|
1279
1441
|
const gz = this.gravity.z;
|
|
@@ -1454,6 +1616,12 @@ export class PhysicsSystem extends System {
|
|
|
1454
1616
|
ccd_resolve(this);
|
|
1455
1617
|
}
|
|
1456
1618
|
|
|
1619
|
+
// Producer: record the post-step authoritative pose of every awake
|
|
1620
|
+
// interpolated body under this step's tick. Before the sleep test, so a
|
|
1621
|
+
// body that settles this step still records its final pose for the last
|
|
1622
|
+
// interpolation interval.
|
|
1623
|
+
this.__interp_record();
|
|
1624
|
+
|
|
1457
1625
|
// Stage 9: sleep test.
|
|
1458
1626
|
this.__sleep_test(dt);
|
|
1459
1627
|
|