incanto 0.74.0 → 0.76.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/bin/incanto-multiplay.mjs +52 -4
- package/dist/2d.d.ts +83 -7
- package/dist/2d.js +4 -3
- package/dist/3d.d.ts +861 -167
- package/dist/3d.js +6 -6
- package/dist/{agent8-CHTT4unP.js → agent8-B-bh3n0X.js} +1 -1
- package/dist/{audio-player-BOrk4eQx.d.ts → audio-player-BRo2uvG6.d.ts} +23 -2
- package/dist/{behavior-J1cmVah0.d.ts → behavior-B_245qRy.d.ts} +15 -1
- package/dist/{create-game-ssvGvP7W.js → create-game-BMIPKrQW.js} +84 -8
- package/dist/{create-game-BAiA-FjP.js → create-game-Dai5vfVu.js} +80 -11
- package/dist/debug.d.ts +1 -1
- package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-Bzt0GzVp.d.ts} +13 -3
- package/dist/editor.js +1600 -1529
- package/dist/env.d.ts +1 -1
- package/dist/{environment-presets-BkqlWewf.js → environment-presets-e_9YCnGu.js} +2361 -104
- package/dist/{gameplay-Bxe1sMVT.js → gameplay-Bfff_beb.js} +10919 -6798
- package/dist/gameplay.d.ts +573 -31
- package/dist/gameplay.js +3 -2
- package/dist/index.d.ts +150 -6
- package/dist/index.js +6 -6
- package/dist/{loader-BAeWMYfF.d.ts → loader-CbkVdXL8.d.ts} +2 -2
- package/dist/net.d.ts +2 -2
- package/dist/net.js +2 -2
- package/dist/{physics-2d-C-SfvjzH.js → physics-2d-Tuf5LAyq.js} +250 -31
- package/dist/{physics-3d-rOzeg890.js → physics-3d-C5MOujfQ.js} +673 -51
- package/dist/{picking-BMV34Pjl.js → picking-DIwE48sU.js} +10 -7
- package/dist/{save-slots-CEUGeLuB.js → pose-ByFB_J3O.js} +217 -11
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/{register-DRQyZKGU.js → register-7E3P1gz6.js} +191 -430
- package/dist/{register-CjociOtt.js → register-B768Qedj.js} +596 -2
- package/dist/{replay-B6BbgGM_.js → replay-DrqQ6dZj.js} +133 -18
- package/dist/{replay-wZK5Ok5c.d.ts → replay-W5nCw_cU.d.ts} +21 -2
- package/dist/{rng-Bb-IutXB.d.ts → rng-BsXZg3D6.d.ts} +7 -0
- package/dist/{split-screen-BxZ5rBe7.js → split-screen-B-dIcQsQ.js} +10 -5
- package/dist/{split-screen-D6sx8z3Z.d.ts → split-screen-CiLJf2-C.d.ts} +3 -3
- package/dist/{sprite-animation-Bz3DHD_4.js → sprite-animation-SQa5gIu2.js} +8 -6
- package/dist/{src-CqtvYtSN.js → src-D823V07k.js} +1 -1
- package/dist/{test-CEl0y2Sw.js → test-CVbxnXlv.js} +487 -82
- package/dist/test.d.ts +90 -12
- package/dist/test.js +2 -2
- package/dist/vite.js +34 -5
- package/editor/assets/{agent8-BlzturGi.js → agent8-DA-I5qhv.js} +1 -1
- package/editor/assets/{debug-BMUi8usj.js → debug-99tEPoGm.js} +1 -1
- package/editor/assets/index-Db74LZY-.js +11579 -0
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +1639 -280
- package/skills/incanto-3d-character.md +383 -20
- package/skills/incanto-audio.md +24 -2
- package/skills/incanto-building-2d-games.md +21 -2
- package/skills/incanto-building-3d-games.md +38 -3
- package/skills/incanto-editor.md +29 -8
- package/skills/incanto-environment.md +257 -14
- package/skills/incanto-game-feel.md +60 -1
- package/skills/incanto-gameplay-behaviors.md +668 -20
- package/skills/incanto-hud.md +116 -0
- package/skills/incanto-multiplayer.md +141 -2
- package/skills/incanto-node-reference.md +398 -16
- package/skills/incanto-performance.md +32 -0
- package/skills/incanto-physics-and-input.md +241 -13
- package/skills/incanto-playtesting.md +2 -2
- package/skills/incanto-save-slots.md +61 -2
- package/skills/incanto-scene-json-authoring.md +13 -0
- package/skills/incanto-verifying-your-game.md +86 -3
- package/templates-app/beacon-isle-3d/PROJECT/Status.md +12 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/game.scene.json +157 -3
- package/templates-app/molehill-2d/PROJECT/Status.md +7 -0
- package/templates-app/molehill-2d/package.json +1 -1
- package/templates-app/molehill-2d/src/game.scene.json +129 -2
- package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
- package/templates-app/platformer-2d/docs/project-2d-rules.md +3 -2
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/behaviors.ts +40 -26
- package/templates-app/platformer-2d/src/game.scene.json +144 -2
- package/templates-app/platformer-2d/verify.ts +18 -1
- package/templates-app/star-survivor/PROJECT/Status.md +7 -0
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/star-survivor/src/behaviors.ts +9 -8
- package/templates-app/star-survivor/src/game.scene.json +142 -4
- package/templates-app/star-survivor/verify.ts +13 -0
- package/templates-app/tps-3d/PROJECT/Status.md +6 -0
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/game.scene.json +71 -10
- package/templates-app/village-quest-3d/PROJECT/Status.md +7 -0
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/behaviors.ts +8 -16
- package/templates-app/village-quest-3d/src/grove.scene.json +121 -5
- package/templates-app/village-quest-3d/src/village.scene.json +126 -2
- package/templates-app/village-quest-3d/verify.ts +52 -2
- package/editor/assets/index-CrUCQoaB.js +0 -11046
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { o as __exportAll } from "./json-CfTjpvW8.js";
|
|
2
|
-
import {
|
|
2
|
+
import { et as diagnose } from "./pose-ByFB_J3O.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
|
-
import {
|
|
4
|
+
import { $ as Water3D, at as PhysicsBody3D, ct as Node3D, it as CharacterBody3D, lt as validateCollider3D, ot as RigidBody3D, rt as Area3D, st as StaticBody3D } from "./gameplay-Bfff_beb.js";
|
|
5
5
|
import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
|
|
6
6
|
import { n as jointContacts, t as withoutRapierInitNoise } from "./quiet-rapier-C6fW4zcW.js";
|
|
7
|
-
import {
|
|
7
|
+
import { B as InstancedMesh3D, F as Terrain3D, H as buildMeshGeometry, z as Joint3D } from "./environment-presets-e_9YCnGu.js";
|
|
8
8
|
import { Euler, Matrix4, Quaternion, Vector3 } from "three";
|
|
9
9
|
//#region src/3d/physics/collider-lines.ts
|
|
10
10
|
/**
|
|
@@ -210,6 +210,89 @@ var physics_3d_exports = /* @__PURE__ */ __exportAll({
|
|
|
210
210
|
});
|
|
211
211
|
const DEG = Math.PI / 180;
|
|
212
212
|
/**
|
|
213
|
+
* A hinge's angle: body A's rotation relative to body B, about A's local
|
|
214
|
+
* `axis`, in degrees on (-180, 180]. The twist of the relative quaternion
|
|
215
|
+
* about the axis — `2·atan2(q·n, q.w)` — which is exactly the turn when the
|
|
216
|
+
* joint holds every other axis still.
|
|
217
|
+
*/
|
|
218
|
+
/**
|
|
219
|
+
* How far two bodies disagree about a joint axis, in degrees: the part of
|
|
220
|
+
* their relative rotation that is NOT about `axis`. Zero when they share a
|
|
221
|
+
* rotation, or differ only by a turn about the axis itself.
|
|
222
|
+
*/
|
|
223
|
+
function axisDisagreementDeg(qa, qb, axis) {
|
|
224
|
+
const bx = -qb.x;
|
|
225
|
+
const by = -qb.y;
|
|
226
|
+
const bz = -qb.z;
|
|
227
|
+
const bw = qb.w;
|
|
228
|
+
const rx = bw * qa.x + bx * qa.w + by * qa.z - bz * qa.y;
|
|
229
|
+
const ry = bw * qa.y - bx * qa.z + by * qa.w + bz * qa.x;
|
|
230
|
+
const rz = bw * qa.z + bx * qa.y - by * qa.x + bz * qa.w;
|
|
231
|
+
const rw = bw * qa.w - bx * qa.x - by * qa.y - bz * qa.z;
|
|
232
|
+
const n = Math.hypot(axis[0] ?? 0, axis[1] ?? 0, axis[2] ?? 0) || 1;
|
|
233
|
+
const ux = (axis[0] ?? 0) / n;
|
|
234
|
+
const uy = (axis[1] ?? 0) / n;
|
|
235
|
+
const uz = (axis[2] ?? 0) / n;
|
|
236
|
+
const proj = rx * ux + ry * uy + rz * uz;
|
|
237
|
+
const tw = Math.hypot(proj, rw) || 1;
|
|
238
|
+
const twist = {
|
|
239
|
+
x: proj * ux / tw,
|
|
240
|
+
y: proj * uy / tw,
|
|
241
|
+
z: proj * uz / tw,
|
|
242
|
+
w: rw / tw
|
|
243
|
+
};
|
|
244
|
+
const sw = rw * twist.w + rx * twist.x + ry * twist.y + rz * twist.z;
|
|
245
|
+
return Math.abs(2 * Math.acos(Math.min(1, Math.abs(sw))) / DEG);
|
|
246
|
+
}
|
|
247
|
+
/** `v` turned by the quaternion `q`. */
|
|
248
|
+
function rotateVec3(q, x, y, z) {
|
|
249
|
+
const cx = q.y * z - q.z * y;
|
|
250
|
+
const cy = q.z * x - q.x * z;
|
|
251
|
+
const cz = q.x * y - q.y * x;
|
|
252
|
+
return [
|
|
253
|
+
x + 2 * (q.w * cx + (q.y * cz - q.z * cy)),
|
|
254
|
+
y + 2 * (q.w * cy + (q.z * cx - q.x * cz)),
|
|
255
|
+
z + 2 * (q.w * cz + (q.x * cy - q.y * cx))
|
|
256
|
+
];
|
|
257
|
+
}
|
|
258
|
+
/** `offset`, turned by the rotation that takes `qFrom` to `qNow` (world frame). */
|
|
259
|
+
function rotateByDelta(qNow, qFrom, ox, oy, oz) {
|
|
260
|
+
const [fx, fy, fz, fw] = [
|
|
261
|
+
-qFrom[0],
|
|
262
|
+
-qFrom[1],
|
|
263
|
+
-qFrom[2],
|
|
264
|
+
qFrom[3]
|
|
265
|
+
];
|
|
266
|
+
const dx = qNow.w * fx + qNow.x * fw + qNow.y * fz - qNow.z * fy;
|
|
267
|
+
const dy = qNow.w * fy - qNow.x * fz + qNow.y * fw + qNow.z * fx;
|
|
268
|
+
const dz = qNow.w * fz + qNow.x * fy - qNow.y * fx + qNow.z * fw;
|
|
269
|
+
const dw = qNow.w * fw - qNow.x * fx - qNow.y * fy - qNow.z * fz;
|
|
270
|
+
const cx = dy * oz - dz * oy;
|
|
271
|
+
const cy = dz * ox - dx * oz;
|
|
272
|
+
const cz = dx * oy - dy * ox;
|
|
273
|
+
return [
|
|
274
|
+
ox + 2 * (dw * cx + (dy * cz - dz * cy)),
|
|
275
|
+
oy + 2 * (dw * cy + (dz * cx - dx * cz)),
|
|
276
|
+
oz + 2 * (dw * cz + (dx * cy - dy * cx))
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
function hingeAngle3D(qa, qb, axis) {
|
|
280
|
+
const bx = -qb.x;
|
|
281
|
+
const by = -qb.y;
|
|
282
|
+
const bz = -qb.z;
|
|
283
|
+
const bw = qb.w;
|
|
284
|
+
const rx = bw * qa.x + bx * qa.w + by * qa.z - bz * qa.y;
|
|
285
|
+
const ry = bw * qa.y - bx * qa.z + by * qa.w + bz * qa.x;
|
|
286
|
+
const rz = bw * qa.z + bx * qa.y - by * qa.x + bz * qa.w;
|
|
287
|
+
const rw = bw * qa.w - bx * qa.x - by * qa.y - bz * qa.z;
|
|
288
|
+
const n = Math.hypot(axis[0] ?? 0, axis[1] ?? 0, axis[2] ?? 0) || 1;
|
|
289
|
+
const proj = (rx * (axis[0] ?? 0) + ry * (axis[1] ?? 0) + rz * (axis[2] ?? 0)) / n;
|
|
290
|
+
let deg = 2 * Math.atan2(proj, rw) / DEG;
|
|
291
|
+
if (deg > 180) deg -= 360;
|
|
292
|
+
if (deg <= -180) deg += 360;
|
|
293
|
+
return deg;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
213
296
|
* Enable 3D physics for an engine. Dynamically imports Rapier (compat build)
|
|
214
297
|
* so games without physics never pay its bundle cost. 1 unit = 1 meter, y-up —
|
|
215
298
|
* three's native space, no conversion.
|
|
@@ -232,6 +315,11 @@ var Physics3D = class {
|
|
|
232
315
|
warnedNoCollider = /* @__PURE__ */ new WeakSet();
|
|
233
316
|
entries = /* @__PURE__ */ new Map();
|
|
234
317
|
byColliderHandle = /* @__PURE__ */ new Map();
|
|
318
|
+
/** Every Water3D in the tree, rescanned when the structure changes. */
|
|
319
|
+
waters = [];
|
|
320
|
+
watersStale = true;
|
|
321
|
+
/** Raycast vehicles, one per Vehicle3D in the tree, on their chassis entries. */
|
|
322
|
+
vehicles = /* @__PURE__ */ new Map();
|
|
235
323
|
world;
|
|
236
324
|
events;
|
|
237
325
|
kcc;
|
|
@@ -294,16 +382,18 @@ var Physics3D = class {
|
|
|
294
382
|
}
|
|
295
383
|
}
|
|
296
384
|
this.syncBodies();
|
|
385
|
+
this.applyBuoyancy(dt);
|
|
297
386
|
this.world.timestep = dt;
|
|
298
387
|
this.world.step(this.events);
|
|
299
388
|
for (const [node, e] of this.entries) {
|
|
300
389
|
const t = e.body.translation();
|
|
301
390
|
if (!e.body.isFixed() && !(node instanceof Area3D)) {
|
|
302
|
-
|
|
391
|
+
parentPose3D(node);
|
|
392
|
+
localV.set(t.x, t.y, t.z).sub(parentPosV).applyQuaternion(invQ.copy(parentQuatQ).invert());
|
|
303
393
|
const next = [
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
394
|
+
localV.x,
|
|
395
|
+
localV.y,
|
|
396
|
+
localV.z
|
|
307
397
|
];
|
|
308
398
|
node._interpPrev = node._interpCurr ?? next;
|
|
309
399
|
node._interpCurr = next;
|
|
@@ -315,7 +405,14 @@ var Physics3D = class {
|
|
|
315
405
|
if (node instanceof RigidBody3D) {
|
|
316
406
|
if (!node.fixedRotation) {
|
|
317
407
|
const q = e.body.rotation();
|
|
318
|
-
|
|
408
|
+
quatScratch.set(q.x, q.y, q.z, q.w);
|
|
409
|
+
e.lastQuat[0] = q.x;
|
|
410
|
+
e.lastQuat[1] = q.y;
|
|
411
|
+
e.lastQuat[2] = q.z;
|
|
412
|
+
e.lastQuat[3] = q.w;
|
|
413
|
+
parentPose3D(node);
|
|
414
|
+
localQ.copy(parentQuatQ).invert().multiply(quatScratch);
|
|
415
|
+
eulerScratch.setFromQuaternion(localQ, "XYZ");
|
|
319
416
|
const rot = node.rotation;
|
|
320
417
|
rot[0] = eulerScratch.x / DEG;
|
|
321
418
|
rot[1] = eulerScratch.y / DEG;
|
|
@@ -337,6 +434,9 @@ var Physics3D = class {
|
|
|
337
434
|
e.lastV[0] = lv.x;
|
|
338
435
|
e.lastV[1] = lv.y;
|
|
339
436
|
e.lastV[2] = lv.z;
|
|
437
|
+
e.lastAV[0] = av.x;
|
|
438
|
+
e.lastAV[1] = av.y;
|
|
439
|
+
e.lastAV[2] = av.z;
|
|
340
440
|
}
|
|
341
441
|
const t2 = e.body.translation();
|
|
342
442
|
e.last[0] = t2.x;
|
|
@@ -347,6 +447,7 @@ var Physics3D = class {
|
|
|
347
447
|
const a = this.byColliderHandle.get(h1);
|
|
348
448
|
const b = this.byColliderHandle.get(h2);
|
|
349
449
|
if (!a || !b) return;
|
|
450
|
+
if (!a.enabled || !b.enabled) return;
|
|
350
451
|
const sig = started ? "triggerEnter" : "triggerExit";
|
|
351
452
|
if (started === (this.overlaps.get(a)?.has(b) ?? false)) return;
|
|
352
453
|
this.trackOverlap(a, b, started);
|
|
@@ -370,13 +471,12 @@ var Physics3D = class {
|
|
|
370
471
|
*/
|
|
371
472
|
poseFromTree() {
|
|
372
473
|
for (const [node, e] of this.entries) {
|
|
373
|
-
|
|
474
|
+
worldPose3D(node, wpV, quatScratch);
|
|
374
475
|
e.body.setTranslation({
|
|
375
|
-
x:
|
|
376
|
-
y:
|
|
377
|
-
z:
|
|
476
|
+
x: wpV.x,
|
|
477
|
+
y: wpV.y,
|
|
478
|
+
z: wpV.z
|
|
378
479
|
}, false);
|
|
379
|
-
quatScratch.setFromEuler(eulerScratch.set((node.rotation[0] ?? 0) * DEG, (node.rotation[1] ?? 0) * DEG, (node.rotation[2] ?? 0) * DEG, "XYZ"));
|
|
380
480
|
e.body.setRotation({
|
|
381
481
|
x: quatScratch.x,
|
|
382
482
|
y: quatScratch.y,
|
|
@@ -386,6 +486,77 @@ var Physics3D = class {
|
|
|
386
486
|
}
|
|
387
487
|
this.world.propagateModifiedBodyPositionsToColliders();
|
|
388
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* @internal Called by Vehicle3D.fixedUpdate: push the throttle, steer and
|
|
491
|
+
* brake into Rapier's raycast vehicle on the chassis and let it apply the
|
|
492
|
+
* step's forces; read the wheels back. The controller is made on first
|
|
493
|
+
* use and remade if the chassis body was rebuilt.
|
|
494
|
+
*/
|
|
495
|
+
_driveVehicle(node, dt) {
|
|
496
|
+
const e = this.ensureEntry(node.body);
|
|
497
|
+
if (!e) return;
|
|
498
|
+
const R = this.R;
|
|
499
|
+
let v = this.vehicles.get(node);
|
|
500
|
+
if (!v || v.chassis !== e.body) {
|
|
501
|
+
if (v) v.ctrl.free();
|
|
502
|
+
const ctrl = new R.DynamicRayCastVehicleController(e.body, this.world.broadPhase, this.world.narrowPhase, this.world.bodies, this.world.colliders);
|
|
503
|
+
ctrl.indexUpAxis = 1;
|
|
504
|
+
ctrl.setIndexForwardAxis = 2;
|
|
505
|
+
node.specs().forEach((w, i) => {
|
|
506
|
+
ctrl.addWheel({
|
|
507
|
+
x: w.position[0],
|
|
508
|
+
y: w.position[1],
|
|
509
|
+
z: w.position[2]
|
|
510
|
+
}, {
|
|
511
|
+
x: 0,
|
|
512
|
+
y: -1,
|
|
513
|
+
z: 0
|
|
514
|
+
}, {
|
|
515
|
+
x: -1,
|
|
516
|
+
y: 0,
|
|
517
|
+
z: 0
|
|
518
|
+
}, w.rest, w.radius);
|
|
519
|
+
ctrl.setWheelSuspensionStiffness(i, node.suspensionStiffness);
|
|
520
|
+
ctrl.setWheelSuspensionCompression(i, node.suspensionCompression);
|
|
521
|
+
ctrl.setWheelSuspensionRelaxation(i, node.suspensionRelaxation);
|
|
522
|
+
ctrl.setWheelMaxSuspensionTravel(i, node.maxTravel);
|
|
523
|
+
ctrl.setWheelFrictionSlip(i, node.friction);
|
|
524
|
+
ctrl.setWheelSideFrictionStiffness(i, node.sideFriction);
|
|
525
|
+
});
|
|
526
|
+
v = {
|
|
527
|
+
ctrl,
|
|
528
|
+
chassis: e.body
|
|
529
|
+
};
|
|
530
|
+
this.vehicles.set(node, v);
|
|
531
|
+
}
|
|
532
|
+
const ctrl = v.ctrl;
|
|
533
|
+
const specs = node.specs();
|
|
534
|
+
const steer = Math.max(-1, Math.min(1, node.steer)) * node.maxSteer;
|
|
535
|
+
const throttle = Math.max(-1, Math.min(1, node.throttle)) * node.engineForce;
|
|
536
|
+
const brake = Math.max(0, Math.min(1, node.brake)) * node.brakeForce;
|
|
537
|
+
specs.forEach((w, i) => {
|
|
538
|
+
ctrl.setWheelSteering(i, w.steer ? -steer : 0);
|
|
539
|
+
ctrl.setWheelEngineForce(i, w.drive ? throttle : 0);
|
|
540
|
+
ctrl.setWheelBrake(i, brake);
|
|
541
|
+
});
|
|
542
|
+
e.body.wakeUp();
|
|
543
|
+
const own = e.collider.handle;
|
|
544
|
+
ctrl.updateVehicle(dt, void 0, void 0, (c) => c.handle !== own && !(this.byColliderHandle.get(c.handle) instanceof Area3D));
|
|
545
|
+
node.speed = ctrl.currentVehicleSpeed();
|
|
546
|
+
node._states = specs.map((w, i) => ({
|
|
547
|
+
contact: ctrl.wheelIsInContact(i),
|
|
548
|
+
suspension: ctrl.wheelSuspensionLength(i) ?? w.rest,
|
|
549
|
+
rotation: ctrl.wheelRotation(i) ?? 0,
|
|
550
|
+
steering: ctrl.wheelSteering(i) ?? 0
|
|
551
|
+
}));
|
|
552
|
+
}
|
|
553
|
+
/** @internal The Vehicle3D left the tree: drop its controller. */
|
|
554
|
+
_freeVehicle(node) {
|
|
555
|
+
const v = this.vehicles.get(node);
|
|
556
|
+
if (!v) return;
|
|
557
|
+
v.ctrl.free();
|
|
558
|
+
this.vehicles.delete(node);
|
|
559
|
+
}
|
|
389
560
|
/** @internal Called by CharacterBody3D.moveAndSlide (during tree fixedUpdate). */
|
|
390
561
|
/**
|
|
391
562
|
* May `candidate` carry `rider`? — see the 2D twin.
|
|
@@ -395,6 +566,7 @@ var Physics3D = class {
|
|
|
395
566
|
* the guard was really for is a carry CYCLE, so that is what it tests.
|
|
396
567
|
*/
|
|
397
568
|
canCarry(rider, candidate) {
|
|
569
|
+
if (this.jointed.get(rider)?.has(candidate)) return false;
|
|
398
570
|
let at = candidate;
|
|
399
571
|
for (let depth = 0; depth < 8 && at; depth++) {
|
|
400
572
|
if (at === rider) return false;
|
|
@@ -419,13 +591,34 @@ var Physics3D = class {
|
|
|
419
591
|
carryX = gx - e.groundAt[0];
|
|
420
592
|
carryY = gy - e.groundAt[1];
|
|
421
593
|
carryZ = gz - e.groundAt[2];
|
|
594
|
+
const gb = this.entries.get(e.ground);
|
|
595
|
+
if (gb && e.groundRot && e.riderAt) {
|
|
596
|
+
const q = gb.body.rotation();
|
|
597
|
+
const [ox, oy, oz] = [
|
|
598
|
+
e.riderAt[0] - e.groundAt[0],
|
|
599
|
+
e.riderAt[1] - e.groundAt[1],
|
|
600
|
+
e.riderAt[2] - e.groundAt[2]
|
|
601
|
+
];
|
|
602
|
+
const [rx, ry, rz] = rotateByDelta(q, e.groundRot, ox, oy, oz);
|
|
603
|
+
carryX += rx - ox;
|
|
604
|
+
carryY += ry - oy;
|
|
605
|
+
carryZ += rz - oz;
|
|
606
|
+
}
|
|
422
607
|
} else e.ground = null;
|
|
423
608
|
const desired = {
|
|
424
609
|
x: (node.velocity[0] ?? 0) * this.lastDt + carryX,
|
|
425
610
|
y: vy * this.lastDt + carryY,
|
|
426
611
|
z: (node.velocity[2] ?? 0) * this.lastDt + carryZ
|
|
427
612
|
};
|
|
428
|
-
const
|
|
613
|
+
const jointedTo = this.jointed.get(node);
|
|
614
|
+
const noContact = jointedTo && [...jointedTo.values()].some((on) => !on) ? jointedTo : null;
|
|
615
|
+
const others = node.collideWithCharacters && !noContact ? void 0 : (c) => {
|
|
616
|
+
const other = this.byColliderHandle.get(c.handle);
|
|
617
|
+
if (!node.collideWithCharacters && other instanceof CharacterBody3D) return false;
|
|
618
|
+
return !(other && noContact?.get(other) === false);
|
|
619
|
+
};
|
|
620
|
+
this.kcc.setApplyImpulsesToDynamicBodies(node.pushes > 0);
|
|
621
|
+
if (node.pushes > 0) this.kcc.setCharacterMass(node.pushes);
|
|
429
622
|
this.kcc.computeColliderMovement(e.collider, desired, this.R.QueryFilterFlags.EXCLUDE_SENSORS, void 0, others);
|
|
430
623
|
const c = this.kcc.computedMovement();
|
|
431
624
|
const t = e.body.translation();
|
|
@@ -465,6 +658,18 @@ var Physics3D = class {
|
|
|
465
658
|
], .4, node);
|
|
466
659
|
if (hit?.node && this.canCarry(node, hit.node)) {
|
|
467
660
|
e.ground = hit.node;
|
|
661
|
+
const gq = this.entries.get(hit.node)?.body.rotation();
|
|
662
|
+
e.groundRot = gq ? [
|
|
663
|
+
gq.x,
|
|
664
|
+
gq.y,
|
|
665
|
+
gq.z,
|
|
666
|
+
gq.w
|
|
667
|
+
] : void 0;
|
|
668
|
+
e.riderAt = [
|
|
669
|
+
nx,
|
|
670
|
+
ny - half,
|
|
671
|
+
nz
|
|
672
|
+
];
|
|
468
673
|
const at = worldPosition3D(hit.node);
|
|
469
674
|
e.groundAt = [
|
|
470
675
|
at[0],
|
|
@@ -519,6 +724,42 @@ var Physics3D = class {
|
|
|
519
724
|
lastRoot = null;
|
|
520
725
|
jointSet = /* @__PURE__ */ new Set();
|
|
521
726
|
joints = /* @__PURE__ */ new Map();
|
|
727
|
+
/** The two bodies each live joint ties, so a departed joint can be unlinked. */
|
|
728
|
+
jointBodies = /* @__PURE__ */ new Map();
|
|
729
|
+
/** What a hinge's limits/motor were last pushed as, so a write re-applies. */
|
|
730
|
+
hingeState = /* @__PURE__ */ new Map();
|
|
731
|
+
/** Joints refused at creation (bodies that disagree about the axis) — said once. */
|
|
732
|
+
refusedJoints = /* @__PURE__ */ new Set();
|
|
733
|
+
/** A rope's or spring's numbers as last made, so a write REMAKES the joint. */
|
|
734
|
+
tetherState = /* @__PURE__ */ new Map();
|
|
735
|
+
tetherKey(joint) {
|
|
736
|
+
return `${joint.length}/${joint.stiffness}/${joint.damping}`;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Who is jointed to whom, and whether that pair's contacts are on.
|
|
740
|
+
*
|
|
741
|
+
* A joint's own bodies are not the world to the character controller: a
|
|
742
|
+
* body jointed to a character with its contacts off is not an obstacle to
|
|
743
|
+
* its shape-cast (the joint said so, for the contact solver; the cast asks
|
|
744
|
+
* its own question), and a body jointed to it is never its GROUND — a
|
|
745
|
+
* chain hanging from a crane hook was the hook's "moving platform", and the
|
|
746
|
+
* carry dragged the hook along by however far the hook had just moved.
|
|
747
|
+
*/
|
|
748
|
+
jointed = /* @__PURE__ */ new Map();
|
|
749
|
+
linkJointed(a, b, contacts) {
|
|
750
|
+
for (const [x, y] of [[a, b], [b, a]]) {
|
|
751
|
+
let m = this.jointed.get(x);
|
|
752
|
+
if (!m) {
|
|
753
|
+
m = /* @__PURE__ */ new Map();
|
|
754
|
+
this.jointed.set(x, m);
|
|
755
|
+
}
|
|
756
|
+
m.set(y, contacts);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
unlinkJointed(a, b) {
|
|
760
|
+
this.jointed.get(a)?.delete(b);
|
|
761
|
+
this.jointed.get(b)?.delete(a);
|
|
762
|
+
}
|
|
522
763
|
/** One fixed body per solid scatter field, carrying a collider per row. */
|
|
523
764
|
scatterBodies = /* @__PURE__ */ new Map();
|
|
524
765
|
bodySet = /* @__PURE__ */ new Set();
|
|
@@ -676,6 +917,7 @@ var Physics3D = class {
|
|
|
676
917
|
}
|
|
677
918
|
this.syncJoints(structureChanged);
|
|
678
919
|
if (structureChanged) {
|
|
920
|
+
this.watersStale = true;
|
|
679
921
|
for (const [node, e] of this.entries) if (!seen.has(node)) {
|
|
680
922
|
this.byColliderHandle.delete(e.collider.handle);
|
|
681
923
|
this.world.removeRigidBody(e.body);
|
|
@@ -770,6 +1012,94 @@ var Physics3D = class {
|
|
|
770
1012
|
* meantime. syncBodies runs every step, so the body appears the moment the
|
|
771
1013
|
* download lands.
|
|
772
1014
|
*/
|
|
1015
|
+
/**
|
|
1016
|
+
* Lift every buoyant dynamic body by the water it is in — before the solver
|
|
1017
|
+
* step, so the impulses are part of this step's motion.
|
|
1018
|
+
*
|
|
1019
|
+
* `Water3D.heightAt` had answered "so gameplay can float a raft, bob a
|
|
1020
|
+
* buoy" since the water shipped, and nothing floated anything but the
|
|
1021
|
+
* swimmer: a RigidBody3D dropped into a lake fell through it to the bed,
|
|
1022
|
+
* whatever it weighed. This is Archimedes on eight sample points: each
|
|
1023
|
+
* octant of the collider is an eighth of its volume, lifted by the water's
|
|
1024
|
+
* density times however much of it is under the surface AT THAT POINT, at
|
|
1025
|
+
* that point — so a tilted hull's low corners push harder and it rights
|
|
1026
|
+
* itself, and a wave under one end rocks it. The water's `drag` then bleeds
|
|
1027
|
+
* velocity and spin in proportion to how much is under, which is what stops
|
|
1028
|
+
* a pushed crate and lets a bob die out.
|
|
1029
|
+
*/
|
|
1030
|
+
applyBuoyancy(dt) {
|
|
1031
|
+
if (this.watersStale) {
|
|
1032
|
+
this.waters.length = 0;
|
|
1033
|
+
const root = this.engine.scene?.root;
|
|
1034
|
+
if (root) collectWaters(root, this.waters);
|
|
1035
|
+
this.watersStale = false;
|
|
1036
|
+
}
|
|
1037
|
+
if (this.waters.length === 0) return;
|
|
1038
|
+
const g = Math.abs(this.world.gravity.y) || 9.81;
|
|
1039
|
+
for (const [node, e] of this.entries) {
|
|
1040
|
+
if (!(node instanceof RigidBody3D) || !(node.buoyancy > 0)) continue;
|
|
1041
|
+
if (node.children.some((c) => typeNameOf(c) === "CharacterController3D")) continue;
|
|
1042
|
+
if (e.hull === void 0) e.hull = hullOf(e.collider, this.R.ShapeType.Capsule);
|
|
1043
|
+
const hull = e.hull;
|
|
1044
|
+
if (!hull) continue;
|
|
1045
|
+
const t = e.body.translation();
|
|
1046
|
+
const q = e.body.rotation();
|
|
1047
|
+
quatScratch.set(q.x, q.y, q.z, q.w);
|
|
1048
|
+
let submerged = 0;
|
|
1049
|
+
let water = null;
|
|
1050
|
+
for (const [ox, oy, oz] of OCTANTS) {
|
|
1051
|
+
buoyScratch.set(ox * hull.hx / 2, oy * hull.hy / 2, oz * hull.hz / 2).applyQuaternion(quatScratch);
|
|
1052
|
+
const px = t.x + buoyScratch.x;
|
|
1053
|
+
const py = t.y + buoyScratch.y;
|
|
1054
|
+
const pz = t.z + buoyScratch.z;
|
|
1055
|
+
const here = this.waterAt(px, pz);
|
|
1056
|
+
if (!here) continue;
|
|
1057
|
+
const f = Math.min(1, Math.max(0, (here.heightAt(px, pz) - (py - hull.hy / 4)) / (hull.hy / 2)));
|
|
1058
|
+
if (f <= 0) continue;
|
|
1059
|
+
water = here;
|
|
1060
|
+
submerged += f / 8;
|
|
1061
|
+
const lift = WATER_DENSITY * g * (hull.volume / 8) * f * node.buoyancy * dt;
|
|
1062
|
+
e.body.applyImpulseAtPoint({
|
|
1063
|
+
x: 0,
|
|
1064
|
+
y: lift,
|
|
1065
|
+
z: 0
|
|
1066
|
+
}, {
|
|
1067
|
+
x: px,
|
|
1068
|
+
y: py,
|
|
1069
|
+
z: pz
|
|
1070
|
+
}, true);
|
|
1071
|
+
}
|
|
1072
|
+
if (submerged <= 0 || !water) continue;
|
|
1073
|
+
const m = e.body.mass();
|
|
1074
|
+
const lv = e.body.linvel();
|
|
1075
|
+
const waterplane = 4 * hull.hx * hull.hz;
|
|
1076
|
+
const heaveK = WATER_DENSITY * g * waterplane * node.buoyancy;
|
|
1077
|
+
const heave = Math.min(1, 2 * HEAVE_ZETA * Math.sqrt(heaveK * m) / m * dt);
|
|
1078
|
+
const k = Math.min(1, water.drag * Math.min(1, 4 * submerged) * dt);
|
|
1079
|
+
e.body.applyImpulse({
|
|
1080
|
+
x: -lv.x * m * k,
|
|
1081
|
+
y: -lv.y * m * heave,
|
|
1082
|
+
z: -lv.z * m * k
|
|
1083
|
+
}, true);
|
|
1084
|
+
if (k <= 0) continue;
|
|
1085
|
+
const inertia = m * (hull.hx * hull.hx + hull.hy * hull.hy + hull.hz * hull.hz) / 3;
|
|
1086
|
+
const spin = Math.min(1, k * SPIN_DRAG);
|
|
1087
|
+
const av = e.body.angvel();
|
|
1088
|
+
e.body.applyTorqueImpulse({
|
|
1089
|
+
x: -av.x * inertia * spin,
|
|
1090
|
+
y: -av.y * inertia * spin,
|
|
1091
|
+
z: -av.z * inertia * spin
|
|
1092
|
+
}, true);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
/** The water whose footprint covers world (x, z), or null. */
|
|
1096
|
+
waterAt(x, z) {
|
|
1097
|
+
for (const w of this.waters) {
|
|
1098
|
+
if (w.tree === null) continue;
|
|
1099
|
+
if (w._waterYAt(x, z) !== null) return w;
|
|
1100
|
+
}
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
773
1103
|
ensureEntry(node) {
|
|
774
1104
|
let e = this.entries.get(node);
|
|
775
1105
|
const R = this.R;
|
|
@@ -780,9 +1110,11 @@ var Physics3D = class {
|
|
|
780
1110
|
if (node instanceof RigidBody3D) desc = R.RigidBodyDesc.dynamic();
|
|
781
1111
|
else if (node instanceof CharacterBody3D || node instanceof Area3D) desc = R.RigidBodyDesc.kinematicPositionBased();
|
|
782
1112
|
else desc = R.RigidBodyDesc.fixed();
|
|
783
|
-
|
|
1113
|
+
worldPose3D(node, wpV, quatScratch);
|
|
1114
|
+
const wx = wpV.x;
|
|
1115
|
+
const wy = wpV.y;
|
|
1116
|
+
const wz = wpV.z;
|
|
784
1117
|
desc.setTranslation(wx, wy, wz);
|
|
785
|
-
quatScratch.setFromEuler(eulerScratch.set((node.rotation[0] ?? 0) * DEG, (node.rotation[1] ?? 0) * DEG, (node.rotation[2] ?? 0) * DEG, "XYZ"));
|
|
786
1118
|
desc.setRotation({
|
|
787
1119
|
x: quatScratch.x,
|
|
788
1120
|
y: quatScratch.y,
|
|
@@ -821,11 +1153,22 @@ var Physics3D = class {
|
|
|
821
1153
|
node.rotation[1] ?? 0,
|
|
822
1154
|
node.rotation[2] ?? 0
|
|
823
1155
|
],
|
|
1156
|
+
lastQuat: [
|
|
1157
|
+
quatScratch.x,
|
|
1158
|
+
quatScratch.y,
|
|
1159
|
+
quatScratch.z,
|
|
1160
|
+
quatScratch.w
|
|
1161
|
+
],
|
|
824
1162
|
lastV: [
|
|
825
1163
|
0,
|
|
826
1164
|
0,
|
|
827
1165
|
0
|
|
828
1166
|
],
|
|
1167
|
+
lastAV: [
|
|
1168
|
+
node instanceof RigidBody3D && !node.fixedRotation ? node.angularVelocity[0] ?? 0 : 0,
|
|
1169
|
+
node instanceof RigidBody3D && !node.fixedRotation ? node.angularVelocity[1] ?? 0 : 0,
|
|
1170
|
+
node instanceof RigidBody3D && !node.fixedRotation ? node.angularVelocity[2] ?? 0 : 0
|
|
1171
|
+
],
|
|
829
1172
|
lastMass: node instanceof RigidBody3D ? node.mass : 1,
|
|
830
1173
|
lastFriction: node.friction ?? .5,
|
|
831
1174
|
lastRestitution: node.restitution ?? 0,
|
|
@@ -853,7 +1196,11 @@ var Physics3D = class {
|
|
|
853
1196
|
node._enabledChanged = false;
|
|
854
1197
|
if (node.enabled) this.armOverlaps(node, e.collider);
|
|
855
1198
|
}
|
|
856
|
-
|
|
1199
|
+
worldPose3D(node, wpV, quatScratch);
|
|
1200
|
+
const p = posScratch;
|
|
1201
|
+
p[0] = wpV.x;
|
|
1202
|
+
p[1] = wpV.y;
|
|
1203
|
+
p[2] = wpV.z;
|
|
857
1204
|
if (p[0] !== e.last[0] || p[1] !== e.last[1] || p[2] !== e.last[2]) {
|
|
858
1205
|
e.body.setTranslation({
|
|
859
1206
|
x: p[0],
|
|
@@ -868,14 +1215,17 @@ var Physics3D = class {
|
|
|
868
1215
|
const rx = node.rotation[0] ?? 0;
|
|
869
1216
|
const ry = node.rotation[1] ?? 0;
|
|
870
1217
|
const rz = node.rotation[2] ?? 0;
|
|
871
|
-
if (
|
|
872
|
-
quatScratch.setFromEuler(eulerScratch.set(rx * DEG, ry * DEG, rz * DEG, "XYZ"));
|
|
1218
|
+
if (quatScratch.x !== e.lastQuat[0] || quatScratch.y !== e.lastQuat[1] || quatScratch.z !== e.lastQuat[2] || quatScratch.w !== e.lastQuat[3]) {
|
|
873
1219
|
e.body.setRotation({
|
|
874
1220
|
x: quatScratch.x,
|
|
875
1221
|
y: quatScratch.y,
|
|
876
1222
|
z: quatScratch.z,
|
|
877
1223
|
w: quatScratch.w
|
|
878
1224
|
}, true);
|
|
1225
|
+
e.lastQuat[0] = quatScratch.x;
|
|
1226
|
+
e.lastQuat[1] = quatScratch.y;
|
|
1227
|
+
e.lastQuat[2] = quatScratch.z;
|
|
1228
|
+
e.lastQuat[3] = quatScratch.w;
|
|
879
1229
|
e.lastRot[0] = rx;
|
|
880
1230
|
e.lastRot[1] = ry;
|
|
881
1231
|
e.lastRot[2] = rz;
|
|
@@ -894,6 +1244,19 @@ var Physics3D = class {
|
|
|
894
1244
|
}, true);
|
|
895
1245
|
e.lastV = v;
|
|
896
1246
|
}
|
|
1247
|
+
const av = [
|
|
1248
|
+
node.angularVelocity[0] ?? 0,
|
|
1249
|
+
node.angularVelocity[1] ?? 0,
|
|
1250
|
+
node.angularVelocity[2] ?? 0
|
|
1251
|
+
];
|
|
1252
|
+
if (av[0] !== e.lastAV[0] || av[1] !== e.lastAV[1] || av[2] !== e.lastAV[2]) {
|
|
1253
|
+
if (!node.fixedRotation) e.body.setAngvel({
|
|
1254
|
+
x: av[0],
|
|
1255
|
+
y: av[1],
|
|
1256
|
+
z: av[2]
|
|
1257
|
+
}, true);
|
|
1258
|
+
e.lastAV = av;
|
|
1259
|
+
}
|
|
897
1260
|
if (node.gravityScale !== e.body.gravityScale()) e.body.setGravityScale(node.gravityScale, true);
|
|
898
1261
|
if (node.mass !== e.lastMass) {
|
|
899
1262
|
e.collider.setMass(node.mass);
|
|
@@ -923,16 +1286,55 @@ var Physics3D = class {
|
|
|
923
1286
|
}
|
|
924
1287
|
/** Apply a world-space impulse to a dynamic body (character controllers). */
|
|
925
1288
|
applyImpulse(node, impulse) {
|
|
926
|
-
this.entries.get(node)
|
|
1289
|
+
const e = this.entries.get(node);
|
|
1290
|
+
if (!e) return;
|
|
1291
|
+
if (node instanceof RigidBody3D) {
|
|
1292
|
+
const v = node.linearVelocity;
|
|
1293
|
+
const vx = v[0] ?? 0;
|
|
1294
|
+
const vy = v[1] ?? 0;
|
|
1295
|
+
const vz = v[2] ?? 0;
|
|
1296
|
+
if (vx !== e.lastV[0] || vy !== e.lastV[1] || vz !== e.lastV[2]) e.body.setLinvel({
|
|
1297
|
+
x: vx,
|
|
1298
|
+
y: vy,
|
|
1299
|
+
z: vz
|
|
1300
|
+
}, true);
|
|
1301
|
+
}
|
|
1302
|
+
e.body.applyImpulse({
|
|
927
1303
|
x: impulse[0],
|
|
928
1304
|
y: impulse[1],
|
|
929
1305
|
z: impulse[2]
|
|
930
1306
|
}, true);
|
|
1307
|
+
if (node instanceof RigidBody3D) {
|
|
1308
|
+
const lv = e.body.linvel();
|
|
1309
|
+
node.linearVelocity[0] = lv.x;
|
|
1310
|
+
node.linearVelocity[1] = lv.y;
|
|
1311
|
+
node.linearVelocity[2] = lv.z;
|
|
1312
|
+
e.lastV = [
|
|
1313
|
+
lv.x,
|
|
1314
|
+
lv.y,
|
|
1315
|
+
lv.z
|
|
1316
|
+
];
|
|
1317
|
+
}
|
|
931
1318
|
}
|
|
932
1319
|
/** Mass the solver actually uses (collider-derived unless overridden). */
|
|
933
1320
|
massOf(node) {
|
|
934
1321
|
return this.entries.get(node)?.body.mass() ?? 1;
|
|
935
1322
|
}
|
|
1323
|
+
/** The solver's rotation of a body, as a quaternion `[x, y, z, w]`. */
|
|
1324
|
+
rotationOf(node) {
|
|
1325
|
+
const q = this.entries.get(node)?.body.rotation();
|
|
1326
|
+
return q ? [
|
|
1327
|
+
q.x,
|
|
1328
|
+
q.y,
|
|
1329
|
+
q.z,
|
|
1330
|
+
q.w
|
|
1331
|
+
] : [
|
|
1332
|
+
0,
|
|
1333
|
+
0,
|
|
1334
|
+
0,
|
|
1335
|
+
1
|
|
1336
|
+
];
|
|
1337
|
+
}
|
|
936
1338
|
/** Current solver velocity (fresher than the node prop mid-step). */
|
|
937
1339
|
velocityOf(node) {
|
|
938
1340
|
const v = this.entries.get(node)?.body.linvel();
|
|
@@ -953,14 +1355,27 @@ var Physics3D = class {
|
|
|
953
1355
|
for (const [joint, impulse] of this.joints) if (!this.jointSet.has(joint)) {
|
|
954
1356
|
this.world.removeImpulseJoint(impulse, true);
|
|
955
1357
|
this.joints.delete(joint);
|
|
1358
|
+
const pair = this.jointBodies.get(joint);
|
|
1359
|
+
if (pair) this.unlinkJointed(pair[0], pair[1]);
|
|
1360
|
+
this.jointBodies.delete(joint);
|
|
1361
|
+
this.hingeState.delete(joint);
|
|
1362
|
+
this.tetherState.delete(joint);
|
|
956
1363
|
}
|
|
957
1364
|
}
|
|
958
1365
|
for (const joint of this.jointSet) {
|
|
959
|
-
if (this.joints.has(joint)) continue;
|
|
1366
|
+
if (this.joints.has(joint) || this.refusedJoints.has(joint)) continue;
|
|
960
1367
|
const { a, b } = joint._resolveBodies();
|
|
961
1368
|
const ea = this.entries.get(a);
|
|
962
1369
|
const eb = this.entries.get(b);
|
|
963
1370
|
if (!ea || !eb) continue;
|
|
1371
|
+
if (joint.type === "revolute" || joint.type === "prismatic") {
|
|
1372
|
+
const off = axisDisagreementDeg(ea.body.rotation(), eb.body.rotation(), joint.axis);
|
|
1373
|
+
if (off > .5) {
|
|
1374
|
+
this.refusedJoints.add(joint);
|
|
1375
|
+
diagnose(this.engine, "error", `[incanto] Joint3D '${joint.getPath()}': its two bodies disagree about the ${joint.type} axis by ${off.toFixed(1)}° — '${a.name}' and '${b.name}' are authored with rotations that differ by more than a turn about ${JSON.stringify(joint.axis)}, and Rapier reads the axis in BOTH bodies' local frames. Give both bodies the same rotation (turn the MESH inside the body instead — an 'auto' collider follows it), or rotate one only about the axis. The joint was not made.`);
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
964
1379
|
const a1 = {
|
|
965
1380
|
x: joint.anchor[0] ?? 0,
|
|
966
1381
|
y: joint.anchor[1] ?? 0,
|
|
@@ -987,10 +1402,93 @@ var Physics3D = class {
|
|
|
987
1402
|
if (joint.type === "fixed") data = R.JointData.fixed(a1, identity, a2, identity);
|
|
988
1403
|
else if (joint.type === "rope") data = R.JointData.rope(length, a1, a2);
|
|
989
1404
|
else if (joint.type === "spring") data = R.JointData.spring(length, joint.stiffness, joint.damping, a1, a2);
|
|
990
|
-
else
|
|
1405
|
+
else if (joint.type === "revolute" || joint.type === "prismatic") {
|
|
1406
|
+
const ax = joint.axis[0] ?? 0;
|
|
1407
|
+
const ay = joint.axis[1] ?? 0;
|
|
1408
|
+
const az = joint.axis[2] ?? 0;
|
|
1409
|
+
const n = Math.hypot(ax, ay, az) || 1;
|
|
1410
|
+
const axis = {
|
|
1411
|
+
x: ax / n,
|
|
1412
|
+
y: ay / n,
|
|
1413
|
+
z: az / n
|
|
1414
|
+
};
|
|
1415
|
+
data = joint.type === "revolute" ? R.JointData.revolute(a1, a2, axis) : R.JointData.prismatic(a1, a2, axis);
|
|
1416
|
+
} else data = R.JointData.spherical(a1, a2);
|
|
991
1417
|
const made = this.world.createImpulseJoint(data, ea.body, eb.body, true);
|
|
992
|
-
|
|
1418
|
+
if (joint.type === "revolute" || joint.type === "prismatic") this.applyHinge(joint, made, true);
|
|
1419
|
+
const contacts = jointContacts(joint.type, joint.collide);
|
|
1420
|
+
made.setContactsEnabled(contacts);
|
|
993
1421
|
this.joints.set(joint, made);
|
|
1422
|
+
this.jointBodies.set(joint, [a, b]);
|
|
1423
|
+
this.linkJointed(a, b, contacts);
|
|
1424
|
+
if (joint.type === "rope" || joint.type === "spring") this.tetherState.set(joint, this.tetherKey(joint));
|
|
1425
|
+
}
|
|
1426
|
+
for (const [joint, made] of this.joints) {
|
|
1427
|
+
if (joint.type !== "rope" && joint.type !== "spring") continue;
|
|
1428
|
+
const key = this.tetherKey(joint);
|
|
1429
|
+
if (this.tetherState.get(joint) === key) continue;
|
|
1430
|
+
this.world.removeImpulseJoint(made, true);
|
|
1431
|
+
this.joints.delete(joint);
|
|
1432
|
+
const pair = this.jointBodies.get(joint);
|
|
1433
|
+
if (pair) this.unlinkJointed(pair[0], pair[1]);
|
|
1434
|
+
this.jointBodies.delete(joint);
|
|
1435
|
+
this.tetherState.delete(joint);
|
|
1436
|
+
}
|
|
1437
|
+
for (const [joint, made] of this.joints) {
|
|
1438
|
+
if (joint.type !== "revolute" && joint.type !== "prismatic") continue;
|
|
1439
|
+
this.applyHinge(joint, made, false);
|
|
1440
|
+
const pair = this.jointBodies.get(joint);
|
|
1441
|
+
const ea = pair && this.entries.get(pair[0]);
|
|
1442
|
+
const eb = pair && this.entries.get(pair[1]);
|
|
1443
|
+
if (!ea || !eb) continue;
|
|
1444
|
+
if (joint.type === "revolute") joint.angle = hingeAngle3D(ea.body.rotation(), eb.body.rotation(), joint.axis);
|
|
1445
|
+
else {
|
|
1446
|
+
const qa = ea.body.rotation();
|
|
1447
|
+
const ta = ea.body.translation();
|
|
1448
|
+
const tb = eb.body.translation();
|
|
1449
|
+
const wa = rotateVec3(qa, joint.anchor[0] ?? 0, joint.anchor[1] ?? 0, joint.anchor[2] ?? 0);
|
|
1450
|
+
const wb = rotateVec3(eb.body.rotation(), joint.targetAnchor[0] ?? 0, joint.targetAnchor[1] ?? 0, joint.targetAnchor[2] ?? 0);
|
|
1451
|
+
const ax = rotateVec3(qa, joint.axis[0] ?? 0, joint.axis[1] ?? 0, joint.axis[2] ?? 0);
|
|
1452
|
+
const n = Math.hypot(ax[0], ax[1], ax[2]) || 1;
|
|
1453
|
+
const dx = ta.x + wa[0] - (tb.x + wb[0]);
|
|
1454
|
+
const dy = ta.y + wa[1] - (tb.y + wb[1]);
|
|
1455
|
+
const dz = ta.z + wa[2] - (tb.z + wb[2]);
|
|
1456
|
+
joint.travel = (dx * ax[0] + dy * ax[1] + dz * ax[2]) / n;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Push a hinge's `limits` and motor to the solver — on creation, and again
|
|
1462
|
+
* whenever either changes. Limits and `angle` are this body relative to the
|
|
1463
|
+
* target; Rapier measures the other way round, hence the flip.
|
|
1464
|
+
*/
|
|
1465
|
+
applyHinge(joint, made, first) {
|
|
1466
|
+
const limits = JSON.stringify(joint.limits);
|
|
1467
|
+
const motor = `${joint.motorSpeed}/${joint.motorStrength}`;
|
|
1468
|
+
const last = this.hingeState.get(joint);
|
|
1469
|
+
const unit = made;
|
|
1470
|
+
const unitScale = joint.type === "revolute" ? DEG : 1;
|
|
1471
|
+
if (first || last?.limits !== limits) {
|
|
1472
|
+
if (joint.limits.length === 2) {
|
|
1473
|
+
const lo = (joint.limits[0] ?? 0) * unitScale;
|
|
1474
|
+
const hi = (joint.limits[1] ?? 0) * unitScale;
|
|
1475
|
+
unit.setLimits(-Math.max(lo, hi), -Math.min(lo, hi));
|
|
1476
|
+
} else if (!first) unit.setLimits(-1e9, 1e9);
|
|
1477
|
+
}
|
|
1478
|
+
if (first || last?.motor !== motor) {
|
|
1479
|
+
if (joint.motorSpeed !== 0) unit.configureMotorVelocity(-joint.motorSpeed * unitScale, joint.motorStrength);
|
|
1480
|
+
else if (!first) unit.configureMotorVelocity(0, 0);
|
|
1481
|
+
}
|
|
1482
|
+
this.hingeState.set(joint, {
|
|
1483
|
+
limits,
|
|
1484
|
+
motor
|
|
1485
|
+
});
|
|
1486
|
+
if (joint.motorSpeed !== 0) {
|
|
1487
|
+
const pair = this.jointBodies.get(joint);
|
|
1488
|
+
for (const b of pair ?? []) {
|
|
1489
|
+
const e = this.entries.get(b);
|
|
1490
|
+
if (e && !e.body.isFixed() && e.body.isSleeping()) e.body.wakeUp();
|
|
1491
|
+
}
|
|
994
1492
|
}
|
|
995
1493
|
}
|
|
996
1494
|
/** Who is currently inside each sensor/body, maintained from the event drain. */
|
|
@@ -1168,6 +1666,111 @@ var Physics3D = class {
|
|
|
1168
1666
|
};
|
|
1169
1667
|
const quatScratch = new Quaternion();
|
|
1170
1668
|
const eulerScratch = new Euler();
|
|
1669
|
+
const buoyScratch = new Vector3();
|
|
1670
|
+
/** Fresh water, kg/m³ — the reference `RigidBody3D.buoyancy: 1` scales. */
|
|
1671
|
+
const WATER_DENSITY = 1e3;
|
|
1672
|
+
/** Damping ratio of a floating body's bob: under 1 so it still rocks once. */
|
|
1673
|
+
const HEAVE_ZETA = .7;
|
|
1674
|
+
/** Spin loses `drag` × this per second — rotation in water is the slow thing. */
|
|
1675
|
+
const SPIN_DRAG = 3;
|
|
1676
|
+
/** The eight octant centres of a collider, in units of its half extents. */
|
|
1677
|
+
const OCTANTS = [
|
|
1678
|
+
[
|
|
1679
|
+
-1,
|
|
1680
|
+
-1,
|
|
1681
|
+
-1
|
|
1682
|
+
],
|
|
1683
|
+
[
|
|
1684
|
+
1,
|
|
1685
|
+
-1,
|
|
1686
|
+
-1
|
|
1687
|
+
],
|
|
1688
|
+
[
|
|
1689
|
+
-1,
|
|
1690
|
+
-1,
|
|
1691
|
+
1
|
|
1692
|
+
],
|
|
1693
|
+
[
|
|
1694
|
+
1,
|
|
1695
|
+
-1,
|
|
1696
|
+
1
|
|
1697
|
+
],
|
|
1698
|
+
[
|
|
1699
|
+
-1,
|
|
1700
|
+
1,
|
|
1701
|
+
-1
|
|
1702
|
+
],
|
|
1703
|
+
[
|
|
1704
|
+
1,
|
|
1705
|
+
1,
|
|
1706
|
+
-1
|
|
1707
|
+
],
|
|
1708
|
+
[
|
|
1709
|
+
-1,
|
|
1710
|
+
1,
|
|
1711
|
+
1
|
|
1712
|
+
],
|
|
1713
|
+
[
|
|
1714
|
+
1,
|
|
1715
|
+
1,
|
|
1716
|
+
1
|
|
1717
|
+
]
|
|
1718
|
+
];
|
|
1719
|
+
const typeNameOf = (node) => node.constructor.typeName;
|
|
1720
|
+
function collectWaters(node, out) {
|
|
1721
|
+
if (node instanceof Water3D) out.push(node);
|
|
1722
|
+
for (const child of node.children) collectWaters(child, out);
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Half extents and volume of a collider, from the shape Rapier built — so an
|
|
1726
|
+
* `auto` box, a fitted hull and an authored sphere all answer. A hull or mesh
|
|
1727
|
+
* is taken as the box around its vertices: a rock floats like its crate,
|
|
1728
|
+
* which is the right answer for a rock and a wrong one nobody will ship.
|
|
1729
|
+
*/
|
|
1730
|
+
function hullOf(collider, capsuleType) {
|
|
1731
|
+
const he = collider.halfExtents();
|
|
1732
|
+
if (he) return {
|
|
1733
|
+
hx: he.x,
|
|
1734
|
+
hy: he.y,
|
|
1735
|
+
hz: he.z,
|
|
1736
|
+
volume: 8 * he.x * he.y * he.z
|
|
1737
|
+
};
|
|
1738
|
+
const r = collider.radius();
|
|
1739
|
+
const hh = collider.halfHeight();
|
|
1740
|
+
if (r !== null && hh !== null && hh > 0) {
|
|
1741
|
+
const capsule = collider.shapeType() === capsuleType;
|
|
1742
|
+
const caps = capsule ? 4 / 3 * Math.PI * r ** 3 : 0;
|
|
1743
|
+
return {
|
|
1744
|
+
hx: r,
|
|
1745
|
+
hy: hh + (capsule ? r : 0),
|
|
1746
|
+
hz: r,
|
|
1747
|
+
volume: Math.PI * r * r * 2 * hh + caps
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
if (r !== null) return {
|
|
1751
|
+
hx: r,
|
|
1752
|
+
hy: r,
|
|
1753
|
+
hz: r,
|
|
1754
|
+
volume: 4 / 3 * Math.PI * r ** 3
|
|
1755
|
+
};
|
|
1756
|
+
const v = collider.vertices();
|
|
1757
|
+
if (!v || v.length < 3) return null;
|
|
1758
|
+
let maxX = 0;
|
|
1759
|
+
let maxY = 0;
|
|
1760
|
+
let maxZ = 0;
|
|
1761
|
+
for (let i = 0; i + 2 < v.length; i += 3) {
|
|
1762
|
+
maxX = Math.max(maxX, Math.abs(v[i] ?? 0));
|
|
1763
|
+
maxY = Math.max(maxY, Math.abs(v[i + 1] ?? 0));
|
|
1764
|
+
maxZ = Math.max(maxZ, Math.abs(v[i + 2] ?? 0));
|
|
1765
|
+
}
|
|
1766
|
+
if (maxX === 0 || maxY === 0 || maxZ === 0) return null;
|
|
1767
|
+
return {
|
|
1768
|
+
hx: maxX,
|
|
1769
|
+
hy: maxY,
|
|
1770
|
+
hz: maxZ,
|
|
1771
|
+
volume: 8 * maxX * maxY * maxZ
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1171
1774
|
function collectJoints(node, out) {
|
|
1172
1775
|
if (node instanceof Joint3D) out.add(node);
|
|
1173
1776
|
for (const c of node.children) collectJoints(c, out);
|
|
@@ -1379,39 +1982,58 @@ function characterHalfHeight(node) {
|
|
|
1379
1982
|
if (collider.shape === "sphere" && typeof collider.radius === "number") return collider.radius;
|
|
1380
1983
|
return 1;
|
|
1381
1984
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1985
|
+
const poseChain = [];
|
|
1986
|
+
const poseV = new Vector3();
|
|
1987
|
+
const poseQ = new Quaternion();
|
|
1988
|
+
const poseStepQ = new Quaternion();
|
|
1989
|
+
const poseEuler = new Euler();
|
|
1990
|
+
/**
|
|
1991
|
+
* The WORLD pose of `node` — position and orientation composed through every
|
|
1992
|
+
* Node3D ancestor, the way the renderer composes them. This used to sum
|
|
1993
|
+
* positions only: a child two metres down +z of a hull turned 90° was put at
|
|
1994
|
+
* world +z while the renderer drew it at world +x, so a guard's grab box "in
|
|
1995
|
+
* front" of a turned guard, a turret's muzzle and a tank's barrel-tip sensor
|
|
1996
|
+
* all sat where nothing was drawn. `outQuat` may be omitted when only the
|
|
1997
|
+
* position is wanted. Scale is not composed (it never was).
|
|
1998
|
+
*/
|
|
1999
|
+
function worldPose3D(node, outPos, outQuat) {
|
|
2000
|
+
poseChain.length = 0;
|
|
2001
|
+
for (let n = node; n; n = n.parent) if (n instanceof Node3D) poseChain.push(n);
|
|
2002
|
+
outPos.set(0, 0, 0);
|
|
2003
|
+
const q = outQuat ?? poseQ;
|
|
2004
|
+
q.identity();
|
|
2005
|
+
for (let i = poseChain.length - 1; i >= 0; i--) {
|
|
2006
|
+
const n = poseChain[i];
|
|
2007
|
+
poseV.set(n.position[0] ?? 0, n.position[1] ?? 0, n.position[2] ?? 0).applyQuaternion(q);
|
|
2008
|
+
outPos.add(poseV);
|
|
2009
|
+
const r = n.rotation;
|
|
2010
|
+
if ((r[0] ?? 0) !== 0 || (r[1] ?? 0) !== 0 || (r[2] ?? 0) !== 0) {
|
|
2011
|
+
poseStepQ.setFromEuler(poseEuler.set((r[0] ?? 0) * DEG, (r[1] ?? 0) * DEG, (r[2] ?? 0) * DEG, "XYZ"));
|
|
2012
|
+
q.multiply(poseStepQ);
|
|
2013
|
+
}
|
|
1390
2014
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
2015
|
+
}
|
|
2016
|
+
const wpV = new Vector3();
|
|
2017
|
+
function worldPosition3D(node) {
|
|
2018
|
+
worldPose3D(node, wpV, null);
|
|
2019
|
+
posScratch[0] = wpV.x;
|
|
2020
|
+
posScratch[1] = wpV.y;
|
|
2021
|
+
posScratch[2] = wpV.z;
|
|
1394
2022
|
return posScratch;
|
|
1395
2023
|
}
|
|
1396
|
-
const
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
/**
|
|
1402
|
-
function
|
|
1403
|
-
let
|
|
1404
|
-
let y = 0;
|
|
1405
|
-
let z = 0;
|
|
2024
|
+
const parentPosV = new Vector3();
|
|
2025
|
+
const parentQuatQ = new Quaternion();
|
|
2026
|
+
const localV = new Vector3();
|
|
2027
|
+
const localQ = new Quaternion();
|
|
2028
|
+
const invQ = new Quaternion();
|
|
2029
|
+
/** The pose of `node`'s PARENT frame in world space — what a body's world pose is measured against. */
|
|
2030
|
+
function parentPose3D(node) {
|
|
2031
|
+
let parent = null;
|
|
1406
2032
|
for (let n = node.parent; n; n = n.parent) if (n instanceof Node3D) {
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
z += n.position[2] ?? 0;
|
|
2033
|
+
parent = n;
|
|
2034
|
+
break;
|
|
1410
2035
|
}
|
|
1411
|
-
|
|
1412
|
-
offsetScratch[1] = y;
|
|
1413
|
-
offsetScratch[2] = z;
|
|
1414
|
-
return offsetScratch;
|
|
2036
|
+
worldPose3D(parent, parentPosV, parentQuatQ);
|
|
1415
2037
|
}
|
|
1416
2038
|
function parseCollider3D(R, node) {
|
|
1417
2039
|
const collider = node.collider;
|