@opsydyn/effect-three-rapier 0.3.0 → 0.6.4

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/src/index.ts CHANGED
@@ -13,339 +13,16 @@
13
13
  * and joints into impulse joints, as its joint hooks do (`src/hooks/joints.ts`).
14
14
  *
15
15
  * Rapier (`@dimforge/rapier3d-compat`) is Apache-2.0; its WebAssembly is embedded, so
16
- * nothing extra is fetched.
16
+ * nothing extra is fetched. For a simulation that must come out the same on every
17
+ * machine (lockstep, replays shared as links), load Rapier's deterministic build from
18
+ * `@opsydyn/effect-three-rapier/deterministic` instead.
17
19
  *
18
20
  * @module
19
21
  */
20
22
  import RAPIER from '@dimforge/rapier3d-compat';
21
- import { interactionBits } from '@opsydyn/effect-three';
22
- import type {
23
- Body,
24
- BodyPose,
25
- Collider,
26
- ColliderShape,
27
- Collision,
28
- Physics,
29
- ContactForce,
30
- JointLimits,
31
- JointMotor,
32
- PhysicsEngine,
33
- PhysicsJoint,
34
- PhysicsWorld,
35
- Vec3,
36
- } from '@opsydyn/effect-three';
23
+ import type { PhysicsEngine } from '@opsydyn/effect-three';
37
24
 
