@rapierphysicsplugin/client 1.0.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/dist/__tests__/clock-sync.test.d.ts +2 -0
- package/dist/__tests__/clock-sync.test.d.ts.map +1 -0
- package/dist/__tests__/clock-sync.test.js +63 -0
- package/dist/__tests__/clock-sync.test.js.map +1 -0
- package/dist/__tests__/interpolator.test.d.ts +2 -0
- package/dist/__tests__/interpolator.test.d.ts.map +1 -0
- package/dist/__tests__/interpolator.test.js +82 -0
- package/dist/__tests__/interpolator.test.js.map +1 -0
- package/dist/__tests__/state-reconciler.test.d.ts +2 -0
- package/dist/__tests__/state-reconciler.test.d.ts.map +1 -0
- package/dist/__tests__/state-reconciler.test.js +86 -0
- package/dist/__tests__/state-reconciler.test.js.map +1 -0
- package/dist/clock-sync.d.ts +17 -0
- package/dist/clock-sync.d.ts.map +1 -0
- package/dist/clock-sync.js +63 -0
- package/dist/clock-sync.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/input-manager.d.ts +18 -0
- package/dist/input-manager.d.ts.map +1 -0
- package/dist/input-manager.js +62 -0
- package/dist/input-manager.js.map +1 -0
- package/dist/interpolator.d.ts +35 -0
- package/dist/interpolator.d.ts.map +1 -0
- package/dist/interpolator.js +198 -0
- package/dist/interpolator.js.map +1 -0
- package/dist/networked-rapier-plugin.d.ts +82 -0
- package/dist/networked-rapier-plugin.d.ts.map +1 -0
- package/dist/networked-rapier-plugin.js +698 -0
- package/dist/networked-rapier-plugin.js.map +1 -0
- package/dist/rapier-body-ops.d.ts +27 -0
- package/dist/rapier-body-ops.d.ts.map +1 -0
- package/dist/rapier-body-ops.js +208 -0
- package/dist/rapier-body-ops.js.map +1 -0
- package/dist/rapier-collision-ops.d.ts +6 -0
- package/dist/rapier-collision-ops.d.ts.map +1 -0
- package/dist/rapier-collision-ops.js +200 -0
- package/dist/rapier-collision-ops.js.map +1 -0
- package/dist/rapier-constraint-ops.d.ts +29 -0
- package/dist/rapier-constraint-ops.d.ts.map +1 -0
- package/dist/rapier-constraint-ops.js +286 -0
- package/dist/rapier-constraint-ops.js.map +1 -0
- package/dist/rapier-plugin.d.ts +145 -0
- package/dist/rapier-plugin.d.ts.map +1 -0
- package/dist/rapier-plugin.js +263 -0
- package/dist/rapier-plugin.js.map +1 -0
- package/dist/rapier-shape-ops.d.ts +21 -0
- package/dist/rapier-shape-ops.d.ts.map +1 -0
- package/dist/rapier-shape-ops.js +314 -0
- package/dist/rapier-shape-ops.js.map +1 -0
- package/dist/rapier-types.d.ts +58 -0
- package/dist/rapier-types.d.ts.map +1 -0
- package/dist/rapier-types.js +4 -0
- package/dist/rapier-types.js.map +1 -0
- package/dist/state-reconciler.d.ts +28 -0
- package/dist/state-reconciler.d.ts.map +1 -0
- package/dist/state-reconciler.js +119 -0
- package/dist/state-reconciler.js.map +1 -0
- package/dist/sync-client.d.ts +110 -0
- package/dist/sync-client.d.ts.map +1 -0
- package/dist/sync-client.js +514 -0
- package/dist/sync-client.js.map +1 -0
- package/package.json +21 -0
- package/src/__tests__/clock-sync.test.ts +72 -0
- package/src/__tests__/interpolator.test.ts +98 -0
- package/src/__tests__/state-reconciler.test.ts +102 -0
- package/src/clock-sync.ts +77 -0
- package/src/index.ts +9 -0
- package/src/input-manager.ts +72 -0
- package/src/interpolator.ts +256 -0
- package/src/networked-rapier-plugin.ts +909 -0
- package/src/rapier-body-ops.ts +251 -0
- package/src/rapier-collision-ops.ts +229 -0
- package/src/rapier-constraint-ops.ts +327 -0
- package/src/rapier-plugin.ts +364 -0
- package/src/rapier-shape-ops.ts +369 -0
- package/src/rapier-types.ts +60 -0
- package/src/state-reconciler.ts +151 -0
- package/src/sync-client.ts +640 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import type RAPIER from '@dimforge/rapier3d-compat';
|
|
2
|
+
import { Vector3, BoundingBox, PhysicsShapeType } from '@babylonjs/core';
|
|
3
|
+
import type {
|
|
4
|
+
PhysicsBody,
|
|
5
|
+
PhysicsShape,
|
|
6
|
+
PhysicsShapeParameters,
|
|
7
|
+
PhysicsMaterial,
|
|
8
|
+
Nullable,
|
|
9
|
+
Quaternion,
|
|
10
|
+
} from '@babylonjs/core';
|
|
11
|
+
import type { RapierPluginState } from './rapier-types.js';
|
|
12
|
+
|
|
13
|
+
export function initShape(state: RapierPluginState, shape: PhysicsShape, type: PhysicsShapeType, options: PhysicsShapeParameters): void {
|
|
14
|
+
let colliderDesc: RAPIER.ColliderDesc;
|
|
15
|
+
|
|
16
|
+
switch (type) {
|
|
17
|
+
case PhysicsShapeType.BOX: {
|
|
18
|
+
const ext = options.extents ?? new Vector3(1, 1, 1);
|
|
19
|
+
colliderDesc = state.rapier.ColliderDesc.cuboid(ext.x / 2, ext.y / 2, ext.z / 2);
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
case PhysicsShapeType.SPHERE: {
|
|
23
|
+
const r = options.radius ?? 0.5;
|
|
24
|
+
colliderDesc = state.rapier.ColliderDesc.ball(r);
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
case PhysicsShapeType.CAPSULE: {
|
|
28
|
+
const pointA = options.pointA ?? new Vector3(0, 0, 0);
|
|
29
|
+
const pointB = options.pointB ?? new Vector3(0, 1, 0);
|
|
30
|
+
const halfHeight = Vector3.Distance(pointA, pointB) / 2;
|
|
31
|
+
const radius = options.radius ?? 0.5;
|
|
32
|
+
colliderDesc = state.rapier.ColliderDesc.capsule(halfHeight, radius);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case PhysicsShapeType.CYLINDER: {
|
|
36
|
+
const pointA = options.pointA ?? new Vector3(0, 0, 0);
|
|
37
|
+
const pointB = options.pointB ?? new Vector3(0, 1, 0);
|
|
38
|
+
const halfHeight = Vector3.Distance(pointA, pointB) / 2;
|
|
39
|
+
const radius = options.radius ?? 0.5;
|
|
40
|
+
colliderDesc = state.rapier.ColliderDesc.cylinder(halfHeight, radius);
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case PhysicsShapeType.MESH: {
|
|
44
|
+
const mesh = options.mesh;
|
|
45
|
+
if (mesh) {
|
|
46
|
+
const positions = mesh.getVerticesData('position');
|
|
47
|
+
const indices = mesh.getIndices();
|
|
48
|
+
if (positions && indices) {
|
|
49
|
+
colliderDesc = state.rapier.ColliderDesc.trimesh(
|
|
50
|
+
new Float32Array(positions),
|
|
51
|
+
new Uint32Array(indices)
|
|
52
|
+
);
|
|
53
|
+
} else {
|
|
54
|
+
colliderDesc = state.rapier.ColliderDesc.ball(0.5);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
colliderDesc = state.rapier.ColliderDesc.ball(0.5);
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case PhysicsShapeType.CONVEX_HULL: {
|
|
62
|
+
const mesh = options.mesh;
|
|
63
|
+
if (mesh) {
|
|
64
|
+
const positions = mesh.getVerticesData('position');
|
|
65
|
+
if (positions) {
|
|
66
|
+
const desc = state.rapier.ColliderDesc.convexHull(new Float32Array(positions));
|
|
67
|
+
colliderDesc = desc ?? state.rapier.ColliderDesc.ball(0.5);
|
|
68
|
+
} else {
|
|
69
|
+
colliderDesc = state.rapier.ColliderDesc.ball(0.5);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
colliderDesc = state.rapier.ColliderDesc.ball(0.5);
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case PhysicsShapeType.CONTAINER: {
|
|
77
|
+
colliderDesc = state.rapier.ColliderDesc.ball(0.001);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case PhysicsShapeType.HEIGHTFIELD: {
|
|
81
|
+
const heights = options.heightFieldData;
|
|
82
|
+
const nrows = (options.numHeightFieldSamplesX ?? 2) - 1;
|
|
83
|
+
const ncols = (options.numHeightFieldSamplesZ ?? 2) - 1;
|
|
84
|
+
const sizeX = options.heightFieldSizeX ?? 1;
|
|
85
|
+
const sizeZ = options.heightFieldSizeZ ?? 1;
|
|
86
|
+
if (heights) {
|
|
87
|
+
colliderDesc = state.rapier.ColliderDesc.heightfield(
|
|
88
|
+
nrows,
|
|
89
|
+
ncols,
|
|
90
|
+
heights,
|
|
91
|
+
new state.rapier.Vector3(sizeX, 1, sizeZ)
|
|
92
|
+
);
|
|
93
|
+
} else {
|
|
94
|
+
colliderDesc = state.rapier.ColliderDesc.ball(0.5);
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
default:
|
|
99
|
+
colliderDesc = state.rapier.ColliderDesc.ball(0.5);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
state.shapeToColliderDesc.set(shape, colliderDesc);
|
|
103
|
+
state.shapeTypeMap.set(shape, type);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function setShape(state: RapierPluginState, body: PhysicsBody, shape: Nullable<PhysicsShape>): void {
|
|
107
|
+
const rb = state.bodyToRigidBody.get(body);
|
|
108
|
+
if (!rb) return;
|
|
109
|
+
|
|
110
|
+
const oldShape = state.bodyToShape.get(body);
|
|
111
|
+
if (oldShape) {
|
|
112
|
+
state.shapeToBody.delete(oldShape);
|
|
113
|
+
state.bodyToShape.delete(body);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const existing = state.bodyToColliders.get(body) ?? [];
|
|
117
|
+
for (const col of existing) {
|
|
118
|
+
state.colliderHandleToBody.delete(col.handle);
|
|
119
|
+
state.world.removeCollider(col, false);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!shape) {
|
|
123
|
+
state.bodyToColliders.set(body, []);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
state.bodyToShape.set(body, shape);
|
|
128
|
+
state.shapeToBody.set(shape, body);
|
|
129
|
+
|
|
130
|
+
const shapeType = state.shapeTypeMap.get(shape);
|
|
131
|
+
if (shapeType === PhysicsShapeType.CONTAINER) {
|
|
132
|
+
rebuildCompoundColliders(state, body, shape);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const desc = state.shapeToColliderDesc.get(shape);
|
|
137
|
+
if (!desc) return;
|
|
138
|
+
|
|
139
|
+
const collider = state.world.createCollider(desc, rb);
|
|
140
|
+
applyShapePropertiesToCollider(state, collider, shape);
|
|
141
|
+
state.colliderHandleToBody.set(collider.handle, body);
|
|
142
|
+
state.bodyToColliders.set(body, [collider]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function disposeShape(state: RapierPluginState, shape: PhysicsShape): void {
|
|
146
|
+
state.shapeToColliderDesc.delete(shape);
|
|
147
|
+
state.shapeTypeMap.delete(shape);
|
|
148
|
+
state.shapeMaterialMap.delete(shape);
|
|
149
|
+
state.shapeDensityMap.delete(shape);
|
|
150
|
+
state.shapeFilterMembership.delete(shape);
|
|
151
|
+
state.shapeFilterCollide.delete(shape);
|
|
152
|
+
state.triggerShapes.delete(shape);
|
|
153
|
+
state.compoundChildren.delete(shape);
|
|
154
|
+
state.shapeToBody.delete(shape);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function setMaterial(state: RapierPluginState, shape: PhysicsShape, material: PhysicsMaterial): void {
|
|
158
|
+
state.shapeMaterialMap.set(shape, material);
|
|
159
|
+
for (const collider of getCollidersForShape(state, shape)) {
|
|
160
|
+
if (material.friction !== undefined) collider.setFriction(material.friction);
|
|
161
|
+
if (material.restitution !== undefined) collider.setRestitution(material.restitution);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function getMaterial(state: RapierPluginState, shape: PhysicsShape): PhysicsMaterial {
|
|
166
|
+
return state.shapeMaterialMap.get(shape) ?? { friction: 0.5, restitution: 0 };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function setDensity(state: RapierPluginState, shape: PhysicsShape, density: number): void {
|
|
170
|
+
state.shapeDensityMap.set(shape, density);
|
|
171
|
+
for (const collider of getCollidersForShape(state, shape)) {
|
|
172
|
+
collider.setDensity(density);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function getDensity(state: RapierPluginState, shape: PhysicsShape): number {
|
|
177
|
+
return state.shapeDensityMap.get(shape) ?? 1.0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function setTrigger(state: RapierPluginState, shape: PhysicsShape, isTrigger: boolean): void {
|
|
181
|
+
if (isTrigger) {
|
|
182
|
+
state.triggerShapes.add(shape);
|
|
183
|
+
} else {
|
|
184
|
+
state.triggerShapes.delete(shape);
|
|
185
|
+
}
|
|
186
|
+
for (const collider of getCollidersForShape(state, shape)) {
|
|
187
|
+
collider.setSensor(isTrigger);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function setShapeFilterMembershipMask(state: RapierPluginState, shape: PhysicsShape, membershipMask: number): void {
|
|
192
|
+
state.shapeFilterMembership.set(shape, membershipMask);
|
|
193
|
+
applyCollisionGroups(state, shape);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function getShapeFilterMembershipMask(state: RapierPluginState, shape: PhysicsShape): number {
|
|
197
|
+
return state.shapeFilterMembership.get(shape) ?? 0xFFFFFFFF;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function setShapeFilterCollideMask(state: RapierPluginState, shape: PhysicsShape, collideMask: number): void {
|
|
201
|
+
state.shapeFilterCollide.set(shape, collideMask);
|
|
202
|
+
applyCollisionGroups(state, shape);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function getShapeFilterCollideMask(state: RapierPluginState, shape: PhysicsShape): number {
|
|
206
|
+
return state.shapeFilterCollide.get(shape) ?? 0xFFFFFFFF;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function addChild(state: RapierPluginState, shape: PhysicsShape, newChild: PhysicsShape, translation?: Vector3, rotation?: Quaternion, scale?: Vector3): void {
|
|
210
|
+
let children = state.compoundChildren.get(shape);
|
|
211
|
+
if (!children) {
|
|
212
|
+
children = [];
|
|
213
|
+
state.compoundChildren.set(shape, children);
|
|
214
|
+
}
|
|
215
|
+
children.push({ child: newChild, translation, rotation, scale });
|
|
216
|
+
|
|
217
|
+
const body = state.shapeToBody.get(shape);
|
|
218
|
+
if (body) {
|
|
219
|
+
rebuildCompoundColliders(state, body, shape);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function removeChild(state: RapierPluginState, shape: PhysicsShape, childIndex: number): void {
|
|
224
|
+
const children = state.compoundChildren.get(shape);
|
|
225
|
+
if (!children || childIndex < 0 || childIndex >= children.length) return;
|
|
226
|
+
children.splice(childIndex, 1);
|
|
227
|
+
|
|
228
|
+
const body = state.shapeToBody.get(shape);
|
|
229
|
+
if (body) {
|
|
230
|
+
rebuildCompoundColliders(state, body, shape);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function getNumChildren(state: RapierPluginState, shape: PhysicsShape): number {
|
|
235
|
+
return state.compoundChildren.get(shape)?.length ?? 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function getBoundingBox(state: RapierPluginState, shape: PhysicsShape): BoundingBox {
|
|
239
|
+
const colliders = getCollidersForShape(state, shape);
|
|
240
|
+
if (colliders.length === 0) {
|
|
241
|
+
return new BoundingBox(new Vector3(-0.5, -0.5, -0.5), new Vector3(0.5, 0.5, 0.5));
|
|
242
|
+
}
|
|
243
|
+
return computeColliderBoundingBox(state, colliders[0]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function getBodyBoundingBox(state: RapierPluginState, body: PhysicsBody): BoundingBox {
|
|
247
|
+
const colliders = state.bodyToColliders.get(body) ?? [];
|
|
248
|
+
if (colliders.length === 0) {
|
|
249
|
+
return new BoundingBox(new Vector3(-0.5, -0.5, -0.5), new Vector3(0.5, 0.5, 0.5));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
253
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
254
|
+
|
|
255
|
+
for (const collider of colliders) {
|
|
256
|
+
const bb = computeColliderBoundingBox(state, collider);
|
|
257
|
+
minX = Math.min(minX, bb.minimum.x);
|
|
258
|
+
minY = Math.min(minY, bb.minimum.y);
|
|
259
|
+
minZ = Math.min(minZ, bb.minimum.z);
|
|
260
|
+
maxX = Math.max(maxX, bb.maximum.x);
|
|
261
|
+
maxY = Math.max(maxY, bb.maximum.y);
|
|
262
|
+
maxZ = Math.max(maxZ, bb.maximum.z);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return new BoundingBox(new Vector3(minX, minY, minZ), new Vector3(maxX, maxY, maxZ));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getCollidersForShape(state: RapierPluginState, shape: PhysicsShape): RAPIER.Collider[] {
|
|
269
|
+
const body = state.shapeToBody.get(shape);
|
|
270
|
+
if (!body) return [];
|
|
271
|
+
return state.bodyToColliders.get(body) ?? [];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function applyCollisionGroups(state: RapierPluginState, shape: PhysicsShape): void {
|
|
275
|
+
const membership = state.shapeFilterMembership.get(shape) ?? 0xFFFF;
|
|
276
|
+
const collide = state.shapeFilterCollide.get(shape) ?? 0xFFFF;
|
|
277
|
+
const groups = ((membership & 0xFFFF) << 16) | (collide & 0xFFFF);
|
|
278
|
+
for (const collider of getCollidersForShape(state, shape)) {
|
|
279
|
+
collider.setCollisionGroups(groups);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function applyShapePropertiesToCollider(state: RapierPluginState, collider: RAPIER.Collider, shape: PhysicsShape): void {
|
|
284
|
+
const material = state.shapeMaterialMap.get(shape);
|
|
285
|
+
if (material) {
|
|
286
|
+
if (material.friction !== undefined) collider.setFriction(material.friction);
|
|
287
|
+
if (material.restitution !== undefined) collider.setRestitution(material.restitution);
|
|
288
|
+
}
|
|
289
|
+
const density = state.shapeDensityMap.get(shape);
|
|
290
|
+
if (density !== undefined) collider.setDensity(density);
|
|
291
|
+
if (state.triggerShapes.has(shape)) collider.setSensor(true);
|
|
292
|
+
const membership = state.shapeFilterMembership.get(shape) ?? 0xFFFF;
|
|
293
|
+
const collide = state.shapeFilterCollide.get(shape) ?? 0xFFFF;
|
|
294
|
+
const groups = ((membership & 0xFFFF) << 16) | (collide & 0xFFFF);
|
|
295
|
+
collider.setCollisionGroups(groups);
|
|
296
|
+
collider.setActiveEvents(state.rapier.ActiveEvents.COLLISION_EVENTS);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function computeColliderBoundingBox(state: RapierPluginState, collider: RAPIER.Collider): BoundingBox {
|
|
300
|
+
const shapeType = collider.shapeType();
|
|
301
|
+
const RAPIER = state.rapier;
|
|
302
|
+
const t = collider.translation();
|
|
303
|
+
|
|
304
|
+
if (shapeType === RAPIER.ShapeType.Cuboid) {
|
|
305
|
+
const he = collider.halfExtents();
|
|
306
|
+
return new BoundingBox(
|
|
307
|
+
new Vector3(t.x - he.x, t.y - he.y, t.z - he.z),
|
|
308
|
+
new Vector3(t.x + he.x, t.y + he.y, t.z + he.z)
|
|
309
|
+
);
|
|
310
|
+
} else if (shapeType === RAPIER.ShapeType.Ball) {
|
|
311
|
+
const r = collider.radius();
|
|
312
|
+
return new BoundingBox(
|
|
313
|
+
new Vector3(t.x - r, t.y - r, t.z - r),
|
|
314
|
+
new Vector3(t.x + r, t.y + r, t.z + r)
|
|
315
|
+
);
|
|
316
|
+
} else if (shapeType === RAPIER.ShapeType.Capsule) {
|
|
317
|
+
const r = collider.radius();
|
|
318
|
+
const hh = collider.halfHeight();
|
|
319
|
+
return new BoundingBox(
|
|
320
|
+
new Vector3(t.x - r, t.y - hh - r, t.z - r),
|
|
321
|
+
new Vector3(t.x + r, t.y + hh + r, t.z + r)
|
|
322
|
+
);
|
|
323
|
+
} else if (shapeType === RAPIER.ShapeType.Cylinder) {
|
|
324
|
+
const r = collider.radius();
|
|
325
|
+
const hh = collider.halfHeight();
|
|
326
|
+
return new BoundingBox(
|
|
327
|
+
new Vector3(t.x - r, t.y - hh, t.z - r),
|
|
328
|
+
new Vector3(t.x + r, t.y + hh, t.z + r)
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return new BoundingBox(
|
|
333
|
+
new Vector3(t.x - 0.5, t.y - 0.5, t.z - 0.5),
|
|
334
|
+
new Vector3(t.x + 0.5, t.y + 0.5, t.z + 0.5)
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function rebuildCompoundColliders(state: RapierPluginState, body: PhysicsBody, shape: PhysicsShape): void {
|
|
339
|
+
const rb = state.bodyToRigidBody.get(body);
|
|
340
|
+
if (!rb) return;
|
|
341
|
+
|
|
342
|
+
const existing = state.bodyToColliders.get(body) ?? [];
|
|
343
|
+
for (const col of existing) {
|
|
344
|
+
state.colliderHandleToBody.delete(col.handle);
|
|
345
|
+
state.world.removeCollider(col, false);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const children = state.compoundChildren.get(shape) ?? [];
|
|
349
|
+
const newColliders: RAPIER.Collider[] = [];
|
|
350
|
+
|
|
351
|
+
for (const entry of children) {
|
|
352
|
+
const childDesc = state.shapeToColliderDesc.get(entry.child);
|
|
353
|
+
if (!childDesc) continue;
|
|
354
|
+
|
|
355
|
+
if (entry.translation) {
|
|
356
|
+
childDesc.setTranslation(entry.translation.x, entry.translation.y, entry.translation.z);
|
|
357
|
+
}
|
|
358
|
+
if (entry.rotation) {
|
|
359
|
+
childDesc.setRotation(new state.rapier.Quaternion(entry.rotation.x, entry.rotation.y, entry.rotation.z, entry.rotation.w));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const collider = state.world.createCollider(childDesc, rb);
|
|
363
|
+
applyShapePropertiesToCollider(state, collider, shape);
|
|
364
|
+
state.colliderHandleToBody.set(collider.handle, body);
|
|
365
|
+
newColliders.push(collider);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
state.bodyToColliders.set(body, newColliders);
|
|
369
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type RAPIER from '@dimforge/rapier3d-compat';
|
|
2
|
+
import type {
|
|
3
|
+
Observable,
|
|
4
|
+
PhysicsBody,
|
|
5
|
+
PhysicsShape,
|
|
6
|
+
PhysicsConstraint,
|
|
7
|
+
PhysicsMaterial,
|
|
8
|
+
PhysicsShapeType,
|
|
9
|
+
IPhysicsCollisionEvent,
|
|
10
|
+
IBasePhysicsCollisionEvent,
|
|
11
|
+
} from '@babylonjs/core';
|
|
12
|
+
import type { Vector3, Quaternion } from '@babylonjs/core';
|
|
13
|
+
import type { Vec3 } from '@rapierphysicsplugin/shared';
|
|
14
|
+
import { PhysicsConstraintAxisLimitMode, PhysicsConstraintMotorType } from '@babylonjs/core';
|
|
15
|
+
|
|
16
|
+
export interface AxisConfig {
|
|
17
|
+
mode?: PhysicsConstraintAxisLimitMode;
|
|
18
|
+
minLimit?: number;
|
|
19
|
+
maxLimit?: number;
|
|
20
|
+
friction?: number;
|
|
21
|
+
motorType?: PhysicsConstraintMotorType;
|
|
22
|
+
motorTarget?: number;
|
|
23
|
+
motorMaxForce?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function v3toVec(v: Vector3): Vec3 {
|
|
27
|
+
return { x: v.x, y: v.y, z: v.z };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RapierPluginState {
|
|
31
|
+
rapier: typeof RAPIER;
|
|
32
|
+
world: RAPIER.World;
|
|
33
|
+
bodyToRigidBody: Map<PhysicsBody, RAPIER.RigidBody>;
|
|
34
|
+
bodyToColliders: Map<PhysicsBody, RAPIER.Collider[]>;
|
|
35
|
+
shapeToColliderDesc: Map<PhysicsShape, RAPIER.ColliderDesc>;
|
|
36
|
+
shapeTypeMap: Map<PhysicsShape, PhysicsShapeType>;
|
|
37
|
+
shapeMaterialMap: Map<PhysicsShape, PhysicsMaterial>;
|
|
38
|
+
shapeDensityMap: Map<PhysicsShape, number>;
|
|
39
|
+
shapeFilterMembership: Map<PhysicsShape, number>;
|
|
40
|
+
shapeFilterCollide: Map<PhysicsShape, number>;
|
|
41
|
+
bodyCollisionObservables: Map<PhysicsBody, Observable<IPhysicsCollisionEvent>>;
|
|
42
|
+
bodyCollisionEndedObservables: Map<PhysicsBody, Observable<IBasePhysicsCollisionEvent>>;
|
|
43
|
+
constraintToJoint: Map<PhysicsConstraint, RAPIER.ImpulseJoint>;
|
|
44
|
+
constraintBodies: Map<PhysicsConstraint, { body: PhysicsBody; childBody: PhysicsBody }>;
|
|
45
|
+
constraintAxisState: Map<PhysicsConstraint, Map<number, AxisConfig>>;
|
|
46
|
+
constraintEnabled: Map<PhysicsConstraint, boolean>;
|
|
47
|
+
constraintDescriptors: Map<PhysicsConstraint, { body: PhysicsBody; childBody: PhysicsBody }>;
|
|
48
|
+
collisionCallbackEnabled: Set<PhysicsBody>;
|
|
49
|
+
collisionEndedCallbackEnabled: Set<PhysicsBody>;
|
|
50
|
+
triggerShapes: Set<PhysicsShape>;
|
|
51
|
+
bodyIdToPhysicsBody: Map<string, PhysicsBody>;
|
|
52
|
+
bodyToShape: Map<PhysicsBody, PhysicsShape>;
|
|
53
|
+
shapeToBody: Map<PhysicsShape, PhysicsBody>;
|
|
54
|
+
compoundChildren: Map<PhysicsShape, Array<{ child: PhysicsShape; translation?: Vector3; rotation?: Quaternion; scale?: Vector3 }>>;
|
|
55
|
+
bodyEventMask: Map<PhysicsBody, number>;
|
|
56
|
+
colliderHandleToBody: Map<number, PhysicsBody>;
|
|
57
|
+
onCollisionObservable: Observable<IPhysicsCollisionEvent>;
|
|
58
|
+
onCollisionEndedObservable: Observable<IBasePhysicsCollisionEvent>;
|
|
59
|
+
onTriggerCollisionObservable: Observable<IBasePhysicsCollisionEvent>;
|
|
60
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { BodyState, RoomSnapshot, ClientInput, Vec3, Quat } from '@rapierphysicsplugin/shared';
|
|
2
|
+
import {
|
|
3
|
+
RECONCILIATION_THRESHOLD,
|
|
4
|
+
POSITION_LERP_SPEED,
|
|
5
|
+
ROTATION_SLERP_SPEED,
|
|
6
|
+
} from '@rapierphysicsplugin/shared';
|
|
7
|
+
import { Interpolator } from './interpolator.js';
|
|
8
|
+
|
|
9
|
+
export interface ReconciliationResult {
|
|
10
|
+
/** Bodies that the local client controls — apply corrections */
|
|
11
|
+
localCorrections: Map<string, BodyState>;
|
|
12
|
+
/** Bodies controlled by remote players — use interpolated states */
|
|
13
|
+
remoteStates: Map<string, BodyState>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class StateReconciler {
|
|
17
|
+
private interpolator: Interpolator;
|
|
18
|
+
private localBodyIds: Set<string> = new Set();
|
|
19
|
+
private pendingInputs: ClientInput[] = [];
|
|
20
|
+
private lastServerTick = 0;
|
|
21
|
+
|
|
22
|
+
constructor(interpolator?: Interpolator) {
|
|
23
|
+
this.interpolator = interpolator ?? new Interpolator();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setLocalBodies(bodyIds: string[]): void {
|
|
27
|
+
this.localBodyIds = new Set(bodyIds);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
addLocalBody(bodyId: string): void {
|
|
31
|
+
this.localBodyIds.add(bodyId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
removeLocalBody(bodyId: string): void {
|
|
35
|
+
this.localBodyIds.delete(bodyId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
addPendingInput(input: ClientInput): void {
|
|
39
|
+
this.pendingInputs.push(input);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
processServerState(snapshot: RoomSnapshot): ReconciliationResult {
|
|
43
|
+
const result: ReconciliationResult = {
|
|
44
|
+
localCorrections: new Map(),
|
|
45
|
+
remoteStates: new Map(),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
this.lastServerTick = snapshot.tick;
|
|
49
|
+
|
|
50
|
+
// Discard inputs that the server has already processed
|
|
51
|
+
this.pendingInputs = this.pendingInputs.filter(
|
|
52
|
+
input => input.tick > snapshot.tick
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
for (const body of snapshot.bodies) {
|
|
56
|
+
if (this.localBodyIds.has(body.id)) {
|
|
57
|
+
// Local body — check if correction needed
|
|
58
|
+
result.localCorrections.set(body.id, body);
|
|
59
|
+
} else {
|
|
60
|
+
// Remote body — feed to interpolator (render loop queries at proper render time)
|
|
61
|
+
this.interpolator.addSnapshot(body.id, body, snapshot.timestamp);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getInterpolatedRemoteState(bodyId: string, currentTime: number): BodyState | null {
|
|
69
|
+
return this.interpolator.getInterpolatedState(bodyId, currentTime);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getPendingInputs(): ClientInput[] {
|
|
73
|
+
return this.pendingInputs;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get lastProcessedServerTick(): number {
|
|
77
|
+
return this.lastServerTick;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getInterpolator(): Interpolator {
|
|
81
|
+
return this.interpolator;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
clear(): void {
|
|
85
|
+
this.localBodyIds.clear();
|
|
86
|
+
this.pendingInputs = [];
|
|
87
|
+
this.interpolator.clear();
|
|
88
|
+
this.lastServerTick = 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function needsCorrection(predicted: BodyState, authoritative: BodyState): boolean {
|
|
93
|
+
const dx = predicted.position.x - authoritative.position.x;
|
|
94
|
+
const dy = predicted.position.y - authoritative.position.y;
|
|
95
|
+
const dz = predicted.position.z - authoritative.position.z;
|
|
96
|
+
const distSq = dx * dx + dy * dy + dz * dz;
|
|
97
|
+
return distSq > RECONCILIATION_THRESHOLD * RECONCILIATION_THRESHOLD;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function blendBodyState(current: BodyState, target: BodyState): BodyState {
|
|
101
|
+
return {
|
|
102
|
+
id: current.id,
|
|
103
|
+
position: lerpVec3(current.position, target.position, POSITION_LERP_SPEED),
|
|
104
|
+
rotation: slerpQuat(current.rotation, target.rotation, ROTATION_SLERP_SPEED),
|
|
105
|
+
linVel: target.linVel,
|
|
106
|
+
angVel: target.angVel,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function lerpVec3(a: Vec3, b: Vec3, t: number): Vec3 {
|
|
111
|
+
return {
|
|
112
|
+
x: a.x + (b.x - a.x) * t,
|
|
113
|
+
y: a.y + (b.y - a.y) * t,
|
|
114
|
+
z: a.z + (b.z - a.z) * t,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function slerpQuat(a: Quat, b: Quat, t: number): Quat {
|
|
119
|
+
let dot = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
|
|
120
|
+
let bx = b.x, by = b.y, bz = b.z, bw = b.w;
|
|
121
|
+
if (dot < 0) {
|
|
122
|
+
dot = -dot;
|
|
123
|
+
bx = -bx; by = -by; bz = -bz; bw = -bw;
|
|
124
|
+
}
|
|
125
|
+
if (dot > 0.9995) {
|
|
126
|
+
const len = Math.sqrt(
|
|
127
|
+
(a.x + (bx - a.x) * t) ** 2 +
|
|
128
|
+
(a.y + (by - a.y) * t) ** 2 +
|
|
129
|
+
(a.z + (bz - a.z) * t) ** 2 +
|
|
130
|
+
(a.w + (bw - a.w) * t) ** 2
|
|
131
|
+
);
|
|
132
|
+
return {
|
|
133
|
+
x: (a.x + (bx - a.x) * t) / len,
|
|
134
|
+
y: (a.y + (by - a.y) * t) / len,
|
|
135
|
+
z: (a.z + (bz - a.z) * t) / len,
|
|
136
|
+
w: (a.w + (bw - a.w) * t) / len,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const theta0 = Math.acos(dot);
|
|
140
|
+
const theta = theta0 * t;
|
|
141
|
+
const sinTheta = Math.sin(theta);
|
|
142
|
+
const sinTheta0 = Math.sin(theta0);
|
|
143
|
+
const s0 = Math.cos(theta) - dot * sinTheta / sinTheta0;
|
|
144
|
+
const s1 = sinTheta / sinTheta0;
|
|
145
|
+
return {
|
|
146
|
+
x: s0 * a.x + s1 * bx,
|
|
147
|
+
y: s0 * a.y + s1 * by,
|
|
148
|
+
z: s0 * a.z + s1 * bz,
|
|
149
|
+
w: s0 * a.w + s1 * bw,
|
|
150
|
+
};
|
|
151
|
+
}
|