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