38
- type Rapier = typeof RAPIER;
39
-
40
- const vector = ([x, y, z]: Vec3) => ({ x, y, z });
41
-
42
- const rotation = ([x, y, z, w]: BodyPose['quaternion']) => ({ x, y, z, w });
43
-
44
- /** Body types by our names. Kinematic bodies are position based, as react-three-rapier's default. */
45
- const bodyTypes = {
46
- dynamic: RAPIER.RigidBodyType.Dynamic,
47
- fixed: RAPIER.RigidBodyType.Fixed,
48
- kinematic: RAPIER.RigidBodyType.KinematicPositionBased,
49
- } satisfies Record<Body['type'], RAPIER.RigidBodyType>;
50
-
51
- /** The collider description for a shape; a hull Rapier cannot build becomes a small ball. */
52
- const shapeDesc = (rapier: Rapier, shape: ColliderShape): RAPIER.ColliderDesc => {
53
- switch (shape._tag) {
54
- case 'Cuboid':
55
- return rapier.ColliderDesc.cuboid(...shape.halfExtents);
56
- case 'Ball':
57
- return rapier.ColliderDesc.ball(shape.radius);
58
- case 'Hull':
59
- return rapier.ColliderDesc.convexHull(shape.points) ?? rapier.ColliderDesc.ball(1e-3);
60
- case 'Capsule':
61
- return rapier.ColliderDesc.capsule(shape.halfHeight, shape.radius);
62
- case 'Cylinder':
63
- return rapier.ColliderDesc.cylinder(shape.halfHeight, shape.radius);
64
- case 'Cone':
65
- return rapier.ColliderDesc.cone(shape.halfHeight, shape.radius);
66
- case 'Trimesh':
67
- return rapier.ColliderDesc.trimesh(shape.vertices, shape.indices);
68
- }
69
- };
70
-
71
- /** A collider placed in its body. */
72
- const colliderDesc = (rapier: Rapier, collider: Collider): RAPIER.ColliderDesc =>
73
- shapeDesc(rapier, collider.shape)
74
- .setTranslation(...collider.translation)
75
- .setRotation(rotation(collider.rotation));
76
-
77
- /**
78
- * The events a body's colliders report: collisions always, contact forces over its
79
- * threshold when it has one (react-three-rapier's `onContactForce` turns on
80
- * `CONTACT_FORCE_EVENTS`).
81
- */
82
- const reporting = (colliders: ReadonlyArray<RAPIER.Collider>, body: Body): void => {
83
- for (const collider of colliders) {
84
- if (body.contactForces === null) {
85
- collider.setActiveEvents(RAPIER.ActiveEvents.COLLISION_EVENTS);
86
- continue;
87
- }
88
- collider.setActiveEvents(
89
- RAPIER.ActiveEvents.COLLISION_EVENTS | RAPIER.ActiveEvents.CONTACT_FORCE_EVENTS,
90
- );
91
- collider.setContactForceEventThreshold(body.contactForces);
92
- }
93
- };
94
-
95
- /**
96
- * Applies a body's surface and mass settings to its colliders. A given mass is shared
97
- * among them; otherwise each has density 1.
98
- */
99
- const surface = (colliders: ReadonlyArray<RAPIER.Collider>, body: Body): void => {
100
- for (const collider of colliders) {
101
- collider.setFriction(body.friction);
102
- collider.setRestitution(body.restitution);
103
- collider.setSensor(body.sensor);
104
- if (body.mass === null) collider.setDensity(1);
105
- else collider.setMass(body.mass / colliders.length);
106
- collider.setCollisionGroups(interactionBits(body.collisionGroups));
107
- collider.setSolverGroups(interactionBits(body.solverGroups));
108
- }
109
- };
110
-
111
- /** Applies a body's motion settings. */
112
- const motion = (rigidBody: RAPIER.RigidBody, body: Body): void => {
113
- rigidBody.setBodyType(bodyTypes[body.type], true);
114
- rigidBody.setLinearDamping(body.linearDamping);
115
- rigidBody.setAngularDamping(body.angularDamping);
116
- rigidBody.setGravityScale(body.gravityScale, true);
117
- rigidBody.enableCcd(body.ccd);
118
- };
119
-
120
- /** The limits a joint has, if it is a hinge or slider with limits. */
121
- const limitsOf = (joint: PhysicsJoint): JointLimits | null =>
122
- joint.kind._tag === 'Revolute' || joint.kind._tag === 'Prismatic' ? joint.kind.limits : null;
123
-
124
- /** The motor a joint has, if it is a hinge or slider with one. */
125
- const motorOf = (joint: PhysicsJoint): JointMotor | null =>
126
- joint.kind._tag === 'Revolute' || joint.kind._tag === 'Prismatic' ? joint.kind.motor : null;
127
-
128
- /** Drives a hinge or slider once made: Rapier's `configureMotor`, set on the joint. */
129
- const drive = (made: RAPIER.ImpulseJoint, motor: JointMotor | null): void => {
130
- if (motor !== null && made instanceof RAPIER.UnitImpulseJoint) {
131
- made.configureMotor(motor.position, motor.velocity, motor.stiffness, motor.damping);
132
- }
133
- };
134
-
135
- /**
136
- * Limits a hinge or slider once made. React-three-rapier sets them on the joint data, but
137
- * Rapier's `JointData.intoRaw` (0.21) passes them on for prismatic joints only, so a
138
- * hinge's would be lost: they are set on the joint instead.
139
- */
140
- const limit = (made: RAPIER.ImpulseJoint, limits: JointLimits | null): void => {
141
- if (limits !== null && made instanceof RAPIER.UnitImpulseJoint) made.setLimits(...limits);
142
- };
143
-
144
- /** Rapier's joint data for a joint, anchors in each body's space. */
145
- const jointData = (rapier: Rapier, joint: PhysicsJoint): RAPIER.JointData => {
146
- const a = vector(joint.anchorA);
147
- const b = vector(joint.anchorB);
148
- const { kind } = joint;
149
- switch (kind._tag) {
150
- case 'Fixed':
151
- return rapier.JointData.fixed(a, rotation(kind.frameA), b, rotation(kind.frameB));
152
- case 'Spherical':
153
- return rapier.JointData.spherical(a, b);
154
- case 'Revolute':
155
- return rapier.JointData.revolute(a, b, vector(kind.axis));
156
- case 'Prismatic':
157
- return rapier.JointData.prismatic(a, b, vector(kind.axis));
158
- case 'Rope':
159
- return rapier.JointData.rope(kind.length, a, b);
160
- case 'Spring':
161
- return rapier.JointData.spring(kind.restLength, kind.stiffness, kind.damping, a, b);
162
- }
163
- };
164
-
165
- /** Which body a collider belongs to, and which part of it, by name. */
166
- type Owner = { readonly key: string; readonly part?: string };
167
-
168
- /** Two colliders' owners as an event's `a` and `b`, with their parts. */
169
- const pair = (a: Owner, b: Owner) => ({
170
- a: a.key,
171
- b: b.key,
172
- ...(a.part === undefined ? {} : { aPart: a.part }),
173
- ...(b.part === undefined ? {} : { bPart: b.part }),
174
- });
175
-
176
- type Held = {
177
- readonly rigidBody: RAPIER.RigidBody;
178
- readonly colliders: ReadonlyArray<RAPIER.Collider>;
179
- };
180
-
181
- const createWorld =
182
- (rapier: Rapier) =>
183
- (initial: Physics): PhysicsWorld => {
184
- const world = new rapier.World(vector(initial.gravity));
185
- const events = new rapier.EventQueue(true);
186
- const bodies = new Map<string, Held>();
187
- /** Each collider's body, and the part of it the collider is (when named). */
188
- const keysByCollider = new Map<number, Owner>();
189
- const joints = new Map<
190
- string,
191
- { readonly joint: RAPIER.ImpulseJoint; readonly a: string; readonly b: string }
192
- >();
193
-
194
- const add: PhysicsWorld['add'] = (key, body, colliders, pose) => {
195
- const desc = new rapier.RigidBodyDesc(bodyTypes[body.type])
196
- .setTranslation(...pose.position)
197
- .setRotation(rotation(pose.quaternion))
198
- .setLinvel(...body.linearVelocity)
199
- .setAngvel(vector(body.angularVelocity))
200
- .setLinearDamping(body.linearDamping)
201
- .setAngularDamping(body.angularDamping)
202
- .setGravityScale(body.gravityScale)
203
- .setCcdEnabled(body.ccd);
204
- const rigidBody = world.createRigidBody(desc);
205
- const made = colliders.map((collider) =>
206
- world.createCollider(colliderDesc(rapier, collider), rigidBody),
207
- );
208
- surface(made, body);
209
- reporting(made, body);
210
- bodies.set(key, { rigidBody, colliders: made });
211
- made.forEach((collider, i) => {
212
- const part = colliders[i]?.part;
213
- keysByCollider.set(collider.handle, { key, ...(part === undefined ? {} : { part }) });
214
- });
215
- };
216
-
217
- const remove: PhysicsWorld['remove'] = (key) => {
218
- const held = bodies.get(key);
219
- if (held === undefined) return;
220
- for (const collider of held.colliders) keysByCollider.delete(collider.handle);
221
- // Rapier removes the body's joints with it; forget them, as their handles are reused.
222
- for (const [id, joined] of joints)
223
- if (joined.a === key || joined.b === key) joints.delete(id);
224
- world.removeRigidBody(held.rigidBody);
225
- bodies.delete(key);
226
- };
227
-
228
- const addJoint: PhysicsWorld['addJoint'] = (id, joint, a, b) => {
229
- const first = bodies.get(a);
230
- const second = bodies.get(b);
231
- if (first === undefined || second === undefined) return;
232
- const made = world.createImpulseJoint(
233
- jointData(rapier, joint),
234
- first.rigidBody,
235
- second.rigidBody,
236
- true,
237
- );
238
- limit(made, limitsOf(joint));
239
- drive(made, motorOf(joint));
240
- made.setContactsEnabled(joint.contacts);
241
- joints.set(id, { joint: made, a, b });
242
- };
243
-
244
- const removeJoint: PhysicsWorld['removeJoint'] = (id) => {
245
- const joined = joints.get(id);
246
- if (joined === undefined) return;
247
- world.removeImpulseJoint(joined.joint, true);
248
- joints.delete(id);
249
- };
250
-
251
- const collisionsOf = (): ReadonlyArray<Collision> => {
252
- const collisions: Array<Collision> = [];
253
- events.drainCollisionEvents((first, second, started) => {
254
- const a = keysByCollider.get(first);
255
- const b = keysByCollider.get(second);
256
- if (a !== undefined && b !== undefined) collisions.push({ ...pair(a, b), started });
257
- });
258
- return collisions;
259
- };
260
-
261
- /** Contact force events since the last call, kept across steps until asked for. */
262
- let forces: Array<ContactForce> = [];
263
- function recordForce(event: RAPIER.TempContactForceEvent): void {
264
- const a = keysByCollider.get(event.collider1());
265
- const b = keysByCollider.get(event.collider2());
266
- if (a === undefined || b === undefined) return;
267
- const total = event.totalForce();
268
- const direction = event.maxForceDirection();
269
- forces.push({
270
- ...pair(a, b),
271
- totalForce: [total.x, total.y, total.z],
272
- totalForceMagnitude: event.totalForceMagnitude(),
273
- maxForceDirection: [direction.x, direction.y, direction.z],
274
- maxForceMagnitude: event.maxForceMagnitude(),
275
- });
276
- }
277
- const drainForces = (): void => events.drainContactForceEvents(recordForce);
278
-
279
- return {
280
- configure: (physics) => {
281
- world.gravity = vector(physics.gravity);
282
- },
283
- add,
284
- update: (key, body) => {
285
- const held = bodies.get(key);
286
- if (held === undefined) return;
287
- motion(held.rigidBody, body);
288
- surface(held.colliders, body);
289
- reporting(held.colliders, body);
290
- },
291
- teleport: (key, pose) => {
292
- const held = bodies.get(key);
293
- if (held === undefined) return;
294
- held.rigidBody.setTranslation(vector(pose.position), true);
295
- held.rigidBody.setRotation(rotation(pose.quaternion), true);
296
- held.rigidBody.setLinvel({ x: 0, y: 0, z: 0 }, true);
297
- held.rigidBody.setAngvel({ x: 0, y: 0, z: 0 }, true);
298
- },
299
- moveKinematic: (key, pose) => {
300
- const held = bodies.get(key);
301
- if (held === undefined) return;
302
- held.rigidBody.setNextKinematicTranslation(vector(pose.position));
303
- held.rigidBody.setNextKinematicRotation(rotation(pose.quaternion));
304
- },
305
- applyImpulse: (key, impulse) => {
306
- bodies.get(key)?.rigidBody.applyImpulse(vector(impulse), true);
307
- },
308
- remove,
309
- addJoint,
310
- removeJoint,
311
- step: (seconds) => {
312
- world.timestep = seconds;
313
- world.step(events);
314
- drainForces();
315
- return collisionsOf();
316
- },
317
- contactForces: () => {
318
- const drained = forces;
319
- forces = [];
320
- return drained;
321
- },
322
- // react-three-rapier's Debug: Rapier's own line rendering of colliders and joints.
323
- debugLines: () => {
324
- const { vertices, colors } = world.debugRender();
325
- return { vertices, colors };
326
- },
327
- pose: (key) => {
328
- const held = bodies.get(key);
329
- if (held === undefined) return undefined;
330
- const { x, y, z } = held.rigidBody.translation();
331
- const turn = held.rigidBody.rotation();
332
- return { position: [x, y, z], quaternion: [turn.x, turn.y, turn.z, turn.w] };
333
- },
334
- awake: () => {
335
- for (const { rigidBody } of bodies.values()) {
336
- if (rigidBody.isDynamic() && !rigidBody.isSleeping()) return true;
337
- }
338
- return false;
339
- },
340
- dispose: () => {
341
- bodies.clear();
342
- keysByCollider.clear();
343
- joints.clear();
344
- events.free();
345
- world.free();
346
- },
347
- };
348
- };
25
+ import { engineOf } from './engine.ts';
349
26
 
