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/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "react-three-rapier-unified",
3
+ "version": "1.0.0",
4
+ "description": "Unified package combining @react-three/rapier and @react-three/rapier-addons",
5
+ "main": "dist/index.cjs.js",
6
+ "module": "dist/index.esm.js",
7
+ "types": "dist/index.d.ts",
8
+ "source": "src/index.ts",
9
+ "files": [
10
+ "dist",
11
+ "src"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc && vite build",
15
+ "dev": "vite build --watch",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "peerDependencies": {
19
+ "@react-three/fiber": "^9.0.4",
20
+ "react": "^19",
21
+ "three": ">=0.159.0"
22
+ },
23
+ "dependencies": {
24
+ "@dimforge/rapier3d-compat": "0.19.3",
25
+ "suspend-react": "^0.1.3",
26
+ "three-stdlib": "^2.35.12"
27
+ },
28
+ "devDependencies": {
29
+ "@react-three/fiber": "9.0.4",
30
+ "@types/react": "19.0.8",
31
+ "@types/three": "0.172.0",
32
+ "react": "19.0.0",
33
+ "three": "0.172.0",
34
+ "typescript": "5.7.3",
35
+ "vite": "^6.0.0",
36
+ "vite-plugin-dts": "^4.0.0"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/MMORPG-Lordson/react-three-rapier-unified.git"
41
+ },
42
+ "keywords": [
43
+ "react",
44
+ "three",
45
+ "threejs",
46
+ "physics",
47
+ "rapier",
48
+ "react-three-fiber",
49
+ "r3f"
50
+ ],
51
+ "author": "",
52
+ "license": "MIT"
53
+ }
@@ -0,0 +1,178 @@
1
+ import React from "react";
2
+ import { InteractionGroups, RigidBody } from "@dimforge/rapier3d-compat";
3
+ import { useBeforePhysicsStep, useRapier } from "../../hooks/hooks";
4
+ import { FC, memo, useRef } from "react";
5
+ import { Object3D, Vector3 } from "three";
6
+ import { ThreeElements } from "@react-three/fiber";
7
+ import { AttractorDebugHelper } from "./AttractorDebugHelper";
8
+
9
+ type Object3DProps = ThreeElements["object3D"];
10
+
11
+ export type AttractorGravityType = "static" | "linear" | "newtonian";
12
+
13
+ export interface AttractorProps {
14
+ /**
15
+ * The relative position of this attractor
16
+ */
17
+ position?: Object3DProps["position"];
18
+
19
+ /**
20
+ * The strength of the attractor.
21
+ * Positive values attract, negative values repel.
22
+ *
23
+ * @defaultValue 1
24
+ */
25
+ strength?: number;
26
+
27
+ /**
28
+ * The range of the attractor. Will not affect objects outside of this range.
29
+ *
30
+ * @defaultValue 10
31
+ * @min 0
32
+ */
33
+ range?: number;
34
+
35
+ /**
36
+ * The type of gravity to use.
37
+ * - static: The gravity is constant and does not change over time.
38
+ * - linear: The gravity is linearly interpolated the closer the object is to the attractor.
39
+ * - newtonian: The gravity is calculated using the newtonian gravity formula.
40
+ * @defaultValue "static"
41
+ */
42
+ type?: AttractorGravityType;
43
+
44
+ /**
45
+ * The mass of the attractor. Used when type is `newtonian`.
46
+ * @defaultValue 6.673e-11
47
+ */
48
+ gravitationalConstant?: number;
49
+
50
+ /**
51
+ * The collision groups that this attractor will apply effects to. If a RigidBody contains one or more colliders that are in one of the mask group, it will be affected by this attractor.
52
+ * If not specified, the attractor will apply effects to all RigidBodies.
53
+ */
54
+ collisionGroups?: InteractionGroups;
55
+ }
56
+
57
+ export interface AttractorState
58
+ extends Required<Omit<AttractorProps, "position" | "collisionGroups">> {
59
+ collisionGroups?: InteractionGroups;
60
+ }
61
+
62
+ const calcForceByType = {
63
+ static: (s: number, m2: number, r: number, d: number, G: number) => s,
64
+ linear: (s: number, m2: number, r: number, d: number, G: number) =>
65
+ s * (d / r),
66
+ newtonian: (s: number, m2: number, r: number, d: number, G: number) =>
67
+ (G * s * m2) / Math.pow(d, 2)
68
+ };
69
+
70
+ const _position = new Vector3();
71
+ const _vector3 = new Vector3();
72
+
73
+ export const applyAttractorForceOnRigidBody = (
74
+ rigidBody: RigidBody,
75
+ {
76
+ object,
77
+ strength,
78
+ range,
79
+ gravitationalConstant,
80
+ collisionGroups,
81
+ type
82
+ }: AttractorState & {
83
+ object: Object3D;
84
+ }
85
+ ) => {
86
+ const rbPosition = rigidBody.translation();
87
+ _position.set(rbPosition.x, rbPosition.y, rbPosition.z);
88
+
89
+ const worldPosition = object.getWorldPosition(new Vector3());
90
+
91
+ const distance: number = worldPosition.distanceTo(_position);
92
+
93
+ if (distance < range) {
94
+ let force = calcForceByType[type](
95
+ strength,
96
+ rigidBody.mass(),
97
+ range,
98
+ distance,
99
+ gravitationalConstant
100
+ );
101
+
102
+ // Prevent wild forces when Attractors collide
103
+ force = force === Infinity ? strength : force;
104
+
105
+ // Naively test if the rigidBody contains a collider in one of the collision groups
106
+ let isRigidBodyInCollisionGroup =
107
+ collisionGroups === undefined ? true : false;
108
+ if (collisionGroups !== undefined) {
109
+ for (let i = 0; i < rigidBody.numColliders(); i++) {
110
+ const collider = rigidBody.collider(i);
111
+ const colliderCollisionGroups = collider.collisionGroups();
112
+ if (
113
+ ((collisionGroups >> 16) & colliderCollisionGroups) != 0 &&
114
+ ((colliderCollisionGroups >> 16) & collisionGroups) != 0
115
+ ) {
116
+ isRigidBodyInCollisionGroup = true;
117
+ break;
118
+ }
119
+ }
120
+ }
121
+
122
+ if (isRigidBodyInCollisionGroup) {
123
+ _vector3
124
+ .set(0, 0, 0)
125
+ .subVectors(worldPosition, _position)
126
+ .normalize()
127
+ .multiplyScalar(force);
128
+
129
+ rigidBody.applyImpulse(_vector3, true);
130
+ }
131
+ }
132
+ };
133
+
134
+ export const Attractor: FC<AttractorProps> = memo((props) => {
135
+ const {
136
+ position = [0, 0, 0],
137
+ strength = 1,
138
+ range = 10,
139
+ type = "static",
140
+ gravitationalConstant = 6.673e-11,
141
+ collisionGroups
142
+ } = props;
143
+ const object = useRef<Object3D>(null!);
144
+ const { isDebug } = useRapier();
145
+
146
+ useBeforePhysicsStep((world) => {
147
+ if (object.current) {
148
+ world.bodies.forEach((body) => {
149
+ if (body.isDynamic()) {
150
+ applyAttractorForceOnRigidBody(body, {
151
+ object: object.current!,
152
+ strength,
153
+ range,
154
+ type,
155
+ gravitationalConstant,
156
+ collisionGroups
157
+ });
158
+ }
159
+ });
160
+ }
161
+ });
162
+
163
+ return (
164
+ <>
165
+ <object3D ref={object} position={position} />
166
+ {isDebug && (
167
+ <AttractorDebugHelper
168
+ strength={strength}
169
+ gravitationalConstant={gravitationalConstant}
170
+ range={range}
171
+ type={type}
172
+ collisionGroups={collisionGroups}
173
+ object={object}
174
+ />
175
+ )}
176
+ </>
177
+ );
178
+ });
@@ -0,0 +1,59 @@
1
+ import React, { RefObject, useEffect, useRef } from "react";
2
+ import { useThree, useFrame } from "@react-three/fiber";
3
+ import {
4
+ Mesh,
5
+ MeshBasicMaterial,
6
+ Object3D,
7
+ SphereGeometry,
8
+ Vector3
9
+ } from "three";
10
+ import { VertexNormalsHelper } from "three-stdlib";
11
+ import { AttractorState } from "./Attractor";
12
+
13
+ const _v3 = new Vector3();
14
+
15
+ export const AttractorDebugHelper = (
16
+ props: AttractorState & {
17
+ object: RefObject<Object3D>;
18
+ }
19
+ ) => {
20
+ const { scene } = useThree();
21
+ const ref = useRef<Mesh>(null!);
22
+ const normalsHelper = useRef<VertexNormalsHelper>(null!);
23
+ const color = props.strength > 0 ? 0x0000ff : 0xff0000;
24
+
25
+ useEffect(() => {
26
+ ref.current = new Mesh(
27
+ new SphereGeometry(0.2, 6, 6),
28
+ new MeshBasicMaterial({ color, wireframe: true })
29
+ );
30
+
31
+ normalsHelper.current = new VertexNormalsHelper(
32
+ ref.current,
33
+ props.range,
34
+ color
35
+ );
36
+ normalsHelper.current.frustumCulled = false;
37
+
38
+ scene.add(ref.current);
39
+ scene.add(normalsHelper.current);
40
+
41
+ return () => {
42
+ if (normalsHelper.current && ref.current) {
43
+ scene.remove(normalsHelper.current);
44
+ scene.remove(ref.current);
45
+ }
46
+ };
47
+ }, [props, color]);
48
+
49
+ useFrame(() => {
50
+ if (ref.current && props.object.current) {
51
+ const worldPosition = props.object.current.getWorldPosition(_v3);
52
+
53
+ ref.current.position.copy(worldPosition);
54
+ normalsHelper.current?.update();
55
+ }
56
+ });
57
+
58
+ return null;
59
+ };
@@ -0,0 +1,286 @@
1
+ import { Collider } from "@dimforge/rapier3d-compat";
2
+ import React, {
3
+ ForwardedRef,
4
+ memo,
5
+ ReactNode,
6
+ Ref,
7
+ useEffect,
8
+ useMemo,
9
+ useRef
10
+ } from "react";
11
+ import { Object3D } from "three";
12
+ import { useRapier } from "../hooks/hooks";
13
+ import { useForwardedRef } from "../hooks/use-forwarded-ref";
14
+ import { useImperativeInstance } from "../hooks/use-imperative-instance";
15
+ import {
16
+ BallArgs,
17
+ CapsuleArgs,
18
+ ColliderOptions,
19
+ ConeArgs,
20
+ ConvexHullArgs,
21
+ CuboidArgs,
22
+ CylinderArgs,
23
+ HeightfieldArgs,
24
+ RoundConeArgs,
25
+ RoundCuboidArgs,
26
+ RoundCylinderArgs,
27
+ TrimeshArgs
28
+ } from "../types";
29
+ import { vec3 } from "../utils/three-object-helpers";
30
+ import {
31
+ cleanRigidBodyPropsForCollider,
32
+ createColliderFromOptions,
33
+ createColliderState,
34
+ getActiveCollisionEventsFromProps,
35
+ immutableColliderOptions,
36
+ useColliderEvents,
37
+ useUpdateColliderOptions
38
+ } from "../utils/utils-collider";
39
+ import { useRigidBodyContext } from "./RigidBody";
40
+
41
+ export interface ColliderProps extends ColliderOptions<any> {
42
+ children?: ReactNode;
43
+ ref?: Ref<Collider>;
44
+ }
45
+
46
+ /**
47
+ * A collider is a shape that can be attached to a rigid body to define its physical properties.
48
+ * @internal
49
+ */
50
+ export const AnyCollider = memo((props: ColliderProps) => {
51
+ const { children, position, rotation, quaternion, scale, name } = props;
52
+ const { world, colliderEvents, colliderStates } = useRapier();
53
+ const rigidBodyContext = useRigidBodyContext();
54
+ const colliderRef = useForwardedRef(props.ref);
55
+ const objectRef = useRef<Object3D>(null);
56
+
57
+ // We spread the props out here to make sure that the ref is updated when the props change.
58
+ const immutablePropArray = immutableColliderOptions.flatMap((key) =>
59
+ // Array.isArray(props[key]) ? [...props[key]] : props[key]
60
+ Array.isArray(props[key]) ? props[key] : [props[key]]
61
+ );
62
+
63
+ const getInstance = useImperativeInstance(
64
+ () => {
65
+ const worldScale = objectRef.current!.getWorldScale(vec3());
66
+
67
+ const collider = createColliderFromOptions(
68
+ props,
69
+ world,
70
+ worldScale,
71
+ rigidBodyContext?.getRigidBody
72
+ );
73
+
74
+ if (typeof props.ref == "function") {
75
+ props.ref(collider);
76
+ }
77
+ colliderRef.current = collider;
78
+
79
+ return collider;
80
+ },
81
+ (collider) => {
82
+ if (world.getCollider(collider.handle)) {
83
+ world.removeCollider(collider, true);
84
+ }
85
+ },
86
+ [...immutablePropArray, rigidBodyContext]
87
+ );
88
+
89
+ useEffect(() => {
90
+ const collider = getInstance();
91
+
92
+ colliderStates.set(
93
+ collider.handle,
94
+ createColliderState(
95
+ collider,
96
+ objectRef.current!,
97
+ rigidBodyContext?.ref.current
98
+ )
99
+ );
100
+
101
+ return () => {
102
+ colliderStates.delete(collider.handle);
103
+ };
104
+ }, [getInstance]);
105
+
106
+ const mergedProps = useMemo(() => {
107
+ return {
108
+ ...cleanRigidBodyPropsForCollider(rigidBodyContext?.options),
109
+ ...props
110
+ };
111
+ }, [props, rigidBodyContext?.options]);
112
+
113
+ useUpdateColliderOptions(getInstance, mergedProps, colliderStates);
114
+ useColliderEvents(
115
+ getInstance,
116
+ mergedProps,
117
+ colliderEvents,
118
+ getActiveCollisionEventsFromProps(rigidBodyContext?.options)
119
+ );
120
+
121
+ return (
122
+ <object3D
123
+ position={position}
124
+ rotation={rotation}
125
+ quaternion={quaternion}
126
+ scale={scale}
127
+ ref={objectRef}
128
+ name={name}
129
+ >
130
+ {children}
131
+ </object3D>
132
+ );
133
+ });
134
+
135
+ export type ColliderOptionsRequiredArgs<T extends unknown[]> = Omit<
136
+ ColliderOptions<T>,
137
+ "args"
138
+ > & {
139
+ args: T;
140
+ children?: ReactNode;
141
+ };
142
+
143
+ export type CuboidColliderProps = ColliderOptionsRequiredArgs<CuboidArgs>;
144
+
145
+ /**
146
+ * A cuboid collider shape
147
+ * @category Colliders
148
+ */
149
+ export const CuboidCollider = React.forwardRef(
150
+ (props: CuboidColliderProps, ref: ForwardedRef<Collider>) => {
151
+ return <AnyCollider {...props} shape="cuboid" ref={ref} />;
152
+ }
153
+ );
154
+ CuboidCollider.displayName = "CuboidCollider";
155
+
156
+ export type RoundCuboidColliderProps =
157
+ ColliderOptionsRequiredArgs<RoundCuboidArgs>;
158
+
159
+ /**
160
+ * A round cuboid collider shape
161
+ * @category Colliders
162
+ */
163
+ export const RoundCuboidCollider = React.forwardRef(
164
+ (props: RoundCuboidColliderProps, ref: ForwardedRef<Collider>) => (
165
+ <AnyCollider {...props} shape="roundCuboid" ref={ref} />
166
+ )
167
+ );
168
+ RoundCuboidCollider.displayName = "RoundCuboidCollider";
169
+
170
+ export type BallColliderProps = ColliderOptionsRequiredArgs<BallArgs>;
171
+ /**
172
+ * A ball collider shape
173
+ * @category Colliders
174
+ */
175
+ export const BallCollider = React.forwardRef(
176
+ (props: BallColliderProps, ref: ForwardedRef<Collider>) => (
177
+ <AnyCollider {...props} shape="ball" ref={ref} />
178
+ )
179
+ );
180
+ BallCollider.displayName = "BallCollider";
181
+
182
+ export type CapsuleColliderProps = ColliderOptionsRequiredArgs<CapsuleArgs>;
183
+
184
+ /**
185
+ * A capsule collider shape
186
+ * @category Colliders
187
+ */
188
+ export const CapsuleCollider = React.forwardRef(
189
+ (props: CapsuleColliderProps, ref: ForwardedRef<Collider>) => (
190
+ <AnyCollider {...props} shape="capsule" ref={ref} />
191
+ )
192
+ );
193
+ CapsuleCollider.displayName = "CapsuleCollider";
194
+
195
+ export type HeightfieldColliderProps =
196
+ ColliderOptionsRequiredArgs<HeightfieldArgs>;
197
+
198
+ /**
199
+ * A heightfield collider shape
200
+ * @category Colliders
201
+ */
202
+ export const HeightfieldCollider = React.forwardRef(
203
+ (props: HeightfieldColliderProps, ref: ForwardedRef<Collider>) => (
204
+ <AnyCollider {...props} shape="heightfield" ref={ref} />
205
+ )
206
+ );
207
+ HeightfieldCollider.displayName = "HeightfieldCollider";
208
+
209
+ export type TrimeshColliderProps = ColliderOptionsRequiredArgs<TrimeshArgs>;
210
+ /**
211
+ * A trimesh collider shape
212
+ * @category Colliders
213
+ */
214
+ export const TrimeshCollider = React.forwardRef(
215
+ (props: TrimeshColliderProps, ref: ForwardedRef<Collider>) => (
216
+ <AnyCollider {...props} shape="trimesh" ref={ref} />
217
+ )
218
+ );
219
+ TrimeshCollider.displayName = "TrimeshCollider";
220
+
221
+ export type ConeColliderProps = ColliderOptionsRequiredArgs<ConeArgs>;
222
+
223
+ /**
224
+ * A cone collider shape
225
+ * @category Colliders
226
+ */
227
+ export const ConeCollider = React.forwardRef(
228
+ (props: ConeColliderProps, ref: ForwardedRef<Collider>) => (
229
+ <AnyCollider {...props} shape="cone" ref={ref} />
230
+ )
231
+ );
232
+ ConeCollider.displayName = "ConeCollider";
233
+
234
+ export type RoundConeColliderProps = ColliderOptionsRequiredArgs<RoundConeArgs>;
235
+
236
+ /**
237
+ * A round cylinder collider shape
238
+ * @category Colliders
239
+ */
240
+ export const RoundConeCollider = React.forwardRef(
241
+ (props: RoundConeColliderProps, ref: ForwardedRef<Collider>) => (
242
+ <AnyCollider {...props} shape="roundCone" ref={ref} />
243
+ )
244
+ );
245
+ RoundConeCollider.displayName = "RoundConeCollider";
246
+
247
+ export type CylinderColliderProps = ColliderOptionsRequiredArgs<CylinderArgs>;
248
+
249
+ /**
250
+ * A cylinder collider shape
251
+ * @category Colliders
252
+ */
253
+ export const CylinderCollider = React.forwardRef(
254
+ (props: CylinderColliderProps, ref: ForwardedRef<Collider>) => (
255
+ <AnyCollider {...props} shape="cylinder" ref={ref} />
256
+ )
257
+ );
258
+ CylinderCollider.displayName = "CylinderCollider";
259
+
260
+ export type RoundCylinderColliderProps =
261
+ ColliderOptionsRequiredArgs<RoundCylinderArgs>;
262
+
263
+ /**
264
+ * A round cylinder collider shape
265
+ * @category Colliders
266
+ */
267
+ export const RoundCylinderCollider = React.forwardRef(
268
+ (props: RoundConeColliderProps, ref: ForwardedRef<Collider>) => (
269
+ <AnyCollider {...props} shape="roundCylinder" ref={ref} />
270
+ )
271
+ );
272
+ CylinderCollider.displayName = "RoundCylinderCollider";
273
+
274
+ export type ConvexHullColliderProps =
275
+ ColliderOptionsRequiredArgs<ConvexHullArgs>;
276
+
277
+ /**
278
+ * A convex hull collider shape
279
+ * @category Colliders
280
+ */
281
+ export const ConvexHullCollider = React.forwardRef(
282
+ (props: ConvexHullColliderProps, ref: ForwardedRef<Collider>) => (
283
+ <AnyCollider {...props} shape="convexHull" ref={ref} />
284
+ )
285
+ );
286
+ ConvexHullCollider.displayName = "ConvexHullCollider";
@@ -0,0 +1,33 @@
1
+ import React, { memo, useRef } from "react";
2
+ import { useFrame } from "@react-three/fiber";
3
+ import { BufferAttribute, BufferGeometry, LineSegments } from "three";
4
+ import { useRapier } from "../hooks/hooks";
5
+ import { _vector3 } from "../utils/shared-objects";
6
+
7
+ export const Debug = memo(() => {
8
+ const { world } = useRapier();
9
+ const ref = useRef<LineSegments>(null);
10
+
11
+ useFrame(() => {
12
+ const mesh = ref.current;
13
+ if (!mesh) return;
14
+
15
+ const buffers = world.debugRender();
16
+
17
+ const geometry = new BufferGeometry();
18
+ geometry.setAttribute("position", new BufferAttribute(buffers.vertices, 3));
19
+ geometry.setAttribute("color", new BufferAttribute(buffers.colors, 4));
20
+
21
+ mesh.geometry.dispose();
22
+ mesh.geometry = geometry;
23
+ });
24
+
25
+ return (
26
+ <group>
27
+ <lineSegments ref={ref} frustumCulled={false}>
28
+ <lineBasicMaterial color={0xffffff} vertexColors />
29
+ <bufferGeometry />
30
+ </lineSegments>
31
+ </group>
32
+ );
33
+ });
@@ -0,0 +1,36 @@
1
+ import { useFrame } from "@react-three/fiber";
2
+ import React, { memo } from "react";
3
+ import { useRaf } from "../utils/utils-physics";
4
+ import { PhysicsProps } from "./Physics";
5
+
6
+ interface FrameStepperProps {
7
+ type?: PhysicsProps["updateLoop"];
8
+ onStep: (dt: number) => void;
9
+ updatePriority?: number;
10
+ }
11
+
12
+ const UseFrameStepper = ({ onStep, updatePriority }: FrameStepperProps) => {
13
+ useFrame((_, dt) => {
14
+ onStep(dt);
15
+ }, updatePriority);
16
+
17
+ return null;
18
+ };
19
+
20
+ const RafStepper = ({ onStep }: FrameStepperProps) => {
21
+ useRaf((dt) => {
22
+ onStep(dt);
23
+ });
24
+
25
+ return null;
26
+ };
27
+
28
+ const FrameStepper = ({ onStep, type, updatePriority }: FrameStepperProps) => {
29
+ return type === "independent" ? (
30
+ <RafStepper onStep={onStep} />
31
+ ) : (
32
+ <UseFrameStepper onStep={onStep} updatePriority={updatePriority} />
33
+ );
34
+ };
35
+
36
+ export default memo(FrameStepper);