@react-three/rapier 0.6.4 → 0.6.7

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.
@@ -1,9 +1,11 @@
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
- import { Quaternion, Euler, Vector3, Object3D, Matrix4, InstancedMesh, CylinderBufferGeometry, BufferGeometry, BufferAttribute, SphereBufferGeometry, BoxBufferGeometry, DynamicDrawUsage } from 'three';
6
+ import { Quaternion, Euler, Vector3, Object3D, Matrix4, InstancedMesh, MeshBasicMaterial, Color, PlaneGeometry, ConeBufferGeometry, CapsuleBufferGeometry, CylinderBufferGeometry, BufferGeometry, BufferAttribute, SphereBufferGeometry, BoxBufferGeometry, DynamicDrawUsage } from 'three';
7
+ import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils';
8
+ import { RoundedBoxGeometry } from 'three-stdlib';
7
9
 
8
10
  const _quaternion = new Quaternion();
9
11
  const _euler = new Euler();
@@ -48,11 +50,13 @@ const decomposeMatrix4 = m => {
48
50
  };
49
51
  };
50
52
  const scaleColliderArgs = (shape, args, scale) => {
51
- // Heightfield only scales the last arg
52
- const newArgs = args.slice();
53
+ const newArgs = args.slice(); // Heightfield uses a vector
53
54
 
54
55
  if (shape === "heightfield") {
55
- newArgs[3] *= scale.x;
56
+ const s = newArgs[3];
57
+ s.x *= scale.x;
58
+ s.x *= scale.y;
59
+ s.x *= scale.z;
56
60
  return newArgs;
57
61
  } // Trimesh and convex scale the vertices
58
62
 
@@ -60,9 +64,10 @@ const scaleColliderArgs = (shape, args, scale) => {
60
64
  if (shape === "trimesh" || shape === "convexHull") {
61
65
  newArgs[0] = scaleVertices(newArgs[0], scale);
62
66
  return newArgs;
63
- }
67
+ } // Prepfill with some extra
68
+
64
69
 
65
- const scaleArray = [scale.x, scale.y, scale.z];
70
+ const scaleArray = [scale.x, scale.y, scale.z, scale.x, scale.x];
66
71
  return newArgs.map((arg, index) => scaleArray[index] * arg);
67
72
  };