350
27
  /**
351
28
  * Loads Rapier's WebAssembly and returns the engine. Call once per page; the engine makes
@@ -353,5 +30,5 @@ const createWorld =
353
30
  */
354
31
  export const loadRapier = async (): Promise<PhysicsEngine> => {
355
32
  await RAPIER.init();
356
- return { createWorld: createWorld(RAPIER) };
33
+ return engineOf(RAPIER);
357
34
  };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * A snapshot of one of this package's worlds: Rapier's own (`World.takeSnapshot`), and an
3
+ * index of which of its bodies, colliders and joints are which, by the keys the scene
4
+ * graph knows them by. Nothing here needs Rapier, so the page side of a worker
5
+ * (`remotePhysics`) can read the index of a snapshot the worker took.
6
+ *
7
+ * The layout: a format byte; the bodies (each its key, handle, and colliders with their
8
+ * handles and parts); the joints (each its id, handle and bodies); then Rapier's bytes.
9
+ * Counts and string lengths are 32-bit, handles 64-bit floats (Rapier's own numbers),
10
+ * all little-endian.
11
+ *
12
+ * @module
13
+ */
14
+
15
+ /** A body in a snapshot: its key, Rapier handle, and its colliders'. */
16
+ export type IndexedBody = {
17
+ readonly key: string;
18
+ readonly handle: number;
19
+ readonly colliders: ReadonlyArray<{ readonly handle: number; readonly part?: string }>;
20
+ };
21
+
22
+ /** A joint in a snapshot: its id, Rapier handle, and the keys of its bodies. */
23
+ export type IndexedJoint = {
24
+ readonly id: string;
25
+ readonly handle: number;
26
+ readonly a: string;
27
+ readonly b: string;
28
+ };
29
+
30
+ export type SnapshotIndex = {
31
+ readonly bodies: ReadonlyArray<IndexedBody>;
32
+ readonly joints: ReadonlyArray<IndexedJoint>;
33
+ };
34
+
35
+ /** The format byte this version writes and reads. */
36
+ const format = 1;
37
+
38
+ const encoder = new TextEncoder();
39
+ const decoder = new TextDecoder();
40
+
41
+ /** Writes `index` and Rapier's `world` bytes as one snapshot. */
42
+ export const writeSnapshot = (index: SnapshotIndex, world: Uint8Array): Uint8Array => {
43
+ const parts: Array<Uint8Array> = [Uint8Array.of(format)];
44
+ const count = (value: number): void => {
45
+ const bytes = new Uint8Array(4);
46
+ new DataView(bytes.buffer).setUint32(0, value, true);
47
+ parts.push(bytes);
48
+ };
49
+ const handle = (value: number): void => {
50
+ const bytes = new Uint8Array(8);
51
+ new DataView(bytes.buffer).setFloat64(0, value, true);
52
+ parts.push(bytes);
53
+ };
54
+ const text = (value: string): void => {
55
+ const bytes = encoder.encode(value);
56
+ count(bytes.length);
57
+ parts.push(bytes);
58
+ };
59
+ count(index.bodies.length);
60
+ for (const body of index.bodies) {
61
+ text(body.key);
62
+ handle(body.handle);
63
+ count(body.colliders.length);
64
+ for (const collider of body.colliders) {
65
+ handle(collider.handle);
66
+ // A part is written with a leading mark, so that no part and an empty one differ.
67
+ text(collider.part === undefined ? '' : `:${collider.part}`);
68
+ }
69
+ }
70
+ count(index.joints.length);
71
+ for (const joint of index.joints) {
72
+ text(joint.id);
73
+ handle(joint.handle);
74
+ text(joint.a);
75
+ text(joint.b);
76
+ }
77
+ parts.push(world);
78
+ const out = new Uint8Array(parts.reduce((length, part) => length + part.length, 0));
79
+ let at = 0;
80
+ for (const part of parts) {
81
+ out.set(part, at);
82
+ at += part.length;
83
+ }
84
+ return out;
85
+ };
86
+
87
+ /** Reads a snapshot written by `writeSnapshot`: its index, and Rapier's bytes. */
88
+ export const readSnapshot = (
89
+ snapshot: Uint8Array,
90
+ ): { readonly index: SnapshotIndex; readonly world: Uint8Array } => {
91
+ if (snapshot[0] !== format) throw new Error('Not a snapshot of an effect-three Rapier world');
92
+ const view = new DataView(snapshot.buffer, snapshot.byteOffset, snapshot.byteLength);
93
+ let at = 1;
94
+ const count = (): number => {
95
+ const value = view.getUint32(at, true);
96
+ at += 4;
97
+ return value;
98
+ };
99
+ const handle = (): number => {
100
+ const value = view.getFloat64(at, true);
101
+ at += 8;
102
+ return value;
103
+ };
104
+ const text = (): string => {
105
+ const length = count();
106
+ const value = decoder.decode(snapshot.subarray(at, at + length));
107
+ at += length;
108
+ return value;
109
+ };
110
+ function collider(): IndexedBody['colliders'][number] {
111
+ const colliderHandle = handle();
112
+ const part = text();
113
+ return part === ''
114
+ ? { handle: colliderHandle }
115
+ : { handle: colliderHandle, part: part.slice(1) };
116
+ }
117
+ function body(): IndexedBody {
118
+ const key = text();
119
+ const bodyHandle = handle();
120
+ return { key, handle: bodyHandle, colliders: Array.from({ length: count() }, collider) };
121
+ }
122
+ const bodies: Array<IndexedBody> = Array.from({ length: count() }, body);
123
+ const joints: Array<IndexedJoint> = Array.from({ length: count() }, () => ({
124
+ id: text(),
125
+ handle: handle(),
126
+ a: text(),
127
+ b: text(),
128
+ }));
129
+ return { index: { bodies, joints }, world: snapshot.subarray(at) };
130
+ };