@jgengine/rapier 0.18.1
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 +1112 -0
- package/README.md +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/rapierBackend.d.ts +10 -0
- package/dist/rapierBackend.js +223 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# @jgengine/rapier
|
|
2
|
+
|
|
3
|
+
Rapier-backed physics adapter for the `@jgengine/core` `PhysicsBackend` seam.
|
|
4
|
+
It provides native capsules, shapecasts, rotation, CCD, joints, and collision queries.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { createRapierBackend } from "@jgengine/rapier";
|
|
8
|
+
|
|
9
|
+
const physics = await createRapierBackend();
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Part of [JGengine](https://github.com/Noisemaker111/jgengine). Apache-2.0.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./rapierBackend.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./rapierBackend.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PhysicsBackend, PhysicsBackendConfig } from "@jgengine/core/physics/physicsBackend";
|
|
2
|
+
/** Construction options for the Rapier-backed {@link PhysicsBackend}. */
|
|
3
|
+
export interface RapierBackendOptions extends PhysicsBackendConfig {
|
|
4
|
+
warn?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Create a Rapier-backed {@link PhysicsBackend}; initialization loads Rapier's WASM module asynchronously.
|
|
7
|
+
*
|
|
8
|
+
* @capability rapier-physics-backend native capsules, shapecasts, rotation, CCD, joints, and collision queries
|
|
9
|
+
*/
|
|
10
|
+
export declare function createRapierBackend(options?: RapierBackendOptions): Promise<PhysicsBackend>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
const q = (r) => ({ x: r?.[0] ?? 0, y: r?.[1] ?? 0, z: r?.[2] ?? 0, w: r?.[3] ?? 1 });
|
|
2
|
+
const vec = (x) => Object.assign([x.x, x.y, x.z], { x: x.x, y: x.y, z: x.z });
|
|
3
|
+
const ALL = 0xffffffff;
|
|
4
|
+
const eventHandle = (value) => {
|
|
5
|
+
if (Number.isInteger(value))
|
|
6
|
+
return value;
|
|
7
|
+
const bits = new ArrayBuffer(8);
|
|
8
|
+
new DataView(bits).setFloat64(0, value, true);
|
|
9
|
+
const decoded = Number(new DataView(bits).getBigUint64(0, true));
|
|
10
|
+
return Number.isSafeInteger(decoded) ? decoded : value;
|
|
11
|
+
};
|
|
12
|
+
/** Create a Rapier-backed {@link PhysicsBackend}; initialization loads Rapier's WASM module asynchronously.
|
|
13
|
+
*
|
|
14
|
+
* @capability rapier-physics-backend native capsules, shapecasts, rotation, CCD, joints, and collision queries
|
|
15
|
+
*/
|
|
16
|
+
export async function createRapierBackend(options = {}) {
|
|
17
|
+
const R = await import("@dimforge/rapier3d-compat");
|
|
18
|
+
await R.init();
|
|
19
|
+
let world = new R.World({ x: options.gravity?.[0] ?? 0, y: options.gravity?.[1] ?? -9.81, z: options.gravity?.[2] ?? 0 });
|
|
20
|
+
const events = new R.EventQueue(false);
|
|
21
|
+
const bodies = new Map();
|
|
22
|
+
const colliders = new Map();
|
|
23
|
+
const joints = new Map();
|
|
24
|
+
let next = 1;
|
|
25
|
+
let nextJoint = 1;
|
|
26
|
+
let hasStepped = false;
|
|
27
|
+
let listener = null;
|
|
28
|
+
const shape = (s) => {
|
|
29
|
+
switch (s.kind) {
|
|
30
|
+
case "box": return R.ColliderDesc.cuboid(...s.halfExtents);
|
|
31
|
+
case "sphere": return R.ColliderDesc.ball(s.radius);
|
|
32
|
+
case "capsule": return R.ColliderDesc.capsule(s.halfHeight, s.radius);
|
|
33
|
+
case "convex": return R.ColliderDesc.convexHull(s.points.flatMap((p) => p)) ?? R.ColliderDesc.ball(0.01);
|
|
34
|
+
case "trimesh": return R.ColliderDesc.trimesh(Array.from(s.vertices), Array.from(s.indices));
|
|
35
|
+
case "heightfield": return R.ColliderDesc.heightfield(s.rows, s.columns, Array.from(s.heights), s.scale);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const get = (h) => {
|
|
39
|
+
const b = bodies.get(h);
|
|
40
|
+
if (!b)
|
|
41
|
+
throw Error(`physics: unknown body handle ${h}`);
|
|
42
|
+
return b;
|
|
43
|
+
};
|
|
44
|
+
const find = (map, native) => {
|
|
45
|
+
if (typeof native === "function")
|
|
46
|
+
native = native();
|
|
47
|
+
if (native && typeof native === "object")
|
|
48
|
+
native = native.handle;
|
|
49
|
+
native = eventHandle(native);
|
|
50
|
+
return [...map].find(([, value]) => {
|
|
51
|
+
const handle = typeof value.handle === "function" ? value.handle() : value.handle;
|
|
52
|
+
return eventHandle(handle) === native;
|
|
53
|
+
})?.[0];
|
|
54
|
+
};
|
|
55
|
+
const updateQueryPipeline = () => {
|
|
56
|
+
if (hasStepped)
|
|
57
|
+
return;
|
|
58
|
+
const timestep = world.timestep;
|
|
59
|
+
world.timestep = 0;
|
|
60
|
+
world.step();
|
|
61
|
+
world.timestep = timestep;
|
|
62
|
+
};
|
|
63
|
+
const queryGroups = (mask) => ((mask ?? ALL) << 16) | 0xffff;
|
|
64
|
+
const backend = {
|
|
65
|
+
name: "rapier",
|
|
66
|
+
capabilities: { rotation: true, ccd: true, shapes: ["box", "sphere", "capsule", "convex", "trimesh", "heightfield"], joints: ["fixed", "hinge", "ball", "distance", "spring"] },
|
|
67
|
+
addBody(d) {
|
|
68
|
+
const h = next++;
|
|
69
|
+
const bd = d.kind === "static" ? R.RigidBodyDesc.fixed() : d.kind === "kinematic" ? R.RigidBodyDesc.kinematicPositionBased() : R.RigidBodyDesc.dynamic();
|
|
70
|
+
bd.setTranslation(...d.position).setRotation(q(d.rotation)).setLinvel(...(d.velocity ?? [0, 0, 0])).setAngvel(vec({ x: (d.angularVelocity ?? [0, 0, 0])[0], y: (d.angularVelocity ?? [0, 0, 0])[1], z: (d.angularVelocity ?? [0, 0, 0])[2] }));
|
|
71
|
+
if (d.mass !== undefined)
|
|
72
|
+
bd.setAdditionalMass(d.mass);
|
|
73
|
+
if (d.ccd)
|
|
74
|
+
bd.setCcdEnabled(true);
|
|
75
|
+
const b = world.createRigidBody(bd);
|
|
76
|
+
const c = world.createCollider(shape(d.shape).setFriction(d.friction ?? 0.5).setRestitution(d.restitution ?? 0).setActiveEvents(R.ActiveEvents.COLLISION_EVENTS), b);
|
|
77
|
+
c.setCollisionGroups(((d.mask ?? ALL) << 16) | (d.layers ?? 1));
|
|
78
|
+
b.__jg = { d, h };
|
|
79
|
+
if (d.asleep)
|
|
80
|
+
b.sleep();
|
|
81
|
+
bodies.set(h, b);
|
|
82
|
+
colliders.set(h, c);
|
|
83
|
+
return h;
|
|
84
|
+
},
|
|
85
|
+
removeBody(h) {
|
|
86
|
+
const b = bodies.get(h);
|
|
87
|
+
if (!b)
|
|
88
|
+
return;
|
|
89
|
+
for (const [id, j] of joints)
|
|
90
|
+
if (j.a === h || j.b === h) {
|
|
91
|
+
world.removeImpulseJoint(j.joint, true);
|
|
92
|
+
joints.delete(id);
|
|
93
|
+
}
|
|
94
|
+
world.removeRigidBody(b);
|
|
95
|
+
bodies.delete(h);
|
|
96
|
+
colliders.delete(h);
|
|
97
|
+
},
|
|
98
|
+
hasBody: (h) => bodies.has(h),
|
|
99
|
+
body(h, out) {
|
|
100
|
+
const b = bodies.get(h);
|
|
101
|
+
if (!b)
|
|
102
|
+
return null;
|
|
103
|
+
const p = b.translation(), r = b.rotation(), v = b.linvel(), a = b.angvel(), s = out ?? {};
|
|
104
|
+
s.position = vec(p);
|
|
105
|
+
s.rotation = [r.x, r.y, r.z, r.w];
|
|
106
|
+
s.velocity = vec(v);
|
|
107
|
+
s.angularVelocity = vec(a);
|
|
108
|
+
s.sleeping = b.isSleeping();
|
|
109
|
+
return s;
|
|
110
|
+
},
|
|
111
|
+
userDataOf: (h) => bodies.get(h)?.__jg.d.userData,
|
|
112
|
+
setPosition(h, p) { get(h).setTranslation({ x: p[0], y: p[1], z: p[2] }, true); },
|
|
113
|
+
setRotation(h, r) { get(h).setRotation(q(r), true); },
|
|
114
|
+
setVelocity(h, p) { get(h).setLinvel({ x: p[0], y: p[1], z: p[2] }, true); },
|
|
115
|
+
setAngularVelocity(h, p) { get(h).setAngvel({ x: p[0], y: p[1], z: p[2] }, true); },
|
|
116
|
+
applyImpulse(h, i, p) { const b = get(h); b.applyImpulseAtPoint({ x: i[0], y: i[1], z: i[2] }, p ? { x: p[0], y: p[1], z: p[2] } : b.translation(), true); },
|
|
117
|
+
setKinematicTarget(h, p, r) { const b = get(h); b.setNextKinematicTranslation({ x: p[0], y: p[1], z: p[2] }); if (r)
|
|
118
|
+
b.setNextKinematicRotation(q(r)); },
|
|
119
|
+
wake(h) { get(h).wakeUp(); },
|
|
120
|
+
addJoint(d) {
|
|
121
|
+
const aa = d.anchorA ?? [0, 0, 0], ab = d.anchorB ?? [0, 0, 0], a = { x: aa[0], y: aa[1], z: aa[2] }, b = { x: ab[0], y: ab[1], z: ab[2] }, ar = d.axis ?? [0, 1, 0], len = Math.hypot(...ar);
|
|
122
|
+
if (d.kind === "hinge" && !(len > 1e-9))
|
|
123
|
+
throw Error("physics: hinge joint axis must be non-zero");
|
|
124
|
+
const ax = { x: ar[0] / len, y: ar[1] / len, z: ar[2] / len }, A = get(d.bodyA), B = d.bodyB === undefined ? null : get(d.bodyB);
|
|
125
|
+
let data;
|
|
126
|
+
switch (d.kind) {
|
|
127
|
+
case "fixed":
|
|
128
|
+
data = R.JointData.fixed(a, { x: 0, y: 0, z: 0, w: 1 }, b, { x: 0, y: 0, z: 0, w: 1 });
|
|
129
|
+
break;
|
|
130
|
+
case "hinge":
|
|
131
|
+
data = R.JointData.revolute(a, b, ax);
|
|
132
|
+
break;
|
|
133
|
+
case "ball":
|
|
134
|
+
data = R.JointData.spherical(a, b);
|
|
135
|
+
break;
|
|
136
|
+
case "distance":
|
|
137
|
+
data = R.JointData.rope(d.restLength ?? 0, a, b);
|
|
138
|
+
break;
|
|
139
|
+
case "spring":
|
|
140
|
+
data = R.JointData.spring(d.restLength ?? 0, d.stiffness ?? 40, d.damping ?? 6, a, b);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
const joint = world.createImpulseJoint(data, A, B, true);
|
|
144
|
+
if (d.kind === "hinge" && d.limits)
|
|
145
|
+
joint.setLimits(d.limits[0], d.limits[1]);
|
|
146
|
+
const h = nextJoint++;
|
|
147
|
+
joints.set(h, { joint, a: d.bodyA, b: d.bodyB ?? -1, d: { ...d } });
|
|
148
|
+
return h;
|
|
149
|
+
},
|
|
150
|
+
removeJoint(h) { const j = joints.get(h); if (j) {
|
|
151
|
+
world.removeImpulseJoint(j.joint, true);
|
|
152
|
+
joints.delete(h);
|
|
153
|
+
} },
|
|
154
|
+
step(dt) {
|
|
155
|
+
if (dt <= 0)
|
|
156
|
+
return;
|
|
157
|
+
world.timestep = options.fixedDt ?? dt;
|
|
158
|
+
world.step(events);
|
|
159
|
+
hasStepped = true;
|
|
160
|
+
events.drainCollisionEvents((rawX, rawY, started) => { if (!listener || !started)
|
|
161
|
+
return; const x = eventHandle(rawX), y = eventHandle(rawY), ca = world.getCollider(x), cb = world.getCollider(y), ha = find(colliders, x), hb = find(colliders, y); if (!ca || !cb || ha === undefined || hb === undefined)
|
|
162
|
+
return; const m = world.contactPair(ca, cb)?.manifolds?.()[0], n = vec(m?.normal?.() ?? { x: 0, y: 1, z: 0 }), va = ca.parent()?.linvel?.() ?? { x: 0, y: 0, z: 0 }, vb = cb.parent()?.linvel?.() ?? { x: 0, y: 0, z: 0 }; listener({ a: ha, b: hb, normal: n, approachSpeed: Math.max(0, -((vb.x - va.x) * n[0] + (vb.y - va.y) * n[1] + (vb.z - va.z) * n[2])), impulse: Number(m?.totalImpulse?.() ?? 0) }); });
|
|
163
|
+
},
|
|
164
|
+
raycast(d) {
|
|
165
|
+
const n = Math.hypot(...d.direction);
|
|
166
|
+
if (!n)
|
|
167
|
+
return null;
|
|
168
|
+
updateQueryPipeline();
|
|
169
|
+
const ray = new R.Ray({ x: d.origin[0], y: d.origin[1], z: d.origin[2] }, { x: d.direction[0] / n, y: d.direction[1] / n, z: d.direction[2] / n });
|
|
170
|
+
const h = world.castRay(ray, d.maxDistance, true, undefined, queryGroups(d.mask));
|
|
171
|
+
if (!h)
|
|
172
|
+
return null;
|
|
173
|
+
const intersection = h.collider.castRayAndGetNormal(ray, h.timeOfImpact, true);
|
|
174
|
+
const b = find(colliders, h.collider ?? h.colliderHandle);
|
|
175
|
+
return b === undefined || b === d.exclude ? null : { body: b, distance: h.timeOfImpact, point: vec(ray.pointAt(h.timeOfImpact)), normal: vec(intersection.normal) };
|
|
176
|
+
},
|
|
177
|
+
shapecast(d) {
|
|
178
|
+
updateQueryPipeline();
|
|
179
|
+
const h = world.castShape({ x: d.position[0], y: d.position[1], z: d.position[2] }, q(d.rotation), { x: d.motion[0], y: d.motion[1], z: d.motion[2] }, shape(d.shape).shape, 0, 1, true, undefined, queryGroups(d.mask));
|
|
180
|
+
if (!h)
|
|
181
|
+
return null;
|
|
182
|
+
if (h.time_of_impact <= 0 && h.normal1 && (h.normal1.x * d.motion[0] + h.normal1.y * d.motion[1] + h.normal1.z * d.motion[2]) > 0)
|
|
183
|
+
return null;
|
|
184
|
+
const horizontal = Math.hypot(d.motion[0], d.motion[2]);
|
|
185
|
+
const castBottom = d.position[1] - (d.shape.kind === "capsule" ? d.shape.halfHeight + d.shape.radius : d.shape.kind === "sphere" ? d.shape.radius : d.shape.kind === "box" ? d.shape.halfExtents[1] : 0);
|
|
186
|
+
const raisedWitness = h.witness1 && h.witness2 && (h.witness1.y > castBottom + 0.01 || h.witness2.y > castBottom + 0.01);
|
|
187
|
+
const edgeNormal = h.normal1 && raisedWitness && horizontal > 0 && h.normal1.y > 0.1
|
|
188
|
+
? (Math.hypot(h.normal1.x, h.normal1.z) > 1e-6
|
|
189
|
+
? { x: h.normal1.x / Math.hypot(h.normal1.x, h.normal1.z), y: 0, z: h.normal1.z / Math.hypot(h.normal1.x, h.normal1.z) }
|
|
190
|
+
: { x: -d.motion[0] / horizontal, y: 0, z: -d.motion[2] / horizontal })
|
|
191
|
+
: h.normal1;
|
|
192
|
+
const cornerLanding = d.motion[1] < 0 && h.normal1 && h.normal1.y > 0 && h.witness1 && h.witness1.y < castBottom - 0.01;
|
|
193
|
+
const contactNormal = cornerLanding ? { x: 0, y: 1, z: 0 } : edgeNormal;
|
|
194
|
+
const b = find(colliders, h.collider ?? h.colliderHandle);
|
|
195
|
+
return b === undefined || b === d.exclude ? null : { body: b, toi: h.time_of_impact, distance: h.time_of_impact * Math.hypot(...d.motion), point: vec(h.witness1), normal: vec(contactNormal) };
|
|
196
|
+
},
|
|
197
|
+
overlap(d) { const out = [], s = shape(d.shape); updateQueryPipeline(); world.intersectionsWithShape({ x: d.position[0], y: d.position[1], z: d.position[2] }, q(d.rotation), s.shape, (c) => { const h = find(colliders, c.handle); if (h !== undefined && h !== d.exclude)
|
|
198
|
+
out.push(h); return true; }, undefined, queryGroups(d.mask)); return out; },
|
|
199
|
+
onContact(l) { listener = l; },
|
|
200
|
+
retune(c) { if (c.gravity)
|
|
201
|
+
world.gravity = { x: c.gravity[0], y: c.gravity[1], z: c.gravity[2] }; },
|
|
202
|
+
snapshot() { return { bytes: world.takeSnapshot(), bodies: [...bodies].map(([h, b]) => ({ h, n: b.handle, d: b.__jg.d })), colliders: [...colliders].map(([h, c]) => ({ h, n: c.handle })), joints: [...joints].map(([h, j]) => ({ h, n: j.joint.handle, a: j.a, b: j.b, d: j.d })), next, nextJoint }; },
|
|
203
|
+
restore(s) { if (!s?.bytes)
|
|
204
|
+
throw Error("physics: invalid Rapier snapshot"); world.free(); world = R.World.restoreSnapshot(s.bytes); bodies.clear(); colliders.clear(); joints.clear(); for (const x of s.bodies ?? []) {
|
|
205
|
+
const b = world.getRigidBody(x.n);
|
|
206
|
+
if (!b)
|
|
207
|
+
throw Error(`physics: snapshot body ${x.h} is missing`);
|
|
208
|
+
b.__jg = { d: x.d, h: x.h };
|
|
209
|
+
bodies.set(x.h, b);
|
|
210
|
+
} for (const x of s.colliders ?? []) {
|
|
211
|
+
const c = world.getCollider(x.n);
|
|
212
|
+
if (!c)
|
|
213
|
+
throw Error(`physics: snapshot collider ${x.h} is missing`);
|
|
214
|
+
colliders.set(x.h, c);
|
|
215
|
+
} for (const x of s.joints ?? []) {
|
|
216
|
+
const j = world.getImpulseJoint(x.n);
|
|
217
|
+
if (j)
|
|
218
|
+
joints.set(x.h, { joint: j, a: x.a, b: x.b, d: x.d });
|
|
219
|
+
} next = s.next ?? (Math.max(0, ...bodies.keys()) + 1); nextJoint = s.nextJoint ?? (Math.max(0, ...joints.keys()) + 1); },
|
|
220
|
+
dispose() { events.free?.(); world.free(); bodies.clear(); colliders.clear(); joints.clear(); listener = null; },
|
|
221
|
+
};
|
|
222
|
+
return backend;
|
|
223
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jgengine/rapier",
|
|
3
|
+
"version": "0.18.1",
|
|
4
|
+
"description": "Rapier physics backend for JGengine",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Noisemaker111/jgengine.git",
|
|
11
|
+
"directory": "packages/rapier"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"CHANGELOG.md"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./*": {
|
|
23
|
+
"types": "./dist/*.d.ts",
|
|
24
|
+
"default": "./dist/*.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsgo -p tsconfig.build.json && bun ../../scripts/fix-extensions.ts dist",
|
|
29
|
+
"check-types": "bun ../../scripts/check-types-preflight.ts && tsgo --noEmit -p tsconfig.json",
|
|
30
|
+
"test": "bun test src"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@jgengine/core": "^0.18.1",
|
|
34
|
+
"@dimforge/rapier3d-compat": "0.19.3"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|