@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,698 @@
|
|
|
1
|
+
import { Vector3, Quaternion, MeshBuilder, StandardMaterial, Color3, PhysicsMotionType, PhysicsShapeType, PhysicsBody, PhysicsShape, VertexData, Texture, } from '@babylonjs/core';
|
|
2
|
+
import { computeGeometryHash, encodeGeometryDef, encodeMeshRef, computeMaterialHash, computeTextureHash, encodeMaterialDef, encodeTextureDef, } from '@rapierphysicsplugin/shared';
|
|
3
|
+
import { RapierPlugin } from './rapier-plugin.js';
|
|
4
|
+
import { PhysicsSyncClient } from './sync-client.js';
|
|
5
|
+
// Colors for different shape types (matches demo)
|
|
6
|
+
const shapeColors = {
|
|
7
|
+
box: new Color3(0.9, 0.2, 0.2),
|
|
8
|
+
sphere: new Color3(0.2, 0.7, 0.9),
|
|
9
|
+
capsule: new Color3(0.2, 0.9, 0.3),
|
|
10
|
+
};
|
|
11
|
+
const staticColor = new Color3(0.4, 0.4, 0.45);
|
|
12
|
+
export class NetworkedRapierPlugin extends RapierPlugin {
|
|
13
|
+
constructor(rapier, gravity, config) {
|
|
14
|
+
super(rapier, gravity);
|
|
15
|
+
this.scene = null;
|
|
16
|
+
// Body ID tracking
|
|
17
|
+
this.bodyToId = new Map();
|
|
18
|
+
this.idToBody = new Map();
|
|
19
|
+
// Pending body registration (between initBody and setShape)
|
|
20
|
+
this.pendingBodies = new Map();
|
|
21
|
+
// Shape params cache (stored in initShape, consumed in setShape)
|
|
22
|
+
this.shapeParamsCache = new Map();
|
|
23
|
+
// Pending descriptor sends (waiting for setMaterial or microtask fallback)
|
|
24
|
+
this.pendingDescriptors = new Map();
|
|
25
|
+
// Guard flags
|
|
26
|
+
this.remoteBodyCreationIds = new Set();
|
|
27
|
+
this.remoteBodies = new Set();
|
|
28
|
+
// Geometry registry (content-hash deduplication)
|
|
29
|
+
this.geometryCache = new Map();
|
|
30
|
+
this.sentGeometryHashes = new Set();
|
|
31
|
+
// Material & texture registry
|
|
32
|
+
this.materialCache = new Map();
|
|
33
|
+
this.textureCache = new Map();
|
|
34
|
+
this.sentMaterialHashes = new Set();
|
|
35
|
+
this.sentTextureHashes = new Set();
|
|
36
|
+
this.textureObjectUrls = new Map();
|
|
37
|
+
// Collision event counter
|
|
38
|
+
this.collisionCount = 0;
|
|
39
|
+
this.simulationResetCallbacks = [];
|
|
40
|
+
this.stateUpdateCallbacks = [];
|
|
41
|
+
this.config = config;
|
|
42
|
+
this.syncClient = new PhysicsSyncClient();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Connect to the physics server and join the configured room.
|
|
46
|
+
* Optionally pass the scene for remote body mesh creation.
|
|
47
|
+
*/
|
|
48
|
+
async connect(scene) {
|
|
49
|
+
if (scene) {
|
|
50
|
+
this.scene = scene;
|
|
51
|
+
}
|
|
52
|
+
await this.syncClient.connect(this.config.serverUrl);
|
|
53
|
+
const snapshot = await this.syncClient.joinRoom(this.config.roomId);
|
|
54
|
+
// Wire up server callbacks
|
|
55
|
+
this.syncClient.onBodyAdded((descriptor) => this.handleBodyAdded(descriptor));
|
|
56
|
+
this.syncClient.onBodyRemoved((bodyId) => this.handleBodyRemoved(bodyId));
|
|
57
|
+
this.syncClient.onMeshBinary((msg) => this.handleMeshBinaryReceived(msg));
|
|
58
|
+
this.syncClient.onGeometryDef((data) => this.handleGeometryDefReceived(data));
|
|
59
|
+
this.syncClient.onMeshRef((data) => this.handleMeshRefReceived(data));
|
|
60
|
+
this.syncClient.onMaterialDef((data) => this.handleMaterialDefReceived(data));
|
|
61
|
+
this.syncClient.onTextureDef((data) => this.handleTextureDefReceived(data));
|
|
62
|
+
this.syncClient.onSimulationStarted((freshSnapshot) => this.handleSimulationStarted(freshSnapshot));
|
|
63
|
+
this.syncClient.onCollisionEvents((events) => {
|
|
64
|
+
this.collisionCount += events.length;
|
|
65
|
+
});
|
|
66
|
+
this.syncClient.onStateUpdate((state) => {
|
|
67
|
+
for (const cb of this.stateUpdateCallbacks)
|
|
68
|
+
cb(state);
|
|
69
|
+
});
|
|
70
|
+
return snapshot;
|
|
71
|
+
}
|
|
72
|
+
// --- Overrides ---
|
|
73
|
+
initBody(body, motionType, position, orientation) {
|
|
74
|
+
super.initBody(body, motionType, position, orientation);
|
|
75
|
+
// Lazily detect scene from the first body's transform node
|
|
76
|
+
if (!this.scene && body.transformNode) {
|
|
77
|
+
this.scene = body.transformNode.getScene();
|
|
78
|
+
}
|
|
79
|
+
if (this.remoteBodyCreationIds.size === 0) {
|
|
80
|
+
// Generate body ID from transform node name or random UUID
|
|
81
|
+
const name = body.transformNode?.name;
|
|
82
|
+
const id = name ?? crypto.randomUUID();
|
|
83
|
+
this.bodyToId.set(body, id);
|
|
84
|
+
this.idToBody.set(id, body);
|
|
85
|
+
// Store pending info — we need the shape before we can send the descriptor
|
|
86
|
+
this.pendingBodies.set(body, {
|
|
87
|
+
motionType,
|
|
88
|
+
position: position.clone(),
|
|
89
|
+
orientation: orientation.clone(),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
initShape(shape, type, options) {
|
|
94
|
+
super.initShape(shape, type, options);
|
|
95
|
+
// Cache shape params for descriptor building in setShape
|
|
96
|
+
this.shapeParamsCache.set(shape, { type, options });
|
|
97
|
+
}
|
|
98
|
+
setShape(body, shape) {
|
|
99
|
+
super.setShape(body, shape);
|
|
100
|
+
if (this.remoteBodyCreationIds.size === 0 && shape) {
|
|
101
|
+
const bodyId = this.bodyToId.get(body);
|
|
102
|
+
const pending = this.pendingBodies.get(body);
|
|
103
|
+
const shapeInfo = this.shapeParamsCache.get(shape);
|
|
104
|
+
if (bodyId && pending && shapeInfo) {
|
|
105
|
+
// Store pending descriptor — setMaterial will send it eagerly when
|
|
106
|
+
// PhysicsAggregate sets material. Microtask fallback handles raw API usage.
|
|
107
|
+
const record = { body, bodyId, pending, shapeInfo, shape, sent: false };
|
|
108
|
+
this.pendingDescriptors.set(shape, record);
|
|
109
|
+
this.pendingBodies.delete(body);
|
|
110
|
+
queueMicrotask(() => {
|
|
111
|
+
if (!record.sent) {
|
|
112
|
+
record.sent = true;
|
|
113
|
+
this.pendingDescriptors.delete(shape);
|
|
114
|
+
const descriptor = this.buildDescriptor(body, bodyId, pending, shapeInfo, shape);
|
|
115
|
+
if (descriptor) {
|
|
116
|
+
this.syncClient.addBody(descriptor);
|
|
117
|
+
this.sendMeshBinaryForBody(body, bodyId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
setMaterial(shape, material) {
|
|
125
|
+
super.setMaterial(shape, material);
|
|
126
|
+
const record = this.pendingDescriptors.get(shape);
|
|
127
|
+
if (record && !record.sent) {
|
|
128
|
+
record.sent = true;
|
|
129
|
+
this.pendingDescriptors.delete(shape);
|
|
130
|
+
const descriptor = this.buildDescriptor(record.body, record.bodyId, record.pending, record.shapeInfo, shape);
|
|
131
|
+
if (descriptor) {
|
|
132
|
+
this.syncClient.addBody(descriptor);
|
|
133
|
+
this.sendMeshBinaryForBody(record.body, record.bodyId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
executeStep(delta, bodies) {
|
|
138
|
+
// Do NOT call super.executeStep() — skip local Rapier stepping entirely.
|
|
139
|
+
// Instead, drain the event queue to prevent it from growing unbounded.
|
|
140
|
+
this.eventQueue.drainCollisionEvents(() => { });
|
|
141
|
+
this.eventQueue.drainContactForceEvents(() => { });
|
|
142
|
+
const clockSync = this.syncClient.getClockSync();
|
|
143
|
+
const reconciler = this.syncClient.getReconciler();
|
|
144
|
+
const interpolator = reconciler.getInterpolator();
|
|
145
|
+
const serverTime = clockSync.getServerTime();
|
|
146
|
+
// Reset per-frame interpolation stats
|
|
147
|
+
interpolator.resetStats();
|
|
148
|
+
for (const body of bodies) {
|
|
149
|
+
const bodyId = this.bodyToId.get(body);
|
|
150
|
+
if (!bodyId)
|
|
151
|
+
continue;
|
|
152
|
+
const interpolated = reconciler.getInterpolatedRemoteState(bodyId, serverTime);
|
|
153
|
+
if (interpolated) {
|
|
154
|
+
// Update BabylonJS transform node
|
|
155
|
+
const tn = body.transformNode;
|
|
156
|
+
if (tn) {
|
|
157
|
+
tn.position.set(interpolated.position.x, interpolated.position.y, interpolated.position.z);
|
|
158
|
+
if (!tn.rotationQuaternion) {
|
|
159
|
+
tn.rotationQuaternion = new Quaternion();
|
|
160
|
+
}
|
|
161
|
+
tn.rotationQuaternion.set(interpolated.rotation.x, interpolated.rotation.y, interpolated.rotation.z, interpolated.rotation.w);
|
|
162
|
+
}
|
|
163
|
+
// Also update the Rapier rigid body for query consistency
|
|
164
|
+
const rb = this.bodyToRigidBody.get(body);
|
|
165
|
+
if (rb) {
|
|
166
|
+
rb.setTranslation(new this.rapier.Vector3(interpolated.position.x, interpolated.position.y, interpolated.position.z), false);
|
|
167
|
+
rb.setRotation(new this.rapier.Quaternion(interpolated.rotation.x, interpolated.rotation.y, interpolated.rotation.z, interpolated.rotation.w), false);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// If null (e.g. static body with no updates), mesh keeps its initial position
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
sync(body) {
|
|
174
|
+
// If this is a networked body, executeStep already wrote transforms — no-op
|
|
175
|
+
if (this.bodyToId.has(body))
|
|
176
|
+
return;
|
|
177
|
+
// Otherwise fall through to default Rapier sync for non-networked bodies
|
|
178
|
+
super.sync(body);
|
|
179
|
+
}
|
|
180
|
+
removeBody(body) {
|
|
181
|
+
const bodyId = this.bodyToId.get(body);
|
|
182
|
+
if (bodyId) {
|
|
183
|
+
// Only tell the server to remove if this is a locally-owned body
|
|
184
|
+
if (!this.remoteBodies.has(bodyId)) {
|
|
185
|
+
this.syncClient.removeBody(bodyId);
|
|
186
|
+
}
|
|
187
|
+
this.bodyToId.delete(body);
|
|
188
|
+
this.idToBody.delete(bodyId);
|
|
189
|
+
this.remoteBodies.delete(bodyId);
|
|
190
|
+
this.pendingBodies.delete(body);
|
|
191
|
+
}
|
|
192
|
+
super.removeBody(body);
|
|
193
|
+
}
|
|
194
|
+
// --- Server callback handlers ---
|
|
195
|
+
handleBodyAdded(descriptor) {
|
|
196
|
+
// Skip if we already know about this body
|
|
197
|
+
if (this.idToBody.has(descriptor.id))
|
|
198
|
+
return;
|
|
199
|
+
if (!this.scene)
|
|
200
|
+
return;
|
|
201
|
+
// Always take the synchronous path — mesh geometry arrives separately via binary channel
|
|
202
|
+
this.createRemoteBody(descriptor, this.createMeshFromDescriptor(descriptor));
|
|
203
|
+
}
|
|
204
|
+
createRemoteBody(descriptor, mesh) {
|
|
205
|
+
const scene = this.scene;
|
|
206
|
+
// Store metadata for click handlers etc.
|
|
207
|
+
mesh.metadata = { bodyId: descriptor.id };
|
|
208
|
+
const motionType = this.motionTypeFromWire(descriptor.motionType);
|
|
209
|
+
// Create a PhysicsBody wrapper if the scene has physics enabled
|
|
210
|
+
const physicsEngine = scene.getPhysicsEngine();
|
|
211
|
+
if (physicsEngine) {
|
|
212
|
+
this.remoteBodyCreationIds.add(descriptor.id);
|
|
213
|
+
try {
|
|
214
|
+
const body = new PhysicsBody(mesh, motionType, false, scene);
|
|
215
|
+
// Create shape from descriptor
|
|
216
|
+
const shape = this.createShapeFromDescriptor(descriptor, mesh);
|
|
217
|
+
if (shape) {
|
|
218
|
+
body.shape = shape;
|
|
219
|
+
}
|
|
220
|
+
// Track this as a remote body
|
|
221
|
+
this.bodyToId.set(body, descriptor.id);
|
|
222
|
+
this.idToBody.set(descriptor.id, body);
|
|
223
|
+
this.remoteBodies.add(descriptor.id);
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
this.remoteBodyCreationIds.delete(descriptor.id);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
handleBodyRemoved(bodyId) {
|
|
231
|
+
const body = this.idToBody.get(bodyId);
|
|
232
|
+
if (body) {
|
|
233
|
+
// Dispose the mesh
|
|
234
|
+
const tn = body.transformNode;
|
|
235
|
+
if (tn) {
|
|
236
|
+
tn.dispose();
|
|
237
|
+
}
|
|
238
|
+
body.dispose();
|
|
239
|
+
this.bodyToId.delete(body);
|
|
240
|
+
this.idToBody.delete(bodyId);
|
|
241
|
+
this.remoteBodies.delete(bodyId);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
handleSimulationStarted(freshSnapshot) {
|
|
245
|
+
// Snapshot entries before clearing to avoid concurrent modification
|
|
246
|
+
const entries = Array.from(this.bodyToId.entries());
|
|
247
|
+
this.bodyToId.clear();
|
|
248
|
+
this.idToBody.clear();
|
|
249
|
+
this.pendingBodies.clear();
|
|
250
|
+
this.remoteBodies.clear();
|
|
251
|
+
this.geometryCache.clear();
|
|
252
|
+
this.sentGeometryHashes.clear();
|
|
253
|
+
this.materialCache.clear();
|
|
254
|
+
this.textureCache.clear();
|
|
255
|
+
this.sentMaterialHashes.clear();
|
|
256
|
+
this.sentTextureHashes.clear();
|
|
257
|
+
// Revoke object URLs to prevent memory leaks
|
|
258
|
+
for (const [, url] of this.textureObjectUrls) {
|
|
259
|
+
URL.revokeObjectURL(url);
|
|
260
|
+
}
|
|
261
|
+
this.textureObjectUrls.clear();
|
|
262
|
+
this.collisionCount = 0;
|
|
263
|
+
// Dispose all existing physics bodies and their meshes
|
|
264
|
+
for (const [body] of entries) {
|
|
265
|
+
const tn = body.transformNode;
|
|
266
|
+
if (tn) {
|
|
267
|
+
tn.dispose();
|
|
268
|
+
}
|
|
269
|
+
// Call super.removeBody to clean up Rapier state without notifying server
|
|
270
|
+
super.removeBody(body);
|
|
271
|
+
}
|
|
272
|
+
// Notify user callbacks so they can re-create local bodies (e.g. ground plane)
|
|
273
|
+
for (const cb of this.simulationResetCallbacks)
|
|
274
|
+
cb();
|
|
275
|
+
// Bodies from the fresh snapshot will arrive via onBodyAdded callbacks
|
|
276
|
+
}
|
|
277
|
+
// --- Descriptor building ---
|
|
278
|
+
buildDescriptor(body, bodyId, pending, shapeInfo, shape) {
|
|
279
|
+
const motionType = this.motionTypeToWire(pending.motionType);
|
|
280
|
+
const shapeDescriptor = this.shapeInfoToDescriptor(shapeInfo);
|
|
281
|
+
if (!shapeDescriptor)
|
|
282
|
+
return null;
|
|
283
|
+
// Get material properties
|
|
284
|
+
const material = this.getMaterial(shape);
|
|
285
|
+
// Get mass from the Rapier rigid body
|
|
286
|
+
const rb = this.bodyToRigidBody.get(body);
|
|
287
|
+
const mass = rb ? rb.mass() : undefined;
|
|
288
|
+
return {
|
|
289
|
+
id: bodyId,
|
|
290
|
+
shape: shapeDescriptor,
|
|
291
|
+
motionType,
|
|
292
|
+
position: { x: pending.position.x, y: pending.position.y, z: pending.position.z },
|
|
293
|
+
rotation: { x: pending.orientation.x, y: pending.orientation.y, z: pending.orientation.z, w: pending.orientation.w },
|
|
294
|
+
mass,
|
|
295
|
+
friction: material.friction,
|
|
296
|
+
restitution: material.restitution,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
shapeInfoToDescriptor(shapeInfo) {
|
|
300
|
+
const { type, options } = shapeInfo;
|
|
301
|
+
switch (type) {
|
|
302
|
+
case PhysicsShapeType.BOX: {
|
|
303
|
+
const ext = options.extents ?? new Vector3(1, 1, 1);
|
|
304
|
+
return {
|
|
305
|
+
type: 'box',
|
|
306
|
+
params: { halfExtents: { x: ext.x / 2, y: ext.y / 2, z: ext.z / 2 } },
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
case PhysicsShapeType.SPHERE: {
|
|
310
|
+
const r = options.radius ?? 0.5;
|
|
311
|
+
return { type: 'sphere', params: { radius: r } };
|
|
312
|
+
}
|
|
313
|
+
case PhysicsShapeType.CAPSULE: {
|
|
314
|
+
const pointA = options.pointA ?? new Vector3(0, 0, 0);
|
|
315
|
+
const pointB = options.pointB ?? new Vector3(0, 1, 0);
|
|
316
|
+
const halfHeight = Vector3.Distance(pointA, pointB) / 2;
|
|
317
|
+
const radius = options.radius ?? 0.5;
|
|
318
|
+
return { type: 'capsule', params: { halfHeight, radius } };
|
|
319
|
+
}
|
|
320
|
+
case PhysicsShapeType.MESH: {
|
|
321
|
+
const mesh = options.mesh;
|
|
322
|
+
if (mesh) {
|
|
323
|
+
const positions = mesh.getVerticesData('position');
|
|
324
|
+
const indices = mesh.getIndices();
|
|
325
|
+
if (positions && indices) {
|
|
326
|
+
return {
|
|
327
|
+
type: 'mesh',
|
|
328
|
+
params: { vertices: new Float32Array(positions), indices: new Uint32Array(indices) },
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
default:
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// --- Binary mesh send/receive ---
|
|
339
|
+
sendMeshBinaryForBody(body, bodyId) {
|
|
340
|
+
const tn = body.transformNode;
|
|
341
|
+
if (!tn || !('geometry' in tn))
|
|
342
|
+
return;
|
|
343
|
+
const mesh = tn;
|
|
344
|
+
const positionsRaw = mesh.getVerticesData('position');
|
|
345
|
+
const indicesRaw = mesh.getIndices();
|
|
346
|
+
if (!positionsRaw || !indicesRaw)
|
|
347
|
+
return;
|
|
348
|
+
const positions = new Float32Array(positionsRaw);
|
|
349
|
+
const indices = new Uint32Array(indicesRaw);
|
|
350
|
+
const normalsRaw = mesh.getVerticesData('normal');
|
|
351
|
+
const normals = normalsRaw ? new Float32Array(normalsRaw) : undefined;
|
|
352
|
+
const uvsRaw = mesh.getVerticesData('uv');
|
|
353
|
+
const uvs = uvsRaw ? new Float32Array(uvsRaw) : undefined;
|
|
354
|
+
const colorsRaw = mesh.getVerticesData('color');
|
|
355
|
+
const colors = colorsRaw ? new Float32Array(colorsRaw) : undefined;
|
|
356
|
+
// Extract full material properties
|
|
357
|
+
let diffuseColor = { r: 0.5, g: 0.5, b: 0.5 };
|
|
358
|
+
let specularColor = { r: 0.3, g: 0.3, b: 0.3 };
|
|
359
|
+
let emissiveColor = { r: 0, g: 0, b: 0 };
|
|
360
|
+
let ambientColor = { r: 0, g: 0, b: 0 };
|
|
361
|
+
let alpha = 1;
|
|
362
|
+
let specularPower = 64;
|
|
363
|
+
let diffuseTextureHash;
|
|
364
|
+
let normalTextureHash;
|
|
365
|
+
let specularTextureHash;
|
|
366
|
+
let emissiveTextureHash;
|
|
367
|
+
const mat = mesh.material;
|
|
368
|
+
if (mat) {
|
|
369
|
+
diffuseColor = { r: mat.diffuseColor.r, g: mat.diffuseColor.g, b: mat.diffuseColor.b };
|
|
370
|
+
specularColor = { r: mat.specularColor.r, g: mat.specularColor.g, b: mat.specularColor.b };
|
|
371
|
+
emissiveColor = { r: mat.emissiveColor.r, g: mat.emissiveColor.g, b: mat.emissiveColor.b };
|
|
372
|
+
ambientColor = { r: mat.ambientColor.r, g: mat.ambientColor.g, b: mat.ambientColor.b };
|
|
373
|
+
alpha = mat.alpha;
|
|
374
|
+
specularPower = mat.specularPower;
|
|
375
|
+
// Extract textures via _buffer (BabylonJS internal)
|
|
376
|
+
if (mat.diffuseTexture) {
|
|
377
|
+
diffuseTextureHash = this.extractAndSendTexture(mat.diffuseTexture);
|
|
378
|
+
}
|
|
379
|
+
if (mat.bumpTexture) {
|
|
380
|
+
normalTextureHash = this.extractAndSendTexture(mat.bumpTexture);
|
|
381
|
+
}
|
|
382
|
+
if (mat.specularTexture) {
|
|
383
|
+
specularTextureHash = this.extractAndSendTexture(mat.specularTexture);
|
|
384
|
+
}
|
|
385
|
+
if (mat.emissiveTexture) {
|
|
386
|
+
emissiveTextureHash = this.extractAndSendTexture(mat.emissiveTexture);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// Content-hash deduplication: send GEOMETRY_DEF only once per unique geometry
|
|
390
|
+
const hash = computeGeometryHash(positions, normals, uvs, colors, indices);
|
|
391
|
+
if (!this.sentGeometryHashes.has(hash)) {
|
|
392
|
+
const geomEncoded = encodeGeometryDef(hash, positions, normals, uvs, colors, indices);
|
|
393
|
+
this.syncClient.sendGeometryDef(geomEncoded);
|
|
394
|
+
this.sentGeometryHashes.add(hash);
|
|
395
|
+
// Cache locally so we can apply geometry if we receive a MeshRef for our own hash
|
|
396
|
+
this.geometryCache.set(hash, { hash, positions, normals, uvs, colors, indices });
|
|
397
|
+
}
|
|
398
|
+
// Compute material hash and send MATERIAL_DEF if new
|
|
399
|
+
const matHash = computeMaterialHash(diffuseColor, specularColor, emissiveColor, ambientColor, alpha, specularPower, diffuseTextureHash, normalTextureHash, specularTextureHash, emissiveTextureHash);
|
|
400
|
+
if (!this.sentMaterialHashes.has(matHash)) {
|
|
401
|
+
const matDef = {
|
|
402
|
+
hash: matHash,
|
|
403
|
+
diffuseColor, specularColor, emissiveColor, ambientColor,
|
|
404
|
+
alpha, specularPower,
|
|
405
|
+
diffuseTextureHash, normalTextureHash, specularTextureHash, emissiveTextureHash,
|
|
406
|
+
};
|
|
407
|
+
const matEncoded = encodeMaterialDef(matDef);
|
|
408
|
+
this.syncClient.sendMaterialDef(matEncoded);
|
|
409
|
+
this.sentMaterialHashes.add(matHash);
|
|
410
|
+
this.materialCache.set(matHash, matDef);
|
|
411
|
+
}
|
|
412
|
+
// Always send MESH_REF (small message per body)
|
|
413
|
+
const refEncoded = encodeMeshRef(bodyId, hash, matHash);
|
|
414
|
+
this.syncClient.sendMeshRef(refEncoded);
|
|
415
|
+
}
|
|
416
|
+
handleMeshBinaryReceived(msg) {
|
|
417
|
+
const body = this.idToBody.get(msg.bodyId);
|
|
418
|
+
if (!body)
|
|
419
|
+
return;
|
|
420
|
+
const scene = this.scene;
|
|
421
|
+
if (!scene)
|
|
422
|
+
return;
|
|
423
|
+
const tn = body.transformNode;
|
|
424
|
+
if (!tn)
|
|
425
|
+
return;
|
|
426
|
+
const mesh = tn;
|
|
427
|
+
// Apply new vertex data in-place (replaces geometry without swapping the mesh,
|
|
428
|
+
// which would break the physics body's internal transform node binding).
|
|
429
|
+
const vertexData = new VertexData();
|
|
430
|
+
vertexData.positions = msg.positions;
|
|
431
|
+
if (msg.normals)
|
|
432
|
+
vertexData.normals = msg.normals;
|
|
433
|
+
if (msg.uvs)
|
|
434
|
+
vertexData.uvs = msg.uvs;
|
|
435
|
+
if (msg.colors)
|
|
436
|
+
vertexData.colors = msg.colors;
|
|
437
|
+
vertexData.indices = msg.indices;
|
|
438
|
+
vertexData.applyToMesh(mesh);
|
|
439
|
+
// Update material colors
|
|
440
|
+
const oldMat = mesh.material;
|
|
441
|
+
if (oldMat)
|
|
442
|
+
oldMat.dispose();
|
|
443
|
+
const mat = new StandardMaterial(`${msg.bodyId}Mat_bin`, scene);
|
|
444
|
+
if (msg.diffuseColor) {
|
|
445
|
+
mat.diffuseColor = new Color3(msg.diffuseColor.r, msg.diffuseColor.g, msg.diffuseColor.b);
|
|
446
|
+
}
|
|
447
|
+
if (msg.specularColor) {
|
|
448
|
+
mat.specularColor = new Color3(msg.specularColor.r, msg.specularColor.g, msg.specularColor.b);
|
|
449
|
+
}
|
|
450
|
+
mesh.material = mat;
|
|
451
|
+
}
|
|
452
|
+
handleGeometryDefReceived(data) {
|
|
453
|
+
// Cache the geometry and mark the hash as known
|
|
454
|
+
this.geometryCache.set(data.hash, data);
|
|
455
|
+
this.sentGeometryHashes.add(data.hash);
|
|
456
|
+
}
|
|
457
|
+
handleMeshRefReceived(data) {
|
|
458
|
+
const body = this.idToBody.get(data.bodyId);
|
|
459
|
+
if (!body)
|
|
460
|
+
return;
|
|
461
|
+
const scene = this.scene;
|
|
462
|
+
if (!scene)
|
|
463
|
+
return;
|
|
464
|
+
const tn = body.transformNode;
|
|
465
|
+
if (!tn)
|
|
466
|
+
return;
|
|
467
|
+
const geom = this.geometryCache.get(data.geometryHash);
|
|
468
|
+
if (!geom)
|
|
469
|
+
return;
|
|
470
|
+
const mesh = tn;
|
|
471
|
+
// Apply geometry from the cached GEOMETRY_DEF
|
|
472
|
+
const vertexData = new VertexData();
|
|
473
|
+
vertexData.positions = geom.positions;
|
|
474
|
+
if (geom.normals)
|
|
475
|
+
vertexData.normals = geom.normals;
|
|
476
|
+
if (geom.uvs)
|
|
477
|
+
vertexData.uvs = geom.uvs;
|
|
478
|
+
if (geom.colors)
|
|
479
|
+
vertexData.colors = geom.colors;
|
|
480
|
+
vertexData.indices = geom.indices;
|
|
481
|
+
vertexData.applyToMesh(mesh);
|
|
482
|
+
// Apply material from the cached MATERIAL_DEF
|
|
483
|
+
const matDef = this.materialCache.get(data.materialHash);
|
|
484
|
+
const oldMat = mesh.material;
|
|
485
|
+
if (oldMat)
|
|
486
|
+
oldMat.dispose();
|
|
487
|
+
const mat = new StandardMaterial(`${data.bodyId}Mat_ref`, scene);
|
|
488
|
+
if (matDef) {
|
|
489
|
+
mat.diffuseColor = new Color3(matDef.diffuseColor.r, matDef.diffuseColor.g, matDef.diffuseColor.b);
|
|
490
|
+
mat.specularColor = new Color3(matDef.specularColor.r, matDef.specularColor.g, matDef.specularColor.b);
|
|
491
|
+
mat.emissiveColor = new Color3(matDef.emissiveColor.r, matDef.emissiveColor.g, matDef.emissiveColor.b);
|
|
492
|
+
mat.ambientColor = new Color3(matDef.ambientColor.r, matDef.ambientColor.g, matDef.ambientColor.b);
|
|
493
|
+
mat.alpha = matDef.alpha;
|
|
494
|
+
mat.specularPower = matDef.specularPower;
|
|
495
|
+
// Apply textures from cache
|
|
496
|
+
if (matDef.diffuseTextureHash) {
|
|
497
|
+
mat.diffuseTexture = this.createTextureFromCache(matDef.diffuseTextureHash, scene);
|
|
498
|
+
}
|
|
499
|
+
if (matDef.normalTextureHash) {
|
|
500
|
+
mat.bumpTexture = this.createTextureFromCache(matDef.normalTextureHash, scene);
|
|
501
|
+
}
|
|
502
|
+
if (matDef.specularTextureHash) {
|
|
503
|
+
mat.specularTexture = this.createTextureFromCache(matDef.specularTextureHash, scene);
|
|
504
|
+
}
|
|
505
|
+
if (matDef.emissiveTextureHash) {
|
|
506
|
+
mat.emissiveTexture = this.createTextureFromCache(matDef.emissiveTextureHash, scene);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
// Fallback: no material def found, use defaults
|
|
511
|
+
mat.diffuseColor = new Color3(0.5, 0.5, 0.5);
|
|
512
|
+
mat.specularColor = new Color3(0.3, 0.3, 0.3);
|
|
513
|
+
}
|
|
514
|
+
mesh.material = mat;
|
|
515
|
+
}
|
|
516
|
+
handleMaterialDefReceived(data) {
|
|
517
|
+
this.materialCache.set(data.hash, data);
|
|
518
|
+
this.sentMaterialHashes.add(data.hash);
|
|
519
|
+
}
|
|
520
|
+
handleTextureDefReceived(data) {
|
|
521
|
+
this.textureCache.set(data.hash, data);
|
|
522
|
+
this.sentTextureHashes.add(data.hash);
|
|
523
|
+
}
|
|
524
|
+
extractAndSendTexture(texture) {
|
|
525
|
+
try {
|
|
526
|
+
// Access BabylonJS internal _buffer for original image bytes
|
|
527
|
+
const buffer = texture._buffer;
|
|
528
|
+
if (!buffer)
|
|
529
|
+
return undefined;
|
|
530
|
+
const imageData = new Uint8Array(buffer);
|
|
531
|
+
const texHash = computeTextureHash(imageData);
|
|
532
|
+
if (!this.sentTextureHashes.has(texHash)) {
|
|
533
|
+
const encoded = encodeTextureDef(texHash, imageData);
|
|
534
|
+
this.syncClient.sendTextureDef(encoded);
|
|
535
|
+
this.sentTextureHashes.add(texHash);
|
|
536
|
+
this.textureCache.set(texHash, { hash: texHash, imageData });
|
|
537
|
+
}
|
|
538
|
+
return texHash;
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
// Gracefully skip textures that can't be extracted
|
|
542
|
+
return undefined;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
createTextureFromCache(hash, scene) {
|
|
546
|
+
const texData = this.textureCache.get(hash);
|
|
547
|
+
if (!texData)
|
|
548
|
+
return null;
|
|
549
|
+
// Reuse existing object URL or create a new one
|
|
550
|
+
let url = this.textureObjectUrls.get(hash);
|
|
551
|
+
if (!url) {
|
|
552
|
+
const blob = new Blob([new Uint8Array(texData.imageData)]);
|
|
553
|
+
url = URL.createObjectURL(blob);
|
|
554
|
+
this.textureObjectUrls.set(hash, url);
|
|
555
|
+
}
|
|
556
|
+
return new Texture(url, scene);
|
|
557
|
+
}
|
|
558
|
+
// --- Mesh creation for remote bodies ---
|
|
559
|
+
createMeshFromDescriptor(descriptor) {
|
|
560
|
+
const scene = this.scene;
|
|
561
|
+
let mesh;
|
|
562
|
+
let colorKey;
|
|
563
|
+
switch (descriptor.shape.type) {
|
|
564
|
+
case 'box': {
|
|
565
|
+
const p = descriptor.shape.params;
|
|
566
|
+
mesh = MeshBuilder.CreateBox(descriptor.id, {
|
|
567
|
+
width: p.halfExtents.x * 2,
|
|
568
|
+
height: p.halfExtents.y * 2,
|
|
569
|
+
depth: p.halfExtents.z * 2,
|
|
570
|
+
}, scene);
|
|
571
|
+
colorKey = 'box';
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
case 'sphere': {
|
|
575
|
+
const p = descriptor.shape.params;
|
|
576
|
+
mesh = MeshBuilder.CreateSphere(descriptor.id, { diameter: p.radius * 2 }, scene);
|
|
577
|
+
colorKey = 'sphere';
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
case 'capsule': {
|
|
581
|
+
const p = descriptor.shape.params;
|
|
582
|
+
mesh = MeshBuilder.CreateCapsule(descriptor.id, {
|
|
583
|
+
height: p.halfHeight * 2 + p.radius * 2,
|
|
584
|
+
radius: p.radius,
|
|
585
|
+
}, scene);
|
|
586
|
+
colorKey = 'capsule';
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
default:
|
|
590
|
+
mesh = MeshBuilder.CreateBox(descriptor.id, { size: 1 }, scene);
|
|
591
|
+
colorKey = 'box';
|
|
592
|
+
}
|
|
593
|
+
mesh.position.set(descriptor.position.x, descriptor.position.y, descriptor.position.z);
|
|
594
|
+
mesh.rotationQuaternion = new Quaternion(descriptor.rotation.x, descriptor.rotation.y, descriptor.rotation.z, descriptor.rotation.w);
|
|
595
|
+
const mat = new StandardMaterial(`${descriptor.id}Mat`, scene);
|
|
596
|
+
if (descriptor.motionType === 'static') {
|
|
597
|
+
mat.diffuseColor = staticColor;
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
mat.diffuseColor = shapeColors[colorKey] ?? new Color3(0.5, 0.5, 0.5);
|
|
601
|
+
}
|
|
602
|
+
mat.specularColor = new Color3(0.3, 0.3, 0.3);
|
|
603
|
+
mesh.material = mat;
|
|
604
|
+
return mesh;
|
|
605
|
+
}
|
|
606
|
+
createShapeFromDescriptor(descriptor, mesh) {
|
|
607
|
+
const scene = this.scene;
|
|
608
|
+
switch (descriptor.shape.type) {
|
|
609
|
+
case 'box': {
|
|
610
|
+
const p = descriptor.shape.params;
|
|
611
|
+
return new PhysicsShape({ type: PhysicsShapeType.BOX, parameters: { extents: new Vector3(p.halfExtents.x * 2, p.halfExtents.y * 2, p.halfExtents.z * 2) } }, scene);
|
|
612
|
+
}
|
|
613
|
+
case 'sphere': {
|
|
614
|
+
const p = descriptor.shape.params;
|
|
615
|
+
return new PhysicsShape({ type: PhysicsShapeType.SPHERE, parameters: { radius: p.radius } }, scene);
|
|
616
|
+
}
|
|
617
|
+
case 'capsule': {
|
|
618
|
+
const p = descriptor.shape.params;
|
|
619
|
+
return new PhysicsShape({
|
|
620
|
+
type: PhysicsShapeType.CAPSULE,
|
|
621
|
+
parameters: {
|
|
622
|
+
pointA: new Vector3(0, -p.halfHeight, 0),
|
|
623
|
+
pointB: new Vector3(0, p.halfHeight, 0),
|
|
624
|
+
radius: p.radius,
|
|
625
|
+
},
|
|
626
|
+
}, scene);
|
|
627
|
+
}
|
|
628
|
+
case 'mesh': {
|
|
629
|
+
return new PhysicsShape({ type: PhysicsShapeType.MESH, parameters: { mesh } }, scene);
|
|
630
|
+
}
|
|
631
|
+
default:
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
// --- Motion type conversion helpers ---
|
|
636
|
+
motionTypeToWire(motionType) {
|
|
637
|
+
switch (motionType) {
|
|
638
|
+
case PhysicsMotionType.DYNAMIC: return 'dynamic';
|
|
639
|
+
case PhysicsMotionType.STATIC: return 'static';
|
|
640
|
+
case PhysicsMotionType.ANIMATED: return 'kinematic';
|
|
641
|
+
default: return 'dynamic';
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
motionTypeFromWire(motionType) {
|
|
645
|
+
switch (motionType) {
|
|
646
|
+
case 'dynamic': return PhysicsMotionType.DYNAMIC;
|
|
647
|
+
case 'static': return PhysicsMotionType.STATIC;
|
|
648
|
+
case 'kinematic': return PhysicsMotionType.ANIMATED;
|
|
649
|
+
default: return PhysicsMotionType.DYNAMIC;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// --- Proxy methods for sync client functionality ---
|
|
653
|
+
startSimulation() {
|
|
654
|
+
this.syncClient.startSimulation();
|
|
655
|
+
}
|
|
656
|
+
sendInput(actions) {
|
|
657
|
+
this.syncClient.sendInput(actions);
|
|
658
|
+
}
|
|
659
|
+
onCollisionEvents(callback) {
|
|
660
|
+
this.syncClient.onCollisionEvents(callback);
|
|
661
|
+
}
|
|
662
|
+
onSimulationReset(callback) {
|
|
663
|
+
this.simulationResetCallbacks.push(callback);
|
|
664
|
+
}
|
|
665
|
+
onStateUpdate(callback) {
|
|
666
|
+
this.stateUpdateCallbacks.push(callback);
|
|
667
|
+
}
|
|
668
|
+
getSyncClient() {
|
|
669
|
+
return this.syncClient;
|
|
670
|
+
}
|
|
671
|
+
getClientId() {
|
|
672
|
+
return this.syncClient.getClientId();
|
|
673
|
+
}
|
|
674
|
+
get simulationRunning() {
|
|
675
|
+
return this.syncClient.simulationRunning;
|
|
676
|
+
}
|
|
677
|
+
get totalBodyCount() {
|
|
678
|
+
return this.syncClient.totalBodyCount;
|
|
679
|
+
}
|
|
680
|
+
get bytesSent() {
|
|
681
|
+
return this.syncClient.bytesSent;
|
|
682
|
+
}
|
|
683
|
+
get bytesReceived() {
|
|
684
|
+
return this.syncClient.bytesReceived;
|
|
685
|
+
}
|
|
686
|
+
get collisionEventCount() {
|
|
687
|
+
return this.collisionCount;
|
|
688
|
+
}
|
|
689
|
+
/** Access the underlying reconciler for debug stats */
|
|
690
|
+
getReconciler() {
|
|
691
|
+
return this.syncClient.getReconciler();
|
|
692
|
+
}
|
|
693
|
+
/** Access the clock sync for debug stats */
|
|
694
|
+
getClockSync() {
|
|
695
|
+
return this.syncClient.getClockSync();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
//# sourceMappingURL=networked-rapier-plugin.js.map
|