@react-three/rapier 0.6.4 → 0.6.5

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.
@@ -19,6 +19,7 @@ export interface RapierContext {
19
19
  colliders: RigidBodyAutoCollider;
20
20
  };
21
21
  rigidBodyEvents: EventMap;
22
+ isPaused: boolean;
22
23
  }
23
24
  export declare const RapierContext: React.Context<RapierContext | undefined>;
24
25
  declare type EventMap = Map<ColliderHandle | RigidBodyHandle, {
@@ -51,14 +52,15 @@ interface RapierWorldProps {
51
52
  * Setting this to a number (eg. 1/60) will run the
52
53
  * simulation at that framerate.
53
54
  *
54
- * "vary" will run the simulation at a delta-value based
55
- * on the users current framerate. This ensures simulations
56
- * run at the same percieved speed at all framerates, but
57
- * can also lead to instability.
55
+ * @defaultValue 1/60
56
+ */
57
+ timeStep?: number;
58
+ /**
59
+ * Maximum number of fixed steps to take per function call.
58
60
  *
59
- * @defaultValue "vary"
61
+ * @defaultValue 10
60
62
  */
61
- timeStep?: number | "vary";
63
+ maxSubSteps?: number;
62
64
  /**
63
65
  * Pause the physics simulation
64
66
  *
@@ -469,10 +469,15 @@ const Physics = ({
469
469
  colliders: _colliders = "cuboid",
470
470
  gravity: _gravity = [0, -9.81, 0],
471
471
  children,
472
- timeStep: _timeStep = "vary",
472
+ timeStep: _timeStep = 1 / 60,
473
+ maxSubSteps: _maxSubSteps = 10,
473
474
  paused: _paused = false
474
475
  }) => {
475
476
  const rapier = useAsset.useAsset(importRapier);
477
+ const [isPaused, setIsPaused] = React.useState(_paused);
478
+ React.useEffect(() => {
479
+ setIsPaused(_paused);
480
+ }, [_paused]);
476
481
  const worldRef = React.useRef();
477
482
  const getWorldRef = React.useRef(() => {
478
483
  if (!worldRef.current) {
@@ -492,7 +497,6 @@ const Physics = ({
492
497
  return () => {
493
498
  if (world) {
494
499
  world.free();
495
- worldRef.current = undefined;
496
500
  }
497
501
  };
498
502
  }, []); // Update gravity
@@ -504,22 +508,46 @@ const Physics = ({
504
508
  world.gravity = vectorArrayToVector3(_gravity);
505
509
  }
506
510
  }, [_gravity]);
507
- const time = React.useRef(performance.now());
508
- fiber.useFrame(context => {
511
+ const [steppingState] = React.useState({
512
+ time: 0,
513
+ lastTime: 0,
514
+ accumulator: 0
515
+ });
516
+ fiber.useFrame((_, delta) => {
509
517
  const world = worldRef.current;
510
- if (!world) return; // Set timestep to current delta, to allow for variable frame rates
511
- // We cap the delta at 100, so that the physics simulation doesn't get wild
512
-
513
- const now = performance.now();
514
- const delta = Math.min(100, now - time.current);
515
-
516
- if (_timeStep === "vary") {
517
- world.timestep = delta / 1000;
518
- } else {
519
- world.timestep = _timeStep;
518
+ if (!world) return;
519
+ world.timestep = _timeStep;
520
+ /**
521
+ * Fixed timeStep simulation progression
522
+ * @see https://gafferongames.com/post/fix_your_timestep/
523
+ */
524
+
525
+ let previousTranslations = {}; // don't step time forwards if paused
526
+
527
+ const nowTime = steppingState.time += _paused ? 0 : delta * 1000;
528
+ const timeStepMs = _timeStep * 1000;
529
+ const timeSinceLast = nowTime - steppingState.lastTime;
530
+ steppingState.lastTime = nowTime;
531
+ steppingState.accumulator += timeSinceLast;
532
+
533
+ if (!_paused) {
534
+ let subSteps = 0;
535
+
536
+ while (steppingState.accumulator >= timeStepMs && subSteps < _maxSubSteps) {
537
+ // Collect previous state
538
+ world.bodies.forEach(b => {
539
+ previousTranslations[b.handle] = {
540
+ rotation: b.rotation(),
541
+ translation: b.translation()
542
+ };
543
+ });
544
+ world.step(eventQueue);
545
+ subSteps++;
546
+ steppingState.accumulator -= timeStepMs;
547
+ }
520
548
  }
521
549
 
522
- if (!_paused) world.step(eventQueue); // Update meshes
550
+ const interpolationAlpha = steppingState.accumulator % timeStepMs / timeStepMs; // Update meshes
523
551
 
524
552
  rigidBodyStates.forEach((state, handle) => {
525
553
  const rigidBody = world.getRigidBody(handle);
@@ -545,7 +573,12 @@ const Physics = ({
545
573
  return;
546
574
  }
547
575
 
548
- state.setMatrix(_matrix4.compose(rapierVector3ToVector3(rigidBody.translation()), rapierQuaternionToQuaternion(rigidBody.rotation()), state.worldScale).premultiply(state.invertedMatrixWorld));
576
+ let oldState = previousTranslations[rigidBody.handle];
577
+ let newTranslation = rapierVector3ToVector3(rigidBody.translation());
578
+ let newRotation = rapierQuaternionToQuaternion(rigidBody.rotation());
579
+ let interpolatedTranslation = oldState ? rapierVector3ToVector3(oldState.translation).lerp(newTranslation, interpolationAlpha) : newTranslation;
580
+ let interpolatedRotation = oldState ? rapierQuaternionToQuaternion(oldState.rotation).slerp(newRotation, interpolationAlpha) : newRotation;
581
+ state.setMatrix(_matrix4.compose(interpolatedTranslation, interpolatedRotation, state.worldScale).premultiply(state.invertedMatrixWorld));
549
582
 
550
583
  if (state.mesh instanceof three.InstancedMesh) {
551
584
  state.mesh.instanceMatrix.needsUpdate = true;
@@ -595,7 +628,6 @@ const Physics = ({
595
628
  });
596
629
  }
597
630
  });
598
- time.current = now;
599
631
  });
600
632
  const api = React.useMemo(() => createWorldApi(getWorldRef), []);
601
633
  const context = React.useMemo(() => ({
@@ -607,8 +639,9 @@ const Physics = ({
607
639
  },
608
640
  colliderMeshes,
609
641
  rigidBodyStates,
610
- rigidBodyEvents
611
- }), []);
642
+ rigidBodyEvents,
643
+ isPaused
644
+ }), [isPaused]);
612
645
  return /*#__PURE__*/React__default["default"].createElement(RapierContext.Provider, {
613
646
  value: context
614
647
  }, children);
