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.
- package/LICENSE +9 -0
- package/README.md +166 -0
- package/dist/index.cjs.js +31 -0
- package/dist/index.d.ts +1191 -0
- package/dist/index.esm.js +1667 -0
- package/package.json +53 -0
- package/src/addons/attractor/Attractor.tsx +178 -0
- package/src/addons/attractor/AttractorDebugHelper.tsx +59 -0
- package/src/components/AnyCollider.tsx +286 -0
- package/src/components/Debug.tsx +33 -0
- package/src/components/FrameStepper.tsx +36 -0
- package/src/components/InstancedRigidBodies.tsx +135 -0
- package/src/components/MeshCollider.tsx +53 -0
- package/src/components/Physics.tsx +894 -0
- package/src/components/RigidBody.tsx +153 -0
- package/src/hooks/hooks.ts +211 -0
- package/src/hooks/joints.ts +221 -0
- package/src/hooks/use-forwarded-ref.ts +19 -0
- package/src/hooks/use-imperative-instance.ts +33 -0
- package/src/index.ts +55 -0
- package/src/types.ts +541 -0
- package/src/utils/interaction-groups.ts +43 -0
- package/src/utils/shared-objects.ts +10 -0
- package/src/utils/singleton-proxy.ts +51 -0
- package/src/utils/three-object-helpers.ts +25 -0
- package/src/utils/utils-collider.ts +491 -0
- package/src/utils/utils-physics.ts +26 -0
- package/src/utils/utils-rigidbody.ts +231 -0
- package/src/utils/utils.ts +105 -0
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
import type Rapier from "@dimforge/rapier3d-compat";
|
|
2
|
+
import {
|
|
3
|
+
Collider,
|
|
4
|
+
ColliderHandle,
|
|
5
|
+
EventQueue,
|
|
6
|
+
PhysicsHooks,
|
|
7
|
+
RigidBody,
|
|
8
|
+
RigidBodyHandle,
|
|
9
|
+
SolverFlags,
|
|
10
|
+
World
|
|
11
|
+
} from "@dimforge/rapier3d-compat";
|
|
12
|
+
import { useThree } from "@react-three/fiber";
|
|
13
|
+
import React, {
|
|
14
|
+
createContext,
|
|
15
|
+
FC,
|
|
16
|
+
ReactNode,
|
|
17
|
+
useCallback,
|
|
18
|
+
useEffect,
|
|
19
|
+
useMemo,
|
|
20
|
+
useState
|
|
21
|
+
} from "react";
|
|
22
|
+
import { MathUtils, Matrix4, Object3D, Quaternion, Vector3 } from "three";
|
|
23
|
+
import { suspend } from "suspend-react";
|
|
24
|
+
import {
|
|
25
|
+
CollisionPayload,
|
|
26
|
+
CollisionEnterHandler,
|
|
27
|
+
CollisionExitHandler,
|
|
28
|
+
ContactForceHandler,
|
|
29
|
+
IntersectionEnterHandler,
|
|
30
|
+
IntersectionExitHandler,
|
|
31
|
+
RigidBodyAutoCollider,
|
|
32
|
+
Vector3Tuple
|
|
33
|
+
} from "../types";
|
|
34
|
+
import {
|
|
35
|
+
_matrix4,
|
|
36
|
+
_position,
|
|
37
|
+
_rotation,
|
|
38
|
+
_scale
|
|
39
|
+
} from "../utils/shared-objects";
|
|
40
|
+
import {
|
|
41
|
+
rapierQuaternionToQuaternion,
|
|
42
|
+
useConst,
|
|
43
|
+
vectorArrayToVector3,
|
|
44
|
+
vector3ToRapierVector
|
|
45
|
+
} from "../utils/utils";
|
|
46
|
+
import FrameStepper from "./FrameStepper";
|
|
47
|
+
import { Debug } from "./Debug";
|
|
48
|
+
import { createSingletonProxy } from "../utils/singleton-proxy";
|
|
49
|
+
|
|
50
|
+
export interface RigidBodyState {
|
|
51
|
+
meshType: "instancedMesh" | "mesh";
|
|
52
|
+
rigidBody: RigidBody;
|
|
53
|
+
object: Object3D;
|
|
54
|
+
invertedWorldMatrix: Matrix4;
|
|
55
|
+
setMatrix: (matrix: Matrix4) => void;
|
|
56
|
+
getMatrix: (matrix: Matrix4) => Matrix4;
|
|
57
|
+
/**
|
|
58
|
+
* Required for instanced rigid bodies.
|
|
59
|
+
*/
|
|
60
|
+
scale: Vector3;
|
|
61
|
+
isSleeping: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type RigidBodyStateMap = Map<RigidBody["handle"], RigidBodyState>;
|
|
65
|
+
|
|
66
|
+
export type WorldStepCallback = (world: World) => void;
|
|
67
|
+
|
|
68
|
+
export type WorldStepCallbackSet = Set<{ current: WorldStepCallback }>;
|
|
69
|
+
|
|
70
|
+
export type FilterContactPairCallback = (
|
|
71
|
+
collider1: ColliderHandle,
|
|
72
|
+
collider2: ColliderHandle,
|
|
73
|
+
body1: RigidBodyHandle,
|
|
74
|
+
body2: RigidBodyHandle
|
|
75
|
+
) => SolverFlags | null;
|
|
76
|
+
|
|
77
|
+
export type FilterIntersectionPairCallback = (
|
|
78
|
+
collider1: ColliderHandle,
|
|
79
|
+
collider2: ColliderHandle,
|
|
80
|
+
body1: RigidBodyHandle,
|
|
81
|
+
body2: RigidBodyHandle
|
|
82
|
+
) => boolean;
|
|
83
|
+
|
|
84
|
+
export type FilterContactPairCallbackSet = Set<{
|
|
85
|
+
current: FilterContactPairCallback;
|
|
86
|
+
}>;
|
|
87
|
+
|
|
88
|
+
export type FilterIntersectionPairCallbackSet = Set<{
|
|
89
|
+
current: FilterIntersectionPairCallback;
|
|
90
|
+
}>;
|
|
91
|
+
|
|
92
|
+
export interface ColliderState {
|
|
93
|
+
collider: Collider;
|
|
94
|
+
object: Object3D;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The parent of which this collider needs to base its
|
|
98
|
+
* world position on, can be empty
|
|
99
|
+
*/
|
|
100
|
+
worldParent?: Object3D;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type ColliderStateMap = Map<Collider["handle"], ColliderState>;
|
|
104
|
+
|
|
105
|
+
export interface RapierContext {
|
|
106
|
+
/**
|
|
107
|
+
* Used by the world to keep track of RigidBody states
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
rigidBodyStates: RigidBodyStateMap;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Used by the world to keep track of Collider states
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
colliderStates: ColliderStateMap;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Used by the world to keep track of RigidBody events
|
|
120
|
+
* @internal
|
|
121
|
+
*/
|
|
122
|
+
rigidBodyEvents: EventMap;
|
|
123
|
+
/**
|
|
124
|
+
* Used by the world to keep track of Collider events
|
|
125
|
+
* @internal
|
|
126
|
+
*/
|
|
127
|
+
colliderEvents: EventMap;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Default options for rigid bodies and colliders
|
|
131
|
+
* @internal
|
|
132
|
+
*/
|
|
133
|
+
physicsOptions: {
|
|
134
|
+
colliders: RigidBodyAutoCollider;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Triggered before the physics world is stepped
|
|
139
|
+
* @internal
|
|
140
|
+
*/
|
|
141
|
+
beforeStepCallbacks: WorldStepCallbackSet;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Triggered after the physics world is stepped
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
147
|
+
afterStepCallbacks: WorldStepCallbackSet;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Hooks to filter contact pairs
|
|
151
|
+
* @internal
|
|
152
|
+
*/
|
|
153
|
+
filterContactPairHooks: FilterContactPairCallbackSet;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Hooks to filter intersection pairs
|
|
157
|
+
* @internal
|
|
158
|
+
*/
|
|
159
|
+
filterIntersectionPairHooks: FilterIntersectionPairCallbackSet;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Direct access to the Rapier instance
|
|
163
|
+
*/
|
|
164
|
+
rapier: typeof Rapier;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The Rapier physics world
|
|
168
|
+
*/
|
|
169
|
+
world: World;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Can be used to overwrite the current World. Useful when working with snapshots.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```tsx
|
|
176
|
+
* import { useRapier } from '@react-three/rapier';
|
|
177
|
+
*
|
|
178
|
+
* const SnapshottingComponent = () => {
|
|
179
|
+
* const { world, setWorld, rapier } = useRapier();
|
|
180
|
+
* const worldSnapshot = useRef<Uint8Array>();
|
|
181
|
+
*
|
|
182
|
+
* // Store the snapshot
|
|
183
|
+
* const takeSnapshot = () => {
|
|
184
|
+
* const snapshot = world.takeSnapshot()
|
|
185
|
+
* worldSnapshot.current = snapshot
|
|
186
|
+
* }
|
|
187
|
+
*
|
|
188
|
+
* // Create a new World from the snapshot, and replace the current one
|
|
189
|
+
* const restoreSnapshot = () => {
|
|
190
|
+
* setWorld(rapier.World.restoreSnapshot(worldSnapshot.current))
|
|
191
|
+
* }
|
|
192
|
+
*
|
|
193
|
+
* return <>
|
|
194
|
+
* <Rigidbody>...</RigidBody>
|
|
195
|
+
* <Rigidbody>...</RigidBody>
|
|
196
|
+
* <Rigidbody>...</RigidBody>
|
|
197
|
+
* <Rigidbody>...</RigidBody>
|
|
198
|
+
* <Rigidbody>...</RigidBody>
|
|
199
|
+
* </>
|
|
200
|
+
* }
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
setWorld: (world: World) => void;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* If the physics simulation is paused
|
|
207
|
+
*/
|
|
208
|
+
isPaused: boolean;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Step the physics world one step
|
|
212
|
+
*
|
|
213
|
+
* @param deltaTime The delta time to step the world with
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```
|
|
217
|
+
* step(1/60)
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
step: (deltaTime: number) => void;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Is debug mode enabled
|
|
224
|
+
*/
|
|
225
|
+
isDebug: boolean;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export const rapierContext = createContext<RapierContext | undefined>(
|
|
229
|
+
undefined
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
type CollisionSource = {
|
|
233
|
+
collider: {
|
|
234
|
+
object: Collider;
|
|
235
|
+
events?: EventMapValue;
|
|
236
|
+
state?: ColliderState;
|
|
237
|
+
};
|
|
238
|
+
rigidBody: {
|
|
239
|
+
object?: RigidBody;
|
|
240
|
+
events?: EventMapValue;
|
|
241
|
+
state?: RigidBodyState;
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const getCollisionPayloadFromSource = (
|
|
246
|
+
target: CollisionSource,
|
|
247
|
+
other: CollisionSource
|
|
248
|
+
): CollisionPayload => ({
|
|
249
|
+
target: {
|
|
250
|
+
rigidBody: target.rigidBody.object,
|
|
251
|
+
collider: target.collider.object,
|
|
252
|
+
colliderObject: target.collider.state?.object,
|
|
253
|
+
rigidBodyObject: target.rigidBody.state?.object
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
other: {
|
|
257
|
+
rigidBody: other.rigidBody.object,
|
|
258
|
+
collider: other.collider.object,
|
|
259
|
+
colliderObject: other.collider.state?.object,
|
|
260
|
+
rigidBodyObject: other.rigidBody.state?.object
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
rigidBody: other.rigidBody.object,
|
|
264
|
+
collider: other.collider.object,
|
|
265
|
+
colliderObject: other.collider.state?.object,
|
|
266
|
+
rigidBodyObject: other.rigidBody.state?.object
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const importRapier = async () => {
|
|
270
|
+
let r = await import("@dimforge/rapier3d-compat");
|
|
271
|
+
await r.init();
|
|
272
|
+
return r;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export type EventMapValue = {
|
|
276
|
+
onSleep?(): void;
|
|
277
|
+
onWake?(): void;
|
|
278
|
+
onCollisionEnter?: CollisionEnterHandler;
|
|
279
|
+
onCollisionExit?: CollisionExitHandler;
|
|
280
|
+
onIntersectionEnter?: IntersectionEnterHandler;
|
|
281
|
+
onIntersectionExit?: IntersectionExitHandler;
|
|
282
|
+
onContactForce?: ContactForceHandler;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export type EventMap = Map<ColliderHandle | RigidBodyHandle, EventMapValue>;
|
|
286
|
+
|
|
287
|
+
export interface PhysicsProps {
|
|
288
|
+
children: ReactNode;
|
|
289
|
+
/**
|
|
290
|
+
* Set the gravity of the physics world
|
|
291
|
+
* @defaultValue [0, -9.81, 0]
|
|
292
|
+
*/
|
|
293
|
+
gravity?: Vector3Tuple;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Amount of penetration the engine wont attempt to correct
|
|
297
|
+
* @defaultValue 0.001
|
|
298
|
+
*/
|
|
299
|
+
allowedLinearError?: number;
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The number of solver iterations run by the constraints solver for calculating forces.
|
|
303
|
+
* The greater this value is, the most rigid and realistic the physics simulation will be.
|
|
304
|
+
* However a greater number of iterations is more computationally intensive.
|
|
305
|
+
*
|
|
306
|
+
* @defaultValue 4
|
|
307
|
+
*/
|
|
308
|
+
numSolverIterations?: number;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration.
|
|
312
|
+
* Increasing this parameter will improve stability of the simulation. It will have a lesser effect than
|
|
313
|
+
* increasing `numSolverIterations` but is also less computationally expensive.
|
|
314
|
+
*
|
|
315
|
+
* @defaultValue 1
|
|
316
|
+
*/
|
|
317
|
+
numInternalPgsIterations?: number;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The maximal distance separating two objects that will generate predictive contacts
|
|
321
|
+
*
|
|
322
|
+
* @defaultValue 0.002
|
|
323
|
+
*
|
|
324
|
+
*/
|
|
325
|
+
predictionDistance?: number;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Minimum number of dynamic bodies in each active island
|
|
329
|
+
*
|
|
330
|
+
* @defaultValue 128
|
|
331
|
+
*/
|
|
332
|
+
minIslandSize?: number;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Maximum number of substeps performed by the solver
|
|
336
|
+
*
|
|
337
|
+
* @defaultValue 1
|
|
338
|
+
*/
|
|
339
|
+
maxCcdSubsteps?: number;
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Directly affects the `erp` (Error Reduction Parameter) which is the proportion (0 to 1) of the positional error to be corrected at each time step.
|
|
343
|
+
* The higher this value is, the more the physics engine will try to correct errors.
|
|
344
|
+
*
|
|
345
|
+
* This prop is currently undocumented in the Rapier documentation.
|
|
346
|
+
*
|
|
347
|
+
* @see https://github.com/dimforge/rapier/pull/651 where this change was made to Rapier
|
|
348
|
+
* @defaultValue 30
|
|
349
|
+
*/
|
|
350
|
+
contactNaturalFrequency?: number;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The approximate size of most dynamic objects in the scene.
|
|
354
|
+
*
|
|
355
|
+
* This value is used internally to estimate some length-based tolerance.
|
|
356
|
+
* This value can be understood as the number of units-per-meter in your physical world compared to a human-sized world in meter.
|
|
357
|
+
*
|
|
358
|
+
* @defaultValue 1
|
|
359
|
+
*/
|
|
360
|
+
lengthUnit?: number;
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Set the base automatic colliders for this physics world
|
|
364
|
+
* All Meshes inside RigidBodies will generate a collider
|
|
365
|
+
* based on this value, if not overridden.
|
|
366
|
+
*/
|
|
367
|
+
colliders?: RigidBodyAutoCollider;
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Set the timestep for the simulation.
|
|
371
|
+
* Setting this to a number (eg. 1/60) will run the
|
|
372
|
+
* simulation at that framerate. Alternatively, you can set this to
|
|
373
|
+
* "vary", which will cause the simulation to always synchronize with
|
|
374
|
+
* the current frame delta times.
|
|
375
|
+
*
|
|
376
|
+
* @defaultValue 1/60
|
|
377
|
+
*/
|
|
378
|
+
timeStep?: number | "vary";
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Pause the physics simulation
|
|
382
|
+
*
|
|
383
|
+
* @defaultValue false
|
|
384
|
+
*/
|
|
385
|
+
paused?: boolean;
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Interpolate the world transform using the frame delta times.
|
|
389
|
+
* Has no effect if timeStep is set to "vary".
|
|
390
|
+
*
|
|
391
|
+
* @defaultValue true
|
|
392
|
+
**/
|
|
393
|
+
interpolate?: boolean;
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* The update priority at which the physics simulation should run.
|
|
397
|
+
* Only used when `updateLoop` is set to "follow".
|
|
398
|
+
*
|
|
399
|
+
* @see https://docs.pmnd.rs/react-three-fiber/api/hooks#taking-over-the-render-loop
|
|
400
|
+
* @defaultValue undefined
|
|
401
|
+
*/
|
|
402
|
+
updatePriority?: number;
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Set the update loop strategy for the physics world.
|
|
406
|
+
*
|
|
407
|
+
* If set to "follow", the physics world will be stepped
|
|
408
|
+
* in a `useFrame` callback, managed by @react-three/fiber.
|
|
409
|
+
* You can use `updatePriority` prop to manage the scheduling.
|
|
410
|
+
*
|
|
411
|
+
* If set to "independent", the physics world will be stepped
|
|
412
|
+
* in a separate loop, not tied to the render loop.
|
|
413
|
+
* This is useful when using the "demand" `frameloop` strategy for the
|
|
414
|
+
* @react-three/fiber `<Canvas />`.
|
|
415
|
+
*
|
|
416
|
+
* @see https://docs.pmnd.rs/react-three-fiber/advanced/scaling-performance#on-demand-rendering
|
|
417
|
+
* @defaultValue "follow"
|
|
418
|
+
*/
|
|
419
|
+
updateLoop?: "follow" | "independent";
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Enable debug rendering of the physics world.
|
|
423
|
+
* @defaultValue false
|
|
424
|
+
*/
|
|
425
|
+
debug?: boolean;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* The main physics component used to create a physics world.
|
|
430
|
+
* @category Components
|
|
431
|
+
*/
|
|
432
|
+
export const Physics: FC<PhysicsProps> = (props) => {
|
|
433
|
+
const {
|
|
434
|
+
colliders = "cuboid",
|
|
435
|
+
children,
|
|
436
|
+
timeStep = 1 / 60,
|
|
437
|
+
paused = false,
|
|
438
|
+
interpolate = true,
|
|
439
|
+
updatePriority,
|
|
440
|
+
updateLoop = "follow",
|
|
441
|
+
debug = false,
|
|
442
|
+
|
|
443
|
+
gravity = [0, -9.81, 0],
|
|
444
|
+
allowedLinearError = 0.001,
|
|
445
|
+
predictionDistance = 0.002,
|
|
446
|
+
numSolverIterations = 4,
|
|
447
|
+
numInternalPgsIterations = 1,
|
|
448
|
+
minIslandSize = 128,
|
|
449
|
+
maxCcdSubsteps = 1,
|
|
450
|
+
contactNaturalFrequency = 30,
|
|
451
|
+
lengthUnit = 1
|
|
452
|
+
} = props;
|
|
453
|
+
const rapier = suspend(importRapier, ["@react-thee/rapier", importRapier]);
|
|
454
|
+
const { invalidate } = useThree();
|
|
455
|
+
|
|
456
|
+
const rigidBodyStates = useConst<RigidBodyStateMap>(() => new Map());
|
|
457
|
+
const colliderStates = useConst<ColliderStateMap>(() => new Map());
|
|
458
|
+
const rigidBodyEvents = useConst<EventMap>(() => new Map());
|
|
459
|
+
const colliderEvents = useConst<EventMap>(() => new Map());
|
|
460
|
+
const eventQueue = useConst(() => new EventQueue(false));
|
|
461
|
+
|
|
462
|
+
const filterContactPairHooks = useConst<FilterContactPairCallbackSet>(
|
|
463
|
+
() => new Set()
|
|
464
|
+
);
|
|
465
|
+
const filterIntersectionPairHooks =
|
|
466
|
+
useConst<FilterIntersectionPairCallbackSet>(() => new Set());
|
|
467
|
+
|
|
468
|
+
const hooks = useConst<PhysicsHooks>(() => ({
|
|
469
|
+
filterContactPair: (...args) => {
|
|
470
|
+
for (const hook of filterContactPairHooks) {
|
|
471
|
+
const result = hook.current(...args);
|
|
472
|
+
if (result !== null) return result;
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
},
|
|
476
|
+
filterIntersectionPair: (...args) => {
|
|
477
|
+
for (const hook of filterIntersectionPairHooks) {
|
|
478
|
+
const result = hook.current(...args);
|
|
479
|
+
if (result === false) return false;
|
|
480
|
+
}
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
}));
|
|
484
|
+
const beforeStepCallbacks = useConst<WorldStepCallbackSet>(() => new Set());
|
|
485
|
+
const afterStepCallbacks = useConst<WorldStepCallbackSet>(() => new Set());
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Initiate the world
|
|
489
|
+
* This creates a singleton proxy, so that the world is only created when
|
|
490
|
+
* something within it is accessed.
|
|
491
|
+
*/
|
|
492
|
+
const {
|
|
493
|
+
proxy: worldProxy,
|
|
494
|
+
reset: resetWorldProxy,
|
|
495
|
+
set: setWorldProxy
|
|
496
|
+
} = useConst(() =>
|
|
497
|
+
createSingletonProxy<World>(
|
|
498
|
+
() => new rapier.World(vectorArrayToVector3(gravity))
|
|
499
|
+
)
|
|
500
|
+
);
|
|
501
|
+
useEffect(() => {
|
|
502
|
+
return () => {
|
|
503
|
+
worldProxy.free();
|
|
504
|
+
resetWorldProxy();
|
|
505
|
+
};
|
|
506
|
+
}, []);
|
|
507
|
+
|
|
508
|
+
// Update mutable props
|
|
509
|
+
useEffect(() => {
|
|
510
|
+
worldProxy.gravity = vector3ToRapierVector(gravity);
|
|
511
|
+
|
|
512
|
+
worldProxy.integrationParameters.numSolverIterations = numSolverIterations;
|
|
513
|
+
worldProxy.integrationParameters.numInternalPgsIterations =
|
|
514
|
+
numInternalPgsIterations;
|
|
515
|
+
|
|
516
|
+
worldProxy.integrationParameters.normalizedAllowedLinearError =
|
|
517
|
+
allowedLinearError;
|
|
518
|
+
worldProxy.integrationParameters.minIslandSize = minIslandSize;
|
|
519
|
+
worldProxy.integrationParameters.maxCcdSubsteps = maxCcdSubsteps;
|
|
520
|
+
worldProxy.integrationParameters.normalizedPredictionDistance =
|
|
521
|
+
predictionDistance;
|
|
522
|
+
worldProxy.lengthUnit = lengthUnit;
|
|
523
|
+
worldProxy.integrationParameters.contact_natural_frequency =
|
|
524
|
+
contactNaturalFrequency;
|
|
525
|
+
}, [
|
|
526
|
+
worldProxy,
|
|
527
|
+
...gravity,
|
|
528
|
+
numSolverIterations,
|
|
529
|
+
numInternalPgsIterations,
|
|
530
|
+
allowedLinearError,
|
|
531
|
+
minIslandSize,
|
|
532
|
+
maxCcdSubsteps,
|
|
533
|
+
predictionDistance,
|
|
534
|
+
lengthUnit,
|
|
535
|
+
contactNaturalFrequency
|
|
536
|
+
]);
|
|
537
|
+
|
|
538
|
+
const getSourceFromColliderHandle = useCallback((handle: ColliderHandle) => {
|
|
539
|
+
const collider = worldProxy.getCollider(handle);
|
|
540
|
+
const colEvents = colliderEvents.get(handle);
|
|
541
|
+
const colliderState = colliderStates.get(handle);
|
|
542
|
+
|
|
543
|
+
const rigidBodyHandle = collider?.parent()?.handle;
|
|
544
|
+
const rigidBody =
|
|
545
|
+
rigidBodyHandle !== undefined
|
|
546
|
+
? worldProxy.getRigidBody(rigidBodyHandle)
|
|
547
|
+
: undefined;
|
|
548
|
+
const rbEvents =
|
|
549
|
+
rigidBody && rigidBodyHandle !== undefined
|
|
550
|
+
? rigidBodyEvents.get(rigidBodyHandle)
|
|
551
|
+
: undefined;
|
|
552
|
+
const rigidBodyState =
|
|
553
|
+
rigidBodyHandle !== undefined
|
|
554
|
+
? rigidBodyStates.get(rigidBodyHandle)
|
|
555
|
+
: undefined;
|
|
556
|
+
|
|
557
|
+
const source: CollisionSource = {
|
|
558
|
+
collider: {
|
|
559
|
+
object: collider,
|
|
560
|
+
events: colEvents,
|
|
561
|
+
state: colliderState
|
|
562
|
+
},
|
|
563
|
+
rigidBody: {
|
|
564
|
+
object: rigidBody,
|
|
565
|
+
events: rbEvents,
|
|
566
|
+
state: rigidBodyState
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
return source;
|
|
571
|
+
}, []);
|
|
572
|
+
|
|
573
|
+
const [steppingState] = useState<{
|
|
574
|
+
accumulator: number;
|
|
575
|
+
previousState: Record<number, any>;
|
|
576
|
+
}>({
|
|
577
|
+
previousState: {},
|
|
578
|
+
accumulator: 0
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const step = useCallback(
|
|
582
|
+
(dt: number) => {
|
|
583
|
+
const world = worldProxy;
|
|
584
|
+
|
|
585
|
+
/* Check if the timestep is supposed to be variable. We'll do this here
|
|
586
|
+
once so we don't have to string-check every frame. */
|
|
587
|
+
const timeStepVariable = timeStep === "vary";
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Fixed timeStep simulation progression
|
|
591
|
+
* @see https://gafferongames.com/post/fix_your_timestep/
|
|
592
|
+
*/
|
|
593
|
+
|
|
594
|
+
const clampedDelta = MathUtils.clamp(dt, 0, 0.5);
|
|
595
|
+
|
|
596
|
+
const stepWorld = (delta: number) => {
|
|
597
|
+
// Trigger beforeStep callbacks
|
|
598
|
+
beforeStepCallbacks.forEach((callback) => {
|
|
599
|
+
callback.current(world);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
world.timestep = delta;
|
|
603
|
+
|
|
604
|
+
const hasHooks =
|
|
605
|
+
filterContactPairHooks.size > 0 ||
|
|
606
|
+
filterIntersectionPairHooks.size > 0;
|
|
607
|
+
|
|
608
|
+
world.step(eventQueue, hasHooks ? hooks : undefined);
|
|
609
|
+
|
|
610
|
+
// Trigger afterStep callbacks
|
|
611
|
+
afterStepCallbacks.forEach((callback) => {
|
|
612
|
+
callback.current(world);
|
|
613
|
+
});
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
if (timeStepVariable) {
|
|
617
|
+
stepWorld(clampedDelta);
|
|
618
|
+
} else {
|
|
619
|
+
// don't step time forwards if paused
|
|
620
|
+
// Increase accumulator
|
|
621
|
+
steppingState.accumulator += clampedDelta;
|
|
622
|
+
|
|
623
|
+
while (steppingState.accumulator >= timeStep) {
|
|
624
|
+
// Set up previous state
|
|
625
|
+
// needed for accurate interpolations if the world steps more than once
|
|
626
|
+
if (interpolate) {
|
|
627
|
+
steppingState.previousState = {};
|
|
628
|
+
world.forEachRigidBody((body) => {
|
|
629
|
+
steppingState.previousState[body.handle] = {
|
|
630
|
+
position: body.translation(),
|
|
631
|
+
rotation: body.rotation()
|
|
632
|
+
};
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
stepWorld(timeStep);
|
|
637
|
+
|
|
638
|
+
steppingState.accumulator -= timeStep;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const interpolationAlpha =
|
|
643
|
+
timeStepVariable || !interpolate || paused
|
|
644
|
+
? 1
|
|
645
|
+
: steppingState.accumulator / timeStep;
|
|
646
|
+
|
|
647
|
+
// Update meshes
|
|
648
|
+
rigidBodyStates.forEach((state, handle) => {
|
|
649
|
+
const rigidBody = world.getRigidBody(handle);
|
|
650
|
+
|
|
651
|
+
const events = rigidBodyEvents.get(handle);
|
|
652
|
+
if (events?.onSleep || events?.onWake) {
|
|
653
|
+
if (rigidBody.isSleeping() && !state.isSleeping) {
|
|
654
|
+
events?.onSleep?.();
|
|
655
|
+
}
|
|
656
|
+
if (!rigidBody.isSleeping() && state.isSleeping) {
|
|
657
|
+
events?.onWake?.();
|
|
658
|
+
}
|
|
659
|
+
state.isSleeping = rigidBody.isSleeping();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (
|
|
663
|
+
!rigidBody ||
|
|
664
|
+
(rigidBody.isSleeping() && !("isInstancedMesh" in state.object)) ||
|
|
665
|
+
!state.setMatrix
|
|
666
|
+
) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// New states
|
|
671
|
+
let t = rigidBody.translation() as Vector3;
|
|
672
|
+
let r = rigidBody.rotation() as Quaternion;
|
|
673
|
+
|
|
674
|
+
let previousState = steppingState.previousState[handle];
|
|
675
|
+
|
|
676
|
+
if (previousState) {
|
|
677
|
+
// Get previous simulated world position
|
|
678
|
+
_matrix4
|
|
679
|
+
.compose(
|
|
680
|
+
previousState.position,
|
|
681
|
+
rapierQuaternionToQuaternion(previousState.rotation),
|
|
682
|
+
state.scale
|
|
683
|
+
)
|
|
684
|
+
.premultiply(state.invertedWorldMatrix)
|
|
685
|
+
.decompose(_position, _rotation, _scale);
|
|
686
|
+
|
|
687
|
+
// Apply previous tick position
|
|
688
|
+
if (state.meshType == "mesh") {
|
|
689
|
+
state.object.position.copy(_position);
|
|
690
|
+
state.object.quaternion.copy(_rotation);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Get new position
|
|
695
|
+
_matrix4
|
|
696
|
+
.compose(t, rapierQuaternionToQuaternion(r), state.scale)
|
|
697
|
+
.premultiply(state.invertedWorldMatrix)
|
|
698
|
+
.decompose(_position, _rotation, _scale);
|
|
699
|
+
|
|
700
|
+
if (state.meshType == "instancedMesh") {
|
|
701
|
+
state.setMatrix(_matrix4);
|
|
702
|
+
} else {
|
|
703
|
+
// Interpolate to new position
|
|
704
|
+
state.object.position.lerp(_position, interpolationAlpha);
|
|
705
|
+
state.object.quaternion.slerp(_rotation, interpolationAlpha);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
eventQueue.drainCollisionEvents((handle1, handle2, started) => {
|
|
710
|
+
const source1 = getSourceFromColliderHandle(handle1);
|
|
711
|
+
const source2 = getSourceFromColliderHandle(handle2);
|
|
712
|
+
|
|
713
|
+
// Collision Events
|
|
714
|
+
if (!source1?.collider.object || !source2?.collider.object) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const collisionPayload1 = getCollisionPayloadFromSource(
|
|
719
|
+
source1,
|
|
720
|
+
source2
|
|
721
|
+
);
|
|
722
|
+
const collisionPayload2 = getCollisionPayloadFromSource(
|
|
723
|
+
source2,
|
|
724
|
+
source1
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
if (started) {
|
|
728
|
+
world.contactPair(
|
|
729
|
+
source1.collider.object,
|
|
730
|
+
source2.collider.object,
|
|
731
|
+
(manifold, flipped) => {
|
|
732
|
+
/* RigidBody events */
|
|
733
|
+
source1.rigidBody.events?.onCollisionEnter?.({
|
|
734
|
+
...collisionPayload1,
|
|
735
|
+
manifold,
|
|
736
|
+
flipped
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
source2.rigidBody.events?.onCollisionEnter?.({
|
|
740
|
+
...collisionPayload2,
|
|
741
|
+
manifold,
|
|
742
|
+
flipped
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
/* Collider events */
|
|
746
|
+
source1.collider.events?.onCollisionEnter?.({
|
|
747
|
+
...collisionPayload1,
|
|
748
|
+
manifold,
|
|
749
|
+
flipped
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
source2.collider.events?.onCollisionEnter?.({
|
|
753
|
+
...collisionPayload2,
|
|
754
|
+
manifold,
|
|
755
|
+
flipped
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
);
|
|
759
|
+
} else {
|
|
760
|
+
source1.rigidBody.events?.onCollisionExit?.(collisionPayload1);
|
|
761
|
+
source2.rigidBody.events?.onCollisionExit?.(collisionPayload2);
|
|
762
|
+
source1.collider.events?.onCollisionExit?.(collisionPayload1);
|
|
763
|
+
source2.collider.events?.onCollisionExit?.(collisionPayload2);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Sensor Intersections
|
|
767
|
+
if (started) {
|
|
768
|
+
if (
|
|
769
|
+
world.intersectionPair(
|
|
770
|
+
source1.collider.object,
|
|
771
|
+
source2.collider.object
|
|
772
|
+
)
|
|
773
|
+
) {
|
|
774
|
+
source1.rigidBody.events?.onIntersectionEnter?.(collisionPayload1);
|
|
775
|
+
|
|
776
|
+
source2.rigidBody.events?.onIntersectionEnter?.(collisionPayload2);
|
|
777
|
+
|
|
778
|
+
source1.collider.events?.onIntersectionEnter?.(collisionPayload1);
|
|
779
|
+
|
|
780
|
+
source2.collider.events?.onIntersectionEnter?.(collisionPayload2);
|
|
781
|
+
}
|
|
782
|
+
} else {
|
|
783
|
+
source1.rigidBody.events?.onIntersectionExit?.(collisionPayload1);
|
|
784
|
+
source2.rigidBody.events?.onIntersectionExit?.(collisionPayload2);
|
|
785
|
+
source1.collider.events?.onIntersectionExit?.(collisionPayload1);
|
|
786
|
+
source2.collider.events?.onIntersectionExit?.(collisionPayload2);
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
eventQueue.drainContactForceEvents((event) => {
|
|
791
|
+
const source1 = getSourceFromColliderHandle(event.collider1());
|
|
792
|
+
const source2 = getSourceFromColliderHandle(event.collider2());
|
|
793
|
+
|
|
794
|
+
// Collision Events
|
|
795
|
+
if (!source1?.collider.object || !source2?.collider.object) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const collisionPayload1 = getCollisionPayloadFromSource(
|
|
800
|
+
source1,
|
|
801
|
+
source2
|
|
802
|
+
);
|
|
803
|
+
const collisionPayload2 = getCollisionPayloadFromSource(
|
|
804
|
+
source2,
|
|
805
|
+
source1
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
source1.rigidBody.events?.onContactForce?.({
|
|
809
|
+
...collisionPayload1,
|
|
810
|
+
totalForce: event.totalForce(),
|
|
811
|
+
totalForceMagnitude: event.totalForceMagnitude(),
|
|
812
|
+
maxForceDirection: event.maxForceDirection(),
|
|
813
|
+
maxForceMagnitude: event.maxForceMagnitude()
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
source2.rigidBody.events?.onContactForce?.({
|
|
817
|
+
...collisionPayload2,
|
|
818
|
+
totalForce: event.totalForce(),
|
|
819
|
+
totalForceMagnitude: event.totalForceMagnitude(),
|
|
820
|
+
maxForceDirection: event.maxForceDirection(),
|
|
821
|
+
maxForceMagnitude: event.maxForceMagnitude()
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
source1.collider.events?.onContactForce?.({
|
|
825
|
+
...collisionPayload1,
|
|
826
|
+
totalForce: event.totalForce(),
|
|
827
|
+
totalForceMagnitude: event.totalForceMagnitude(),
|
|
828
|
+
maxForceDirection: event.maxForceDirection(),
|
|
829
|
+
maxForceMagnitude: event.maxForceMagnitude()
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
source2.collider.events?.onContactForce?.({
|
|
833
|
+
...collisionPayload2,
|
|
834
|
+
totalForce: event.totalForce(),
|
|
835
|
+
totalForceMagnitude: event.totalForceMagnitude(),
|
|
836
|
+
maxForceDirection: event.maxForceDirection(),
|
|
837
|
+
maxForceMagnitude: event.maxForceMagnitude()
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
world.forEachActiveRigidBody(() => {
|
|
842
|
+
invalidate();
|
|
843
|
+
});
|
|
844
|
+
},
|
|
845
|
+
[paused, timeStep, interpolate, worldProxy]
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
const context = useMemo<RapierContext>(
|
|
849
|
+
() => ({
|
|
850
|
+
rapier,
|
|
851
|
+
world: worldProxy,
|
|
852
|
+
setWorld: (world: World) => {
|
|
853
|
+
setWorldProxy(world);
|
|
854
|
+
},
|
|
855
|
+
physicsOptions: {
|
|
856
|
+
colliders,
|
|
857
|
+
gravity
|
|
858
|
+
},
|
|
859
|
+
rigidBodyStates,
|
|
860
|
+
colliderStates,
|
|
861
|
+
rigidBodyEvents,
|
|
862
|
+
colliderEvents,
|
|
863
|
+
beforeStepCallbacks,
|
|
864
|
+
afterStepCallbacks,
|
|
865
|
+
isPaused: paused,
|
|
866
|
+
isDebug: debug,
|
|
867
|
+
step,
|
|
868
|
+
filterContactPairHooks,
|
|
869
|
+
filterIntersectionPairHooks
|
|
870
|
+
}),
|
|
871
|
+
[paused, step, debug, colliders, gravity]
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
const stepCallback = useCallback(
|
|
875
|
+
(delta: number) => {
|
|
876
|
+
if (!paused) {
|
|
877
|
+
step(delta);
|
|
878
|
+
}
|
|
879
|
+
},
|
|
880
|
+
[paused, step]
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
return (
|
|
884
|
+
<rapierContext.Provider value={context}>
|
|
885
|
+
<FrameStepper
|
|
886
|
+
onStep={stepCallback}
|
|
887
|
+
type={updateLoop}
|
|
888
|
+
updatePriority={updatePriority}
|
|
889
|
+
/>
|
|
890
|
+
{debug && <Debug />}
|
|
891
|
+
{children}
|
|
892
|
+
</rapierContext.Provider>
|
|
893
|
+
);
|
|
894
|
+
};
|