@aethervtt/shard-physics 0.0.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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/character.d.ts +62 -0
- package/dist/character.js +509 -0
- package/dist/components.d.ts +155 -0
- package/dist/components.js +340 -0
- package/dist/gravity.d.ts +17 -0
- package/dist/gravity.js +73 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +64 -0
- package/dist/methods.d.ts +2 -0
- package/dist/methods.js +131 -0
- package/dist/plugin.d.ts +13 -0
- package/dist/plugin.js +853 -0
- package/dist/pose.d.ts +41 -0
- package/dist/pose.js +341 -0
- package/dist/world.d.ts +246 -0
- package/dist/world.js +1104 -0
- package/package.json +40 -0
package/dist/world.js
ADDED
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
import { assetServer } from "@aethervtt/shard-assets";
|
|
2
|
+
import {
|
|
3
|
+
Children,
|
|
4
|
+
defineResource,
|
|
5
|
+
ShardError
|
|
6
|
+
} from "@aethervtt/shard-core";
|
|
7
|
+
import { Meshes } from "@aethervtt/shard-render";
|
|
8
|
+
import { LogResource } from "@aethervtt/shard-runtime";
|
|
9
|
+
import { GlobalTransform, GridFramesResource } from "@aethervtt/shard-transform";
|
|
10
|
+
import {
|
|
11
|
+
BODY_KINDS,
|
|
12
|
+
CharacterController,
|
|
13
|
+
Collider,
|
|
14
|
+
Joint,
|
|
15
|
+
Mass,
|
|
16
|
+
PhysicsParked,
|
|
17
|
+
RigidBody,
|
|
18
|
+
Velocity
|
|
19
|
+
} from "./components.js";
|
|
20
|
+
import { createPose, mulQuat, parentOf, poseOf, rotate } from "./pose.js";
|
|
21
|
+
const Physics = defineResource("physics/World", {
|
|
22
|
+
description: "The Rapier world: raycast, shapeCast, overlapPoint, overlapShape, describe. Present after the physics plugin is ready."
|
|
23
|
+
});
|
|
24
|
+
const modules = {};
|
|
25
|
+
function loadRapier(dim) {
|
|
26
|
+
modules[dim] ??= (async () => {
|
|
27
|
+
const mod = dim === 3 ? await import("@dimforge/rapier3d-compat") : await import("@dimforge/rapier2d-compat");
|
|
28
|
+
const R = mod.default ?? mod;
|
|
29
|
+
await R.init();
|
|
30
|
+
return R;
|
|
31
|
+
})();
|
|
32
|
+
return modules[dim];
|
|
33
|
+
}
|
|
34
|
+
const CHARACTER_OFFSET = 0.01;
|
|
35
|
+
const createRayHit = () => ({
|
|
36
|
+
entity: -1,
|
|
37
|
+
body: -1,
|
|
38
|
+
distance: 0,
|
|
39
|
+
point: new Float32Array(3),
|
|
40
|
+
normal: new Float32Array(3)
|
|
41
|
+
});
|
|
42
|
+
const KIND_DYNAMIC = 0;
|
|
43
|
+
const KIND_FIXED = 1;
|
|
44
|
+
const KIND_KINEMATIC_POSITION = 2;
|
|
45
|
+
class PhysicsWorld {
|
|
46
|
+
R;
|
|
47
|
+
dim;
|
|
48
|
+
raw;
|
|
49
|
+
events;
|
|
50
|
+
bodies = /* @__PURE__ */ new Map();
|
|
51
|
+
/** Every body record, for loops that shouldn't allocate an iterator. */
|
|
52
|
+
list = [];
|
|
53
|
+
bodyByHandle = /* @__PURE__ */ new Map();
|
|
54
|
+
colliders = /* @__PURE__ */ new Map();
|
|
55
|
+
colliderEntity = /* @__PURE__ */ new Map();
|
|
56
|
+
/** Collider entity → the body entity it's attached to (absent: fixed). */
|
|
57
|
+
colliderOwner = /* @__PURE__ */ new Map();
|
|
58
|
+
/** Shape kind per collider entity, for describe and the overlay. */
|
|
59
|
+
colliderShape = /* @__PURE__ */ new Map();
|
|
60
|
+
joints = /* @__PURE__ */ new Map();
|
|
61
|
+
/** Character controllers by entity; each also has a body record and a capsule collider. */
|
|
62
|
+
characters = /* @__PURE__ */ new Map();
|
|
63
|
+
/** Colliders to (re)build at the next sync, e.g. after their body changed. */
|
|
64
|
+
dirtyColliders = /* @__PURE__ */ new Set();
|
|
65
|
+
dirtyJoints = /* @__PURE__ */ new Set();
|
|
66
|
+
dirtyBodies = /* @__PURE__ */ new Set();
|
|
67
|
+
/** Collider entities waiting for their mesh to load. */
|
|
68
|
+
pendingMesh = /* @__PURE__ */ new Set();
|
|
69
|
+
/** Mesh colliders and the mesh version they were built from, to rebuild on hot reload. */
|
|
70
|
+
meshColliders = /* @__PURE__ */ new Map();
|
|
71
|
+
/** Last step's poses (slot × 7: translation, rotation), and the ones before, for interpolation. */
|
|
72
|
+
prev = new Float64Array(7 * 64);
|
|
73
|
+
curr = new Float64Array(7 * 64);
|
|
74
|
+
freeSlots = [];
|
|
75
|
+
nextSlot = 0;
|
|
76
|
+
/** Ticks: when sync-out last wrote Transforms, and when interpolation did. */
|
|
77
|
+
writeTick = 0;
|
|
78
|
+
interpTick = 0;
|
|
79
|
+
/** Whether the world had grids at the last sync (spec 0040), so a change resyncs every body. */
|
|
80
|
+
gridsActive = false;
|
|
81
|
+
/** Bodies parked out of PhysicsRange. */
|
|
82
|
+
parkedCount = 0;
|
|
83
|
+
stepMs = 0;
|
|
84
|
+
steps = 0;
|
|
85
|
+
config;
|
|
86
|
+
scratchPose = createPose();
|
|
87
|
+
scratchPose2 = createPose();
|
|
88
|
+
v3 = { x: 0, y: 0, z: 0 };
|
|
89
|
+
q4 = { x: 0, y: 0, z: 0, w: 1 };
|
|
90
|
+
rawVec;
|
|
91
|
+
ray;
|
|
92
|
+
logged = /* @__PURE__ */ new Set();
|
|
93
|
+
constructor(R, dim, config) {
|
|
94
|
+
this.R = R;
|
|
95
|
+
this.dim = dim;
|
|
96
|
+
this.config = config;
|
|
97
|
+
this.raw = new R.World(this.gravityVector());
|
|
98
|
+
this.events = new R.EventQueue(true);
|
|
99
|
+
this.ray = new R.Ray({ x: 0, y: 0, z: 0 }, { x: 0, y: -1, z: 0 });
|
|
100
|
+
const ops = R.VectorOps;
|
|
101
|
+
this.rawVec = ops?.intoRaw({ x: 0, y: 0, z: 0 });
|
|
102
|
+
}
|
|
103
|
+
gravityVector() {
|
|
104
|
+
const g = this.config.gravity;
|
|
105
|
+
return { x: g[0], y: g[1], z: g[2] };
|
|
106
|
+
}
|
|
107
|
+
// --- helpers ---------------------------------------------------------------
|
|
108
|
+
/** Writes a vector into the reused {x, y, z} object (Rapier's 2D API ignores z). */
|
|
109
|
+
vec(x, y, z) {
|
|
110
|
+
this.v3.x = x;
|
|
111
|
+
this.v3.y = y;
|
|
112
|
+
this.v3.z = z;
|
|
113
|
+
return this.v3;
|
|
114
|
+
}
|
|
115
|
+
/** A rotation for Rapier from a quaternion at offset o (in 2D, its angle around Z). */
|
|
116
|
+
rotation(q, o = 0) {
|
|
117
|
+
if (this.dim === 2) return quatToAngle(q, o);
|
|
118
|
+
this.q4.x = q[o];
|
|
119
|
+
this.q4.y = q[o + 1];
|
|
120
|
+
this.q4.z = q[o + 2];
|
|
121
|
+
this.q4.w = q[o + 3];
|
|
122
|
+
return this.q4;
|
|
123
|
+
}
|
|
124
|
+
/** Logs a problem once per entity and code, so a bad collider doesn't flood the log. */
|
|
125
|
+
report(world, entity, err) {
|
|
126
|
+
const key = `${entity}|${err.code}`;
|
|
127
|
+
if (this.logged.has(key)) return;
|
|
128
|
+
this.logged.add(key);
|
|
129
|
+
world.tryResource(LogResource)?.error(err);
|
|
130
|
+
}
|
|
131
|
+
/** Slots the interpolation buffers hold. */
|
|
132
|
+
get slotCapacity() {
|
|
133
|
+
return this.curr.length / 7;
|
|
134
|
+
}
|
|
135
|
+
allocSlot() {
|
|
136
|
+
const slot = this.freeSlots.pop() ?? this.nextSlot++;
|
|
137
|
+
if ((slot + 1) * 7 > this.curr.length) {
|
|
138
|
+
const grow = (a) => {
|
|
139
|
+
const b = new Float64Array(a.length * 2);
|
|
140
|
+
b.set(a);
|
|
141
|
+
return b;
|
|
142
|
+
};
|
|
143
|
+
this.prev = grow(this.prev);
|
|
144
|
+
this.curr = grow(this.curr);
|
|
145
|
+
}
|
|
146
|
+
return slot;
|
|
147
|
+
}
|
|
148
|
+
// --- bodies ----------------------------------------------------------------
|
|
149
|
+
createBody(world, entity) {
|
|
150
|
+
this.removeBody(world, entity);
|
|
151
|
+
const rb = world.get(entity, RigidBody);
|
|
152
|
+
const R = this.R;
|
|
153
|
+
const kind = BODY_KINDS.indexOf(rb.kind);
|
|
154
|
+
const desc = kind === KIND_DYNAMIC ? R.RigidBodyDesc.dynamic() : kind === KIND_FIXED ? R.RigidBodyDesc.fixed() : kind === KIND_KINEMATIC_POSITION ? R.RigidBodyDesc.kinematicPositionBased() : R.RigidBodyDesc.kinematicVelocityBased();
|
|
155
|
+
const pose = poseOf(world, entity, this.scratchPose);
|
|
156
|
+
desc.setTranslation(pose[0], pose[1], pose[2]);
|
|
157
|
+
desc.setRotation(this.rotation(pose, 3));
|
|
158
|
+
desc.setGravityScale(rb.gravityScale);
|
|
159
|
+
desc.setLinearDamping(rb.linearDamping);
|
|
160
|
+
desc.setAngularDamping(rb.angularDamping);
|
|
161
|
+
desc.setCcdEnabled(rb.ccd);
|
|
162
|
+
desc.setCanSleep(rb.canSleep);
|
|
163
|
+
desc.setDominanceGroup(rb.dominance);
|
|
164
|
+
const [lx, ly, lz] = rb.lockTranslation;
|
|
165
|
+
const [rx, ry, rz] = rb.lockRotation;
|
|
166
|
+
if (this.dim === 3) {
|
|
167
|
+
if (lx || ly || lz) desc.enabledTranslations(!lx, !ly, !lz);
|
|
168
|
+
if (rx || ry || rz) desc.enabledRotations(!rx, !ry, !rz);
|
|
169
|
+
} else {
|
|
170
|
+
const d2 = desc;
|
|
171
|
+
if (lx || ly) d2.enabledTranslations(!lx, !ly);
|
|
172
|
+
if (rz) d2.lockRotations();
|
|
173
|
+
}
|
|
174
|
+
const velocity = world.tryGet(entity, Velocity);
|
|
175
|
+
if (velocity) {
|
|
176
|
+
const [vx, vy, vz] = velocity.linear;
|
|
177
|
+
const [ax, ay, az] = velocity.angular;
|
|
178
|
+
desc.setLinvel(vx, vy, vz);
|
|
179
|
+
if (this.dim === 3) desc.setAngvel({ x: ax, y: ay, z: az });
|
|
180
|
+
else desc.setAngvel(az);
|
|
181
|
+
}
|
|
182
|
+
const record = this.addBody(entity, this.raw.createRigidBody(desc), kind);
|
|
183
|
+
if (world.has(entity, PhysicsParked)) {
|
|
184
|
+
record.body.setEnabled(false);
|
|
185
|
+
record.parked = true;
|
|
186
|
+
this.parkedCount++;
|
|
187
|
+
}
|
|
188
|
+
this.markSubtreeColliders(world, entity);
|
|
189
|
+
for (const [jointEntity, j] of this.joints) {
|
|
190
|
+
if (j.a === entity || j.b === entity) this.dirtyJoints.add(jointEntity);
|
|
191
|
+
}
|
|
192
|
+
if (world.has(entity, Joint)) this.dirtyJoints.add(entity);
|
|
193
|
+
for (const e of world.query({ with: [Joint] }).tables) {
|
|
194
|
+
const others = e.column(Joint, "other");
|
|
195
|
+
for (let i = 0; i < e.count; i++) {
|
|
196
|
+
if (others[i] === entity) this.dirtyJoints.add(e.entities[i]);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
addBody(entity, body, kind) {
|
|
201
|
+
const record = {
|
|
202
|
+
entity,
|
|
203
|
+
handle: body.handle,
|
|
204
|
+
body,
|
|
205
|
+
kind,
|
|
206
|
+
slot: this.allocSlot(),
|
|
207
|
+
index: this.list.length,
|
|
208
|
+
parked: false
|
|
209
|
+
};
|
|
210
|
+
this.list.push(record);
|
|
211
|
+
this.bodies.set(entity, record);
|
|
212
|
+
this.bodyByHandle.set(body.handle, record);
|
|
213
|
+
this.storePose(record);
|
|
214
|
+
this.prev.set(this.curr.subarray(record.slot * 7, record.slot * 7 + 7), record.slot * 7);
|
|
215
|
+
return record;
|
|
216
|
+
}
|
|
217
|
+
removeBody(world, entity) {
|
|
218
|
+
const record = this.bodies.get(entity);
|
|
219
|
+
if (!record) return;
|
|
220
|
+
const character = this.characters.get(entity);
|
|
221
|
+
if (character) {
|
|
222
|
+
this.raw.removeCharacterController(character.kcc);
|
|
223
|
+
this.characters.delete(entity);
|
|
224
|
+
}
|
|
225
|
+
for (const [collider, owner] of this.colliderOwner) {
|
|
226
|
+
if (owner !== entity) continue;
|
|
227
|
+
this.forgetCollider(collider);
|
|
228
|
+
if (world.isAlive(collider) && world.has(collider, Collider)) {
|
|
229
|
+
this.dirtyColliders.add(collider);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const [jointEntity, j] of this.joints) {
|
|
233
|
+
if (j.a !== entity && j.b !== entity) continue;
|
|
234
|
+
this.joints.delete(jointEntity);
|
|
235
|
+
if (world.isAlive(jointEntity) && world.has(jointEntity, Joint)) {
|
|
236
|
+
this.dirtyJoints.add(jointEntity);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
this.raw.removeRigidBody(record.body);
|
|
240
|
+
if (record.parked) this.parkedCount--;
|
|
241
|
+
const last = this.list.pop();
|
|
242
|
+
if (last !== record) {
|
|
243
|
+
this.list[record.index] = last;
|
|
244
|
+
last.index = record.index;
|
|
245
|
+
}
|
|
246
|
+
this.bodies.delete(entity);
|
|
247
|
+
this.bodyByHandle.delete(record.handle);
|
|
248
|
+
this.freeSlots.push(record.slot);
|
|
249
|
+
}
|
|
250
|
+
/** Copies a body's current pose into `curr` at its slot. */
|
|
251
|
+
storePose(record) {
|
|
252
|
+
const o = record.slot * 7;
|
|
253
|
+
const c = this.curr;
|
|
254
|
+
const body = record.body;
|
|
255
|
+
const t = body.translation(this.v3);
|
|
256
|
+
c[o] = t.x;
|
|
257
|
+
c[o + 1] = t.y;
|
|
258
|
+
c[o + 2] = this.dim === 3 ? t.z : 0;
|
|
259
|
+
if (this.dim === 3) {
|
|
260
|
+
const r = body.rotation(this.q4);
|
|
261
|
+
c[o + 3] = r.x;
|
|
262
|
+
c[o + 4] = r.y;
|
|
263
|
+
c[o + 5] = r.z;
|
|
264
|
+
c[o + 6] = r.w;
|
|
265
|
+
} else {
|
|
266
|
+
const angle = body.rotation();
|
|
267
|
+
c[o + 3] = 0;
|
|
268
|
+
c[o + 4] = 0;
|
|
269
|
+
c[o + 5] = Math.sin(angle / 2);
|
|
270
|
+
c[o + 6] = Math.cos(angle / 2);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/** Teleports a body (or sets a kinematic target) to the entity's current world pose. */
|
|
274
|
+
moveBody(world, record, wake = true) {
|
|
275
|
+
const pose = poseOf(world, record.entity, this.scratchPose);
|
|
276
|
+
const body = record.body;
|
|
277
|
+
const t = this.vec(pose[0], pose[1], pose[2]);
|
|
278
|
+
if (record.kind === KIND_KINEMATIC_POSITION && wake) {
|
|
279
|
+
body.setNextKinematicTranslation(t);
|
|
280
|
+
body.setNextKinematicRotation(this.rotation(pose, 3));
|
|
281
|
+
} else {
|
|
282
|
+
body.setTranslation(t, wake);
|
|
283
|
+
body.setRotation(this.rotation(pose, 3), wake);
|
|
284
|
+
}
|
|
285
|
+
this.storePose(record);
|
|
286
|
+
this.prev.set(this.curr.subarray(record.slot * 7, record.slot * 7 + 7), record.slot * 7);
|
|
287
|
+
}
|
|
288
|
+
setVelocity(world, record) {
|
|
289
|
+
const table = world.entityTableUnchecked(record.entity);
|
|
290
|
+
const row = world.entityRowUnchecked(record.entity);
|
|
291
|
+
const lin = table.column(Velocity, "linear");
|
|
292
|
+
const ang = table.column(Velocity, "angular");
|
|
293
|
+
const o = row * 3;
|
|
294
|
+
record.body.setLinvel(this.vec(lin[o], lin[o + 1], lin[o + 2]), true);
|
|
295
|
+
if (this.dim === 3) record.body.setAngvel(this.vec(ang[o], ang[o + 1], ang[o + 2]), true);
|
|
296
|
+
else
|
|
297
|
+
record.body.setAngvel(
|
|
298
|
+
ang[o + 2],
|
|
299
|
+
true
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
// --- large worlds (spec 0040) --------------------------------------------------
|
|
303
|
+
/**
|
|
304
|
+
* The floating origin moved: `offset` (m) takes a position in the old origin frame to the new
|
|
305
|
+
* one. Moves every body and free collider by it without waking them (velocities and contacts
|
|
306
|
+
* are relative, so they don't change), shifts the interpolation buffers, and refreshes the
|
|
307
|
+
* broad phase with a zero-length step so queries see the new places before the next step.
|
|
308
|
+
* O(bodies), once per cell crossing.
|
|
309
|
+
*/
|
|
310
|
+
shiftOrigin(ox, oy, oz, onCollision) {
|
|
311
|
+
if (ox === 0 && oy === 0 && oz === 0) return;
|
|
312
|
+
if (this.dim === 2) oz = 0;
|
|
313
|
+
const curr = this.curr;
|
|
314
|
+
const prev = this.prev;
|
|
315
|
+
for (let i = 0; i < this.list.length; i++) {
|
|
316
|
+
const r = this.list[i];
|
|
317
|
+
const t = r.body.translation(this.v3);
|
|
318
|
+
r.body.setTranslation(this.vec(t.x + ox, t.y + oy, this.dim === 3 ? t.z + oz : 0), false);
|
|
319
|
+
const o = r.slot * 7;
|
|
320
|
+
curr[o] = curr[o] + ox;
|
|
321
|
+
curr[o + 1] = curr[o + 1] + oy;
|
|
322
|
+
curr[o + 2] = curr[o + 2] + oz;
|
|
323
|
+
prev[o] = prev[o] + ox;
|
|
324
|
+
prev[o + 1] = prev[o + 1] + oy;
|
|
325
|
+
prev[o + 2] = prev[o + 2] + oz;
|
|
326
|
+
}
|
|
327
|
+
for (const [entity, collider] of this.colliders) {
|
|
328
|
+
if (this.colliderOwner.has(entity)) continue;
|
|
329
|
+
const t = collider.translation();
|
|
330
|
+
collider.setTranslation(this.vec(t.x + ox, t.y + oy, this.dim === 3 ? t.z + oz : 0));
|
|
331
|
+
}
|
|
332
|
+
this.raw.propagateModifiedBodyPositionsToColliders();
|
|
333
|
+
this.raw.timestep = 0;
|
|
334
|
+
this.raw.step(this.events);
|
|
335
|
+
this.events.drainCollisionEvents((h1, h2, started) => {
|
|
336
|
+
const a = this.colliderEntity.get(h1);
|
|
337
|
+
const b = this.colliderEntity.get(h2);
|
|
338
|
+
if (a === void 0 || b === void 0) return;
|
|
339
|
+
onCollision({
|
|
340
|
+
kind: started ? "started" : "stopped",
|
|
341
|
+
a,
|
|
342
|
+
b,
|
|
343
|
+
bodyA: this.colliderOwner.get(a) ?? -1,
|
|
344
|
+
bodyB: this.colliderOwner.get(b) ?? -1,
|
|
345
|
+
sensor: this.colliders.get(a).isSensor() || this.colliders.get(b).isSensor()
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
this.events.drainContactForceEvents(() => {
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* The frame Rapier simulates in changed without an OriginShift (grids appeared or went away):
|
|
353
|
+
* puts every body and free collider back where its Transform says.
|
|
354
|
+
*/
|
|
355
|
+
resync(world) {
|
|
356
|
+
for (let i = 0; i < this.list.length; i++) {
|
|
357
|
+
const r = this.list[i];
|
|
358
|
+
if (world.isAlive(r.entity)) this.moveBody(world, r, false);
|
|
359
|
+
}
|
|
360
|
+
for (const entity of this.colliders.keys()) {
|
|
361
|
+
if (!this.colliderOwner.has(entity)) this.dirtyColliders.add(entity);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Parks bodies farther than `radius` from the origin and brings parked ones back within
|
|
366
|
+
* 0.95 × radius. Active bodies are measured by their last simulated pose, parked ones by their
|
|
367
|
+
* GlobalTransform. Allocation-free unless a body changes state.
|
|
368
|
+
*/
|
|
369
|
+
updateParking(world, radius) {
|
|
370
|
+
const far = radius * radius;
|
|
371
|
+
const near = far * 0.95 * 0.95;
|
|
372
|
+
const curr = this.curr;
|
|
373
|
+
let cx = 0;
|
|
374
|
+
let cy = 0;
|
|
375
|
+
let cz = 0;
|
|
376
|
+
const origin = world.tryResource(GridFramesResource)?.originEntity;
|
|
377
|
+
if (origin !== void 0 && origin >= 0 && world.isAlive(origin)) {
|
|
378
|
+
const table = world.entityTableUnchecked(origin);
|
|
379
|
+
if (table.has(GlobalTransform)) {
|
|
380
|
+
const m = table.column(GlobalTransform, "matrix");
|
|
381
|
+
const o = world.entityRowUnchecked(origin) * 12;
|
|
382
|
+
cx = m[o + 3];
|
|
383
|
+
cy = m[o + 7];
|
|
384
|
+
cz = m[o + 11];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (let i = 0; i < this.list.length; i++) {
|
|
388
|
+
const r = this.list[i];
|
|
389
|
+
if (!r.parked) {
|
|
390
|
+
const o2 = r.slot * 7;
|
|
391
|
+
const x2 = curr[o2] - cx;
|
|
392
|
+
const y2 = curr[o2 + 1] - cy;
|
|
393
|
+
const z2 = this.dim === 3 ? curr[o2 + 2] - cz : 0;
|
|
394
|
+
if (x2 * x2 + y2 * y2 + z2 * z2 > far) this.park(world, r);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const table = world.entityTableUnchecked(r.entity);
|
|
398
|
+
if (!table.has(GlobalTransform)) continue;
|
|
399
|
+
const m = table.column(GlobalTransform, "matrix");
|
|
400
|
+
const o = world.entityRowUnchecked(r.entity) * 12;
|
|
401
|
+
const x = m[o + 3] - cx;
|
|
402
|
+
const y = m[o + 7] - cy;
|
|
403
|
+
const z = this.dim === 3 ? m[o + 11] - cz : 0;
|
|
404
|
+
if (x * x + y * y + z * z < near) this.unpark(world, r);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/** Takes a body out of the simulation, storing its velocity on PhysicsParked. */
|
|
408
|
+
park(world, record) {
|
|
409
|
+
const body = record.body;
|
|
410
|
+
const v = body.linvel();
|
|
411
|
+
const linear = [v.x, v.y, this.dim === 3 ? v.z : 0];
|
|
412
|
+
const angular = [0, 0, 0];
|
|
413
|
+
if (this.dim === 3) {
|
|
414
|
+
const a = body.angvel();
|
|
415
|
+
angular[0] = a.x;
|
|
416
|
+
angular[1] = a.y;
|
|
417
|
+
angular[2] = a.z;
|
|
418
|
+
} else {
|
|
419
|
+
angular[2] = body.angvel();
|
|
420
|
+
}
|
|
421
|
+
body.setEnabled(false);
|
|
422
|
+
record.parked = true;
|
|
423
|
+
this.parkedCount++;
|
|
424
|
+
world.add(record.entity, PhysicsParked, { linear, angular });
|
|
425
|
+
}
|
|
426
|
+
/** Puts a parked body back where its Transform says, moving with its stored velocity. */
|
|
427
|
+
unpark(world, record) {
|
|
428
|
+
const body = record.body;
|
|
429
|
+
body.setEnabled(true);
|
|
430
|
+
record.parked = false;
|
|
431
|
+
this.parkedCount--;
|
|
432
|
+
this.moveBody(world, record, false);
|
|
433
|
+
body.wakeUp();
|
|
434
|
+
const stored = world.tryGet(record.entity, PhysicsParked);
|
|
435
|
+
if (stored && record.kind !== KIND_FIXED && record.kind !== KIND_KINEMATIC_POSITION) {
|
|
436
|
+
const [lx, ly, lz] = stored.linear;
|
|
437
|
+
const [ax, ay, az] = stored.angular;
|
|
438
|
+
body.setLinvel(this.vec(lx, ly, lz), true);
|
|
439
|
+
if (this.dim === 3) body.setAngvel(this.vec(ax, ay, az), true);
|
|
440
|
+
else body.setAngvel(az, true);
|
|
441
|
+
}
|
|
442
|
+
if (stored) world.remove(record.entity, PhysicsParked);
|
|
443
|
+
}
|
|
444
|
+
/** Grids went away: nothing is out of range any more. */
|
|
445
|
+
unparkAll(world) {
|
|
446
|
+
for (let i = 0; i < this.list.length; i++) {
|
|
447
|
+
const r = this.list[i];
|
|
448
|
+
if (r.parked && world.isAlive(r.entity)) this.unpark(world, r);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// --- characters ------------------------------------------------------------
|
|
452
|
+
/**
|
|
453
|
+
* (Re)builds a character's kinematic body, capsule collider, and Rapier controller from its
|
|
454
|
+
* CharacterController. The capsule is centered on the entity, its axis along the entity's +Y.
|
|
455
|
+
*/
|
|
456
|
+
createCharacter(world, entity) {
|
|
457
|
+
if (world.has(entity, RigidBody) || world.has(entity, Collider)) {
|
|
458
|
+
this.report(
|
|
459
|
+
world,
|
|
460
|
+
entity,
|
|
461
|
+
new ShardError(
|
|
462
|
+
"physics/character-has-body",
|
|
463
|
+
"A CharacterController entity also has a RigidBody or Collider",
|
|
464
|
+
{
|
|
465
|
+
hint: "The controller makes its own kinematic body and capsule. Remove RigidBody and Collider, or put extra colliders on a child."
|
|
466
|
+
}
|
|
467
|
+
)
|
|
468
|
+
);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
this.removeBody(world, entity);
|
|
472
|
+
const c = world.get(entity, CharacterController);
|
|
473
|
+
const R = this.R;
|
|
474
|
+
const pose = poseOf(world, entity, this.scratchPose);
|
|
475
|
+
const desc = R.RigidBodyDesc.kinematicPositionBased();
|
|
476
|
+
desc.setTranslation(pose[0], pose[1], pose[2]);
|
|
477
|
+
desc.setRotation(this.rotation(pose, 3));
|
|
478
|
+
const record = this.addBody(entity, this.raw.createRigidBody(desc), KIND_KINEMATIC_POSITION);
|
|
479
|
+
const radius = Math.max(c.radius, 1e-3);
|
|
480
|
+
const halfHeight = Math.max(0, c.height / 2 - radius);
|
|
481
|
+
const shape = halfHeight > 0 ? "capsule" : "ball";
|
|
482
|
+
const cdesc = halfHeight > 0 ? R.ColliderDesc.capsule(halfHeight, radius) : R.ColliderDesc.ball(radius);
|
|
483
|
+
cdesc.setFriction(0);
|
|
484
|
+
cdesc.setCollisionGroups((c.layers & 65535) << 16 | c.mask & 65535);
|
|
485
|
+
const collider = this.raw.createCollider(cdesc, record.body);
|
|
486
|
+
this.colliders.set(entity, collider);
|
|
487
|
+
this.colliderEntity.set(collider.handle, entity);
|
|
488
|
+
this.colliderShape.set(entity, shape);
|
|
489
|
+
this.colliderOwner.set(entity, entity);
|
|
490
|
+
const kcc = this.raw.createCharacterController(CHARACTER_OFFSET);
|
|
491
|
+
const slope = Math.min(Math.max(c.maxSlope, 0), 90) * Math.PI / 180;
|
|
492
|
+
kcc.setMaxSlopeClimbAngle(slope);
|
|
493
|
+
kcc.setMinSlopeSlideAngle(slope);
|
|
494
|
+
kcc.setSlideEnabled(true);
|
|
495
|
+
if (c.stepHeight > 0) kcc.enableAutostep(c.stepHeight, radius * 0.5, false);
|
|
496
|
+
if (c.snapDistance > 0) kcc.enableSnapToGround(c.snapDistance);
|
|
497
|
+
this.characters.set(entity, {
|
|
498
|
+
entity,
|
|
499
|
+
body: record,
|
|
500
|
+
collider,
|
|
501
|
+
kcc,
|
|
502
|
+
snapping: c.snapDistance > 0,
|
|
503
|
+
snapDistance: c.snapDistance,
|
|
504
|
+
cosMaxSlope: Math.cos(slope),
|
|
505
|
+
bottom: radius + halfHeight
|
|
506
|
+
});
|
|
507
|
+
this.markSubtreeColliders(world, entity);
|
|
508
|
+
}
|
|
509
|
+
// --- colliders -------------------------------------------------------------
|
|
510
|
+
markSubtreeColliders(world, entity) {
|
|
511
|
+
if (world.has(entity, Collider)) this.dirtyColliders.add(entity);
|
|
512
|
+
const children = world.tryGet(entity, Children)?.entities;
|
|
513
|
+
if (!children) return;
|
|
514
|
+
for (const child of children) {
|
|
515
|
+
if (child === null || !world.isAlive(child)) continue;
|
|
516
|
+
if (child !== entity && world.has(child, RigidBody)) continue;
|
|
517
|
+
this.markSubtreeColliders(world, child);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/** Marks colliders and bodies under an entity whose place in the hierarchy changed. */
|
|
521
|
+
reparented(world, entity) {
|
|
522
|
+
if (!world.isAlive(entity)) return;
|
|
523
|
+
const walk = (e) => {
|
|
524
|
+
if (world.has(e, Collider)) this.dirtyColliders.add(e);
|
|
525
|
+
if (this.bodies.has(e)) this.dirtyBodies.add(e);
|
|
526
|
+
const children = world.tryGet(e, Children)?.entities;
|
|
527
|
+
if (children) {
|
|
528
|
+
for (const c of children) if (c !== null && world.isAlive(c)) walk(c);
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
walk(entity);
|
|
532
|
+
}
|
|
533
|
+
forgetCollider(entity) {
|
|
534
|
+
const collider = this.colliders.get(entity);
|
|
535
|
+
if (!collider) return;
|
|
536
|
+
this.colliderEntity.delete(collider.handle);
|
|
537
|
+
this.colliders.delete(entity);
|
|
538
|
+
this.colliderOwner.delete(entity);
|
|
539
|
+
this.colliderShape.delete(entity);
|
|
540
|
+
this.meshColliders.delete(entity);
|
|
541
|
+
}
|
|
542
|
+
removeCollider(entity) {
|
|
543
|
+
const collider = this.colliders.get(entity);
|
|
544
|
+
if (!collider) return;
|
|
545
|
+
this.forgetCollider(entity);
|
|
546
|
+
this.raw.removeCollider(collider, true);
|
|
547
|
+
}
|
|
548
|
+
/** The nearest ancestor-or-self with a body. */
|
|
549
|
+
ownerOf(world, entity) {
|
|
550
|
+
let e = entity;
|
|
551
|
+
while (e !== void 0) {
|
|
552
|
+
if (this.bodies.has(e)) return e;
|
|
553
|
+
e = parentOf(world, e);
|
|
554
|
+
}
|
|
555
|
+
return void 0;
|
|
556
|
+
}
|
|
557
|
+
createCollider(world, entity) {
|
|
558
|
+
this.removeCollider(entity);
|
|
559
|
+
this.pendingMesh.delete(entity);
|
|
560
|
+
const c = world.get(entity, Collider);
|
|
561
|
+
const colPose = poseOf(world, entity, this.scratchPose);
|
|
562
|
+
const owner = this.ownerOf(world, entity);
|
|
563
|
+
const sx = Math.abs(colPose[7]);
|
|
564
|
+
const sy = Math.abs(colPose[8]);
|
|
565
|
+
const sz = Math.abs(colPose[9]);
|
|
566
|
+
let mesh;
|
|
567
|
+
if ((c.shape === "convex" || c.shape === "trimesh" || c.shape === "polyline" || c.shape === "segment") && c.mesh) {
|
|
568
|
+
mesh = this.meshFor(world, entity, c.mesh);
|
|
569
|
+
if (!mesh) return;
|
|
570
|
+
}
|
|
571
|
+
let desc;
|
|
572
|
+
try {
|
|
573
|
+
desc = this.shapeDesc(c, mesh, sx, sy, sz);
|
|
574
|
+
} catch (err) {
|
|
575
|
+
if (err instanceof ShardError) {
|
|
576
|
+
this.report(world, entity, err);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
throw err;
|
|
580
|
+
}
|
|
581
|
+
if (!desc) {
|
|
582
|
+
this.report(
|
|
583
|
+
world,
|
|
584
|
+
entity,
|
|
585
|
+
new ShardError("physics/invalid-shape", `Collider "${c.shape}" has no volume or points`, {
|
|
586
|
+
hint: "Check radius, halfExtents, halfHeight, points, or the mesh."
|
|
587
|
+
})
|
|
588
|
+
);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (owner !== void 0) {
|
|
592
|
+
const bodyPose = poseOf(world, owner, this.scratchPose2);
|
|
593
|
+
const dx = colPose[0] - bodyPose[0];
|
|
594
|
+
const dy = colPose[1] - bodyPose[1];
|
|
595
|
+
const dz = colPose[2] - bodyPose[2];
|
|
596
|
+
bodyPose[3] = -bodyPose[3];
|
|
597
|
+
bodyPose[4] = -bodyPose[4];
|
|
598
|
+
bodyPose[5] = -bodyPose[5];
|
|
599
|
+
rotate(colPose, 0, bodyPose, 3, dx, dy, dz);
|
|
600
|
+
mulQuat(colPose, 3, bodyPose, 3, colPose, 3);
|
|
601
|
+
}
|
|
602
|
+
desc.setTranslation(colPose[0], colPose[1], colPose[2]);
|
|
603
|
+
desc.setRotation(this.rotation(colPose, 3));
|
|
604
|
+
desc.setFriction(c.friction);
|
|
605
|
+
desc.setRestitution(c.restitution);
|
|
606
|
+
desc.setDensity(c.density);
|
|
607
|
+
desc.setSensor(c.sensor);
|
|
608
|
+
desc.setCollisionGroups((c.layers & 65535) << 16 | c.mask & 65535);
|
|
609
|
+
const R = this.R;
|
|
610
|
+
let events = 0;
|
|
611
|
+
if (c.events) events |= R.ActiveEvents.COLLISION_EVENTS;
|
|
612
|
+
if (c.forceThreshold > 0) {
|
|
613
|
+
events |= R.ActiveEvents.CONTACT_FORCE_EVENTS;
|
|
614
|
+
desc.setContactForceEventThreshold(c.forceThreshold);
|
|
615
|
+
}
|
|
616
|
+
if (events) desc.setActiveEvents(events);
|
|
617
|
+
if (c.sensor) {
|
|
618
|
+
desc.setActiveCollisionTypes(R.ActiveCollisionTypes.ALL);
|
|
619
|
+
}
|
|
620
|
+
const body = owner !== void 0 ? this.bodies.get(owner).body : void 0;
|
|
621
|
+
const collider = this.raw.createCollider(desc, body);
|
|
622
|
+
this.colliders.set(entity, collider);
|
|
623
|
+
this.colliderEntity.set(collider.handle, entity);
|
|
624
|
+
this.colliderShape.set(entity, c.shape);
|
|
625
|
+
if (owner !== void 0) this.colliderOwner.set(entity, owner);
|
|
626
|
+
if (mesh) this.meshColliders.set(entity, { mesh, version: mesh.version });
|
|
627
|
+
if (owner !== void 0 && world.has(owner, Mass)) this.fixMass(world, owner);
|
|
628
|
+
}
|
|
629
|
+
meshFor(world, entity, ref) {
|
|
630
|
+
const mesh = world.tryResource(Meshes)?.get(ref);
|
|
631
|
+
if (mesh) return mesh;
|
|
632
|
+
const server = assetServer(world);
|
|
633
|
+
const entry = server.entry(ref);
|
|
634
|
+
if (entry?.state === "failed") {
|
|
635
|
+
this.report(
|
|
636
|
+
world,
|
|
637
|
+
entity,
|
|
638
|
+
new ShardError("physics/invalid-shape", `The collider's mesh ${ref.path} failed to load`)
|
|
639
|
+
);
|
|
640
|
+
return void 0;
|
|
641
|
+
}
|
|
642
|
+
if (entry && entry.state === "unloaded") void server.request(entry.guid).catch(() => {
|
|
643
|
+
});
|
|
644
|
+
this.pendingMesh.add(entity);
|
|
645
|
+
return void 0;
|
|
646
|
+
}
|
|
647
|
+
/** Scales every collider's density so the body weighs exactly its Mass. */
|
|
648
|
+
fixMass(world, owner) {
|
|
649
|
+
const record = this.bodies.get(owner);
|
|
650
|
+
if (!record) return;
|
|
651
|
+
const target = world.get(owner, Mass).mass;
|
|
652
|
+
record.body.setAdditionalMass(0, false);
|
|
653
|
+
record.body.recomputeMassPropertiesFromColliders();
|
|
654
|
+
const current = record.body.mass();
|
|
655
|
+
if (current > 1e-9) {
|
|
656
|
+
const k = target / current;
|
|
657
|
+
for (const [entity, o] of this.colliderOwner) {
|
|
658
|
+
if (o !== owner) continue;
|
|
659
|
+
const collider = this.colliders.get(entity);
|
|
660
|
+
collider.setDensity(collider.density() * k);
|
|
661
|
+
}
|
|
662
|
+
record.body.recomputeMassPropertiesFromColliders();
|
|
663
|
+
} else {
|
|
664
|
+
record.body.setAdditionalMass(target, true);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
shapeDesc(c, mesh, sx, sy, sz) {
|
|
668
|
+
const D = this.R.ColliderDesc;
|
|
669
|
+
const is2d = this.dim === 2;
|
|
670
|
+
const [hx, hy, hz] = c.halfExtents;
|
|
671
|
+
switch (c.shape) {
|
|
672
|
+
case "ball":
|
|
673
|
+
return D.ball(c.radius * Math.max(sx, sy, is2d ? 0 : sz));
|
|
674
|
+
case "cuboid":
|
|
675
|
+
return is2d ? D.cuboid(
|
|
676
|
+
hx * sx,
|
|
677
|
+
hy * sy
|
|
678
|
+
) : D.cuboid(hx * sx, hy * sy, hz * sz);
|
|
679
|
+
case "capsule":
|
|
680
|
+
return D.capsule(c.halfHeight * sy, c.radius * (is2d ? sx : Math.max(sx, sz)));
|
|
681
|
+
case "cylinder":
|
|
682
|
+
case "cone":
|
|
683
|
+
if (is2d) throw unsupported(c.shape);
|
|
684
|
+
return c.shape === "cylinder" ? D.cylinder(c.halfHeight * sy, c.radius * Math.max(sx, sz)) : D.cone(c.halfHeight * sy, c.radius * Math.max(sx, sz));
|
|
685
|
+
case "convex": {
|
|
686
|
+
const points = this.points(c, mesh, sx, sy, sz);
|
|
687
|
+
return points.length < (is2d ? 6 : 12) ? null : D.convexHull(points);
|
|
688
|
+
}
|
|
689
|
+
case "trimesh": {
|
|
690
|
+
const points = this.points(c, mesh, sx, sy, sz);
|
|
691
|
+
const n = points.length / this.dim;
|
|
692
|
+
if (n < 3) return null;
|
|
693
|
+
const indices = mesh?.indices ? mesh.indices instanceof Uint32Array ? mesh.indices : Uint32Array.from(mesh.indices) : Uint32Array.from({ length: n - n % 3 }, (_, i) => i);
|
|
694
|
+
return D.trimesh(points, indices);
|
|
695
|
+
}
|
|
696
|
+
case "polyline": {
|
|
697
|
+
const points = this.points(c, mesh, sx, sy, sz);
|
|
698
|
+
if (points.length < this.dim * 2) return null;
|
|
699
|
+
return D.polyline(points, null);
|
|
700
|
+
}
|
|
701
|
+
case "segment": {
|
|
702
|
+
const p = this.points(c, mesh, sx, sy, sz);
|
|
703
|
+
if (p.length < this.dim * 2) return null;
|
|
704
|
+
return is2d ? D.segment(
|
|
705
|
+
{ x: p[0], y: p[1] },
|
|
706
|
+
{ x: p[2], y: p[3] }
|
|
707
|
+
) : D.segment({ x: p[0], y: p[1], z: p[2] }, { x: p[3], y: p[4], z: p[5] });
|
|
708
|
+
}
|
|
709
|
+
case "heightfield": {
|
|
710
|
+
const hf = c.heightfield;
|
|
711
|
+
const rows = hf.rows;
|
|
712
|
+
const cols = hf.cols;
|
|
713
|
+
if (is2d) {
|
|
714
|
+
const n = cols || hf.heights.length;
|
|
715
|
+
if (n < 2 || hf.heights.length < n) return null;
|
|
716
|
+
return D.heightfield(Float32Array.from(hf.heights.slice(0, n)), {
|
|
717
|
+
x: 2 * hx * sx,
|
|
718
|
+
y: hy * sy
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
if (rows < 2 || cols < 2 || hf.heights.length < rows * cols) return null;
|
|
722
|
+
const heights = new Float32Array(rows * cols);
|
|
723
|
+
for (let r = 0; r < rows; r++) {
|
|
724
|
+
for (let col = 0; col < cols; col++) heights[col * rows + r] = hf.heights[r * cols + col];
|
|
725
|
+
}
|
|
726
|
+
return D.heightfield(rows - 1, cols - 1, heights, {
|
|
727
|
+
x: 2 * hx * sx,
|
|
728
|
+
y: hy * sy,
|
|
729
|
+
z: 2 * hz * sz
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
/** Shape points as a flat array for the current dimension, scaled. */
|
|
736
|
+
points(c, mesh, sx, sy, sz) {
|
|
737
|
+
const d = this.dim;
|
|
738
|
+
if (mesh) {
|
|
739
|
+
const p = mesh.positions;
|
|
740
|
+
const n = p.length / 3;
|
|
741
|
+
const out2 = new Float32Array(n * d);
|
|
742
|
+
for (let i = 0; i < n; i++) {
|
|
743
|
+
out2[i * d] = p[i * 3] * sx;
|
|
744
|
+
out2[i * d + 1] = p[i * 3 + 1] * sy;
|
|
745
|
+
if (d === 3) out2[i * d + 2] = p[i * 3 + 2] * sz;
|
|
746
|
+
}
|
|
747
|
+
return out2;
|
|
748
|
+
}
|
|
749
|
+
const out = new Float32Array(c.points.length * d);
|
|
750
|
+
c.points.forEach((pt, i) => {
|
|
751
|
+
out[i * d] = pt[0] * sx;
|
|
752
|
+
out[i * d + 1] = pt[1] * sy;
|
|
753
|
+
if (d === 3) out[i * d + 2] = pt[2] * sz;
|
|
754
|
+
});
|
|
755
|
+
return out;
|
|
756
|
+
}
|
|
757
|
+
// --- joints ----------------------------------------------------------------
|
|
758
|
+
removeJoint(entity) {
|
|
759
|
+
const j = this.joints.get(entity);
|
|
760
|
+
if (!j) return;
|
|
761
|
+
this.joints.delete(entity);
|
|
762
|
+
if (this.raw.impulseJoints.contains(j.joint.handle)) this.raw.removeImpulseJoint(j.joint, true);
|
|
763
|
+
}
|
|
764
|
+
createJoint(world, entity) {
|
|
765
|
+
this.removeJoint(entity);
|
|
766
|
+
const j = world.get(entity, Joint);
|
|
767
|
+
const a = this.bodies.get(entity);
|
|
768
|
+
const other = j.other;
|
|
769
|
+
const b = other !== null && other >= 0 ? this.bodies.get(other) : void 0;
|
|
770
|
+
if (!a || !b) return;
|
|
771
|
+
const R = this.R;
|
|
772
|
+
const is2d = this.dim === 2;
|
|
773
|
+
const anchorA = { x: j.anchor[0], y: j.anchor[1], z: j.anchor[2] };
|
|
774
|
+
const anchorB = { x: j.otherAnchor[0], y: j.otherAnchor[1], z: j.otherAnchor[2] };
|
|
775
|
+
const axis = { x: j.axis[0], y: j.axis[1], z: j.axis[2] };
|
|
776
|
+
const J = R.JointData;
|
|
777
|
+
let data;
|
|
778
|
+
switch (j.kind) {
|
|
779
|
+
case "fixed": {
|
|
780
|
+
if (is2d) {
|
|
781
|
+
const ra = a.body.rotation();
|
|
782
|
+
const rb = b.body.rotation();
|
|
783
|
+
data = J.fixed(anchorA, 0, anchorB, ra - rb);
|
|
784
|
+
} else {
|
|
785
|
+
const ra = a.body.rotation();
|
|
786
|
+
const rb = b.body.rotation();
|
|
787
|
+
const q = new Float64Array(4);
|
|
788
|
+
mulQuat(q, 0, [-rb.x, -rb.y, -rb.z, rb.w], 0, [ra.x, ra.y, ra.z, ra.w], 0);
|
|
789
|
+
data = J.fixed(anchorA, { x: 0, y: 0, z: 0, w: 1 }, anchorB, {
|
|
790
|
+
x: q[0],
|
|
791
|
+
y: q[1],
|
|
792
|
+
z: q[2],
|
|
793
|
+
w: q[3]
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
case "revolute":
|
|
799
|
+
data = is2d ? J.revolute(anchorA, anchorB) : J.revolute(anchorA, anchorB, axis);
|
|
800
|
+
break;
|
|
801
|
+
case "prismatic":
|
|
802
|
+
data = J.prismatic(anchorA, anchorB, axis);
|
|
803
|
+
break;
|
|
804
|
+
case "spherical":
|
|
805
|
+
if (is2d) {
|
|
806
|
+
this.report(world, entity, unsupported("spherical joint"));
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
data = J.spherical(anchorA, anchorB);
|
|
810
|
+
break;
|
|
811
|
+
case "rope": {
|
|
812
|
+
let length = j.limits[1];
|
|
813
|
+
if (!(length > 0)) {
|
|
814
|
+
const pa = a.body.translation();
|
|
815
|
+
const pb = b.body.translation();
|
|
816
|
+
length = Math.sqrt(
|
|
817
|
+
(pa.x - pb.x) ** 2 + (pa.y - pb.y) ** 2 + (is2d ? 0 : (pa.z - pb.z) ** 2)
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
data = J.rope(length, anchorA, anchorB);
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
const joint = this.raw.createImpulseJoint(data, a.body, b.body, true);
|
|
825
|
+
const limited = joint;
|
|
826
|
+
if ((j.kind === "revolute" || j.kind === "prismatic") && j.limits[0] < j.limits[1]) {
|
|
827
|
+
limited.setLimits?.(j.limits[0], j.limits[1]);
|
|
828
|
+
}
|
|
829
|
+
if ((j.kind === "revolute" || j.kind === "prismatic") && j.motorVelocity !== 0) {
|
|
830
|
+
limited.configureMotorVelocity?.(j.motorVelocity, j.motorFactor);
|
|
831
|
+
}
|
|
832
|
+
this.joints.set(entity, { joint, a: entity, b: other });
|
|
833
|
+
}
|
|
834
|
+
// --- stepping --------------------------------------------------------------
|
|
835
|
+
step(dt, onCollision, onForce) {
|
|
836
|
+
const start = performance.now();
|
|
837
|
+
const g = this.config.gravity;
|
|
838
|
+
this.raw.gravity = this.dim === 3 ? { x: g[0], y: g[1], z: g[2] } : { x: g[0], y: g[1] };
|
|
839
|
+
this.raw.timestep = dt;
|
|
840
|
+
this.raw.numSolverIterations = this.config.iterations;
|
|
841
|
+
this.raw.step(this.events);
|
|
842
|
+
this.events.drainCollisionEvents((h1, h2, started) => {
|
|
843
|
+
const a = this.colliderEntity.get(h1);
|
|
844
|
+
const b = this.colliderEntity.get(h2);
|
|
845
|
+
if (a === void 0 || b === void 0) return;
|
|
846
|
+
const ca = this.colliders.get(a);
|
|
847
|
+
const cb = this.colliders.get(b);
|
|
848
|
+
onCollision({
|
|
849
|
+
kind: started ? "started" : "stopped",
|
|
850
|
+
a,
|
|
851
|
+
b,
|
|
852
|
+
bodyA: this.colliderOwner.get(a) ?? -1,
|
|
853
|
+
bodyB: this.colliderOwner.get(b) ?? -1,
|
|
854
|
+
sensor: ca.isSensor() || cb.isSensor()
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
this.events.drainContactForceEvents((event) => {
|
|
858
|
+
const a = this.colliderEntity.get(event.collider1());
|
|
859
|
+
const b = this.colliderEntity.get(event.collider2());
|
|
860
|
+
if (a !== void 0 && b !== void 0) onForce(a, b, event.totalForceMagnitude());
|
|
861
|
+
});
|
|
862
|
+
this.stepMs = performance.now() - start;
|
|
863
|
+
this.steps++;
|
|
864
|
+
}
|
|
865
|
+
// --- queries ---------------------------------------------------------------
|
|
866
|
+
filter(options) {
|
|
867
|
+
const R = this.R;
|
|
868
|
+
const flags = options?.sensors ? 0 : R.QueryFilterFlags.EXCLUDE_SENSORS;
|
|
869
|
+
const groups = ((options?.layers ?? 65535) & 65535) << 16 | (options?.mask ?? 65535) & 65535;
|
|
870
|
+
let excludeCollider;
|
|
871
|
+
let excludeBody;
|
|
872
|
+
if (options?.exclude !== void 0) {
|
|
873
|
+
excludeBody = this.bodies.get(options.exclude)?.body;
|
|
874
|
+
if (!excludeBody) excludeCollider = this.colliders.get(options.exclude);
|
|
875
|
+
}
|
|
876
|
+
return { flags, groups, excludeCollider, excludeBody };
|
|
877
|
+
}
|
|
878
|
+
/** The nearest hit along a ray, written into `out`. Direction needn't be normalized. */
|
|
879
|
+
raycast(origin, direction, options, out) {
|
|
880
|
+
const d = this.dim;
|
|
881
|
+
let dx = direction[0];
|
|
882
|
+
let dy = direction[1];
|
|
883
|
+
let dz = d === 3 ? direction[2] ?? 0 : 0;
|
|
884
|
+
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
885
|
+
if (len === 0) return false;
|
|
886
|
+
dx /= len;
|
|
887
|
+
dy /= len;
|
|
888
|
+
dz /= len;
|
|
889
|
+
const ray = this.ray;
|
|
890
|
+
ray.origin.x = origin[0];
|
|
891
|
+
ray.origin.y = origin[1];
|
|
892
|
+
if (d === 3) ray.origin.z = origin[2] ?? 0;
|
|
893
|
+
ray.dir.x = dx;
|
|
894
|
+
ray.dir.y = dy;
|
|
895
|
+
if (d === 3) ray.dir.z = dz;
|
|
896
|
+
const f = this.filter(options);
|
|
897
|
+
const max = options?.maxDistance && options.maxDistance > 0 ? options.maxDistance : 1e9;
|
|
898
|
+
const hit = this.raw.castRayAndGetNormal(
|
|
899
|
+
ray,
|
|
900
|
+
max,
|
|
901
|
+
options?.solid ?? true,
|
|
902
|
+
f.flags,
|
|
903
|
+
f.groups,
|
|
904
|
+
f.excludeCollider,
|
|
905
|
+
f.excludeBody
|
|
906
|
+
);
|
|
907
|
+
if (!hit) return false;
|
|
908
|
+
const entity = this.colliderEntity.get(hit.collider.handle);
|
|
909
|
+
if (entity === void 0) return false;
|
|
910
|
+
out.entity = entity;
|
|
911
|
+
out.body = this.colliderOwner.get(entity) ?? -1;
|
|
912
|
+
out.distance = hit.timeOfImpact;
|
|
913
|
+
out.point[0] = origin[0] + dx * hit.timeOfImpact;
|
|
914
|
+
out.point[1] = origin[1] + dy * hit.timeOfImpact;
|
|
915
|
+
out.point[2] = d === 3 ? (origin[2] ?? 0) + dz * hit.timeOfImpact : 0;
|
|
916
|
+
out.normal[0] = hit.normal.x;
|
|
917
|
+
out.normal[1] = hit.normal.y;
|
|
918
|
+
out.normal[2] = d === 3 ? hit.normal.z : 0;
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
/** Every hit along a ray, nearest first. Cold path. */
|
|
922
|
+
raycastAll(origin, direction, options) {
|
|
923
|
+
const d = this.dim;
|
|
924
|
+
const dirLen = Math.sqrt(
|
|
925
|
+
direction[0] ** 2 + direction[1] ** 2 + (d === 3 ? (direction[2] ?? 0) ** 2 : 0)
|
|
926
|
+
);
|
|
927
|
+
if (dirLen === 0) return [];
|
|
928
|
+
const dir = [
|
|
929
|
+
direction[0] / dirLen,
|
|
930
|
+
direction[1] / dirLen,
|
|
931
|
+
d === 3 ? (direction[2] ?? 0) / dirLen : 0
|
|
932
|
+
];
|
|
933
|
+
const ray = new this.R.Ray(
|
|
934
|
+
{ x: origin[0], y: origin[1], z: origin[2] ?? 0 },
|
|
935
|
+
{ x: dir[0], y: dir[1], z: dir[2] }
|
|
936
|
+
);
|
|
937
|
+
const f = this.filter(options);
|
|
938
|
+
const hits = [];
|
|
939
|
+
const max = options?.maxDistance && options.maxDistance > 0 ? options.maxDistance : 1e9;
|
|
940
|
+
this.raw.intersectionsWithRay(
|
|
941
|
+
ray,
|
|
942
|
+
max,
|
|
943
|
+
options?.solid ?? true,
|
|
944
|
+
(hit) => {
|
|
945
|
+
const entity = this.colliderEntity.get(hit.collider.handle);
|
|
946
|
+
if (entity === void 0) return true;
|
|
947
|
+
const h = createRayHit();
|
|
948
|
+
h.entity = entity;
|
|
949
|
+
h.body = this.colliderOwner.get(entity) ?? -1;
|
|
950
|
+
h.distance = hit.timeOfImpact;
|
|
951
|
+
for (let k = 0; k < 3; k++) h.point[k] = (origin[k] ?? 0) + dir[k] * hit.timeOfImpact;
|
|
952
|
+
h.normal[0] = hit.normal.x;
|
|
953
|
+
h.normal[1] = hit.normal.y;
|
|
954
|
+
h.normal[2] = d === 3 ? hit.normal.z : 0;
|
|
955
|
+
hits.push(h);
|
|
956
|
+
return true;
|
|
957
|
+
},
|
|
958
|
+
f.flags,
|
|
959
|
+
f.groups,
|
|
960
|
+
f.excludeCollider,
|
|
961
|
+
f.excludeBody
|
|
962
|
+
);
|
|
963
|
+
return hits.sort((a, b) => a.distance - b.distance);
|
|
964
|
+
}
|
|
965
|
+
queryShape(q) {
|
|
966
|
+
const R = this.R;
|
|
967
|
+
const he = q.halfExtents ?? [0.5, 0.5, 0.5];
|
|
968
|
+
switch (q.shape) {
|
|
969
|
+
case "ball":
|
|
970
|
+
return new R.Ball(q.radius ?? 0.5);
|
|
971
|
+
case "cuboid":
|
|
972
|
+
return this.dim === 3 ? new R.Cuboid(he[0], he[1], he[2]) : new R.Cuboid(he[0], he[1]);
|
|
973
|
+
case "capsule":
|
|
974
|
+
return new R.Capsule(q.halfHeight ?? 0.5, q.radius ?? 0.5);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/** Sweeps a shape along `velocity` × maxDistance; the first hit's entity and distance. */
|
|
978
|
+
shapeCast(shape, position, rotation, direction, options, out) {
|
|
979
|
+
const f = this.filter(options);
|
|
980
|
+
const d = this.dim;
|
|
981
|
+
const len = Math.sqrt(
|
|
982
|
+
direction[0] ** 2 + direction[1] ** 2 + (d === 3 ? (direction[2] ?? 0) ** 2 : 0)
|
|
983
|
+
);
|
|
984
|
+
if (len === 0) return false;
|
|
985
|
+
const vel = {
|
|
986
|
+
x: direction[0] / len,
|
|
987
|
+
y: direction[1] / len,
|
|
988
|
+
z: d === 3 ? (direction[2] ?? 0) / len : 0
|
|
989
|
+
};
|
|
990
|
+
const max = options?.maxDistance && options.maxDistance > 0 ? options.maxDistance : 1e9;
|
|
991
|
+
const hit = this.raw.castShape(
|
|
992
|
+
{ x: position[0], y: position[1], z: position[2] ?? 0 },
|
|
993
|
+
this.rotation(rotation),
|
|
994
|
+
vel,
|
|
995
|
+
this.queryShape(shape),
|
|
996
|
+
0,
|
|
997
|
+
max,
|
|
998
|
+
true,
|
|
999
|
+
f.flags,
|
|
1000
|
+
f.groups,
|
|
1001
|
+
f.excludeCollider,
|
|
1002
|
+
f.excludeBody
|
|
1003
|
+
);
|
|
1004
|
+
if (!hit) return false;
|
|
1005
|
+
const entity = this.colliderEntity.get(hit.collider.handle);
|
|
1006
|
+
if (entity === void 0) return false;
|
|
1007
|
+
out.entity = entity;
|
|
1008
|
+
out.body = this.colliderOwner.get(entity) ?? -1;
|
|
1009
|
+
out.distance = hit.time_of_impact;
|
|
1010
|
+
const w = hit.witness1;
|
|
1011
|
+
out.point[0] = w.x;
|
|
1012
|
+
out.point[1] = w.y;
|
|
1013
|
+
out.point[2] = d === 3 ? w.z : 0;
|
|
1014
|
+
const n = hit.normal1;
|
|
1015
|
+
out.normal[0] = n.x;
|
|
1016
|
+
out.normal[1] = n.y;
|
|
1017
|
+
out.normal[2] = d === 3 ? n.z : 0;
|
|
1018
|
+
return true;
|
|
1019
|
+
}
|
|
1020
|
+
/** Calls `visit` for each collider entity containing the point; return false to stop. */
|
|
1021
|
+
overlapPoint(point, options, visit) {
|
|
1022
|
+
const f = this.filter(options);
|
|
1023
|
+
this.raw.intersectionsWithPoint(
|
|
1024
|
+
{ x: point[0], y: point[1], z: point[2] ?? 0 },
|
|
1025
|
+
(collider) => {
|
|
1026
|
+
const e = this.colliderEntity.get(collider.handle);
|
|
1027
|
+
return e === void 0 ? true : visit(e) !== false;
|
|
1028
|
+
},
|
|
1029
|
+
f.flags,
|
|
1030
|
+
f.groups,
|
|
1031
|
+
f.excludeCollider,
|
|
1032
|
+
f.excludeBody
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
/** Calls `visit` for each collider entity overlapping the shape; return false to stop. */
|
|
1036
|
+
overlapShape(shape, position, rotation, options, visit) {
|
|
1037
|
+
const f = this.filter(options);
|
|
1038
|
+
this.raw.intersectionsWithShape(
|
|
1039
|
+
{ x: position[0], y: position[1], z: position[2] ?? 0 },
|
|
1040
|
+
this.rotation(rotation),
|
|
1041
|
+
this.queryShape(shape),
|
|
1042
|
+
(collider) => {
|
|
1043
|
+
const e = this.colliderEntity.get(collider.handle);
|
|
1044
|
+
return e === void 0 ? true : visit(e) !== false;
|
|
1045
|
+
},
|
|
1046
|
+
f.flags,
|
|
1047
|
+
f.groups,
|
|
1048
|
+
f.excludeCollider,
|
|
1049
|
+
f.excludeBody
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
describe() {
|
|
1053
|
+
const bodies = {};
|
|
1054
|
+
let sleeping = 0;
|
|
1055
|
+
for (const r of this.bodies.values()) {
|
|
1056
|
+
if (this.characters.has(r.entity)) continue;
|
|
1057
|
+
const kind = BODY_KINDS[r.kind];
|
|
1058
|
+
bodies[kind] = (bodies[kind] ?? 0) + 1;
|
|
1059
|
+
if (r.body.isSleeping()) sleeping++;
|
|
1060
|
+
}
|
|
1061
|
+
const colliders = {};
|
|
1062
|
+
for (const [entity, shape] of this.colliderShape) {
|
|
1063
|
+
if (!this.characters.has(entity)) colliders[shape] = (colliders[shape] ?? 0) + 1;
|
|
1064
|
+
}
|
|
1065
|
+
let contacts = 0;
|
|
1066
|
+
for (const c of this.colliders.values()) {
|
|
1067
|
+
this.raw.contactPairsWith(c, () => {
|
|
1068
|
+
contacts++;
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
return {
|
|
1072
|
+
bodies,
|
|
1073
|
+
sleeping,
|
|
1074
|
+
colliders,
|
|
1075
|
+
joints: this.joints.size,
|
|
1076
|
+
characters: this.characters.size,
|
|
1077
|
+
contacts: contacts / 2,
|
|
1078
|
+
stepMs: this.stepMs,
|
|
1079
|
+
steps: this.steps,
|
|
1080
|
+
pending: this.pendingMesh.size,
|
|
1081
|
+
parked: this.parkedCount
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
free() {
|
|
1085
|
+
this.raw.free();
|
|
1086
|
+
this.events.free();
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
function unsupported(what) {
|
|
1090
|
+
return new ShardError("physics/unsupported-shape", `2D physics has no ${what}`, {
|
|
1091
|
+
hint: "2D shapes: ball, cuboid, capsule, convex, trimesh, heightfield, segment, polyline."
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
function quatToAngle(q, o = 0) {
|
|
1095
|
+
return 2 * Math.atan2(q[o + 2], q[o + 3]);
|
|
1096
|
+
}
|
|
1097
|
+
export {
|
|
1098
|
+
CHARACTER_OFFSET,
|
|
1099
|
+
Physics,
|
|
1100
|
+
PhysicsWorld,
|
|
1101
|
+
createRayHit,
|
|
1102
|
+
loadRapier,
|
|
1103
|
+
quatToAngle
|
|
1104
|
+
};
|