react-three-rapier-unified 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.
@@ -0,0 +1,491 @@
1
+ import {
2
+ Collider,
3
+ ColliderDesc,
4
+ ActiveEvents,
5
+ RigidBody,
6
+ World
7
+ } from "@dimforge/rapier3d-compat";
8
+ import { useEffect, useMemo } from "react";
9
+ import { BufferGeometry, Euler, Mesh, Object3D, Vector3 } from "three";
10
+ import { mergeVertices } from "three-stdlib";
11
+ import { ColliderProps, RigidBodyProps } from "..";
12
+ import {
13
+ ColliderState,
14
+ ColliderStateMap,
15
+ EventMap
16
+ } from "../components/Physics";
17
+ import {
18
+ _matrix4,
19
+ _position,
20
+ _rotation,
21
+ _scale,
22
+ _vector3
23
+ } from "./shared-objects";
24
+ import { ColliderShape, RigidBodyAutoCollider } from "../types";
25
+ import { scaleVertices, vectorToTuple } from "./utils";
26
+
27
+ export const scaleColliderArgs = (
28
+ shape: ColliderShape,
29
+ args: (number | ArrayLike<number> | { x: number; y: number; z: number })[],
30
+ scale: Vector3
31
+ ) => {
32
+ const newArgs = args.slice();
33
+
34
+ // Heightfield uses a vector
35
+ if (shape === "heightfield") {
36
+ const s = newArgs[3] as { x: number; y: number; z: number };
37
+ s.x *= scale.x;
38
+ s.x *= scale.y;
39
+ s.x *= scale.z;
40
+
41
+ return newArgs;
42
+ }
43
+
44
+ // Trimesh and convex scale the vertices
45
+ if (shape === "trimesh" || shape === "convexHull") {
46
+ newArgs[0] = scaleVertices(newArgs[0] as ArrayLike<number>, scale);
47
+ return newArgs;
48
+ }
49
+
50
+ // Prepfill with some extra
51
+ const scaleArray = [scale.x, scale.y, scale.z, scale.x, scale.x];
52
+ return newArgs.map((arg, index) => scaleArray[index] * (arg as number));
53
+ };
54
+
55
+ export const createColliderFromOptions = (
56
+ options: ColliderProps,
57
+ world: World,
58
+ scale: Vector3,
59
+ getRigidBody?: () => RigidBody
60
+ ) => {
61
+ const scaledArgs = scaleColliderArgs(options.shape!, options.args, scale);
62
+ // @ts-ignore
63
+ const desc = ColliderDesc[options.shape!](...scaledArgs);
64
+
65
+ return world.createCollider(desc!, getRigidBody?.());
66
+ };
67
+
68
+ type ImmutableColliderOptions = (keyof ColliderProps)[];
69
+
70
+ export const immutableColliderOptions: ImmutableColliderOptions = [
71
+ "shape",
72
+ "args"
73
+ ];
74
+
75
+ type MutableColliderOptions = {
76
+ [key in keyof ColliderProps]: (
77
+ collider: Collider,
78
+ value: Exclude<ColliderProps[key], undefined>,
79
+ options: ColliderProps
80
+ ) => void;
81
+ };
82
+
83
+ const massPropertiesConflictError =
84
+ "Please pick ONLY ONE of the `density`, `mass` and `massProperties` options.";
85
+
86
+ type MassPropertiesType = "mass" | "massProperties" | "density";
87
+ const setColliderMassOptions = (
88
+ collider: Collider,
89
+ options: Pick<ColliderProps, MassPropertiesType>
90
+ ) => {
91
+ if (options.density !== undefined) {
92
+ if (options.mass !== undefined || options.massProperties !== undefined) {
93
+ throw new Error(massPropertiesConflictError);
94
+ }
95
+ collider.setDensity(options.density);
96
+
97
+ return;
98
+ }
99
+
100
+ if (options.mass !== undefined) {
101
+ if (options.massProperties !== undefined) {
102
+ throw new Error(massPropertiesConflictError);
103
+ }
104
+
105
+ collider.setMass(options.mass);
106
+ return;
107
+ }
108
+
109
+ if (options.massProperties !== undefined) {
110
+ collider.setMassProperties(
111
+ options.massProperties.mass,
112
+ options.massProperties.centerOfMass,
113
+ options.massProperties.principalAngularInertia,
114
+ options.massProperties.angularInertiaLocalFrame
115
+ );
116
+ }
117
+ };
118
+
119
+ const mutableColliderOptions: MutableColliderOptions = {
120
+ sensor: (collider, value: boolean) => {
121
+ collider.setSensor(value);
122
+ },
123
+ collisionGroups: (collider, value: number) => {
124
+ collider.setCollisionGroups(value);
125
+ },
126
+ solverGroups: (collider, value: number) => {
127
+ collider.setSolverGroups(value);
128
+ },
129
+ friction: (collider, value: number) => {
130
+ collider.setFriction(value);
131
+ },
132
+ frictionCombineRule: (collider, value) => {
133
+ collider.setFrictionCombineRule(value);
134
+ },
135
+ restitution: (collider, value: number) => {
136
+ collider.setRestitution(value);
137
+ },
138
+ restitutionCombineRule: (collider, value) => {
139
+ collider.setRestitutionCombineRule(value);
140
+ },
141
+ activeCollisionTypes: (collider, value: number) => {
142
+ collider.setActiveCollisionTypes(value);
143
+ },
144
+ contactSkin: (collider, value: number) => {
145
+ collider.setContactSkin(value);
146
+ },
147
+ // To make sure the options all mutable options are listed
148
+ quaternion: () => {},
149
+ position: () => {},
150
+ rotation: () => {},
151
+ scale: () => {}
152
+ };
153
+
154
+ const mutableColliderOptionKeys = Object.keys(
155
+ mutableColliderOptions
156
+ ) as (keyof ColliderProps)[];
157
+
158
+ export const setColliderOptions = (
159
+ collider: Collider,
160
+ options: ColliderProps,
161
+ states: ColliderStateMap
162
+ ) => {
163
+ const state = states.get(collider.handle);
164
+
165
+ if (state) {
166
+ // Update collider position based on the object's position
167
+ const parentWorldScale = state.object.parent!.getWorldScale(_vector3);
168
+ const parentInvertedWorldMatrix = state.worldParent?.matrixWorld
169
+ .clone()
170
+ .invert();
171
+
172
+ state.object.updateWorldMatrix(true, false);
173
+
174
+ _matrix4.copy(state.object.matrixWorld);
175
+
176
+ if (parentInvertedWorldMatrix) {
177
+ _matrix4.premultiply(parentInvertedWorldMatrix);
178
+ }
179
+
180
+ _matrix4.decompose(_position, _rotation, _scale);
181
+
182
+ if (collider.parent()) {
183
+ collider.setTranslationWrtParent({
184
+ x: _position.x * parentWorldScale.x,
185
+ y: _position.y * parentWorldScale.y,
186
+ z: _position.z * parentWorldScale.z
187
+ });
188
+ collider.setRotationWrtParent(_rotation);
189
+ } else {
190
+ collider.setTranslation({
191
+ x: _position.x * parentWorldScale.x,
192
+ y: _position.y * parentWorldScale.y,
193
+ z: _position.z * parentWorldScale.z
194
+ });
195
+ collider.setRotation(_rotation);
196
+ }
197
+
198
+ mutableColliderOptionKeys.forEach((key) => {
199
+ if (key in options) {
200
+ const option = options[key];
201
+ mutableColliderOptions[key]!(
202
+ collider,
203
+ // @ts-ignore Option does not want to fit into the function, but it will
204
+ option,
205
+ options
206
+ );
207
+ }
208
+ });
209
+
210
+ // handle mass separately, because the assignments
211
+ // are exclusive.
212
+ setColliderMassOptions(collider, options);
213
+ }
214
+ };
215
+
216
+ export const useUpdateColliderOptions = (
217
+ getCollider: () => Collider,
218
+ props: ColliderProps,
219
+ states: ColliderStateMap
220
+ ) => {
221
+ // TODO: Improve this, split each prop into its own effect
222
+ const mutablePropsAsFlatArray = useMemo(
223
+ () =>
224
+ mutableColliderOptionKeys.flatMap((key) => {
225
+ return vectorToTuple(props[key as keyof ColliderProps]);
226
+ }),
227
+ [props]
228
+ );
229
+
230
+ useEffect(() => {
231
+ const collider = getCollider();
232
+ setColliderOptions(collider, props, states);
233
+ }, [...mutablePropsAsFlatArray, getCollider]);
234
+ };
235
+
236
+ const isChildOfMeshCollider = (child: Mesh) => {
237
+ let flag = false;
238
+ child.traverseAncestors((a) => {
239
+ if (a.userData.r3RapierType === "MeshCollider") flag = true;
240
+ });
241
+ return flag;
242
+ };
243
+
244
+ export const createColliderState = (
245
+ collider: Collider,
246
+ object: Object3D,
247
+ rigidBodyObject?: Object3D | null
248
+ ): ColliderState => {
249
+ return {
250
+ collider,
251
+ worldParent: rigidBodyObject || undefined,
252
+ object
253
+ };
254
+ };
255
+
256
+ const autoColliderMap: Record<string, string> = {
257
+ cuboid: "cuboid",
258
+ ball: "ball",
259
+ hull: "convexHull",
260
+ trimesh: "trimesh"
261
+ };
262
+
263
+ interface CreateColliderPropsFromChildren {
264
+ (options: {
265
+ object: Object3D;
266
+ ignoreMeshColliders: boolean;
267
+ options: RigidBodyProps;
268
+ }): ColliderProps[];
269
+ }
270
+
271
+ export const createColliderPropsFromChildren: CreateColliderPropsFromChildren =
272
+ ({ object, ignoreMeshColliders = true, options }): ColliderProps[] => {
273
+ const childColliderProps: ColliderProps[] = [];
274
+
275
+ object.updateWorldMatrix(true, false);
276
+ const invertedParentMatrixWorld = object.matrixWorld.clone().invert();
277
+
278
+ const colliderFromChild = (child: Object3D) => {
279
+ if ("isMesh" in child) {
280
+ if (ignoreMeshColliders && isChildOfMeshCollider(child as Mesh)) return;
281
+
282
+ const worldScale = child.getWorldScale(_scale);
283
+ const shape = autoColliderMap[
284
+ options.colliders || "cuboid"
285
+ ] as ColliderShape;
286
+
287
+ child.updateWorldMatrix(true, false);
288
+ _matrix4
289
+ .copy(child.matrixWorld)
290
+ .premultiply(invertedParentMatrixWorld)
291
+ .decompose(_position, _rotation, _scale);
292
+
293
+ const rotationEuler = new Euler().setFromQuaternion(_rotation, "XYZ");
294
+
295
+ const { geometry } = child as Mesh;
296
+ const { args, offset } = getColliderArgsFromGeometry(
297
+ geometry,
298
+ options.colliders || "cuboid"
299
+ );
300
+
301
+ const colliderProps: ColliderProps = {
302
+ ...cleanRigidBodyPropsForCollider(options),
303
+ args: args,
304
+ shape: shape,
305
+ rotation: [rotationEuler.x, rotationEuler.y, rotationEuler.z],
306
+ position: [
307
+ _position.x + offset.x * worldScale.x,
308
+ _position.y + offset.y * worldScale.y,
309
+ _position.z + offset.z * worldScale.z
310
+ ],
311
+ scale: [worldScale.x, worldScale.y, worldScale.z]
312
+ };
313
+
314
+ childColliderProps.push(colliderProps);
315
+ }
316
+ };
317
+
318
+ if (options.includeInvisible) {
319
+ object.traverse(colliderFromChild);
320
+ } else {
321
+ object.traverseVisible(colliderFromChild);
322
+ }
323
+
324
+ return childColliderProps;
325
+ };
326
+
327
+ export const getColliderArgsFromGeometry = (
328
+ geometry: BufferGeometry,
329
+ colliders: RigidBodyAutoCollider
330
+ ): { args: unknown[]; offset: Vector3 } => {
331
+ switch (colliders) {
332
+ case "cuboid":
333
+ {
334
+ geometry.computeBoundingBox();
335
+ const { boundingBox } = geometry;
336
+
337
+ const size = boundingBox!.getSize(new Vector3());
338
+
339
+ return {
340
+ args: [size.x / 2, size.y / 2, size.z / 2],
341
+ offset: boundingBox!.getCenter(new Vector3())
342
+ };
343
+ }
344
+ break;
345
+
346
+ case "ball":
347
+ {
348
+ geometry.computeBoundingSphere();
349
+ const { boundingSphere } = geometry;
350
+
351
+ const radius = boundingSphere!.radius;
352
+
353
+ return {
354
+ args: [radius],
355
+ offset: boundingSphere!.center
356
+ };
357
+ }
358
+ break;
359
+
360
+ case "trimesh":
361
+ {
362
+ const clonedGeometry = geometry.index
363
+ ? geometry.clone()
364
+ : mergeVertices(geometry);
365
+
366
+ return {
367
+ args: [
368
+ clonedGeometry.attributes.position.array as Float32Array,
369
+ clonedGeometry.index?.array as Uint32Array
370
+ ],
371
+ offset: new Vector3()
372
+ };
373
+ }
374
+ break;
375
+
376
+ case "hull":
377
+ {
378
+ const g = geometry.clone();
379
+
380
+ return {
381
+ args: [g.attributes.position.array as Float32Array],
382
+ offset: new Vector3()
383
+ };
384
+ }
385
+ break;
386
+ }
387
+
388
+ return { args: [], offset: new Vector3() };
389
+ };
390
+
391
+ export const getActiveCollisionEventsFromProps = (props?: ColliderProps) => {
392
+ return {
393
+ collision: !!(
394
+ props?.onCollisionEnter ||
395
+ props?.onCollisionExit ||
396
+ props?.onIntersectionEnter ||
397
+ props?.onIntersectionExit
398
+ ),
399
+ contactForce: !!props?.onContactForce
400
+ };
401
+ };
402
+
403
+ export const useColliderEvents = (
404
+ getCollider: () => Collider,
405
+ props: ColliderProps,
406
+ events: EventMap,
407
+ /**
408
+ * The RigidBody can pass down active events to the collider without attaching the event listners
409
+ */
410
+ activeEvents: {
411
+ collision?: boolean;
412
+ contactForce?: boolean;
413
+ } = {}
414
+ ) => {
415
+ const {
416
+ onCollisionEnter,
417
+ onCollisionExit,
418
+ onIntersectionEnter,
419
+ onIntersectionExit,
420
+ onContactForce
421
+ } = props;
422
+
423
+ useEffect(() => {
424
+ const collider = getCollider();
425
+
426
+ if (collider) {
427
+ const {
428
+ collision: collisionEventsActive,
429
+ contactForce: contactForceEventsActive
430
+ } = getActiveCollisionEventsFromProps(props);
431
+
432
+ const hasCollisionEvent = collisionEventsActive || activeEvents.collision;
433
+ const hasContactForceEvent =
434
+ contactForceEventsActive || activeEvents.contactForce;
435
+
436
+ if (hasCollisionEvent && hasContactForceEvent) {
437
+ collider.setActiveEvents(
438
+ ActiveEvents.COLLISION_EVENTS | ActiveEvents.CONTACT_FORCE_EVENTS
439
+ );
440
+ } else if (hasCollisionEvent) {
441
+ collider.setActiveEvents(ActiveEvents.COLLISION_EVENTS);
442
+ } else if (hasContactForceEvent) {
443
+ collider.setActiveEvents(ActiveEvents.CONTACT_FORCE_EVENTS);
444
+ }
445
+
446
+ events.set(collider.handle, {
447
+ onCollisionEnter,
448
+ onCollisionExit,
449
+ onIntersectionEnter,
450
+ onIntersectionExit,
451
+ onContactForce
452
+ });
453
+ }
454
+
455
+ return () => {
456
+ if (collider) {
457
+ events.delete(collider.handle);
458
+ }
459
+ };
460
+ }, [
461
+ onCollisionEnter,
462
+ onCollisionExit,
463
+ onIntersectionEnter,
464
+ onIntersectionExit,
465
+ onContactForce,
466
+ activeEvents
467
+ ]);
468
+ };
469
+
470
+ export const cleanRigidBodyPropsForCollider = (props: RigidBodyProps = {}) => {
471
+ const {
472
+ mass,
473
+ linearDamping,
474
+ angularDamping,
475
+ type,
476
+ onCollisionEnter,
477
+ onCollisionExit,
478
+ onIntersectionEnter,
479
+ onIntersectionExit,
480
+ onContactForce,
481
+ children,
482
+ canSleep,
483
+ ccd,
484
+ gravityScale,
485
+ softCcdPrediction,
486
+ ref,
487
+ ...rest
488
+ } = props;
489
+
490
+ return rest;
491
+ };
@@ -0,0 +1,26 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ export const useRaf = (callback: (dt: number) => void) => {
4
+ const cb = useRef(callback);
5
+ const raf = useRef(0);
6
+ const lastFrame = useRef(0);
7
+
8
+ useEffect(() => {
9
+ cb.current = callback;
10
+ }, [callback]);
11
+
12
+ useEffect(() => {
13
+ const loop = () => {
14
+ const now = performance.now();
15
+ const delta = now - lastFrame.current;
16
+
17
+ raf.current = requestAnimationFrame(loop);
18
+ cb.current(delta / 1000);
19
+ lastFrame.current = now;
20
+ };
21
+
22
+ raf.current = requestAnimationFrame(loop);
23
+
24
+ return () => cancelAnimationFrame(raf.current);
25
+ }, []);
26
+ };