@opsydyn/effect-three-rapier 0.0.0-stage → 0.1.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/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +42 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +254 -0
- package/dist/index.js.map +10 -0
- package/package.json +62 -4
- package/src/index.ts +357 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-10-18)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **effect-three:** a rigid body per instance ([cc39435](https://github.com/opsydyn/fold-kit-experiments/commit/cc39435b878c781c7bf35e00e4b50ddc6d92b035))
|
|
8
|
+
- **effect-three:** after R5 — WebGPU, render kits, glide, physics groups, XR spaces ([e5bde71](https://github.com/opsydyn/fold-kit-experiments/commit/e5bde71212353879d74aa32567c3eb9cbbeb38ac))
|
|
9
|
+
- **effect-three:** bodies on groups and loaded models ([b06179c](https://github.com/opsydyn/fold-kit-experiments/commit/b06179c4baa878fb65dc2f15a43a3e275b7bf682))
|
|
10
|
+
- **effect-three:** close the parity gaps and prepare the packages for npm ([994fe5d](https://github.com/opsydyn/fold-kit-experiments/commit/994fe5dd640583c787746a95e8465d8f051d78df))
|
|
11
|
+
- **effect-three:** colliders of a body's own, motors, contact forces and debug lines ([2a16c9e](https://github.com/opsydyn/fold-kit-experiments/commit/2a16c9e0f8432872e008bbd747eede1472c6ba44))
|
|
12
|
+
- **effect-three:** collision groups and colliders named in collisions ([9531a01](https://github.com/opsydyn/fold-kit-experiments/commit/9531a01f54e9e19b8ccc301152167d0ac52c3229))
|
|
13
|
+
- **effect-three:** joints, and bodies no longer remade by rounding ([66ebf4d](https://github.com/opsydyn/fold-kit-experiments/commit/66ebf4d0a4bcd580699e9a7f371919397e95585c))
|
|
14
|
+
- **effect-three:** physics as data, simulated by Rapier ([dd99e1c](https://github.com/opsydyn/fold-kit-experiments/commit/dd99e1cbde789e5b578ec6b28278893e5725b326))
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alan P Currie
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @opsydyn/effect-three-rapier
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Rapier physics for `@opsydyn/effect-three`. Bodies are described on meshes, groups, loaded
|
|
4
|
+
models and instances (with colliders from their geometry or their own), and joints (with
|
|
5
|
+
motors) in `physics({ joints })`; this package is the engine that simulates them, reports
|
|
6
|
+
collisions and contact forces, and draws its debug lines ([ADR-007](../../docs/effect-three/adr/007-physics-as-a-pose-source.md)).
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @opsydyn/effect-three-rapier @opsydyn/effect-three three
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Released with the core at one version: see its [Stability](../effect-three/README.md#stability)
|
|
13
|
+
and [Limits](../effect-three/README.md#limits).
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { body, box, mesh, physics, scene, standard } from '@opsydyn/effect-three';
|
|
17
|
+
import * as EffectThreeFoldkit from '@opsydyn/effect-three-foldkit';
|
|
18
|
+
import { loadRapier } from '@opsydyn/effect-three-rapier';
|
|
19
|
+
|
|
20
|
+
const environment = EffectThreeFoldkit.makeEnvironment(window, { physics: loadRapier() });
|
|
21
|
+
|
|
22
|
+
scene({
|
|
23
|
+
physics: physics(),
|
|
24
|
+
children: [
|
|
25
|
+
mesh({
|
|
26
|
+
key: 'floor',
|
|
27
|
+
geometry: box({ width: 10, height: 1, depth: 10 }),
|
|
28
|
+
material: standard(),
|
|
29
|
+
body: body({ type: 'fixed' }),
|
|
30
|
+
}),
|
|
31
|
+
mesh({
|
|
32
|
+
key: 'crate',
|
|
33
|
+
position: [0, 4, 0],
|
|
34
|
+
geometry: box(),
|
|
35
|
+
material: standard(),
|
|
36
|
+
body: body(),
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Rapier (`@dimforge/rapier3d-compat`, Apache-2.0) embeds its WebAssembly, so nothing else is
|
|
43
|
+
fetched; load it only on pages that need physics.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PhysicsEngine } from '@opsydyn/effect-three';
|
|
2
|
+
/**
|
|
3
|
+
* Loads Rapier's WebAssembly and returns the engine. Call once per page; the engine makes
|
|
4
|
+
* a world for each scene with physics.
|
|
5
|
+
*/
|
|
6
|
+
export declare const loadRapier: () => Promise<PhysicsEngine>;
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAUV,aAAa,EAId,MAAM,uBAAuB,CAAC;AA0T/B;;;GAGG;AACH,eAAO,MAAM,UAAU,QAAa,OAAO,CAAC,aAAa,CAGxD,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import RAPIER from "@dimforge/rapier3d-compat";
|
|
3
|
+
import { interactionBits } from "@opsydyn/effect-three";
|
|
4
|
+
var vector = ([x, y, z]) => ({ x, y, z });
|
|
5
|
+
var rotation = ([x, y, z, w]) => ({ x, y, z, w });
|
|
6
|
+
var bodyTypes = {
|
|
7
|
+
dynamic: RAPIER.RigidBodyType.Dynamic,
|
|
8
|
+
fixed: RAPIER.RigidBodyType.Fixed,
|
|
9
|
+
kinematic: RAPIER.RigidBodyType.KinematicPositionBased
|
|
10
|
+
};
|
|
11
|
+
var shapeDesc = (rapier, shape) => {
|
|
12
|
+
switch (shape._tag) {
|
|
13
|
+
case "Cuboid":
|
|
14
|
+
return rapier.ColliderDesc.cuboid(...shape.halfExtents);
|
|
15
|
+
case "Ball":
|
|
16
|
+
return rapier.ColliderDesc.ball(shape.radius);
|
|
17
|
+
case "Hull":
|
|
18
|
+
return rapier.ColliderDesc.convexHull(shape.points) ?? rapier.ColliderDesc.ball(0.001);
|
|
19
|
+
case "Capsule":
|
|
20
|
+
return rapier.ColliderDesc.capsule(shape.halfHeight, shape.radius);
|
|
21
|
+
case "Cylinder":
|
|
22
|
+
return rapier.ColliderDesc.cylinder(shape.halfHeight, shape.radius);
|
|
23
|
+
case "Cone":
|
|
24
|
+
return rapier.ColliderDesc.cone(shape.halfHeight, shape.radius);
|
|
25
|
+
case "Trimesh":
|
|
26
|
+
return rapier.ColliderDesc.trimesh(shape.vertices, shape.indices);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var colliderDesc = (rapier, collider) => shapeDesc(rapier, collider.shape).setTranslation(...collider.translation).setRotation(rotation(collider.rotation));
|
|
30
|
+
var reporting = (colliders, body) => {
|
|
31
|
+
for (const collider of colliders) {
|
|
32
|
+
if (body.contactForces === null) {
|
|
33
|
+
collider.setActiveEvents(RAPIER.ActiveEvents.COLLISION_EVENTS);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
collider.setActiveEvents(RAPIER.ActiveEvents.COLLISION_EVENTS | RAPIER.ActiveEvents.CONTACT_FORCE_EVENTS);
|
|
37
|
+
collider.setContactForceEventThreshold(body.contactForces);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var surface = (colliders, body) => {
|
|
41
|
+
for (const collider of colliders) {
|
|
42
|
+
collider.setFriction(body.friction);
|
|
43
|
+
collider.setRestitution(body.restitution);
|
|
44
|
+
collider.setSensor(body.sensor);
|
|
45
|
+
if (body.mass === null)
|
|
46
|
+
collider.setDensity(1);
|
|
47
|
+
else
|
|
48
|
+
collider.setMass(body.mass / colliders.length);
|
|
49
|
+
collider.setCollisionGroups(interactionBits(body.collisionGroups));
|
|
50
|
+
collider.setSolverGroups(interactionBits(body.solverGroups));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var motion = (rigidBody, body) => {
|
|
54
|
+
rigidBody.setBodyType(bodyTypes[body.type], true);
|
|
55
|
+
rigidBody.setLinearDamping(body.linearDamping);
|
|
56
|
+
rigidBody.setAngularDamping(body.angularDamping);
|
|
57
|
+
rigidBody.setGravityScale(body.gravityScale, true);
|
|
58
|
+
rigidBody.enableCcd(body.ccd);
|
|
59
|
+
};
|
|
60
|
+
var limitsOf = (joint) => joint.kind._tag === "Revolute" || joint.kind._tag === "Prismatic" ? joint.kind.limits : null;
|
|
61
|
+
var motorOf = (joint) => joint.kind._tag === "Revolute" || joint.kind._tag === "Prismatic" ? joint.kind.motor : null;
|
|
62
|
+
var drive = (made, motor) => {
|
|
63
|
+
if (motor !== null && made instanceof RAPIER.UnitImpulseJoint) {
|
|
64
|
+
made.configureMotor(motor.position, motor.velocity, motor.stiffness, motor.damping);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var limit = (made, limits) => {
|
|
68
|
+
if (limits !== null && made instanceof RAPIER.UnitImpulseJoint)
|
|
69
|
+
made.setLimits(...limits);
|
|
70
|
+
};
|
|
71
|
+
var jointData = (rapier, joint) => {
|
|
72
|
+
const a = vector(joint.anchorA);
|
|
73
|
+
const b = vector(joint.anchorB);
|
|
74
|
+
const { kind } = joint;
|
|
75
|
+
switch (kind._tag) {
|
|
76
|
+
case "Fixed":
|
|
77
|
+
return rapier.JointData.fixed(a, rotation(kind.frameA), b, rotation(kind.frameB));
|
|
78
|
+
case "Spherical":
|
|
79
|
+
return rapier.JointData.spherical(a, b);
|
|
80
|
+
case "Revolute":
|
|
81
|
+
return rapier.JointData.revolute(a, b, vector(kind.axis));
|
|
82
|
+
case "Prismatic":
|
|
83
|
+
return rapier.JointData.prismatic(a, b, vector(kind.axis));
|
|
84
|
+
case "Rope":
|
|
85
|
+
return rapier.JointData.rope(kind.length, a, b);
|
|
86
|
+
case "Spring":
|
|
87
|
+
return rapier.JointData.spring(kind.restLength, kind.stiffness, kind.damping, a, b);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var pair = (a, b) => ({
|
|
91
|
+
a: a.key,
|
|
92
|
+
b: b.key,
|
|
93
|
+
...a.part === undefined ? {} : { aPart: a.part },
|
|
94
|
+
...b.part === undefined ? {} : { bPart: b.part }
|
|
95
|
+
});
|
|
96
|
+
var createWorld = (rapier) => (initial) => {
|
|
97
|
+
const world = new rapier.World(vector(initial.gravity));
|
|
98
|
+
const events = new rapier.EventQueue(true);
|
|
99
|
+
const bodies = new Map;
|
|
100
|
+
const keysByCollider = new Map;
|
|
101
|
+
const joints = new Map;
|
|
102
|
+
const add = (key, body, colliders, pose) => {
|
|
103
|
+
const desc = new rapier.RigidBodyDesc(bodyTypes[body.type]).setTranslation(...pose.position).setRotation(rotation(pose.quaternion)).setLinvel(...body.linearVelocity).setAngvel(vector(body.angularVelocity)).setLinearDamping(body.linearDamping).setAngularDamping(body.angularDamping).setGravityScale(body.gravityScale).setCcdEnabled(body.ccd);
|
|
104
|
+
const rigidBody = world.createRigidBody(desc);
|
|
105
|
+
const made = colliders.map((collider) => world.createCollider(colliderDesc(rapier, collider), rigidBody));
|
|
106
|
+
surface(made, body);
|
|
107
|
+
reporting(made, body);
|
|
108
|
+
bodies.set(key, { rigidBody, colliders: made });
|
|
109
|
+
made.forEach((collider, i) => {
|
|
110
|
+
const part = colliders[i]?.part;
|
|
111
|
+
keysByCollider.set(collider.handle, { key, ...part === undefined ? {} : { part } });
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
const remove = (key) => {
|
|
115
|
+
const held = bodies.get(key);
|
|
116
|
+
if (held === undefined)
|
|
117
|
+
return;
|
|
118
|
+
for (const collider of held.colliders)
|
|
119
|
+
keysByCollider.delete(collider.handle);
|
|
120
|
+
for (const [id, joined] of joints)
|
|
121
|
+
if (joined.a === key || joined.b === key)
|
|
122
|
+
joints.delete(id);
|
|
123
|
+
world.removeRigidBody(held.rigidBody);
|
|
124
|
+
bodies.delete(key);
|
|
125
|
+
};
|
|
126
|
+
const addJoint = (id, joint, a, b) => {
|
|
127
|
+
const first = bodies.get(a);
|
|
128
|
+
const second = bodies.get(b);
|
|
129
|
+
if (first === undefined || second === undefined)
|
|
130
|
+
return;
|
|
131
|
+
const made = world.createImpulseJoint(jointData(rapier, joint), first.rigidBody, second.rigidBody, true);
|
|
132
|
+
limit(made, limitsOf(joint));
|
|
133
|
+
drive(made, motorOf(joint));
|
|
134
|
+
made.setContactsEnabled(joint.contacts);
|
|
135
|
+
joints.set(id, { joint: made, a, b });
|
|
136
|
+
};
|
|
137
|
+
const removeJoint = (id) => {
|
|
138
|
+
const joined = joints.get(id);
|
|
139
|
+
if (joined === undefined)
|
|
140
|
+
return;
|
|
141
|
+
world.removeImpulseJoint(joined.joint, true);
|
|
142
|
+
joints.delete(id);
|
|
143
|
+
};
|
|
144
|
+
const collisionsOf = () => {
|
|
145
|
+
const collisions = [];
|
|
146
|
+
events.drainCollisionEvents((first, second, started) => {
|
|
147
|
+
const a = keysByCollider.get(first);
|
|
148
|
+
const b = keysByCollider.get(second);
|
|
149
|
+
if (a !== undefined && b !== undefined)
|
|
150
|
+
collisions.push({ ...pair(a, b), started });
|
|
151
|
+
});
|
|
152
|
+
return collisions;
|
|
153
|
+
};
|
|
154
|
+
let forces = [];
|
|
155
|
+
function recordForce(event) {
|
|
156
|
+
const a = keysByCollider.get(event.collider1());
|
|
157
|
+
const b = keysByCollider.get(event.collider2());
|
|
158
|
+
if (a === undefined || b === undefined)
|
|
159
|
+
return;
|
|
160
|
+
const total = event.totalForce();
|
|
161
|
+
const direction = event.maxForceDirection();
|
|
162
|
+
forces.push({
|
|
163
|
+
...pair(a, b),
|
|
164
|
+
totalForce: [total.x, total.y, total.z],
|
|
165
|
+
totalForceMagnitude: event.totalForceMagnitude(),
|
|
166
|
+
maxForceDirection: [direction.x, direction.y, direction.z],
|
|
167
|
+
maxForceMagnitude: event.maxForceMagnitude()
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const drainForces = () => events.drainContactForceEvents(recordForce);
|
|
171
|
+
return {
|
|
172
|
+
configure: (physics) => {
|
|
173
|
+
world.gravity = vector(physics.gravity);
|
|
174
|
+
},
|
|
175
|
+
add,
|
|
176
|
+
update: (key, body) => {
|
|
177
|
+
const held = bodies.get(key);
|
|
178
|
+
if (held === undefined)
|
|
179
|
+
return;
|
|
180
|
+
motion(held.rigidBody, body);
|
|
181
|
+
surface(held.colliders, body);
|
|
182
|
+
reporting(held.colliders, body);
|
|
183
|
+
},
|
|
184
|
+
teleport: (key, pose) => {
|
|
185
|
+
const held = bodies.get(key);
|
|
186
|
+
if (held === undefined)
|
|
187
|
+
return;
|
|
188
|
+
held.rigidBody.setTranslation(vector(pose.position), true);
|
|
189
|
+
held.rigidBody.setRotation(rotation(pose.quaternion), true);
|
|
190
|
+
held.rigidBody.setLinvel({ x: 0, y: 0, z: 0 }, true);
|
|
191
|
+
held.rigidBody.setAngvel({ x: 0, y: 0, z: 0 }, true);
|
|
192
|
+
},
|
|
193
|
+
moveKinematic: (key, pose) => {
|
|
194
|
+
const held = bodies.get(key);
|
|
195
|
+
if (held === undefined)
|
|
196
|
+
return;
|
|
197
|
+
held.rigidBody.setNextKinematicTranslation(vector(pose.position));
|
|
198
|
+
held.rigidBody.setNextKinematicRotation(rotation(pose.quaternion));
|
|
199
|
+
},
|
|
200
|
+
applyImpulse: (key, impulse) => {
|
|
201
|
+
bodies.get(key)?.rigidBody.applyImpulse(vector(impulse), true);
|
|
202
|
+
},
|
|
203
|
+
remove,
|
|
204
|
+
addJoint,
|
|
205
|
+
removeJoint,
|
|
206
|
+
step: (seconds) => {
|
|
207
|
+
world.timestep = seconds;
|
|
208
|
+
world.step(events);
|
|
209
|
+
drainForces();
|
|
210
|
+
return collisionsOf();
|
|
211
|
+
},
|
|
212
|
+
contactForces: () => {
|
|
213
|
+
const drained = forces;
|
|
214
|
+
forces = [];
|
|
215
|
+
return drained;
|
|
216
|
+
},
|
|
217
|
+
debugLines: () => {
|
|
218
|
+
const { vertices, colors } = world.debugRender();
|
|
219
|
+
return { vertices, colors };
|
|
220
|
+
},
|
|
221
|
+
pose: (key) => {
|
|
222
|
+
const held = bodies.get(key);
|
|
223
|
+
if (held === undefined)
|
|
224
|
+
return;
|
|
225
|
+
const { x, y, z } = held.rigidBody.translation();
|
|
226
|
+
const turn = held.rigidBody.rotation();
|
|
227
|
+
return { position: [x, y, z], quaternion: [turn.x, turn.y, turn.z, turn.w] };
|
|
228
|
+
},
|
|
229
|
+
awake: () => {
|
|
230
|
+
for (const { rigidBody } of bodies.values()) {
|
|
231
|
+
if (rigidBody.isDynamic() && !rigidBody.isSleeping())
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
},
|
|
236
|
+
dispose: () => {
|
|
237
|
+
bodies.clear();
|
|
238
|
+
keysByCollider.clear();
|
|
239
|
+
joints.clear();
|
|
240
|
+
events.free();
|
|
241
|
+
world.free();
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
var loadRapier = async () => {
|
|
246
|
+
await RAPIER.init();
|
|
247
|
+
return { createWorld: createWorld(RAPIER) };
|
|
248
|
+
};
|
|
249
|
+
export {
|
|
250
|
+
loadRapier
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
//# debugId=1F8003691871788E64756E2164756E21
|
|
254
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Rapier physics for `@opsydyn/effect-three`: a `PhysicsEngine` that the scene graph\n * steps. Load it once, before making the environment, and pass it on:\n *\n * ```ts\n * const physics = await loadRapier();\n * const environment = EffectThreeFoldkit.makeEnvironment(window, { physics });\n * ```\n *\n * Bodies are described on meshes (`body()`) under a scene's `physics()`; this package\n * only turns those descriptions into Rapier bodies and colliders, as react-three-rapier's\n * `RigidBody` does (`ae5c3fe`, `src/utils/utils-rigidbody.ts` and `utils-collider.ts`),\n * and joints into impulse joints, as its joint hooks do (`src/hooks/joints.ts`).\n *\n * Rapier (`@dimforge/rapier3d-compat`) is Apache-2.0; its WebAssembly is embedded, so\n * nothing extra is fetched.\n *\n * @module\n */\nimport RAPIER from '@dimforge/rapier3d-compat';\nimport { interactionBits } from '@opsydyn/effect-three';\nimport type {\n Body,\n BodyPose,\n Collider,\n ColliderShape,\n Collision,\n Physics,\n ContactForce,\n JointLimits,\n JointMotor,\n PhysicsEngine,\n PhysicsJoint,\n PhysicsWorld,\n Vec3,\n} from '@opsydyn/effect-three';\n\ntype Rapier = typeof RAPIER;\n\nconst vector = ([x, y, z]: Vec3) => ({ x, y, z });\n\nconst rotation = ([x, y, z, w]: BodyPose['quaternion']) => ({ x, y, z, w });\n\n/** Body types by our names. Kinematic bodies are position based, as react-three-rapier's default. */\nconst bodyTypes = {\n dynamic: RAPIER.RigidBodyType.Dynamic,\n fixed: RAPIER.RigidBodyType.Fixed,\n kinematic: RAPIER.RigidBodyType.KinematicPositionBased,\n} satisfies Record<Body['type'], RAPIER.RigidBodyType>;\n\n/** The collider description for a shape; a hull Rapier cannot build becomes a small ball. */\nconst shapeDesc = (rapier: Rapier, shape: ColliderShape): RAPIER.ColliderDesc => {\n switch (shape._tag) {\n case 'Cuboid':\n return rapier.ColliderDesc.cuboid(...shape.halfExtents);\n case 'Ball':\n return rapier.ColliderDesc.ball(shape.radius);\n case 'Hull':\n return rapier.ColliderDesc.convexHull(shape.points) ?? rapier.ColliderDesc.ball(1e-3);\n case 'Capsule':\n return rapier.ColliderDesc.capsule(shape.halfHeight, shape.radius);\n case 'Cylinder':\n return rapier.ColliderDesc.cylinder(shape.halfHeight, shape.radius);\n case 'Cone':\n return rapier.ColliderDesc.cone(shape.halfHeight, shape.radius);\n case 'Trimesh':\n return rapier.ColliderDesc.trimesh(shape.vertices, shape.indices);\n }\n};\n\n/** A collider placed in its body. */\nconst colliderDesc = (rapier: Rapier, collider: Collider): RAPIER.ColliderDesc =>\n shapeDesc(rapier, collider.shape)\n .setTranslation(...collider.translation)\n .setRotation(rotation(collider.rotation));\n\n/**\n * The events a body's colliders report: collisions always, contact forces over its\n * threshold when it has one (react-three-rapier's `onContactForce` turns on\n * `CONTACT_FORCE_EVENTS`).\n */\nconst reporting = (colliders: ReadonlyArray<RAPIER.Collider>, body: Body): void => {\n for (const collider of colliders) {\n if (body.contactForces === null) {\n collider.setActiveEvents(RAPIER.ActiveEvents.COLLISION_EVENTS);\n continue;\n }\n collider.setActiveEvents(\n RAPIER.ActiveEvents.COLLISION_EVENTS | RAPIER.ActiveEvents.CONTACT_FORCE_EVENTS,\n );\n collider.setContactForceEventThreshold(body.contactForces);\n }\n};\n\n/**\n * Applies a body's surface and mass settings to its colliders. A given mass is shared\n * among them; otherwise each has density 1.\n */\nconst surface = (colliders: ReadonlyArray<RAPIER.Collider>, body: Body): void => {\n for (const collider of colliders) {\n collider.setFriction(body.friction);\n collider.setRestitution(body.restitution);\n collider.setSensor(body.sensor);\n if (body.mass === null) collider.setDensity(1);\n else collider.setMass(body.mass / colliders.length);\n collider.setCollisionGroups(interactionBits(body.collisionGroups));\n collider.setSolverGroups(interactionBits(body.solverGroups));\n }\n};\n\n/** Applies a body's motion settings. */\nconst motion = (rigidBody: RAPIER.RigidBody, body: Body): void => {\n rigidBody.setBodyType(bodyTypes[body.type], true);\n rigidBody.setLinearDamping(body.linearDamping);\n rigidBody.setAngularDamping(body.angularDamping);\n rigidBody.setGravityScale(body.gravityScale, true);\n rigidBody.enableCcd(body.ccd);\n};\n\n/** The limits a joint has, if it is a hinge or slider with limits. */\nconst limitsOf = (joint: PhysicsJoint): JointLimits | null =>\n joint.kind._tag === 'Revolute' || joint.kind._tag === 'Prismatic' ? joint.kind.limits : null;\n\n/** The motor a joint has, if it is a hinge or slider with one. */\nconst motorOf = (joint: PhysicsJoint): JointMotor | null =>\n joint.kind._tag === 'Revolute' || joint.kind._tag === 'Prismatic' ? joint.kind.motor : null;\n\n/** Drives a hinge or slider once made: Rapier's `configureMotor`, set on the joint. */\nconst drive = (made: RAPIER.ImpulseJoint, motor: JointMotor | null): void => {\n if (motor !== null && made instanceof RAPIER.UnitImpulseJoint) {\n made.configureMotor(motor.position, motor.velocity, motor.stiffness, motor.damping);\n }\n};\n\n/**\n * Limits a hinge or slider once made. React-three-rapier sets them on the joint data, but\n * Rapier's `JointData.intoRaw` (0.21) passes them on for prismatic joints only, so a\n * hinge's would be lost: they are set on the joint instead.\n */\nconst limit = (made: RAPIER.ImpulseJoint, limits: JointLimits | null): void => {\n if (limits !== null && made instanceof RAPIER.UnitImpulseJoint) made.setLimits(...limits);\n};\n\n/** Rapier's joint data for a joint, anchors in each body's space. */\nconst jointData = (rapier: Rapier, joint: PhysicsJoint): RAPIER.JointData => {\n const a = vector(joint.anchorA);\n const b = vector(joint.anchorB);\n const { kind } = joint;\n switch (kind._tag) {\n case 'Fixed':\n return rapier.JointData.fixed(a, rotation(kind.frameA), b, rotation(kind.frameB));\n case 'Spherical':\n return rapier.JointData.spherical(a, b);\n case 'Revolute':\n return rapier.JointData.revolute(a, b, vector(kind.axis));\n case 'Prismatic':\n return rapier.JointData.prismatic(a, b, vector(kind.axis));\n case 'Rope':\n return rapier.JointData.rope(kind.length, a, b);\n case 'Spring':\n return rapier.JointData.spring(kind.restLength, kind.stiffness, kind.damping, a, b);\n }\n};\n\n/** Which body a collider belongs to, and which part of it, by name. */\ntype Owner = { readonly key: string; readonly part?: string };\n\n/** Two colliders' owners as an event's `a` and `b`, with their parts. */\nconst pair = (a: Owner, b: Owner) => ({\n a: a.key,\n b: b.key,\n ...(a.part === undefined ? {} : { aPart: a.part }),\n ...(b.part === undefined ? {} : { bPart: b.part }),\n});\n\ntype Held = {\n readonly rigidBody: RAPIER.RigidBody;\n readonly colliders: ReadonlyArray<RAPIER.Collider>;\n};\n\nconst createWorld =\n (rapier: Rapier) =>\n (initial: Physics): PhysicsWorld => {\n const world = new rapier.World(vector(initial.gravity));\n const events = new rapier.EventQueue(true);\n const bodies = new Map<string, Held>();\n /** Each collider's body, and the part of it the collider is (when named). */\n const keysByCollider = new Map<number, Owner>();\n const joints = new Map<\n string,\n { readonly joint: RAPIER.ImpulseJoint; readonly a: string; readonly b: string }\n >();\n\n const add: PhysicsWorld['add'] = (key, body, colliders, pose) => {\n const desc = new rapier.RigidBodyDesc(bodyTypes[body.type])\n .setTranslation(...pose.position)\n .setRotation(rotation(pose.quaternion))\n .setLinvel(...body.linearVelocity)\n .setAngvel(vector(body.angularVelocity))\n .setLinearDamping(body.linearDamping)\n .setAngularDamping(body.angularDamping)\n .setGravityScale(body.gravityScale)\n .setCcdEnabled(body.ccd);\n const rigidBody = world.createRigidBody(desc);\n const made = colliders.map((collider) =>\n world.createCollider(colliderDesc(rapier, collider), rigidBody),\n );\n surface(made, body);\n reporting(made, body);\n bodies.set(key, { rigidBody, colliders: made });\n made.forEach((collider, i) => {\n const part = colliders[i]?.part;\n keysByCollider.set(collider.handle, { key, ...(part === undefined ? {} : { part }) });\n });\n };\n\n const remove: PhysicsWorld['remove'] = (key) => {\n const held = bodies.get(key);\n if (held === undefined) return;\n for (const collider of held.colliders) keysByCollider.delete(collider.handle);\n // Rapier removes the body's joints with it; forget them, as their handles are reused.\n for (const [id, joined] of joints)\n if (joined.a === key || joined.b === key) joints.delete(id);\n world.removeRigidBody(held.rigidBody);\n bodies.delete(key);\n };\n\n const addJoint: PhysicsWorld['addJoint'] = (id, joint, a, b) => {\n const first = bodies.get(a);\n const second = bodies.get(b);\n if (first === undefined || second === undefined) return;\n const made = world.createImpulseJoint(\n jointData(rapier, joint),\n first.rigidBody,\n second.rigidBody,\n true,\n );\n limit(made, limitsOf(joint));\n drive(made, motorOf(joint));\n made.setContactsEnabled(joint.contacts);\n joints.set(id, { joint: made, a, b });\n };\n\n const removeJoint: PhysicsWorld['removeJoint'] = (id) => {\n const joined = joints.get(id);\n if (joined === undefined) return;\n world.removeImpulseJoint(joined.joint, true);\n joints.delete(id);\n };\n\n const collisionsOf = (): ReadonlyArray<Collision> => {\n const collisions: Array<Collision> = [];\n events.drainCollisionEvents((first, second, started) => {\n const a = keysByCollider.get(first);\n const b = keysByCollider.get(second);\n if (a !== undefined && b !== undefined) collisions.push({ ...pair(a, b), started });\n });\n return collisions;\n };\n\n /** Contact force events since the last call, kept across steps until asked for. */\n let forces: Array<ContactForce> = [];\n function recordForce(event: RAPIER.TempContactForceEvent): void {\n const a = keysByCollider.get(event.collider1());\n const b = keysByCollider.get(event.collider2());\n if (a === undefined || b === undefined) return;\n const total = event.totalForce();\n const direction = event.maxForceDirection();\n forces.push({\n ...pair(a, b),\n totalForce: [total.x, total.y, total.z],\n totalForceMagnitude: event.totalForceMagnitude(),\n maxForceDirection: [direction.x, direction.y, direction.z],\n maxForceMagnitude: event.maxForceMagnitude(),\n });\n }\n const drainForces = (): void => events.drainContactForceEvents(recordForce);\n\n return {\n configure: (physics) => {\n world.gravity = vector(physics.gravity);\n },\n add,\n update: (key, body) => {\n const held = bodies.get(key);\n if (held === undefined) return;\n motion(held.rigidBody, body);\n surface(held.colliders, body);\n reporting(held.colliders, body);\n },\n teleport: (key, pose) => {\n const held = bodies.get(key);\n if (held === undefined) return;\n held.rigidBody.setTranslation(vector(pose.position), true);\n held.rigidBody.setRotation(rotation(pose.quaternion), true);\n held.rigidBody.setLinvel({ x: 0, y: 0, z: 0 }, true);\n held.rigidBody.setAngvel({ x: 0, y: 0, z: 0 }, true);\n },\n moveKinematic: (key, pose) => {\n const held = bodies.get(key);\n if (held === undefined) return;\n held.rigidBody.setNextKinematicTranslation(vector(pose.position));\n held.rigidBody.setNextKinematicRotation(rotation(pose.quaternion));\n },\n applyImpulse: (key, impulse) => {\n bodies.get(key)?.rigidBody.applyImpulse(vector(impulse), true);\n },\n remove,\n addJoint,\n removeJoint,\n step: (seconds) => {\n world.timestep = seconds;\n world.step(events);\n drainForces();\n return collisionsOf();\n },\n contactForces: () => {\n const drained = forces;\n forces = [];\n return drained;\n },\n // react-three-rapier's Debug: Rapier's own line rendering of colliders and joints.\n debugLines: () => {\n const { vertices, colors } = world.debugRender();\n return { vertices, colors };\n },\n pose: (key) => {\n const held = bodies.get(key);\n if (held === undefined) return undefined;\n const { x, y, z } = held.rigidBody.translation();\n const turn = held.rigidBody.rotation();\n return { position: [x, y, z], quaternion: [turn.x, turn.y, turn.z, turn.w] };\n },\n awake: () => {\n for (const { rigidBody } of bodies.values()) {\n if (rigidBody.isDynamic() && !rigidBody.isSleeping()) return true;\n }\n return false;\n },\n dispose: () => {\n bodies.clear();\n keysByCollider.clear();\n joints.clear();\n events.free();\n world.free();\n },\n };\n };\n\n/**\n * Loads Rapier's WebAssembly and returns the engine. Call once per page; the engine makes\n * a world for each scene with physics.\n */\nexport const loadRapier = async (): Promise<PhysicsEngine> => {\n await RAPIER.init();\n return { createWorld: createWorld(RAPIER) };\n};\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "1F8003691871788E64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opsydyn/effect-three-rapier",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Rapier physics for @opsydyn/effect-three: rigid bodies described as data.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/opsydyn/fold-kit-experiments.git",
|
|
9
|
+
"directory": "packages/effect-three-rapier"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"src",
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"!dist/.tsbuildinfo",
|
|
18
|
+
"!dist/api-extractor",
|
|
19
|
+
"!dist/pack"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"bun": "./src/index.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"typecheck": "tsc -b && tsc -p tsconfig.test.json",
|
|
36
|
+
"lint": "bunx --bun oxlint --config oxlint.strict.ts",
|
|
37
|
+
"check": "bun run lint",
|
|
38
|
+
"test": "bun test --pass-with-no-tests ./src ./test",
|
|
39
|
+
"build": "tsc -b && bun build ./src/index.ts --root ./src --outdir ./dist --target browser --packages external --sourcemap=linked",
|
|
40
|
+
"api:report": "api-extractor run --local",
|
|
41
|
+
"api:check": "api-extractor run",
|
|
42
|
+
"pack:check": "bun run build && publint && rm -rf dist/pack && bun pm pack --destination dist/pack && attw dist/pack/*.tgz --profile esm-only"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@dimforge/rapier3d-compat": "0.21.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@arethetypeswrong/cli": "0.18.5",
|
|
49
|
+
"@microsoft/api-extractor": "7.59.4",
|
|
50
|
+
"@opsydyn/effect-three": "0.1.0",
|
|
51
|
+
"@opsydyn/oxlint-effect": "2.0.1",
|
|
52
|
+
"@types/bun": "1.4.3",
|
|
53
|
+
"@types/three": "0.186.0",
|
|
54
|
+
"oxlint": "1.88.0",
|
|
55
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
56
|
+
"publint": "0.3.25",
|
|
57
|
+
"three": "0.186.1",
|
|
58
|
+
"typescript": "7.0.2"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@opsydyn/effect-three": "0.1.0",
|
|
62
|
+
"three": ">=0.186.0 <0.187.0"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rapier physics for `@opsydyn/effect-three`: a `PhysicsEngine` that the scene graph
|
|
3
|
+
* steps. Load it once, before making the environment, and pass it on:
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* const physics = await loadRapier();
|
|
7
|
+
* const environment = EffectThreeFoldkit.makeEnvironment(window, { physics });
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Bodies are described on meshes (`body()`) under a scene's `physics()`; this package
|
|
11
|
+
* only turns those descriptions into Rapier bodies and colliders, as react-three-rapier's
|
|
12
|
+
* `RigidBody` does (`ae5c3fe`, `src/utils/utils-rigidbody.ts` and `utils-collider.ts`),
|
|
13
|
+
* and joints into impulse joints, as its joint hooks do (`src/hooks/joints.ts`).
|
|
14
|
+
*
|
|
15
|
+
* Rapier (`@dimforge/rapier3d-compat`) is Apache-2.0; its WebAssembly is embedded, so
|
|
16
|
+
* nothing extra is fetched.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
import RAPIER from '@dimforge/rapier3d-compat';
|
|
21
|
+
import { interactionBits } from '@opsydyn/effect-three';
|
|
22
|
+
import type {
|
|
23
|
+
Body,
|
|
24
|
+
BodyPose,
|
|
25
|
+
Collider,
|
|
26
|
+
ColliderShape,
|
|
27
|
+
Collision,
|
|
28
|
+
Physics,
|
|
29
|
+
ContactForce,
|
|
30
|
+
JointLimits,
|
|
31
|
+
JointMotor,
|
|
32
|
+
PhysicsEngine,
|
|
33
|
+
PhysicsJoint,
|
|
34
|
+
PhysicsWorld,
|
|
35
|
+
Vec3,
|
|
36
|
+
} from '@opsydyn/effect-three';
|
|
37
|
+
|
|
38
|
+
type Rapier = typeof RAPIER;
|
|
39
|
+
|
|
40
|
+
const vector = ([x, y, z]: Vec3) => ({ x, y, z });
|
|
41
|
+
|
|
42
|
+
const rotation = ([x, y, z, w]: BodyPose['quaternion']) => ({ x, y, z, w });
|
|
43
|
+
|
|
44
|
+
/** Body types by our names. Kinematic bodies are position based, as react-three-rapier's default. */
|
|
45
|
+
const bodyTypes = {
|
|
46
|
+
dynamic: RAPIER.RigidBodyType.Dynamic,
|
|
47
|
+
fixed: RAPIER.RigidBodyType.Fixed,
|
|
48
|
+
kinematic: RAPIER.RigidBodyType.KinematicPositionBased,
|
|
49
|
+
} satisfies Record<Body['type'], RAPIER.RigidBodyType>;
|
|
50
|
+
|
|
51
|
+
/** The collider description for a shape; a hull Rapier cannot build becomes a small ball. */
|
|
52
|
+
const shapeDesc = (rapier: Rapier, shape: ColliderShape): RAPIER.ColliderDesc => {
|
|
53
|
+
switch (shape._tag) {
|
|
54
|
+
case 'Cuboid':
|
|
55
|
+
return rapier.ColliderDesc.cuboid(...shape.halfExtents);
|
|
56
|
+
case 'Ball':
|
|
57
|
+
return rapier.ColliderDesc.ball(shape.radius);
|
|
58
|
+
case 'Hull':
|
|
59
|
+
return rapier.ColliderDesc.convexHull(shape.points) ?? rapier.ColliderDesc.ball(1e-3);
|
|
60
|
+
case 'Capsule':
|
|
61
|
+
return rapier.ColliderDesc.capsule(shape.halfHeight, shape.radius);
|
|
62
|
+
case 'Cylinder':
|
|
63
|
+
return rapier.ColliderDesc.cylinder(shape.halfHeight, shape.radius);
|
|
64
|
+
case 'Cone':
|
|
65
|
+
return rapier.ColliderDesc.cone(shape.halfHeight, shape.radius);
|
|
66
|
+
case 'Trimesh':
|
|
67
|
+
return rapier.ColliderDesc.trimesh(shape.vertices, shape.indices);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** A collider placed in its body. */
|
|
72
|
+
const colliderDesc = (rapier: Rapier, collider: Collider): RAPIER.ColliderDesc =>
|
|
73
|
+
shapeDesc(rapier, collider.shape)
|
|
74
|
+
.setTranslation(...collider.translation)
|
|
75
|
+
.setRotation(rotation(collider.rotation));
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The events a body's colliders report: collisions always, contact forces over its
|
|
79
|
+
* threshold when it has one (react-three-rapier's `onContactForce` turns on
|
|
80
|
+
* `CONTACT_FORCE_EVENTS`).
|
|
81
|
+
*/
|
|
82
|
+
const reporting = (colliders: ReadonlyArray<RAPIER.Collider>, body: Body): void => {
|
|
83
|
+
for (const collider of colliders) {
|
|
84
|
+
if (body.contactForces === null) {
|
|
85
|
+
collider.setActiveEvents(RAPIER.ActiveEvents.COLLISION_EVENTS);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
collider.setActiveEvents(
|
|
89
|
+
RAPIER.ActiveEvents.COLLISION_EVENTS | RAPIER.ActiveEvents.CONTACT_FORCE_EVENTS,
|
|
90
|
+
);
|
|
91
|
+
collider.setContactForceEventThreshold(body.contactForces);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Applies a body's surface and mass settings to its colliders. A given mass is shared
|
|
97
|
+
* among them; otherwise each has density 1.
|
|
98
|
+
*/
|
|
99
|
+
const surface = (colliders: ReadonlyArray<RAPIER.Collider>, body: Body): void => {
|
|
100
|
+
for (const collider of colliders) {
|
|
101
|
+
collider.setFriction(body.friction);
|
|
102
|
+
collider.setRestitution(body.restitution);
|
|
103
|
+
collider.setSensor(body.sensor);
|
|
104
|
+
if (body.mass === null) collider.setDensity(1);
|
|
105
|
+
else collider.setMass(body.mass / colliders.length);
|
|
106
|
+
collider.setCollisionGroups(interactionBits(body.collisionGroups));
|
|
107
|
+
collider.setSolverGroups(interactionBits(body.solverGroups));
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Applies a body's motion settings. */
|
|
112
|
+
const motion = (rigidBody: RAPIER.RigidBody, body: Body): void => {
|
|
113
|
+
rigidBody.setBodyType(bodyTypes[body.type], true);
|
|
114
|
+
rigidBody.setLinearDamping(body.linearDamping);
|
|
115
|
+
rigidBody.setAngularDamping(body.angularDamping);
|
|
116
|
+
rigidBody.setGravityScale(body.gravityScale, true);
|
|
117
|
+
rigidBody.enableCcd(body.ccd);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** The limits a joint has, if it is a hinge or slider with limits. */
|
|
121
|
+
const limitsOf = (joint: PhysicsJoint): JointLimits | null =>
|
|
122
|
+
joint.kind._tag === 'Revolute' || joint.kind._tag === 'Prismatic' ? joint.kind.limits : null;
|
|
123
|
+
|
|
124
|
+
/** The motor a joint has, if it is a hinge or slider with one. */
|
|
125
|
+
const motorOf = (joint: PhysicsJoint): JointMotor | null =>
|
|
126
|
+
joint.kind._tag === 'Revolute' || joint.kind._tag === 'Prismatic' ? joint.kind.motor : null;
|
|
127
|
+
|
|
128
|
+
/** Drives a hinge or slider once made: Rapier's `configureMotor`, set on the joint. */
|
|
129
|
+
const drive = (made: RAPIER.ImpulseJoint, motor: JointMotor | null): void => {
|
|
130
|
+
if (motor !== null && made instanceof RAPIER.UnitImpulseJoint) {
|
|
131
|
+
made.configureMotor(motor.position, motor.velocity, motor.stiffness, motor.damping);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Limits a hinge or slider once made. React-three-rapier sets them on the joint data, but
|
|
137
|
+
* Rapier's `JointData.intoRaw` (0.21) passes them on for prismatic joints only, so a
|
|
138
|
+
* hinge's would be lost: they are set on the joint instead.
|
|
139
|
+
*/
|
|
140
|
+
const limit = (made: RAPIER.ImpulseJoint, limits: JointLimits | null): void => {
|
|
141
|
+
if (limits !== null && made instanceof RAPIER.UnitImpulseJoint) made.setLimits(...limits);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Rapier's joint data for a joint, anchors in each body's space. */
|
|
145
|
+
const jointData = (rapier: Rapier, joint: PhysicsJoint): RAPIER.JointData => {
|
|
146
|
+
const a = vector(joint.anchorA);
|
|
147
|
+
const b = vector(joint.anchorB);
|
|
148
|
+
const { kind } = joint;
|
|
149
|
+
switch (kind._tag) {
|
|
150
|
+
case 'Fixed':
|
|
151
|
+
return rapier.JointData.fixed(a, rotation(kind.frameA), b, rotation(kind.frameB));
|
|
152
|
+
case 'Spherical':
|
|
153
|
+
return rapier.JointData.spherical(a, b);
|
|
154
|
+
case 'Revolute':
|
|
155
|
+
return rapier.JointData.revolute(a, b, vector(kind.axis));
|
|
156
|
+
case 'Prismatic':
|
|
157
|
+
return rapier.JointData.prismatic(a, b, vector(kind.axis));
|
|
158
|
+
case 'Rope':
|
|
159
|
+
return rapier.JointData.rope(kind.length, a, b);
|
|
160
|
+
case 'Spring':
|
|
161
|
+
return rapier.JointData.spring(kind.restLength, kind.stiffness, kind.damping, a, b);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** Which body a collider belongs to, and which part of it, by name. */
|
|
166
|
+
type Owner = { readonly key: string; readonly part?: string };
|
|
167
|
+
|
|
168
|
+
/** Two colliders' owners as an event's `a` and `b`, with their parts. */
|
|
169
|
+
const pair = (a: Owner, b: Owner) => ({
|
|
170
|
+
a: a.key,
|
|
171
|
+
b: b.key,
|
|
172
|
+
...(a.part === undefined ? {} : { aPart: a.part }),
|
|
173
|
+
...(b.part === undefined ? {} : { bPart: b.part }),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
type Held = {
|
|
177
|
+
readonly rigidBody: RAPIER.RigidBody;
|
|
178
|
+
readonly colliders: ReadonlyArray<RAPIER.Collider>;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const createWorld =
|
|
182
|
+
(rapier: Rapier) =>
|
|
183
|
+
(initial: Physics): PhysicsWorld => {
|
|
184
|
+
const world = new rapier.World(vector(initial.gravity));
|
|
185
|
+
const events = new rapier.EventQueue(true);
|
|
186
|
+
const bodies = new Map<string, Held>();
|
|
187
|
+
/** Each collider's body, and the part of it the collider is (when named). */
|
|
188
|
+
const keysByCollider = new Map<number, Owner>();
|
|
189
|
+
const joints = new Map<
|
|
190
|
+
string,
|
|
191
|
+
{ readonly joint: RAPIER.ImpulseJoint; readonly a: string; readonly b: string }
|
|
192
|
+
>();
|
|
193
|
+
|
|
194
|
+
const add: PhysicsWorld['add'] = (key, body, colliders, pose) => {
|
|
195
|
+
const desc = new rapier.RigidBodyDesc(bodyTypes[body.type])
|
|
196
|
+
.setTranslation(...pose.position)
|
|
197
|
+
.setRotation(rotation(pose.quaternion))
|
|
198
|
+
.setLinvel(...body.linearVelocity)
|
|
199
|
+
.setAngvel(vector(body.angularVelocity))
|
|
200
|
+
.setLinearDamping(body.linearDamping)
|
|
201
|
+
.setAngularDamping(body.angularDamping)
|
|
202
|
+
.setGravityScale(body.gravityScale)
|
|
203
|
+
.setCcdEnabled(body.ccd);
|
|
204
|
+
const rigidBody = world.createRigidBody(desc);
|
|
205
|
+
const made = colliders.map((collider) =>
|
|
206
|
+
world.createCollider(colliderDesc(rapier, collider), rigidBody),
|
|
207
|
+
);
|
|
208
|
+
surface(made, body);
|
|
209
|
+
reporting(made, body);
|
|
210
|
+
bodies.set(key, { rigidBody, colliders: made });
|
|
211
|
+
made.forEach((collider, i) => {
|
|
212
|
+
const part = colliders[i]?.part;
|
|
213
|
+
keysByCollider.set(collider.handle, { key, ...(part === undefined ? {} : { part }) });
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const remove: PhysicsWorld['remove'] = (key) => {
|
|
218
|
+
const held = bodies.get(key);
|
|
219
|
+
if (held === undefined) return;
|
|
220
|
+
for (const collider of held.colliders) keysByCollider.delete(collider.handle);
|
|
221
|
+
// Rapier removes the body's joints with it; forget them, as their handles are reused.
|
|
222
|
+
for (const [id, joined] of joints)
|
|
223
|
+
if (joined.a === key || joined.b === key) joints.delete(id);
|
|
224
|
+
world.removeRigidBody(held.rigidBody);
|
|
225
|
+
bodies.delete(key);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const addJoint: PhysicsWorld['addJoint'] = (id, joint, a, b) => {
|
|
229
|
+
const first = bodies.get(a);
|
|
230
|
+
const second = bodies.get(b);
|
|
231
|
+
if (first === undefined || second === undefined) return;
|
|
232
|
+
const made = world.createImpulseJoint(
|
|
233
|
+
jointData(rapier, joint),
|
|
234
|
+
first.rigidBody,
|
|
235
|
+
second.rigidBody,
|
|
236
|
+
true,
|
|
237
|
+
);
|
|
238
|
+
limit(made, limitsOf(joint));
|
|
239
|
+
drive(made, motorOf(joint));
|
|
240
|
+
made.setContactsEnabled(joint.contacts);
|
|
241
|
+
joints.set(id, { joint: made, a, b });
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const removeJoint: PhysicsWorld['removeJoint'] = (id) => {
|
|
245
|
+
const joined = joints.get(id);
|
|
246
|
+
if (joined === undefined) return;
|
|
247
|
+
world.removeImpulseJoint(joined.joint, true);
|
|
248
|
+
joints.delete(id);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const collisionsOf = (): ReadonlyArray<Collision> => {
|
|
252
|
+
const collisions: Array<Collision> = [];
|
|
253
|
+
events.drainCollisionEvents((first, second, started) => {
|
|
254
|
+
const a = keysByCollider.get(first);
|
|
255
|
+
const b = keysByCollider.get(second);
|
|
256
|
+
if (a !== undefined && b !== undefined) collisions.push({ ...pair(a, b), started });
|
|
257
|
+
});
|
|
258
|
+
return collisions;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/** Contact force events since the last call, kept across steps until asked for. */
|
|
262
|
+
let forces: Array<ContactForce> = [];
|
|
263
|
+
function recordForce(event: RAPIER.TempContactForceEvent): void {
|
|
264
|
+
const a = keysByCollider.get(event.collider1());
|
|
265
|
+
const b = keysByCollider.get(event.collider2());
|
|
266
|
+
if (a === undefined || b === undefined) return;
|
|
267
|
+
const total = event.totalForce();
|
|
268
|
+
const direction = event.maxForceDirection();
|
|
269
|
+
forces.push({
|
|
270
|
+
...pair(a, b),
|
|
271
|
+
totalForce: [total.x, total.y, total.z],
|
|
272
|
+
totalForceMagnitude: event.totalForceMagnitude(),
|
|
273
|
+
maxForceDirection: [direction.x, direction.y, direction.z],
|
|
274
|
+
maxForceMagnitude: event.maxForceMagnitude(),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
const drainForces = (): void => events.drainContactForceEvents(recordForce);
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
configure: (physics) => {
|
|
281
|
+
world.gravity = vector(physics.gravity);
|
|
282
|
+
},
|
|
283
|
+
add,
|
|
284
|
+
update: (key, body) => {
|
|
285
|
+
const held = bodies.get(key);
|
|
286
|
+
if (held === undefined) return;
|
|
287
|
+
motion(held.rigidBody, body);
|
|
288
|
+
surface(held.colliders, body);
|
|
289
|
+
reporting(held.colliders, body);
|
|
290
|
+
},
|
|
291
|
+
teleport: (key, pose) => {
|
|
292
|
+
const held = bodies.get(key);
|
|
293
|
+
if (held === undefined) return;
|
|
294
|
+
held.rigidBody.setTranslation(vector(pose.position), true);
|
|
295
|
+
held.rigidBody.setRotation(rotation(pose.quaternion), true);
|
|
296
|
+
held.rigidBody.setLinvel({ x: 0, y: 0, z: 0 }, true);
|
|
297
|
+
held.rigidBody.setAngvel({ x: 0, y: 0, z: 0 }, true);
|
|
298
|
+
},
|
|
299
|
+
moveKinematic: (key, pose) => {
|
|
300
|
+
const held = bodies.get(key);
|
|
301
|
+
if (held === undefined) return;
|
|
302
|
+
held.rigidBody.setNextKinematicTranslation(vector(pose.position));
|
|
303
|
+
held.rigidBody.setNextKinematicRotation(rotation(pose.quaternion));
|
|
304
|
+
},
|
|
305
|
+
applyImpulse: (key, impulse) => {
|
|
306
|
+
bodies.get(key)?.rigidBody.applyImpulse(vector(impulse), true);
|
|
307
|
+
},
|
|
308
|
+
remove,
|
|
309
|
+
addJoint,
|
|
310
|
+
removeJoint,
|
|
311
|
+
step: (seconds) => {
|
|
312
|
+
world.timestep = seconds;
|
|
313
|
+
world.step(events);
|
|
314
|
+
drainForces();
|
|
315
|
+
return collisionsOf();
|
|
316
|
+
},
|
|
317
|
+
contactForces: () => {
|
|
318
|
+
const drained = forces;
|
|
319
|
+
forces = [];
|
|
320
|
+
return drained;
|
|
321
|
+
},
|
|
322
|
+
// react-three-rapier's Debug: Rapier's own line rendering of colliders and joints.
|
|
323
|
+
debugLines: () => {
|
|
324
|
+
const { vertices, colors } = world.debugRender();
|
|
325
|
+
return { vertices, colors };
|
|
326
|
+
},
|
|
327
|
+
pose: (key) => {
|
|
328
|
+
const held = bodies.get(key);
|
|
329
|
+
if (held === undefined) return undefined;
|
|
330
|
+
const { x, y, z } = held.rigidBody.translation();
|
|
331
|
+
const turn = held.rigidBody.rotation();
|
|
332
|
+
return { position: [x, y, z], quaternion: [turn.x, turn.y, turn.z, turn.w] };
|
|
333
|
+
},
|
|
334
|
+
awake: () => {
|
|
335
|
+
for (const { rigidBody } of bodies.values()) {
|
|
336
|
+
if (rigidBody.isDynamic() && !rigidBody.isSleeping()) return true;
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
},
|
|
340
|
+
dispose: () => {
|
|
341
|
+
bodies.clear();
|
|
342
|
+
keysByCollider.clear();
|
|
343
|
+
joints.clear();
|
|
344
|
+
events.free();
|
|
345
|
+
world.free();
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Loads Rapier's WebAssembly and returns the engine. Call once per page; the engine makes
|
|
352
|
+
* a world for each scene with physics.
|
|
353
|
+
*/
|
|
354
|
+
export const loadRapier = async (): Promise<PhysicsEngine> => {
|
|
355
|
+
await RAPIER.init();
|
|
356
|
+
return { createWorld: createWorld(RAPIER) };
|
|
357
|
+
};
|