@@ -469,10 +469,15 @@ const Physics = ({
469
469
  colliders: _colliders = "cuboid",
470
470
  gravity: _gravity = [0, -9.81, 0],
471
471
  children,
472
- timeStep: _timeStep = "vary",
472
+ timeStep: _timeStep = 1 / 60,
473
+ maxSubSteps: _maxSubSteps = 10,
473
474
  paused: _paused = false
474
475
  }) => {
475
476
  const rapier = useAsset.useAsset(importRapier);
477
+ const [isPaused, setIsPaused] = React.useState(_paused);
478
+ React.useEffect(() => {
479
+ setIsPaused(_paused);
480
+ }, [_paused]);
476
481
  const worldRef = React.useRef();
477
482
  const getWorldRef = React.useRef(() => {
478
483
  if (!worldRef.current) {
@@ -492,7 +497,6 @@ const Physics = ({
492
497
  return () => {
493
498
  if (world) {
494
499
  world.free();
495
- worldRef.current = undefined;
496
500
  }
497
501
  };
498
502
  }, []); // Update gravity
@@ -504,22 +508,46 @@ const Physics = ({
504
508
  world.gravity = vectorArrayToVector3(_gravity);
505
509
  }
506
510
  }, [_gravity]);
507
- const time = React.useRef(performance.now());
508
- fiber.useFrame(context => {
511
+ const [steppingState] = React.useState({
512
+ time: 0,
513
+ lastTime: 0,
514
+ accumulator: 0
515
+ });
516
+ fiber.useFrame((_, delta) => {
509
517
  const world = worldRef.current;
510
- if (!world) return; // Set timestep to current delta, to allow for variable frame rates
511
- // We cap the delta at 100, so that the physics simulation doesn't get wild
512
-
513
- const now = performance.now();
514
- const delta = Math.min(100, now - time.current);
515
-
516
- if (_timeStep === "vary") {
517
- world.timestep = delta / 1000;
518
- } else {
519
- world.timestep = _timeStep;
518
+ if (!world) return;
519
+ world.timestep = _timeStep;
520
+ /**
521
+ * Fixed timeStep simulation progression
522
+ * @see https://gafferongames.com/post/fix_your_timestep/
523
+ */
524
+
525
+ let previousTranslations = {}; // don't step time forwards if paused
526
+
527
+ const nowTime = steppingState.time += _paused ? 0 : delta * 1000;
528
+ const timeStepMs = _timeStep * 1000;
529
+ const timeSinceLast = nowTime - steppingState.lastTime;
530
+ steppingState.lastTime = nowTime;
531
+ steppingState.accumulator += timeSinceLast;
532
+
533
+ if (!_paused) {
534
+ let subSteps = 0;
535
+
536
+ while (steppingState.accumulator >= timeStepMs && subSteps < _maxSubSteps) {
537
+ // Collect previous state
538
+ world.bodies.forEach(b => {
539
+ previousTranslations[b.handle] = {
540
+ rotation: b.rotation(),
541
+ translation: b.translation()
542
+ };
543
+ });
544
+ world.step(eventQueue);
545
+ subSteps++;
546
+ steppingState.accumulator -= timeStepMs;
547
+ }
520
548
  }
521
549
 
522
- if (!_paused) world.step(eventQueue); // Update meshes
550
+ const interpolationAlpha = steppingState.accumulator % timeStepMs / timeStepMs; // Update meshes
523
551
 
524
552
  rigidBodyStates.forEach((state, handle) => {
525
553
  const rigidBody = world.getRigidBody(handle);
@@ -545,7 +573,12 @@ const Physics = ({
545
573
  return;
546
574
  }
547
575
 
548
- state.setMatrix(_matrix4.compose(rapierVector3ToVector3(rigidBody.translation()), rapierQuaternionToQuaternion(rigidBody.rotation()), state.worldScale).premultiply(state.invertedMatrixWorld));
576
+ let oldState = previousTranslations[rigidBody.handle];
577
+ let newTranslation = rapierVector3ToVector3(rigidBody.translation());
578
+ let newRotation = rapierQuaternionToQuaternion(rigidBody.rotation());
579
+ let interpolatedTranslation = oldState ? rapierVector3ToVector3(oldState.translation).lerp(newTranslation, interpolationAlpha) : newTranslation;
580
+ let interpolatedRotation = oldState ? rapierQuaternionToQuaternion(oldState.rotation).slerp(newRotation, interpolationAlpha) : newRotation;
581
+ state.setMatrix(_matrix4.compose(interpolatedTranslation, interpolatedRotation, state.worldScale).premultiply(state.invertedMatrixWorld));
549
582
 
550
583
  if (state.mesh instanceof three.InstancedMesh) {
551
584
  state.mesh.instanceMatrix.needsUpdate = true;
@@ -595,7 +628,6 @@ const Physics = ({
595
628
  });
596
629
  }
597
630
  });
598
- time.current = now;
599
631
  });
600
632
  const api = React.useMemo(() => createWorldApi(getWorldRef), []);
601
633
  const context = React.useMemo(() => ({
@@ -607,8 +639,9 @@ const Physics = ({
607
639
  },
608
640
  colliderMeshes,
609
641
  rigidBodyStates,
610
- rigidBodyEvents
611
- }), []);
642
+ rigidBodyEvents,
643
+ isPaused
644
+ }), [isPaused]);
612
645
  return /*#__PURE__*/React__default["default"].createElement(RapierContext.Provider, {
613
646
  value: context
614
647
  }, children);
@@ -1,6 +1,6 @@
1
1
  import { ColliderDesc, ActiveEvents, RigidBodyDesc, CoefficientCombineRule, EventQueue, ShapeType } from '@dimforge/rapier3d-compat';
2
2
  export { CoefficientCombineRule, Collider as RapierCollider, RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat';
3
- import React, { useRef, useState, useEffect, useMemo, createContext, useContext, forwardRef, useImperativeHandle, memo, useLayoutEffect } from 'react';
3
+ import React, { useState, useEffect, useRef, useMemo, createContext, useContext, forwardRef, useImperativeHandle, memo, useLayoutEffect } from 'react';
4
4
  import { useAsset } from 'use-asset';
5
5
  import { useFrame } from '@react-three/fiber';
6
6
  import { Quaternion, Euler, Vector3, Object3D, Matrix4, InstancedMesh, CylinderBufferGeometry, BufferGeometry, BufferAttribute, SphereBufferGeometry, BoxBufferGeometry, DynamicDrawUsage } from 'three';
@@ -444,10 +444,15 @@ const Physics = ({
444
444
  colliders: _colliders = "cuboid",
445
445
  gravity: _gravity = [0, -9.81, 0],
446
446
  children,
447
- timeStep: _timeStep = "vary",
447
+ timeStep: _timeStep = 1 / 60,
448
+ maxSubSteps: _maxSubSteps = 10,
448
449
  paused: _paused = false
449
450
  }) => {
450
451
  const rapier = useAsset(importRapier);
452
+ const [isPaused, setIsPaused] = useState(_paused);
453
+ useEffect(() => {
454
+ setIsPaused(_paused);
455
+ }, [_paused]);
451
456
  const worldRef = useRef();
452
457
  const getWorldRef = useRef(() => {
453
458
  if (!worldRef.current) {
@@ -467,7 +472,6 @@ const Physics = ({
467
472
  return () => {
468
473
  if (world) {
469
474
  world.free();
470
- worldRef.current = undefined;
471
475
  }
472
476
  };
473
477
  }, []); // Update gravity
@@ -479,22 +483,46 @@ const Physics = ({
479
483
  world.gravity = vectorArrayToVector3(_gravity);
480
484
  }
481
485
  }, [_gravity]);
482
- const time = useRef(performance.now());
483
- useFrame(context => {
486
+ const [steppingState] = useState({
487
+ time: 0,
488
+ lastTime: 0,
489
+ accumulator: 0
490
+ });
491
+ useFrame((_, delta) => {
484
492
  const world = worldRef.current;
485
- if (!world) return; // Set timestep to current delta, to allow for variable frame rates
486
- // We cap the delta at 100, so that the physics simulation doesn't get wild
487
-
488
- const now = performance.now();
489
- const delta = Math.min(100, now - time.current);
490
-
491
- if (_timeStep === "vary") {
492
- world.timestep = delta / 1000;
493
- } else {
494
- world.timestep = _timeStep;
493
+ if (!world) return;
494
+ world.timestep = _timeStep;
495
+ /**
496
+ * Fixed timeStep simulation progression
497
+ * @see https://gafferongames.com/post/fix_your_timestep/
498
+ */
499
+
500
+ let previousTranslations = {}; // don't step time forwards if paused
501
+
502
+ const nowTime = steppingState.time += _paused ? 0 : delta * 1000;
503
+ const timeStepMs = _timeStep * 1000;
504
+ const timeSinceLast = nowTime - steppingState.lastTime;
505
+ steppingState.lastTime = nowTime;
506
+ steppingState.accumulator += timeSinceLast;
507
+
508
+ if (!_paused) {
509
+ let subSteps = 0;
510
+
511
+ while (steppingState.accumulator >= timeStepMs && subSteps < _maxSubSteps) {
512
+ // Collect previous state
513
+ world.bodies.forEach(b => {
514
+ previousTranslations[b.handle] = {
515
+ rotation: b.rotation(),
516
+ translation: b.translation()
517
+ };
518
+ });
519
+ world.step(eventQueue);
520
+ subSteps++;
521
+ steppingState.accumulator -= timeStepMs;
522
+ }
495
523
  }
496
524
 
497
- if (!_paused) world.step(eventQueue); // Update meshes
525
+ const interpolationAlpha = steppingState.accumulator % timeStepMs / timeStepMs; // Update meshes
498
526
 
499
527
  rigidBodyStates.forEach((state, handle) => {
500
528
  const rigidBody = world.getRigidBody(handle);
@@ -520,7 +548,12 @@ const Physics = ({
520
548
  return;
521
549
  }
522
550
 
523
- state.setMatrix(_matrix4.compose(rapierVector3ToVector3(rigidBody.translation()), rapierQuaternionToQuaternion(rigidBody.rotation()), state.worldScale).premultiply(state.invertedMatrixWorld));
551
+ let oldState = previousTranslations[rigidBody.handle];
552
+ let newTranslation = rapierVector3ToVector3(rigidBody.translation());
553
+ let newRotation = rapierQuaternionToQuaternion(rigidBody.rotation());
554
+ let interpolatedTranslation = oldState ? rapierVector3ToVector3(oldState.translation).lerp(newTranslation, interpolationAlpha) : newTranslation;
555
+ let interpolatedRotation = oldState ? rapierQuaternionToQuaternion(oldState.rotation).slerp(newRotation, interpolationAlpha) : newRotation;
556
+ state.setMatrix(_matrix4.compose(interpolatedTranslation, interpolatedRotation, state.worldScale).premultiply(state.invertedMatrixWorld));
524
557
 
525
558
  if (state.mesh instanceof InstancedMesh) {
526
559
  state.mesh.instanceMatrix.needsUpdate = true;
@@ -570,7 +603,6 @@ const Physics = ({
570
603
  });
571
604
  }
572
605
  });
573
- time.current = now;
574
606
  });
575
607
  const api = useMemo(() => createWorldApi(getWorldRef), []);
576
608
  const context = useMemo(() => ({
@@ -582,8 +614,9 @@ const Physics = ({
582
614
  },
583
615
  colliderMeshes,
584
616
  rigidBodyStates,
585
- rigidBodyEvents
586
- }), []);
617
+ rigidBodyEvents,
618
+ isPaused
619
+ }), [isPaused]);
587
620
  return /*#__PURE__*/React.createElement(RapierContext.Provider, {
588
621
  value: context
589
622
  }, children);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-three/rapier",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "source": "src/index.ts",
5
5
  "main": "dist/react-three-rapier.cjs.js",
6
6
  "module": "dist/react-three-rapier.esm.js",
package/readme.md CHANGED
@@ -251,6 +251,7 @@ In order, but also not necessarily:
251
251
  - [x] Collision events
252
252
  - [x] Colliders outside RigidBodies
253
253
  - [x] InstancedMesh support
254
+ - [x] Timestep improvements for determinism
254
255
  - [ ] Normalize and improve collision events (add events to single Colliders, InstancedRigidBodies, etc)
255
256
  - [ ] Docs
256
257
  - [ ] CodeSandbox examples