68
73
  const createColliderFromOptions = ({
@@ -92,6 +97,10 @@ const createColliderFromOptions = ({
92
97
  w: qRotation.w
93
98
  }).setRestitution((_options$restitution = options === null || options === void 0 ? void 0 : options.restitution) !== null && _options$restitution !== void 0 ? _options$restitution : 0).setRestitutionCombineRule((_options$restitutionC = options === null || options === void 0 ? void 0 : options.restitutionCombineRule) !== null && _options$restitutionC !== void 0 ? _options$restitutionC : CoefficientCombineRule.Average).setFriction((_options$friction = options === null || options === void 0 ? void 0 : options.friction) !== null && _options$friction !== void 0 ? _options$friction : 0.7).setFrictionCombineRule((_options$frictionComb = options === null || options === void 0 ? void 0 : options.frictionCombineRule) !== null && _options$frictionComb !== void 0 ? _options$frictionComb : CoefficientCombineRule.Average);
94
99
 
100
+ if (colliderShape === "heightfield") {
101
+ console.log(colliderDesc);
102
+ }
103
+
95
104
  if (hasCollisionEvents) {
96
105
  colliderDesc = colliderDesc.setActiveEvents(ActiveEvents.COLLISION_EVENTS);
97
106
  } // If any of the mass properties are specified, add mass properties
@@ -219,15 +228,15 @@ const colliderDescFromGeometry = (geometry, colliders, scale, hasCollisionEvents
219
228
  {
220
229
  var _g$index;
221
230
 
222
- const _g = geometry.clone().scale(scale.x, scale.y, scale.z);
223
-
224
- desc = ColliderDesc.trimesh(_g.attributes.position.array, (_g$index = _g.index) === null || _g$index === void 0 ? void 0 : _g$index.array);
231
+ const clonedGeometry = geometry.index ? geometry.clone() : mergeVertices(geometry);
232
+ const g = clonedGeometry.scale(scale.x, scale.y, scale.z);
233
+ desc = ColliderDesc.trimesh(g.attributes.position.array, (_g$index = g.index) === null || _g$index === void 0 ? void 0 : _g$index.array);
225
234
  }
226
235
  break;
227
236
 
228
237
  case "hull":
229
- const g = geometry.clone().scale(scale.x, scale.y, scale.z);
230
238
  {
239
+ const g = geometry.clone().scale(scale.x, scale.y, scale.z);
231
240
  desc = ColliderDesc.convexHull(g.attributes.position.array);
232
241
  }
233
242
  break;
@@ -444,10 +453,15 @@ const Physics = ({
444
453
  colliders: _colliders = "cuboid",
445
454
  gravity: _gravity = [0, -9.81, 0],
446
455
  children,
447
- timeStep: _timeStep = "vary",
456
+ timeStep: _timeStep = 1 / 60,
457
+ maxSubSteps: _maxSubSteps = 10,
448
458
  paused: _paused = false
449
459
  }) => {
450
460
  const rapier = useAsset(importRapier);
461
+ const [isPaused, setIsPaused] = useState(_paused);
462
+ useEffect(() => {
463
+ setIsPaused(_paused);
464
+ }, [_paused]);
451
465
  const worldRef = useRef();
452
466
  const getWorldRef = useRef(() => {
453
467
  if (!worldRef.current) {
@@ -467,7 +481,6 @@ const Physics = ({
467
481
  return () => {
468
482
  if (world) {
469
483
  world.free();
470
- worldRef.current = undefined;
471
484
  }
472
485
  };
473
486
  }, []); // Update gravity
@@ -479,22 +492,46 @@ const Physics = ({
479
492
  world.gravity = vectorArrayToVector3(_gravity);
480
493
  }
481
494
  }, [_gravity]);
482
- const time = useRef(performance.now());
483
- useFrame(context => {
495
+ const [steppingState] = useState({
496
+ time: 0,
497
+ lastTime: 0,
498
+ accumulator: 0
499
+ });
500
+ useFrame((_, delta) => {
484
501
  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;
502
+ if (!world) return;
503
+ world.timestep = _timeStep;
504
+ /**
505
+ * Fixed timeStep simulation progression
506
+ * @see https://gafferongames.com/post/fix_your_timestep/
507
+ */
508
+
509
+ let previousTranslations = {}; // don't step time forwards if paused
510
+
511
+ const nowTime = steppingState.time += _paused ? 0 : delta * 1000;
512
+ const timeStepMs = _timeStep * 1000;
513
+ const timeSinceLast = nowTime - steppingState.lastTime;
514
+ steppingState.lastTime = nowTime;
515
+ steppingState.accumulator += timeSinceLast;
516
+
517
+ if (!_paused) {
518
+ let subSteps = 0;
519
+
520
+ while (steppingState.accumulator >= timeStepMs && subSteps < _maxSubSteps) {
521
+ // Collect previous state
522
+ world.bodies.forEach(b => {
523
+ previousTranslations[b.handle] = {
524
+ rotation: rapierQuaternionToQuaternion(b.rotation()).normalize(),
525
+ translation: rapierVector3ToVector3(b.translation())
526
+ };
527
+ });
528
+ world.step(eventQueue);
529
+ subSteps++;
530
+ steppingState.accumulator -= timeStepMs;
531
+ }
495
532
  }
496
533
 
497
- if (!_paused) world.step(eventQueue); // Update meshes
534
+ const interpolationAlpha = steppingState.accumulator % timeStepMs / timeStepMs; // Update meshes
498
535
 
499
536
  rigidBodyStates.forEach((state, handle) => {
500
537
  const rigidBody = world.getRigidBody(handle);
@@ -516,11 +553,16 @@ const Physics = ({
516
553
  state.isSleeping = rigidBody.isSleeping();
517
554
  }
518
555
 
519
- if (!rigidBody || rigidBody.isSleeping() || rigidBody.isFixed() || !state.setMatrix) {
556
+ if (!rigidBody || rigidBody.isSleeping() || !state.setMatrix) {
520
557
  return;
521
558
  }
522
559
 
523
- state.setMatrix(_matrix4.compose(rapierVector3ToVector3(rigidBody.translation()), rapierQuaternionToQuaternion(rigidBody.rotation()), state.worldScale).premultiply(state.invertedMatrixWorld));
560
+ let oldState = previousTranslations[rigidBody.handle];
561
+ let newTranslation = rapierVector3ToVector3(rigidBody.translation());
562
+ let newRotation = rapierQuaternionToQuaternion(rigidBody.rotation());
563
+ let interpolatedTranslation = oldState ? oldState.translation.lerp(newTranslation, 1) : newTranslation;
564
+ let interpolatedRotation = oldState ? oldState.rotation.slerp(newRotation, interpolationAlpha) : newRotation;
565
+ state.setMatrix(_matrix4.compose(interpolatedTranslation, interpolatedRotation, state.worldScale).premultiply(state.invertedMatrixWorld));
524
566
 
525
567
  if (state.mesh instanceof InstancedMesh) {
526
568
  state.mesh.instanceMatrix.needsUpdate = true;
@@ -570,7 +612,6 @@ const Physics = ({
570
612
  });
571
613
  }
572
614
  });
573
- time.current = now;
574
615
  });
575
616
  const api = useMemo(() => createWorldApi(getWorldRef), []);
576
617
  const context = useMemo(() => ({
@@ -582,13 +623,32 @@ const Physics = ({
582
623
  },
583
624
  colliderMeshes,
584
625
  rigidBodyStates,
585
- rigidBodyEvents
586
- }), []);
626
+ rigidBodyEvents,
627
+ isPaused
628
+ }), [isPaused]);
587
629
  return /*#__PURE__*/React.createElement(RapierContext.Provider, {
588
630
  value: context
589
631
  }, children);
590
632
  };
591
633
 
634
+ function _extends() {
635
+ _extends = Object.assign || function (target) {
636
+ for (var i = 1; i < arguments.length; i++) {
637
+ var source = arguments[i];
638
+
639
+ for (var key in source) {
640
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
641
+ target[key] = source[key];
642
+ }
643
+ }
644
+ }
645
+
646
+ return target;
647
+ };
648
+
649
+ return _extends.apply(this, arguments);
650
+ }
651
+
592
652
  function _objectWithoutPropertiesLoose(source, excluded) {
593
653
  if (source == null) return {};
594
654
  var target = {};
@@ -855,7 +915,8 @@ const usePrismaticJoint = (body1, body2, [body1Anchor, body2Anchor, axis]) => {
855
915
  return useImpulseJoint(body1, body2, rapier.JointData.prismatic(vectorArrayToVector3(body1Anchor), vectorArrayToVector3(body2Anchor), vectorArrayToVector3(axis)));
856
916
  };
857
917
 
858
- const _excluded$1 = ["children"];
918
+ const _excluded$1 = ["children"],
919
+ _excluded2 = ["type", "position", "rotation"];
859
920
  const RigidBodyContext = /*#__PURE__*/createContext(undefined);
860
921
  const useRigidBodyContext = () => useContext(RigidBodyContext); // RigidBody
861
922
 
@@ -866,6 +927,9 @@ const RigidBody = /*#__PURE__*/forwardRef((_ref, ref) => {
866
927
  props = _objectWithoutProperties(_ref, _excluded$1);
867
928
 
868
929
  const [object, api] = useRigidBody(props);
930
+
931
+ const objectProps = _objectWithoutProperties(props, _excluded2);
932
+
869
933
  useImperativeHandle(ref, () => api);
870
934
  return /*#__PURE__*/React.createElement(RigidBodyContext.Provider, {
871
935
  value: {
@@ -874,9 +938,9 @@ const RigidBody = /*#__PURE__*/forwardRef((_ref, ref) => {
874
938
  hasCollisionEvents: !!(props.onCollisionEnter || props.onCollisionExit),
875
939
  options: props
876
940
  }
877
- }, /*#__PURE__*/React.createElement("object3D", {
941
+ }, /*#__PURE__*/React.createElement("object3D", _extends({
878
942
  ref: object
879
- }, children));
943
+ }, objectProps), children));
880
944
  });
881
945
 
882
946
  const MeshCollider = ({
@@ -939,6 +1003,17 @@ const geometryFromCollider = collider => {
939
1003
  return new BoxBufferGeometry(x * 2 + 0.01, y * 2 + 0.01, z * 2 + 0.01);
940
1004
  }
941
1005
 
1006
+ case ShapeType.RoundCuboid:
1007
+ {
1008
+ const {
1009
+ x,
1010
+ y,
1011
+ z
1012
+ } = collider.shape.halfExtents;
1013
+ const radius = collider.shape.borderRadius;
1014
+ return new RoundedBoxGeometry(x * 2 + radius * 2, y * 2 + radius * 2, z * 2 + radius * 2, 8, radius);
1015
+ }
1016
+
942
1017
  case ShapeType.Ball:
943
1018
  {
944
1019
  const r = collider.shape.radius;
@@ -974,7 +1049,38 @@ const geometryFromCollider = collider => {
974
1049
  {
975
1050
  const r = collider.shape.radius;
976
1051
  const h = collider.shape.halfHeight;
977
- const g = new CylinderBufferGeometry(r, r, h);
1052
+ const g = new CylinderBufferGeometry(r, r, h * 2);
1053
+ return g;
1054
+ }
1055
+
1056
+ case ShapeType.Capsule:
1057
+ {
1058
+ const r = collider.shape.radius;
1059
+ const h = collider.shape.halfHeight;
1060
+ const g = new CapsuleBufferGeometry(r, h * 2, 4, 8);
1061
+ return g;
1062
+ }
1063
+
1064
+ case ShapeType.Cone:
1065
+ {
1066
+ const r = collider.shape.radius;
1067
+ const h = collider.shape.halfHeight;
1068
+ const g = new ConeBufferGeometry(r, h * 2, 16);
1069
+ return g;
1070
+ }
1071
+
1072
+ case ShapeType.HeightField:
1073
+ {
1074
+ const rows = collider.shape.nrows;
1075
+ const cols = collider.shape.ncols;
1076
+ const heights = collider.shape.heights;
1077
+ const scale = collider.shape.scale;
1078
+ const g = new PlaneGeometry(scale.x, scale.z, cols, rows);
1079
+ const verts = g.attributes.position.array;
1080
+ verts.forEach((v, index) => verts[index * 3 + 2] = heights[index] * scale.y);
1081
+ g.scale(1, -1, 1);
1082
+ g.rotateX(-Math.PI / 2);
1083
+ g.rotateY(-Math.PI / 2);
978
1084
  return g;
979
1085
  }
980
1086
  }
@@ -983,12 +1089,18 @@ const geometryFromCollider = collider => {
983
1089
  };
984
1090
 
985
1091
  const DebugShape = /*#__PURE__*/memo(({
986
- colliderHandle
1092
+ colliderHandle,
1093
+ color,
1094
+ sleepColor
987
1095
  }) => {
988
1096
  const {
989
1097
  world
990
1098
  } = useRapier();
991
1099
  const ref = useRef(null);
1100
+ const [material] = useState(new MeshBasicMaterial({
1101
+ color,
1102
+ wireframe: true
1103
+ }));
992
1104
  useFrame(() => {
993
1105
  const collider = world.getCollider(colliderHandle);
994
1106
 
@@ -1004,6 +1116,14 @@ const DebugShape = /*#__PURE__*/memo(({
1004
1116
  y,
1005
1117
  z
1006
1118
  } = collider.translation();
1119
+ const parent = collider.parent();
1120
+
1121
+ if (parent !== null && parent !== void 0 && parent.isSleeping() || parent !== null && parent !== void 0 && parent.isFixed() || parent !== null && parent !== void 0 && parent.isKinematic()) {
1122
+ material.color = new Color(sleepColor);
1123
+ } else {
1124
+ material.color = new Color(color);
1125
+ }
1126
+
1007
1127
  ref.current.position.set(x, y, z);
1008
1128
  ref.current.rotation.setFromQuaternion(new Quaternion(rx, ry, rz, rw));
1009
1129
  }
@@ -1013,16 +1133,17 @@ const DebugShape = /*#__PURE__*/memo(({
1013
1133
  return geometryFromCollider(collider);
1014
1134
  }, [colliderHandle]);
1015
1135
  return /*#__PURE__*/React.createElement("mesh", {
1016
- ref: ref
1136
+ ref: ref,
1137
+ material: material
1017
1138
  }, /*#__PURE__*/React.createElement("primitive", {
1018
1139
  object: geometry,
1019
1140
  attach: "geometry"
1020
- }), /*#__PURE__*/React.createElement("meshBasicMaterial", {
1021
- color: "red",
1022
- wireframe: true
1023
1141
  }));
1024
1142
  });
1025
- const Debug = () => {
1143
+ const Debug = ({
1144
+ color: _color = "red",
1145
+ sleepColor: _sleepColor = "blue"
1146
+ }) => {
1026
1147
  const {
1027
1148
  world
1028
1149
  } = useRapier();
@@ -1038,7 +1159,9 @@ const Debug = () => {
1038
1159
  });
1039
1160
  return /*#__PURE__*/React.createElement("group", null, colliders.map(handle => /*#__PURE__*/React.createElement(DebugShape, {
1040
1161
  key: handle,
1041
- colliderHandle: handle
1162
+ colliderHandle: handle,
1163
+ color: _color,
1164
+ sleepColor: _sleepColor
1042
1165
  })));
1043
1166
  };
1044
1167
 
@@ -1166,24 +1289,6 @@ const InstancedRigidBodies = /*#__PURE__*/forwardRef((props, ref) => {
1166
1289
  }, props.children));
1167
1290
  });
1168
1291
 
1169
- function _extends() {
1170
- _extends = Object.assign || function (target) {
1171
- for (var i = 1; i < arguments.length; i++) {
1172
- var source = arguments[i];
1173
-
1174
- for (var key in source) {
1175
- if (Object.prototype.hasOwnProperty.call(source, key)) {
1176
- target[key] = source[key];
1177
- }
1178
- }
1179
- }
1180
-
1181
- return target;
1182
- };
1183
-
1184
- return _extends.apply(this, arguments);
1185
- }
1186
-
1187
1292
  const _excluded = ["children"];
1188
1293
 
1189
1294
  const AnyCollider = _ref => {
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.7",
4
4
  "source": "src/index.ts",
5
5
  "main": "dist/react-three-rapier.cjs.js",
6
6
  "module": "dist/react-three-rapier.esm.js",
@@ -20,7 +20,8 @@
20
20
  "peerDependencies": {
21
21
  "@react-three/fiber": "^8.0.12",
22
22
  "react": "^18.0.0",
23
- "three": "^0.139.2"
23
+ "three": "^0.139.2",
24
+ "three-stdlib": "^2.15.0"
24
25
  },
25
26
  "dependencies": {
26
27
  "@dimforge/rapier3d-compat": "0.9.0",
package/readme.md CHANGED
@@ -80,8 +80,8 @@ const Scene = () => (
80
80
  {/* Make a compound shape with two custom BallColliders */}
81
81
  <RigidBody position={[0, 10, 0]}>
82
82
  <Sphere />
83
- <BallCollider args={0.5} />
84
- <BallCollider args={0.5} position={[1, 0, 0]} />
83
+ <BallCollider args={[0.5]} />
84
+ <BallCollider args={[0.5]} position={[1, 0, 0]} />
85
85
  </RigidBody>
86
86
 
87
87
  {/* Make a compound shape with two custom BallColliders, an automatic BallCollider,
@@ -97,8 +97,8 @@ const Scene = () => (
97
97
 
98
98
  <Sphere />
99
99
 
100
- <BallCollider args={0.5} />
101
- <BallCollider args={0.5} position={[1, 0, 0]} />
100
+ <BallCollider args={[0.5]} />
101
+ <BallCollider args={[0.5]} position={[1, 0, 0]} />
102
102
  </RigidBody>
103
103
  </Physics>
104
104
  );
@@ -185,6 +185,9 @@ const Scene = () => {
185
185
 
186
186
  Use the Debug component to see live representations of all colliders in a scene.
187
187
 
188
+ - The `color` prop sets the color of awake colliders that are affected by forces.
189
+ - The `sleepColor` prop set the color of a static (fixed, or kinematic) or sleeping collider.
190
+
188
191
  > Note: Experimental. Not all shapes are supported. Unsupported shapes are always represented by cubes.
189
192
 
190
193
  ```tsx
@@ -194,7 +197,7 @@ import { RigidBody, Debug } from "@react-three/rapier";
194
197
  const Scene = () => {
195
198
  return (
196
199
  <Physics>
197
- <Debug />
200
+ <Debug color="red" sleepColor="blue" />
198
201
 
199
202
  <RigidBody>
200
203
  <Box />
@@ -251,6 +254,7 @@ In order, but also not necessarily:
251
254
  - [x] Collision events
252
255
  - [x] Colliders outside RigidBodies
253
256
  - [x] InstancedMesh support
257
+ - [x] Timestep improvements for determinism
254
258
  - [ ] Normalize and improve collision events (add events to single Colliders, InstancedRigidBodies, etc)
255
259
  - [ ] Docs
256
260
  - [ ] CodeSandbox examples