spoint 0.1.58 → 0.1.60
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/client/app.js +23 -3
- package/package.json +1 -1
- package/src/client/JitterBuffer.js +76 -153
- package/src/client/KalmanFilter.js +68 -71
- package/src/client/SmoothInterpolation.js +65 -91
package/client/app.js
CHANGED
|
@@ -686,6 +686,7 @@ const entityGroups = new Map()
|
|
|
686
686
|
const appModules = new Map()
|
|
687
687
|
const entityAppMap = new Map()
|
|
688
688
|
const playerTargets = new Map()
|
|
689
|
+
const entityTargets = new Map()
|
|
689
690
|
let inputHandler = null
|
|
690
691
|
const uiRoot = document.getElementById('ui-root')
|
|
691
692
|
const clickPrompt = document.getElementById('click-prompt')
|
|
@@ -1047,8 +1048,13 @@ const client = new PhysicsNetworkClient({
|
|
|
1047
1048
|
}
|
|
1048
1049
|
for (const e of smoothState.entities) {
|
|
1049
1050
|
const mesh = entityMeshes.get(e.id)
|
|
1050
|
-
if (mesh && e.position)
|
|
1051
|
-
|
|
1051
|
+
if (mesh && e.position) {
|
|
1052
|
+
const et = entityTargets.get(e.id)
|
|
1053
|
+
if (et) { et.x = e.position[0]; et.y = e.position[1]; et.z = e.position[2]; et.rx = e.rotation?.[0] || 0; et.ry = e.rotation?.[1] || 0; et.rz = e.rotation?.[2] || 0; et.rw = e.rotation?.[3] || 1 }
|
|
1054
|
+
else entityTargets.set(e.id, { x: e.position[0], y: e.position[1], z: e.position[2], rx: e.rotation?.[0] || 0, ry: e.rotation?.[1] || 0, rz: e.rotation?.[2] || 0, rw: e.rotation?.[3] || 1 })
|
|
1055
|
+
const dx = e.position[0] - mesh.position.x, dy = e.position[1] - mesh.position.y, dz = e.position[2] - mesh.position.z
|
|
1056
|
+
if (!mesh.userData.entInit || dx * dx + dy * dy + dz * dz > 100) { mesh.position.set(e.position[0], e.position[1], e.position[2]); if (e.rotation) mesh.quaternion.set(e.rotation[0], e.rotation[1], e.rotation[2], e.rotation[3]); mesh.userData.entInit = true }
|
|
1057
|
+
}
|
|
1052
1058
|
if (!entityMeshes.has(e.id)) loadEntityModel(e.id, e)
|
|
1053
1059
|
}
|
|
1054
1060
|
rebuildEntityHierarchy(smoothState.entities)
|
|
@@ -1064,7 +1070,7 @@ const client = new PhysicsNetworkClient({
|
|
|
1064
1070
|
onPlayerJoined: (id) => { if (!playerMeshes.has(id)) createPlayerVRM(id) },
|
|
1065
1071
|
onPlayerLeft: (id) => removePlayerMesh(id),
|
|
1066
1072
|
onEntityAdded: (id, state) => loadEntityModel(id, state),
|
|
1067
|
-
onEntityRemoved: (id) => { const m = entityMeshes.get(id); if (m) { scene.remove(m); m.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose() }); entityMeshes.delete(id) }; pendingLoads.delete(id) },
|
|
1073
|
+
onEntityRemoved: (id) => { const m = entityMeshes.get(id); if (m) { scene.remove(m); m.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose() }); entityMeshes.delete(id) }; entityTargets.delete(id); pendingLoads.delete(id) },
|
|
1068
1074
|
onWorldDef: (wd) => {
|
|
1069
1075
|
loadingMgr.setStage('SERVER_SYNC')
|
|
1070
1076
|
worldConfig = wd
|
|
@@ -1212,6 +1218,7 @@ async function initAR() {
|
|
|
1212
1218
|
arEnabled = true
|
|
1213
1219
|
scene.background = null
|
|
1214
1220
|
ground.visible = false
|
|
1221
|
+
renderer.domElement.style.display = 'none'
|
|
1215
1222
|
console.log('[AR] AR mode started')
|
|
1216
1223
|
return true
|
|
1217
1224
|
}
|
|
@@ -1221,6 +1228,7 @@ async function initAR() {
|
|
|
1221
1228
|
arEnabled = false
|
|
1222
1229
|
scene.background = new THREE.Color(0x87ceeb)
|
|
1223
1230
|
ground.visible = true
|
|
1231
|
+
renderer.domElement.style.display = 'block'
|
|
1224
1232
|
if (arButton) {
|
|
1225
1233
|
arButton.textContent = 'Enter XR'
|
|
1226
1234
|
arButton.style.background = 'rgba(0, 150, 0, 0.8)'
|
|
@@ -1494,6 +1502,18 @@ function animate(timestamp) {
|
|
|
1494
1502
|
if (features?._headBone) features._headBone.rotation.x = -(ps.lookPitch || 0) * 0.6
|
|
1495
1503
|
}
|
|
1496
1504
|
})
|
|
1505
|
+
entityTargets.forEach((target, id) => {
|
|
1506
|
+
const mesh = entityMeshes.get(id)
|
|
1507
|
+
if (!mesh) return
|
|
1508
|
+
mesh.position.x += (target.x - mesh.position.x) * lerpFactor
|
|
1509
|
+
mesh.position.y += (target.y - mesh.position.y) * lerpFactor
|
|
1510
|
+
mesh.position.z += (target.z - mesh.position.z) * lerpFactor
|
|
1511
|
+
mesh.quaternion.x += (target.rx - mesh.quaternion.x) * lerpFactor
|
|
1512
|
+
mesh.quaternion.y += (target.ry - mesh.quaternion.y) * lerpFactor
|
|
1513
|
+
mesh.quaternion.z += (target.rz - mesh.quaternion.z) * lerpFactor
|
|
1514
|
+
mesh.quaternion.w += (target.rw - mesh.quaternion.w) * lerpFactor
|
|
1515
|
+
mesh.quaternion.normalize()
|
|
1516
|
+
})
|
|
1497
1517
|
entityMeshes.forEach((mesh) => {
|
|
1498
1518
|
if (mesh.userData.spin) mesh.rotation.y += mesh.userData.spin * frameDt
|
|
1499
1519
|
if (mesh.userData.hover) {
|
package/package.json
CHANGED
|
@@ -1,207 +1,130 @@
|
|
|
1
1
|
export class JitterBuffer {
|
|
2
2
|
constructor(config = {}) {
|
|
3
|
-
this.maxSize = config.maxSize ||
|
|
4
|
-
this.maxAge = config.maxAge || 200
|
|
3
|
+
this.maxSize = config.maxSize || 64
|
|
5
4
|
this.minBufferSize = config.minBufferSize || 2
|
|
6
|
-
this.
|
|
7
|
-
|
|
5
|
+
this.baseDelay = config.baseDelay || 30
|
|
6
|
+
|
|
8
7
|
this.buffer = []
|
|
9
|
-
this.lastProcessTime = 0
|
|
10
8
|
this.lastServerTime = 0
|
|
11
|
-
this.
|
|
9
|
+
this.lastClientTime = 0
|
|
12
10
|
this.rtt = config.initialRtt || 50
|
|
13
11
|
this.rttVariance = 0
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
16
|
-
this.lastClientTime = 0
|
|
12
|
+
this.jitter = 0
|
|
13
|
+
this.targetDelay = this.baseDelay
|
|
17
14
|
}
|
|
18
|
-
|
|
15
|
+
|
|
19
16
|
addSnapshot(snapshot) {
|
|
20
17
|
const now = Date.now()
|
|
21
|
-
const clientTime = now
|
|
22
18
|
const serverTime = snapshot.timestamp || now
|
|
23
|
-
|
|
24
|
-
if (this.lastServerTime > 0) {
|
|
19
|
+
|
|
20
|
+
if (this.lastServerTime > 0 && this.lastClientTime > 0) {
|
|
25
21
|
const serverDelta = serverTime - this.lastServerTime
|
|
26
|
-
const clientDelta =
|
|
27
|
-
|
|
22
|
+
const clientDelta = now - this.lastClientTime
|
|
28
23
|
if (serverDelta > 0 && clientDelta > 0) {
|
|
29
|
-
const
|
|
30
|
-
this.
|
|
31
|
-
this.clockDelta = this.clockDelta * 0.9 + instantClockDelta * 0.1
|
|
24
|
+
const instantJitter = Math.abs(clientDelta - serverDelta)
|
|
25
|
+
this.jitter = this.jitter * 0.9 + instantJitter * 0.1
|
|
32
26
|
}
|
|
33
27
|
}
|
|
34
|
-
|
|
28
|
+
|
|
35
29
|
this.lastServerTime = serverTime
|
|
36
|
-
this.lastClientTime =
|
|
37
|
-
|
|
38
|
-
this.buffer.push({
|
|
39
|
-
snapshot,
|
|
40
|
-
clientTime,
|
|
41
|
-
serverTime,
|
|
42
|
-
tick: snapshot.tick || 0
|
|
43
|
-
})
|
|
44
|
-
|
|
30
|
+
this.lastClientTime = now
|
|
31
|
+
|
|
32
|
+
this.buffer.push({ snapshot, clientTime: now, serverTime, tick: snapshot.tick || 0 })
|
|
45
33
|
this.buffer.sort((a, b) => a.tick - b.tick)
|
|
46
|
-
|
|
47
|
-
while (this.buffer.length > this.maxSize)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
_pruneOld(now) {
|
|
55
|
-
const cutoff = now - this.maxAge
|
|
56
|
-
while (this.buffer.length > 0 && this.buffer[0].clientTime < cutoff) {
|
|
57
|
-
this.buffer.shift()
|
|
58
|
-
}
|
|
34
|
+
|
|
35
|
+
while (this.buffer.length > this.maxSize) this.buffer.shift()
|
|
36
|
+
|
|
37
|
+
const maxAge = Math.max(300, this.rtt + this.jitter * 3)
|
|
38
|
+
const cutoff = now - maxAge
|
|
39
|
+
while (this.buffer.length > 0 && this.buffer[0].clientTime < cutoff) this.buffer.shift()
|
|
59
40
|
}
|
|
60
|
-
|
|
41
|
+
|
|
61
42
|
getSnapshotToRender(now = Date.now()) {
|
|
43
|
+
if (this.buffer.length === 0) return null
|
|
62
44
|
if (this.buffer.length < this.minBufferSize) {
|
|
63
|
-
if (this.buffer.length === 0) return null
|
|
64
45
|
return this.buffer[this.buffer.length - 1].snapshot
|
|
65
46
|
}
|
|
66
|
-
|
|
47
|
+
|
|
67
48
|
const renderTime = now - this.targetDelay
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (renderTime
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (renderTime <= oldest.clientTime) {
|
|
77
|
-
return oldest.snapshot
|
|
78
|
-
}
|
|
79
|
-
|
|
49
|
+
const newest = this.buffer[this.buffer.length - 1]
|
|
50
|
+
const oldest = this.buffer[0]
|
|
51
|
+
|
|
52
|
+
if (renderTime >= newest.clientTime) return newest.snapshot
|
|
53
|
+
if (renderTime <= oldest.clientTime) return oldest.snapshot
|
|
54
|
+
|
|
80
55
|
for (let i = 0; i < this.buffer.length - 1; i++) {
|
|
81
56
|
const curr = this.buffer[i]
|
|
82
57
|
const next = this.buffer[i + 1]
|
|
83
|
-
|
|
84
58
|
if (renderTime >= curr.clientTime && renderTime <= next.clientTime) {
|
|
85
59
|
const range = next.clientTime - curr.clientTime
|
|
86
60
|
if (range === 0) return curr.snapshot
|
|
87
|
-
|
|
88
61
|
const alpha = (renderTime - curr.clientTime) / range
|
|
89
62
|
return this._interpolateSnapshots(curr.snapshot, next.snapshot, alpha)
|
|
90
63
|
}
|
|
91
64
|
}
|
|
92
|
-
|
|
65
|
+
|
|
93
66
|
return newest.snapshot
|
|
94
67
|
}
|
|
95
|
-
|
|
68
|
+
|
|
96
69
|
_interpolateSnapshots(older, newer, alpha) {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
entities: []
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const olderPlayers = new Map()
|
|
105
|
-
for (const p of older.players || []) {
|
|
106
|
-
olderPlayers.set(p.id, p)
|
|
107
|
-
}
|
|
108
|
-
|
|
70
|
+
const result = { tick: newer.tick, timestamp: newer.timestamp, players: [], entities: [] }
|
|
71
|
+
|
|
72
|
+
const oldP = new Map()
|
|
73
|
+
for (const p of older.players || []) oldP.set(p.id, p)
|
|
109
74
|
for (const np of newer.players || []) {
|
|
110
|
-
const op =
|
|
75
|
+
const op = oldP.get(np.id)
|
|
111
76
|
if (op) {
|
|
112
|
-
|
|
77
|
+
result.players.push({
|
|
78
|
+
id: np.id,
|
|
79
|
+
position: [_l(op.position[0], np.position[0], alpha), _l(op.position[1], np.position[1], alpha), _l(op.position[2], np.position[2], alpha)],
|
|
80
|
+
rotation: np.rotation,
|
|
81
|
+
velocity: [_l(op.velocity?.[0] || 0, np.velocity?.[0] || 0, alpha), _l(op.velocity?.[1] || 0, np.velocity?.[1] || 0, alpha), _l(op.velocity?.[2] || 0, np.velocity?.[2] || 0, alpha)],
|
|
82
|
+
onGround: np.onGround, health: np.health, inputSequence: np.inputSequence,
|
|
83
|
+
crouch: np.crouch,
|
|
84
|
+
lookPitch: _l(op.lookPitch || 0, np.lookPitch || 0, alpha),
|
|
85
|
+
lookYaw: _l(op.lookYaw || 0, np.lookYaw || 0, alpha)
|
|
86
|
+
})
|
|
113
87
|
} else {
|
|
114
|
-
|
|
88
|
+
result.players.push({ ...np })
|
|
115
89
|
}
|
|
116
90
|
}
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
for (const e of older.entities || [])
|
|
120
|
-
olderEntities.set(e.id, e)
|
|
121
|
-
}
|
|
122
|
-
|
|
91
|
+
|
|
92
|
+
const oldE = new Map()
|
|
93
|
+
for (const e of older.entities || []) oldE.set(e.id, e)
|
|
123
94
|
for (const ne of newer.entities || []) {
|
|
124
|
-
const oe =
|
|
95
|
+
const oe = oldE.get(ne.id)
|
|
125
96
|
if (oe) {
|
|
126
|
-
|
|
97
|
+
result.entities.push({
|
|
98
|
+
id: ne.id, model: ne.model,
|
|
99
|
+
position: [_l(oe.position[0], ne.position[0], alpha), _l(oe.position[1], ne.position[1], alpha), _l(oe.position[2], ne.position[2], alpha)],
|
|
100
|
+
rotation: [_l(oe.rotation[0], ne.rotation[0], alpha), _l(oe.rotation[1], ne.rotation[1], alpha), _l(oe.rotation[2], ne.rotation[2], alpha), _l(oe.rotation[3], ne.rotation[3], alpha)],
|
|
101
|
+
bodyType: ne.bodyType, custom: ne.custom
|
|
102
|
+
})
|
|
127
103
|
} else {
|
|
128
|
-
|
|
104
|
+
result.entities.push({ ...ne })
|
|
129
105
|
}
|
|
130
106
|
}
|
|
131
|
-
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
_interpolatePlayer(older, newer, alpha) {
|
|
136
|
-
return {
|
|
137
|
-
id: newer.id,
|
|
138
|
-
position: [
|
|
139
|
-
this._lerp(older.position[0], newer.position[0], alpha),
|
|
140
|
-
this._lerp(older.position[1], newer.position[1], alpha),
|
|
141
|
-
this._lerp(older.position[2], newer.position[2], alpha)
|
|
142
|
-
],
|
|
143
|
-
rotation: newer.rotation,
|
|
144
|
-
velocity: [
|
|
145
|
-
this._lerp(older.velocity?.[0] || 0, newer.velocity?.[0] || 0, alpha),
|
|
146
|
-
this._lerp(older.velocity?.[1] || 0, newer.velocity?.[1] || 0, alpha),
|
|
147
|
-
this._lerp(older.velocity?.[2] || 0, newer.velocity?.[2] || 0, alpha)
|
|
148
|
-
],
|
|
149
|
-
onGround: newer.onGround,
|
|
150
|
-
health: this._lerp(older.health || 100, newer.health || 100, alpha),
|
|
151
|
-
inputSequence: newer.inputSequence,
|
|
152
|
-
crouch: newer.crouch,
|
|
153
|
-
lookPitch: this._lerp(older.lookPitch || 0, newer.lookPitch || 0, alpha),
|
|
154
|
-
lookYaw: this._lerp(older.lookYaw || 0, newer.lookYaw || 0, alpha)
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
_interpolateEntity(older, newer, alpha) {
|
|
159
|
-
return {
|
|
160
|
-
id: newer.id,
|
|
161
|
-
model: newer.model,
|
|
162
|
-
position: [
|
|
163
|
-
this._lerp(older.position[0], newer.position[0], alpha),
|
|
164
|
-
this._lerp(older.position[1], newer.position[1], alpha),
|
|
165
|
-
this._lerp(older.position[2], newer.position[2], alpha)
|
|
166
|
-
],
|
|
167
|
-
rotation: [
|
|
168
|
-
this._lerp(older.rotation[0], newer.rotation[0], alpha),
|
|
169
|
-
this._lerp(older.rotation[1], newer.rotation[1], alpha),
|
|
170
|
-
this._lerp(older.rotation[2], newer.rotation[2], alpha),
|
|
171
|
-
this._lerp(older.rotation[3], newer.rotation[3], alpha)
|
|
172
|
-
],
|
|
173
|
-
bodyType: newer.bodyType,
|
|
174
|
-
custom: newer.custom
|
|
175
|
-
}
|
|
107
|
+
|
|
108
|
+
return result
|
|
176
109
|
}
|
|
177
|
-
|
|
178
|
-
_lerp(a, b, t) {
|
|
179
|
-
return a + (b - a) * t
|
|
180
|
-
}
|
|
181
|
-
|
|
110
|
+
|
|
182
111
|
updateRTT(pingTime, pongTime) {
|
|
183
|
-
const
|
|
184
|
-
this.rttVariance = this.rttVariance * 0.75 + Math.abs(
|
|
185
|
-
this.rtt = this.rtt * 0.875 +
|
|
186
|
-
|
|
187
|
-
this.targetDelay = Math.min(100, Math.max(20, this.rtt / 2 + this.rttVariance))
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
getBufferHealth() {
|
|
191
|
-
return this.buffer.length
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
getRTT() {
|
|
195
|
-
return this.rtt
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
getClockDelta() {
|
|
199
|
-
return this.clockDelta
|
|
112
|
+
const instant = pongTime - pingTime
|
|
113
|
+
this.rttVariance = this.rttVariance * 0.75 + Math.abs(instant - this.rtt) * 0.25
|
|
114
|
+
this.rtt = this.rtt * 0.875 + instant * 0.125
|
|
115
|
+
this.targetDelay = this.baseDelay + this.rtt * 0.5 + this.jitter * 2
|
|
200
116
|
}
|
|
201
|
-
|
|
117
|
+
|
|
118
|
+
getBufferHealth() { return this.buffer.length }
|
|
119
|
+
getRTT() { return this.rtt }
|
|
120
|
+
getJitter() { return this.jitter }
|
|
121
|
+
getTargetDelay() { return this.targetDelay }
|
|
122
|
+
|
|
202
123
|
clear() {
|
|
203
124
|
this.buffer = []
|
|
204
125
|
this.lastServerTime = 0
|
|
205
126
|
this.lastClientTime = 0
|
|
206
127
|
}
|
|
207
|
-
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function _l(a, b, t) { return a + (b - a) * t }
|
|
@@ -1,89 +1,92 @@
|
|
|
1
1
|
export class KalmanFilter3D {
|
|
2
2
|
constructor(config = {}) {
|
|
3
|
-
this.
|
|
4
|
-
this.
|
|
5
|
-
this.
|
|
6
|
-
|
|
3
|
+
this.positionQ = config.positionQ ?? 2.0
|
|
4
|
+
this.velocityQ = config.velocityQ ?? 4.0
|
|
5
|
+
this.positionR = config.positionR ?? 0.01
|
|
6
|
+
this.velocityR = config.velocityR ?? 0.1
|
|
7
|
+
|
|
7
8
|
this.x = [0, 0, 0]
|
|
8
9
|
this.v = [0, 0, 0]
|
|
9
|
-
|
|
10
|
-
this.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
[0, 0, this.uncertainty]
|
|
14
|
-
]
|
|
15
|
-
|
|
10
|
+
|
|
11
|
+
this.Pp = [1, 1, 1]
|
|
12
|
+
this.Pv = [1, 1, 1]
|
|
13
|
+
|
|
16
14
|
this.initialized = false
|
|
15
|
+
this._prevPos = null
|
|
16
|
+
this._lastUpdateMs = 0
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
init(position, velocity =
|
|
18
|
+
|
|
19
|
+
init(position, velocity = null, now = Date.now()) {
|
|
20
20
|
this.x = [...position]
|
|
21
|
-
this.v = [...velocity]
|
|
21
|
+
this.v = velocity ? [...velocity] : [0, 0, 0]
|
|
22
|
+
this._prevPos = [...position]
|
|
23
|
+
this._lastUpdateMs = now
|
|
22
24
|
this.initialized = true
|
|
23
25
|
}
|
|
24
|
-
|
|
26
|
+
|
|
25
27
|
predict(dt) {
|
|
26
|
-
if (!this.initialized) return { position: this.x, velocity: this.v }
|
|
27
|
-
|
|
28
|
+
if (!this.initialized || dt <= 0) return { position: [...this.x], velocity: [...this.v] }
|
|
29
|
+
|
|
28
30
|
for (let i = 0; i < 3; i++) {
|
|
29
31
|
this.x[i] += this.v[i] * dt
|
|
32
|
+
this.Pp[i] += this.positionQ * dt
|
|
33
|
+
this.Pv[i] += this.velocityQ * dt
|
|
30
34
|
}
|
|
31
|
-
|
|
32
|
-
const q = this.processNoise * dt * dt
|
|
33
|
-
for (let i = 0; i < 3; i++) {
|
|
34
|
-
this.P[i][i] += q
|
|
35
|
-
}
|
|
36
|
-
|
|
35
|
+
|
|
37
36
|
return { position: [...this.x], velocity: [...this.v] }
|
|
38
37
|
}
|
|
39
|
-
|
|
40
|
-
update(measuredPosition, measuredVelocity = null) {
|
|
38
|
+
|
|
39
|
+
update(measuredPosition, measuredVelocity = null, now = Date.now()) {
|
|
41
40
|
if (!this.initialized) {
|
|
42
|
-
this.init(measuredPosition, measuredVelocity
|
|
41
|
+
this.init(measuredPosition, measuredVelocity, now)
|
|
43
42
|
return { position: [...this.x], velocity: [...this.v] }
|
|
44
43
|
}
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
44
|
+
|
|
45
|
+
const elapsedMs = now - this._lastUpdateMs
|
|
46
|
+
if (elapsedMs < 1) return { position: [...this.x], velocity: [...this.v] }
|
|
47
|
+
const elapsed = elapsedMs / 1000
|
|
48
|
+
this._lastUpdateMs = now
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < 3; i++) {
|
|
51
|
+
this.x[i] += this.v[i] * elapsed
|
|
52
|
+
this.Pp[i] += this.positionQ * elapsed
|
|
53
|
+
this.Pv[i] += this.velocityQ * elapsed
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
for (let i = 0; i < 3; i++) {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
const Kp = this.Pp[i] / (this.Pp[i] + this.positionR)
|
|
58
|
+
this.x[i] += Kp * (measuredPosition[i] - this.x[i])
|
|
59
|
+
this.Pp[i] = (1 - Kp) * this.Pp[i]
|
|
60
|
+
|
|
61
|
+
let measuredV
|
|
54
62
|
if (measuredVelocity) {
|
|
55
|
-
|
|
63
|
+
measuredV = measuredVelocity[i]
|
|
64
|
+
} else if (this._prevPos) {
|
|
65
|
+
measuredV = (measuredPosition[i] - this._prevPos[i]) / elapsed
|
|
66
|
+
} else {
|
|
67
|
+
measuredV = 0
|
|
56
68
|
}
|
|
57
|
-
|
|
58
|
-
this.
|
|
69
|
+
|
|
70
|
+
const Kv = this.Pv[i] / (this.Pv[i] + this.velocityR)
|
|
71
|
+
this.v[i] += Kv * (measuredV - this.v[i])
|
|
72
|
+
this.Pv[i] = (1 - Kv) * this.Pv[i]
|
|
59
73
|
}
|
|
60
|
-
|
|
74
|
+
|
|
75
|
+
this._prevPos = [...measuredPosition]
|
|
61
76
|
return { position: [...this.x], velocity: [...this.v] }
|
|
62
77
|
}
|
|
63
|
-
|
|
64
|
-
getState() {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
setPosition(pos) {
|
|
72
|
-
this.x = [...pos]
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
setVelocity(vel) {
|
|
76
|
-
this.v = [...vel]
|
|
77
|
-
}
|
|
78
|
-
|
|
78
|
+
|
|
79
|
+
getState() { return { position: [...this.x], velocity: [...this.v] } }
|
|
80
|
+
setPosition(pos) { this.x = [...pos]; this._prevPos = [...pos] }
|
|
81
|
+
setVelocity(vel) { this.v = [...vel] }
|
|
82
|
+
|
|
79
83
|
reset(position = [0, 0, 0]) {
|
|
80
84
|
this.x = [...position]
|
|
81
85
|
this.v = [0, 0, 0]
|
|
82
|
-
this.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
]
|
|
86
|
+
this.Pp = [1, 1, 1]
|
|
87
|
+
this.Pv = [1, 1, 1]
|
|
88
|
+
this._prevPos = null
|
|
89
|
+
this._lastUpdateMs = 0
|
|
87
90
|
this.initialized = false
|
|
88
91
|
}
|
|
89
92
|
}
|
|
@@ -94,7 +97,7 @@ export class SmoothStateTracker {
|
|
|
94
97
|
this.maxAge = config.maxAge || 5000
|
|
95
98
|
this.defaultConfig = config.filterConfig || {}
|
|
96
99
|
}
|
|
97
|
-
|
|
100
|
+
|
|
98
101
|
getFilter(id) {
|
|
99
102
|
let filter = this.filters.get(id)
|
|
100
103
|
if (!filter) {
|
|
@@ -103,23 +106,17 @@ export class SmoothStateTracker {
|
|
|
103
106
|
}
|
|
104
107
|
return filter
|
|
105
108
|
}
|
|
106
|
-
|
|
109
|
+
|
|
107
110
|
update(id, position, velocity, dt) {
|
|
108
111
|
const filter = this.getFilter(id)
|
|
109
|
-
filter.predict(dt)
|
|
110
112
|
return filter.update(position, velocity)
|
|
111
113
|
}
|
|
112
|
-
|
|
114
|
+
|
|
113
115
|
predict(id, dt) {
|
|
114
116
|
const filter = this.getFilter(id)
|
|
115
117
|
return filter.predict(dt)
|
|
116
118
|
}
|
|
117
|
-
|
|
118
|
-
remove(id) {
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
clear() {
|
|
123
|
-
this.filters.clear()
|
|
124
|
-
}
|
|
119
|
+
|
|
120
|
+
remove(id) { this.filters.delete(id) }
|
|
121
|
+
clear() { this.filters.clear() }
|
|
125
122
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KalmanFilter3D
|
|
1
|
+
import { KalmanFilter3D } from './KalmanFilter.js'
|
|
2
2
|
import { JitterBuffer } from './JitterBuffer.js'
|
|
3
3
|
|
|
4
4
|
export class SmoothInterpolation {
|
|
@@ -6,122 +6,96 @@ export class SmoothInterpolation {
|
|
|
6
6
|
this.jitterBuffer = new JitterBuffer(config.jitter || {})
|
|
7
7
|
this.playerFilters = new Map()
|
|
8
8
|
this.entityFilters = new Map()
|
|
9
|
-
this.
|
|
10
|
-
|
|
11
|
-
measurementNoise: 0.3,
|
|
12
|
-
uncertainty: 0.5
|
|
9
|
+
this.playerKalmanConfig = config.playerKalman || {
|
|
10
|
+
positionQ: 2.0, velocityQ: 4.0, positionR: 0.01, velocityR: 0.1
|
|
13
11
|
}
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
this.entityKalmanConfig = config.entityKalman || {
|
|
13
|
+
positionQ: 2.0, velocityQ: 4.0, positionR: 0.01, velocityR: 0.5
|
|
14
|
+
}
|
|
15
|
+
|
|
16
16
|
this.localPlayerId = null
|
|
17
17
|
this.predictionEnabled = config.predictionEnabled !== false
|
|
18
|
-
this.
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
setLocalPlayer(id) {
|
|
22
|
-
this.localPlayerId = id
|
|
18
|
+
this._lastDisplayTime = 0
|
|
23
19
|
}
|
|
24
|
-
|
|
20
|
+
|
|
21
|
+
setLocalPlayer(id) { this.localPlayerId = id }
|
|
22
|
+
|
|
25
23
|
addSnapshot(snapshot) {
|
|
26
24
|
this.jitterBuffer.addSnapshot(snapshot)
|
|
25
|
+
const now = Date.now()
|
|
26
|
+
for (const p of snapshot.players || []) {
|
|
27
|
+
if (p.id === this.localPlayerId && this.predictionEnabled) continue
|
|
28
|
+
let filter = this.playerFilters.get(p.id)
|
|
29
|
+
if (!filter) {
|
|
30
|
+
filter = new KalmanFilter3D(this.playerKalmanConfig)
|
|
31
|
+
this.playerFilters.set(p.id, filter)
|
|
32
|
+
}
|
|
33
|
+
filter.update(p.position, p.velocity, now)
|
|
34
|
+
}
|
|
35
|
+
for (const e of snapshot.entities || []) {
|
|
36
|
+
let filter = this.entityFilters.get(e.id)
|
|
37
|
+
if (!filter) {
|
|
38
|
+
filter = new KalmanFilter3D(this.entityKalmanConfig)
|
|
39
|
+
this.entityFilters.set(e.id, filter)
|
|
40
|
+
}
|
|
41
|
+
filter.update(e.position, null, now)
|
|
42
|
+
}
|
|
27
43
|
}
|
|
28
|
-
|
|
44
|
+
|
|
29
45
|
getDisplayState(now = Date.now()) {
|
|
30
46
|
const snapshot = this.jitterBuffer.getSnapshotToRender(now)
|
|
31
47
|
if (!snapshot) return { players: [], entities: [] }
|
|
32
|
-
|
|
33
|
-
const dt = Math.min((now - this.
|
|
34
|
-
this.
|
|
35
|
-
|
|
48
|
+
|
|
49
|
+
const dt = this._lastDisplayTime > 0 ? Math.min((now - this._lastDisplayTime) / 1000, 0.1) : 0
|
|
50
|
+
this._lastDisplayTime = now
|
|
51
|
+
|
|
36
52
|
const displayPlayers = []
|
|
37
53
|
for (const player of snapshot.players || []) {
|
|
38
54
|
if (player.id === this.localPlayerId && this.predictionEnabled) {
|
|
39
55
|
displayPlayers.push(player)
|
|
40
56
|
continue
|
|
41
57
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
const filter = this.playerFilters.get(player.id)
|
|
59
|
+
if (filter && dt > 0) {
|
|
60
|
+
const predicted = filter.predict(dt)
|
|
61
|
+
displayPlayers.push({ ...player, position: predicted.position, velocity: predicted.velocity })
|
|
62
|
+
} else {
|
|
63
|
+
displayPlayers.push(player)
|
|
64
|
+
}
|
|
45
65
|
}
|
|
46
|
-
|
|
66
|
+
|
|
47
67
|
const displayEntities = []
|
|
48
68
|
for (const entity of snapshot.entities || []) {
|
|
49
|
-
const
|
|
50
|
-
|
|
69
|
+
const filter = this.entityFilters.get(entity.id)
|
|
70
|
+
if (filter && dt > 0) {
|
|
71
|
+
const predicted = filter.predict(dt)
|
|
72
|
+
displayEntities.push({ ...entity, position: predicted.position })
|
|
73
|
+
} else {
|
|
74
|
+
displayEntities.push(entity)
|
|
75
|
+
}
|
|
51
76
|
}
|
|
52
|
-
|
|
77
|
+
|
|
53
78
|
return { players: displayPlayers, entities: displayEntities }
|
|
54
79
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
...player,
|
|
67
|
-
position: state.position,
|
|
68
|
-
velocity: state.velocity
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
_smoothEntity(entity, dt) {
|
|
73
|
-
let filter = this.entityFilters.get(entity.id)
|
|
74
|
-
if (!filter) {
|
|
75
|
-
filter = new KalmanFilter3D(this.kalmanConfig)
|
|
76
|
-
this.entityFilters.set(entity.id, filter)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const state = filter.update(entity.position)
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
...entity,
|
|
83
|
-
position: state.position
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
predictStep(dt) {
|
|
88
|
-
for (const [id, filter] of this.playerFilters) {
|
|
89
|
-
filter.predict(dt)
|
|
90
|
-
}
|
|
91
|
-
for (const [id, filter] of this.entityFilters) {
|
|
92
|
-
filter.predict(dt)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
removePlayer(id) {
|
|
97
|
-
this.playerFilters.delete(id)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
removeEntity(id) {
|
|
101
|
-
this.entityFilters.delete(id)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
updateRTT(pingTime, pongTime) {
|
|
105
|
-
this.jitterBuffer.updateRTT(pingTime, pongTime)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
getRTT() {
|
|
109
|
-
return this.jitterBuffer.getRTT()
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
getBufferHealth() {
|
|
113
|
-
return this.jitterBuffer.getBufferHealth()
|
|
114
|
-
}
|
|
115
|
-
|
|
80
|
+
|
|
81
|
+
removePlayer(id) { this.playerFilters.delete(id) }
|
|
82
|
+
removeEntity(id) { this.entityFilters.delete(id) }
|
|
83
|
+
|
|
84
|
+
updateRTT(pingTime, pongTime) { this.jitterBuffer.updateRTT(pingTime, pongTime) }
|
|
85
|
+
getRTT() { return this.jitterBuffer.getRTT() }
|
|
86
|
+
getJitter() { return this.jitterBuffer.getJitter() }
|
|
87
|
+
getTargetDelay() { return this.jitterBuffer.getTargetDelay() }
|
|
88
|
+
getBufferHealth() { return this.jitterBuffer.getBufferHealth() }
|
|
89
|
+
|
|
116
90
|
reset() {
|
|
117
91
|
this.jitterBuffer.clear()
|
|
118
92
|
this.playerFilters.clear()
|
|
119
93
|
this.entityFilters.clear()
|
|
94
|
+
this._lastDisplayTime = 0
|
|
120
95
|
}
|
|
121
|
-
|
|
96
|
+
|
|
122
97
|
setConfig(config) {
|
|
123
|
-
if (config.
|
|
124
|
-
|
|
125
|
-
}
|
|
98
|
+
if (config.playerKalman) this.playerKalmanConfig = { ...this.playerKalmanConfig, ...config.playerKalman }
|
|
99
|
+
if (config.entityKalman) this.entityKalmanConfig = { ...this.entityKalmanConfig, ...config.entityKalman }
|
|
126
100
|
}
|
|
127
|
-
}
|
|
101
|
+
}
|