@series-inc/rundot-syncplay 5.23.0 → 5.24.0-beta.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/README.md +20 -0
- package/dist/browser.d.ts +4 -1
- package/dist/browser.js +1 -0
- package/dist/checksum-basis.d.ts +4 -0
- package/dist/checksum-basis.js +17 -0
- package/dist/cjs/browser.js +4 -2
- package/dist/cjs/checksum-basis.js +22 -0
- package/dist/cjs/collider-cooking.js +834 -0
- package/dist/cjs/deterministic-session.js +2 -0
- package/dist/cjs/engine-parity-matrix.js +32 -9
- package/dist/cjs/engine-parity.js +1 -1
- package/dist/cjs/index.js +8 -2
- package/dist/cjs/instant-replay.js +4 -9
- package/dist/cjs/movement3d.js +17 -1
- package/dist/cjs/networked-client.js +11 -9
- package/dist/cjs/node.js +5 -1
- package/dist/cjs/offline-session.js +52 -24
- package/dist/cjs/physics-cert.js +7 -2
- package/dist/cjs/physics3d-hull.js +381 -0
- package/dist/cjs/physics3d-joints.js +163 -31
- package/dist/cjs/physics3d-recipes.js +197 -0
- package/dist/cjs/physics3d-shared.js +6 -0
- package/dist/cjs/physics3d-solver.js +746 -89
- package/dist/cjs/physics3d-stacking.js +154 -0
- package/dist/cjs/physics3d-toi.js +251 -0
- package/dist/cjs/physics3d-vehicle.js +35 -10
- package/dist/cjs/physics3d.js +401 -20
- package/dist/cjs/runtime-support.js +71 -0
- package/dist/cjs/session-promotion.js +5 -5
- package/dist/cjs/synctest.js +49 -0
- package/dist/cjs/testing.js +4 -1
- package/dist/cli.js +163 -41
- package/dist/collider-cooking.d.ts +141 -0
- package/dist/collider-cooking.js +829 -0
- package/dist/deterministic-session.js +2 -0
- package/dist/engine-parity-matrix.js +32 -9
- package/dist/engine-parity.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/instant-replay.d.ts +2 -2
- package/dist/instant-replay.js +4 -9
- package/dist/movement3d.js +18 -2
- package/dist/networked-client.js +11 -9
- package/dist/node.d.ts +2 -0
- package/dist/node.js +2 -0
- package/dist/offline-session.js +53 -25
- package/dist/physics-cert.js +7 -2
- package/dist/physics3d-hull.d.ts +53 -0
- package/dist/physics3d-hull.js +376 -0
- package/dist/physics3d-joints.d.ts +7 -0
- package/dist/physics3d-joints.js +163 -31
- package/dist/physics3d-recipes.d.ts +29 -0
- package/dist/physics3d-recipes.js +193 -0
- package/dist/physics3d-shared.js +6 -0
- package/dist/physics3d-solver.d.ts +65 -1
- package/dist/physics3d-solver.js +744 -89
- package/dist/physics3d-stacking.d.ts +26 -0
- package/dist/physics3d-stacking.js +147 -0
- package/dist/physics3d-toi.d.ts +70 -0
- package/dist/physics3d-toi.js +245 -0
- package/dist/physics3d-vehicle.js +35 -10
- package/dist/physics3d.d.ts +20 -2
- package/dist/physics3d.js +400 -21
- package/dist/runtime-support.d.ts +60 -0
- package/dist/runtime-support.js +66 -0
- package/dist/session-promotion.js +5 -5
- package/dist/synctest.d.ts +1 -1
- package/dist/synctest.js +49 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +1 -0
- package/dist/types.d.ts +21 -2
- package/package.json +8 -3
package/dist/cjs/physics3d.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.applyDeterministicImpulse3D = applyDeterministicImpulse3D;
|
|
|
7
7
|
exports.setDeterministicBodyVelocity3D = setDeterministicBodyVelocity3D;
|
|
8
8
|
exports.stepDeterministicPhysicsWorld3D = stepDeterministicPhysicsWorld3D;
|
|
9
9
|
exports.deterministicPhysicsBodyById3D = deterministicPhysicsBodyById3D;
|
|
10
|
+
exports.__staticTreeBuildCount = __staticTreeBuildCount;
|
|
10
11
|
exports.raycastDeterministicPhysics3D = raycastDeterministicPhysics3D;
|
|
11
12
|
exports.raycastDeterministicPhysics3DDetailed = raycastDeterministicPhysics3DDetailed;
|
|
12
13
|
exports.overlapDeterministicPhysics3D = overlapDeterministicPhysics3D;
|
|
@@ -20,11 +21,14 @@ exports.runDeterministicPhysics3DDetailFixture = runDeterministicPhysics3DDetail
|
|
|
20
21
|
exports.runDeterministicPhysics3DStress = runDeterministicPhysics3DStress;
|
|
21
22
|
exports.runDeterministicPhysics3DStackFixture = runDeterministicPhysics3DStackFixture;
|
|
22
23
|
exports.deriveInverseInertia = deriveInverseInertia;
|
|
24
|
+
exports.__countCandidatePairsCalls = __countCandidatePairsCalls;
|
|
23
25
|
const canonical_js_1 = require("./canonical.js");
|
|
24
26
|
const deterministic_primitives_js_1 = require("./deterministic-primitives.js");
|
|
25
27
|
const physics3d_shared_js_1 = require("./physics3d-shared.js");
|
|
26
28
|
const physics3d_solver_js_1 = require("./physics3d-solver.js");
|
|
27
29
|
const physics3d_joints_js_1 = require("./physics3d-joints.js");
|
|
30
|
+
const physics3d_hull_js_1 = require("./physics3d-hull.js");
|
|
31
|
+
const physics3d_toi_js_1 = require("./physics3d-toi.js");
|
|
28
32
|
exports.deterministicPhysicsCallback3D = {
|
|
29
33
|
collisionEnter: 1,
|
|
30
34
|
collisionStay: 2,
|
|
@@ -288,9 +292,13 @@ function stepDeterministicPhysicsWorld3D(world, options = {}) {
|
|
|
288
292
|
if (!Number.isInteger(velocityIterations) || velocityIterations <= 0) {
|
|
289
293
|
throw new Error(`physics3d step: velocityIterations must be a positive integer, received ${velocityIterations}`);
|
|
290
294
|
}
|
|
295
|
+
const substeps = options.substeps ?? physics3d_solver_js_1.SOLVER_CONSTANTS.SUBSTEPS;
|
|
296
|
+
if (!Number.isInteger(substeps) || substeps <= 0) {
|
|
297
|
+
throw new Error(`physics3d step: substeps must be a positive integer, received ${substeps}`);
|
|
298
|
+
}
|
|
291
299
|
const dt = options.dtTicks ?? 1;
|
|
292
300
|
const gravityY = options.gravityY ?? 0;
|
|
293
|
-
const rigid = runRigidSolver(world, dt, gravityY, velocityIterations, options.joints ?? []);
|
|
301
|
+
const rigid = runRigidSolver(world, dt, gravityY, velocityIterations, substeps, options.joints ?? [], options.pairs);
|
|
294
302
|
const constrained = solveConstraints3D(rigid.bodies, options.constraints ?? []);
|
|
295
303
|
const solvedBodies = constrained.bodies;
|
|
296
304
|
// Dynamic-involved solid-collision events come from the solver's manifolds (so
|
|
@@ -365,7 +373,9 @@ function stepDeterministicPhysicsWorld3D(world, options = {}) {
|
|
|
365
373
|
scheduledQueryResults,
|
|
366
374
|
prePhysicsScheduledQueryResults,
|
|
367
375
|
postPhysicsScheduledQueryResults,
|
|
368
|
-
broadphasePairs:
|
|
376
|
+
broadphasePairs: options.computeBroadphasePairs === true
|
|
377
|
+
? countCandidatePairs(solvedBodies)
|
|
378
|
+
: 0,
|
|
369
379
|
narrowphaseContacts: rigid.narrowphaseContacts,
|
|
370
380
|
solverCorrections,
|
|
371
381
|
checksum: options.computeChecksum === false ? '' : (0, canonical_js_1.defaultChecksum)({ nextWorld, scheduledQueryResults, events, solverCorrections }),
|
|
@@ -379,7 +389,9 @@ function fastStressStepPhysicsWorld3D(world, scheduledQueries) {
|
|
|
379
389
|
const nextWorld = {
|
|
380
390
|
frame: world.frame + 1,
|
|
381
391
|
bodies: world.bodies.map((body) => {
|
|
382
|
-
|
|
392
|
+
// Kinematic bodies advance on their authored velocity here too — the
|
|
393
|
+
// extrapolated path skips the solver, not the author's driving motion.
|
|
394
|
+
if (body.kind === 'static') {
|
|
383
395
|
return body;
|
|
384
396
|
}
|
|
385
397
|
return {
|
|
@@ -538,7 +550,12 @@ function buildQueryIndexTree(bodies) {
|
|
|
538
550
|
* subtree — usually the bulk of a level — is reused frame over frame; only
|
|
539
551
|
* the dynamic subtree is rebuilt. Identity comparison makes reuse exact.
|
|
540
552
|
*/
|
|
541
|
-
|
|
553
|
+
const STATIC_TREE_CACHE_SLOTS = 4;
|
|
554
|
+
const staticQueryTreeCache = [];
|
|
555
|
+
let staticTreeBuildCount = 0;
|
|
556
|
+
function __staticTreeBuildCount() {
|
|
557
|
+
return staticTreeBuildCount;
|
|
558
|
+
}
|
|
542
559
|
function sameBodyIdentities(left, right) {
|
|
543
560
|
if (left.length !== right.length) {
|
|
544
561
|
return false;
|
|
@@ -550,6 +567,21 @@ function sameBodyIdentities(left, right) {
|
|
|
550
567
|
}
|
|
551
568
|
return true;
|
|
552
569
|
}
|
|
570
|
+
function staticTreeFor(statics) {
|
|
571
|
+
const hit = staticQueryTreeCache.findIndex((entry) => sameBodyIdentities(entry.statics, statics));
|
|
572
|
+
if (hit !== -1) {
|
|
573
|
+
const [entry] = staticQueryTreeCache.splice(hit, 1);
|
|
574
|
+
staticQueryTreeCache.unshift(entry);
|
|
575
|
+
return entry.tree;
|
|
576
|
+
}
|
|
577
|
+
const tree = buildQueryIndexTree(statics);
|
|
578
|
+
staticTreeBuildCount += 1;
|
|
579
|
+
staticQueryTreeCache.unshift({ statics, tree });
|
|
580
|
+
if (staticQueryTreeCache.length > STATIC_TREE_CACHE_SLOTS) {
|
|
581
|
+
staticQueryTreeCache.pop();
|
|
582
|
+
}
|
|
583
|
+
return tree;
|
|
584
|
+
}
|
|
553
585
|
function queryIndexFor(world) {
|
|
554
586
|
const cached = queryIndexCache.get(world);
|
|
555
587
|
if (cached !== undefined) {
|
|
@@ -565,14 +597,7 @@ function queryIndexFor(world) {
|
|
|
565
597
|
movers.push(body);
|
|
566
598
|
}
|
|
567
599
|
}
|
|
568
|
-
|
|
569
|
-
if (staticQueryTreeCache !== undefined && sameBodyIdentities(staticQueryTreeCache.statics, statics)) {
|
|
570
|
-
staticTree = staticQueryTreeCache.tree;
|
|
571
|
-
}
|
|
572
|
-
else {
|
|
573
|
-
staticTree = buildQueryIndexTree(statics);
|
|
574
|
-
staticQueryTreeCache = { statics, tree: staticTree };
|
|
575
|
-
}
|
|
600
|
+
const staticTree = staticTreeFor(statics);
|
|
576
601
|
const index = {
|
|
577
602
|
trees: movers.length > 0 ? [staticTree, buildQueryIndexTree(movers)] : [staticTree],
|
|
578
603
|
bodyById: new Map(world.bodies.map((body) => [body.id, body])),
|
|
@@ -1084,7 +1109,11 @@ function runDeterministicPhysics3DStress(bodies, frames, options = {}) {
|
|
|
1084
1109
|
},
|
|
1085
1110
|
];
|
|
1086
1111
|
const result = shouldRunFullStep
|
|
1087
|
-
? stepDeterministicPhysicsWorld3D(world, {
|
|
1112
|
+
? stepDeterministicPhysicsWorld3D(world, {
|
|
1113
|
+
computeChecksum: false,
|
|
1114
|
+
computeBroadphasePairs: true,
|
|
1115
|
+
scheduledQueries,
|
|
1116
|
+
})
|
|
1088
1117
|
: fastStressStepPhysicsWorld3D(world, scheduledQueries);
|
|
1089
1118
|
const shapeCastHits = frame % 64 === 0
|
|
1090
1119
|
? shapeCastDeterministicPhysics3D(world, {
|
|
@@ -1231,7 +1260,11 @@ function runPhysics3DStackWorld(initialWorld, frames) {
|
|
|
1231
1260
|
if (frame === snapshotFrame) {
|
|
1232
1261
|
snapshot = (0, canonical_js_1.cloneCanonical)(world);
|
|
1233
1262
|
}
|
|
1234
|
-
const result = stepDeterministicPhysicsWorld3D(world, {
|
|
1263
|
+
const result = stepDeterministicPhysicsWorld3D(world, {
|
|
1264
|
+
gravityY: -0.001,
|
|
1265
|
+
computeChecksum: false,
|
|
1266
|
+
computeBroadphasePairs: true,
|
|
1267
|
+
});
|
|
1235
1268
|
world = result.world;
|
|
1236
1269
|
contactCount += result.narrowphaseContacts;
|
|
1237
1270
|
solverCorrections += result.solverCorrections;
|
|
@@ -1304,7 +1337,9 @@ function callbackFixtureBody(id, x, trigger, callbackMask) {
|
|
|
1304
1337
|
}
|
|
1305
1338
|
const DEFAULT_LINEAR_DAMPING = 0.995;
|
|
1306
1339
|
const DEFAULT_ANGULAR_DAMPING = 0.98;
|
|
1307
|
-
|
|
1340
|
+
// Per-substep iterations under the soft-step (TGS-Soft) scheme; the total solve
|
|
1341
|
+
// passes are substeps × (velocityIterations + 1 relax). Was 10 pre-soft-step.
|
|
1342
|
+
const DEFAULT_VELOCITY_ITERATIONS = 3;
|
|
1308
1343
|
function toSolverPrimitiveShape(shape) {
|
|
1309
1344
|
if (shape.type === 'box') {
|
|
1310
1345
|
return { kind: 'box', half: { x: shape.halfX, y: shape.halfY, z: shape.halfZ } };
|
|
@@ -1312,12 +1347,18 @@ function toSolverPrimitiveShape(shape) {
|
|
|
1312
1347
|
if (shape.type === 'sphere') {
|
|
1313
1348
|
return { kind: 'sphere', radius: shape.radius };
|
|
1314
1349
|
}
|
|
1350
|
+
if (shape.type === 'hull') {
|
|
1351
|
+
return { kind: 'hull', data: (0, physics3d_hull_js_1.buildSolverHull)(shape.vertices) };
|
|
1352
|
+
}
|
|
1315
1353
|
return { kind: 'capsule', radius: shape.radius, halfHeight: shape.halfHeight };
|
|
1316
1354
|
}
|
|
1317
1355
|
function toSolverShape(shape) {
|
|
1318
1356
|
if (shape.type === 'box' || shape.type === 'sphere' || shape.type === 'capsule') {
|
|
1319
1357
|
return toSolverPrimitiveShape(shape);
|
|
1320
1358
|
}
|
|
1359
|
+
if (shape.type === 'hull') {
|
|
1360
|
+
return { kind: 'hull', data: (0, physics3d_hull_js_1.buildSolverHull)(shape.vertices) };
|
|
1361
|
+
}
|
|
1321
1362
|
if (shape.type === 'compound') {
|
|
1322
1363
|
return {
|
|
1323
1364
|
kind: 'compound',
|
|
@@ -1356,8 +1397,8 @@ function deriveInverseInertia(shape, mass) {
|
|
|
1356
1397
|
const iLateral = (mass / 12) * (3 * r * r + h * h);
|
|
1357
1398
|
return { x: 1 / iLateral, y: 1 / iAxial, z: 1 / iLateral };
|
|
1358
1399
|
}
|
|
1359
|
-
if (shape.type === 'compound') {
|
|
1360
|
-
// union-AABB box approximation of the
|
|
1400
|
+
if (shape.type === 'compound' || shape.type === 'hull') {
|
|
1401
|
+
// union-AABB box approximation of the inertia (v1 — same as compound).
|
|
1361
1402
|
const bounds = shapePoseBounds({ x: 0, y: 0, z: 0, shape });
|
|
1362
1403
|
const half = { x: (bounds.maxX - bounds.minX) / 2, y: (bounds.maxY - bounds.minY) / 2, z: (bounds.maxZ - bounds.minZ) / 2 };
|
|
1363
1404
|
return deriveInverseInertia({ type: 'box', halfX: Math.max(half.x, 1e-3), halfY: Math.max(half.y, 1e-3), halfZ: Math.max(half.z, 1e-3) }, mass);
|
|
@@ -1367,6 +1408,7 @@ function deriveInverseInertia(shape, mass) {
|
|
|
1367
1408
|
}
|
|
1368
1409
|
function buildSolverBody(body, index) {
|
|
1369
1410
|
const isDynamic = body.kind === 'dynamic';
|
|
1411
|
+
const isKinematic = body.kind === 'kinematic';
|
|
1370
1412
|
const mass = body.mass ?? 1;
|
|
1371
1413
|
const baseInvMass = isDynamic ? 1 / mass : 0;
|
|
1372
1414
|
const lockLinear = body.lockLinear ?? {};
|
|
@@ -1376,6 +1418,7 @@ function buildSolverBody(body, index) {
|
|
|
1376
1418
|
index,
|
|
1377
1419
|
id: body.id,
|
|
1378
1420
|
isDynamic,
|
|
1421
|
+
isKinematic,
|
|
1379
1422
|
sleeping: body.sleeping === true,
|
|
1380
1423
|
p: { x: body.x, y: body.y, z: body.z },
|
|
1381
1424
|
q: body.orientation ?? { x: 0, y: 0, z: 0, w: 1 },
|
|
@@ -1439,7 +1482,236 @@ function warmCacheToMap(cache, indexById) {
|
|
|
1439
1482
|
}
|
|
1440
1483
|
return warm;
|
|
1441
1484
|
}
|
|
1442
|
-
|
|
1485
|
+
/**
|
|
1486
|
+
* Per-step fast-ccd set (box3d b3_isFast) with each fast body's exact first
|
|
1487
|
+
* time-of-impact. A body qualifies only when it is dynamic, awake, `ccd`, collidable
|
|
1488
|
+
* (not a trigger/sensor), has a positive minExtent and a GJK-supported shape, and its
|
|
1489
|
+
* axis-lock-masked intended motion exceeds `CCD_FAST_MOTION_FRACTION · minExtent`.
|
|
1490
|
+
*
|
|
1491
|
+
* Candidate targets are found with a cheap conservative bounding-sphere cast through
|
|
1492
|
+
* the shared query index, then the EXACT surface TOI against each eligible candidate is
|
|
1493
|
+
* computed by GJK conservative advancement (`conservativeToi`); the nearest is kept. A
|
|
1494
|
+
* grazing/separating approach yields no TOI (the exact sweep never touches), so nothing
|
|
1495
|
+
* to clamp — the body slides by. Derived fresh every step (never persisted) so it is
|
|
1496
|
+
* rollback-safe; iterated in body-index order.
|
|
1497
|
+
*/
|
|
1498
|
+
function computeFastCcdClamps(world, source, solverBodies, dt, indexById, noCollidePairs) {
|
|
1499
|
+
const clamps = new Map();
|
|
1500
|
+
for (let index = 0; index < solverBodies.length; index += 1) {
|
|
1501
|
+
const sb = solverBodies[index];
|
|
1502
|
+
if (!sb.isDynamic || !sb.ccd || sb.sleeping || sb.noCollide) {
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
const minExtent = (0, physics3d_solver_js_1.minExtentOf)(sb.shape, sb.q);
|
|
1506
|
+
if (!(minExtent > 0)) {
|
|
1507
|
+
continue; // degenerate (flat/zero-extent) shape ⇒ never fast (no divide)
|
|
1508
|
+
}
|
|
1509
|
+
// Intended per-step motion is the solver-effective velocity × dt; a solver-locked
|
|
1510
|
+
// axis (invMassVec component 0) contributes no motion, so a locked axis is ignored.
|
|
1511
|
+
const dx = sb.invMassVec.x === 0 ? 0 : sb.v.x * dt;
|
|
1512
|
+
const dy = sb.invMassVec.y === 0 ? 0 : sb.v.y * dt;
|
|
1513
|
+
const dz = sb.invMassVec.z === 0 ? 0 : sb.v.z * dt;
|
|
1514
|
+
const motion = (0, physics3d_shared_js_1.deterministicSqrt)(dx * dx + dy * dy + dz * dz);
|
|
1515
|
+
if (motion <= physics3d_solver_js_1.SOLVER_CONSTANTS.CCD_FAST_MOTION_FRACTION * minExtent) {
|
|
1516
|
+
continue; // slow enough that ordinary discrete contact suffices
|
|
1517
|
+
}
|
|
1518
|
+
const fastProxy = (0, physics3d_toi_js_1.toiProxyOf)(sb.shape, sb.p, sb.q);
|
|
1519
|
+
if (fastProxy === null) {
|
|
1520
|
+
continue; // mesh/compound ⇒ leave to the discrete solver
|
|
1521
|
+
}
|
|
1522
|
+
const invMotion = 1 / motion;
|
|
1523
|
+
const dirX = dx * invMotion;
|
|
1524
|
+
const dirY = dy * invMotion;
|
|
1525
|
+
const dirZ = dz * invMotion;
|
|
1526
|
+
const hit = firstExactToi(world, source, solverBodies, source[index], index, fastProxy, dirX, dirY, dirZ, motion, indexById, noCollidePairs);
|
|
1527
|
+
if (hit === undefined) {
|
|
1528
|
+
continue; // nothing eligible actually in the swept path ⇒ leave to the solver
|
|
1529
|
+
}
|
|
1530
|
+
clamps.set(index, {
|
|
1531
|
+
prePos: { x: sb.p.x, y: sb.p.y, z: sb.p.z },
|
|
1532
|
+
dirX,
|
|
1533
|
+
dirY,
|
|
1534
|
+
dirZ,
|
|
1535
|
+
maxAdvance: hit.toi,
|
|
1536
|
+
normalX: hit.normal.x,
|
|
1537
|
+
normalY: hit.normal.y,
|
|
1538
|
+
normalZ: hit.normal.z,
|
|
1539
|
+
targetIndices: hit.targetIndices,
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
return clamps;
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Exact first surface TOI for a fast body swept along (dir) over `motion`. A conservative
|
|
1546
|
+
* bounding-sphere cast prunes candidates through the shared query index (a superset of
|
|
1547
|
+
* real hits); the exact TOI against each eligible candidate is then found by GJK
|
|
1548
|
+
* conservative advancement, and the nearest is returned. Eligibility mirrors the narrow
|
|
1549
|
+
* phase (self, trigger/sensor, two-way layer/mask, and `disableCollision`-suppressed
|
|
1550
|
+
* pairs excluded). Returns undefined when nothing eligible is actually struck within the
|
|
1551
|
+
* sweep (including a grazing pass that the fat bounding sphere flags but the true shapes
|
|
1552
|
+
* miss).
|
|
1553
|
+
*/
|
|
1554
|
+
function firstExactToi(world, source, solverBodies, fastBody, fastIndex, fastProxy, dirX, dirY, dirZ, motion, indexById, noCollidePairs) {
|
|
1555
|
+
const sb = solverBodies[fastIndex];
|
|
1556
|
+
const hits = shapeCastDeterministicPhysics3D(world, {
|
|
1557
|
+
shape: { type: 'sphere', radius: (0, physics3d_solver_js_1.boundingRadiusOf)(sb.shape) },
|
|
1558
|
+
x: sb.p.x,
|
|
1559
|
+
y: sb.p.y,
|
|
1560
|
+
z: sb.p.z,
|
|
1561
|
+
dx: dirX,
|
|
1562
|
+
dy: dirY,
|
|
1563
|
+
dz: dirZ,
|
|
1564
|
+
maxDistance: motion,
|
|
1565
|
+
hitTriggers: false,
|
|
1566
|
+
hitSensors: false,
|
|
1567
|
+
});
|
|
1568
|
+
// Collect every eligible body struck at (or within slop of) the first TOI — a body
|
|
1569
|
+
// driven into the seam between two stacked boxes contacts both, and sharing the impact
|
|
1570
|
+
// impulse across them keeps each one's momentum below the coupled relax's stability
|
|
1571
|
+
// limit (a single concentrated impulse diverges the stack).
|
|
1572
|
+
let bestToi = Infinity;
|
|
1573
|
+
let bestNormal = { x: 0, y: 0, z: 0 };
|
|
1574
|
+
const targetIndices = [];
|
|
1575
|
+
for (const hit of hits) {
|
|
1576
|
+
if (hit.bodyId === fastBody.id) {
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
const targetIndex = indexById.get(hit.bodyId);
|
|
1580
|
+
if (targetIndex === undefined) {
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
const target = source[targetIndex];
|
|
1584
|
+
if ((fastBody.mask & target.layer) === 0 || (target.mask & fastBody.layer) === 0) {
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
const key = Math.min(fastIndex, targetIndex) * physics3d_solver_js_1.WARM_KEY_BASE + Math.max(fastIndex, targetIndex);
|
|
1588
|
+
if (noCollidePairs.has(key)) {
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
const tb = solverBodies[targetIndex];
|
|
1592
|
+
const targetProxy = (0, physics3d_toi_js_1.toiProxyOf)(tb.shape, tb.p, tb.q);
|
|
1593
|
+
if (targetProxy === null) {
|
|
1594
|
+
continue; // mesh/compound target has no convex proxy — CCD unsupported (Gap 2), discrete owns it
|
|
1595
|
+
}
|
|
1596
|
+
const toiHit = (0, physics3d_toi_js_1.conservativeToi)(fastProxy, dirX, dirY, dirZ, motion, targetProxy, physics3d_solver_js_1.SOLVER_CONSTANTS.SLOP);
|
|
1597
|
+
if (toiHit === undefined) {
|
|
1598
|
+
continue; // clean graze — not actually struck, look past it
|
|
1599
|
+
}
|
|
1600
|
+
if ('overlapping' in toiHit) {
|
|
1601
|
+
// The fast body already touches this body at the step start — a zero-distance
|
|
1602
|
+
// contact. Resolve the impact HERE (advance 0, arrest along the motion axis)
|
|
1603
|
+
// instead of skipping it to bind a farther target's TOI (which would sweep the
|
|
1604
|
+
// fast body through this one). Overlaps take priority over any positive TOI.
|
|
1605
|
+
if (bestToi > 0) {
|
|
1606
|
+
bestToi = 0;
|
|
1607
|
+
bestNormal = { x: -dirX, y: -dirY, z: -dirZ };
|
|
1608
|
+
targetIndices.length = 0;
|
|
1609
|
+
targetIndices.push(targetIndex);
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
targetIndices.push(targetIndex); // another overlapping body at zero distance
|
|
1613
|
+
}
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
if (bestToi === 0) {
|
|
1617
|
+
continue; // a zero-distance overlap already owns this step; ignore positive TOIs
|
|
1618
|
+
}
|
|
1619
|
+
if (toiHit.distance < bestToi - physics3d_solver_js_1.SOLVER_CONSTANTS.SLOP) {
|
|
1620
|
+
bestToi = toiHit.distance;
|
|
1621
|
+
bestNormal = toiHit.normal;
|
|
1622
|
+
targetIndices.length = 0;
|
|
1623
|
+
targetIndices.push(targetIndex);
|
|
1624
|
+
}
|
|
1625
|
+
else if (toiHit.distance <= bestToi + physics3d_solver_js_1.SOLVER_CONSTANTS.SLOP) {
|
|
1626
|
+
// A tie at the first contact (e.g. both boxes of a seam).
|
|
1627
|
+
if (toiHit.distance < bestToi) {
|
|
1628
|
+
bestToi = toiHit.distance;
|
|
1629
|
+
bestNormal = toiHit.normal;
|
|
1630
|
+
}
|
|
1631
|
+
targetIndices.push(targetIndex);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
return targetIndices.length === 0 ? undefined : { toi: bestToi, normal: bestNormal, targetIndices };
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Advance a fast body to its exact TOI contact and resolve the impact as a single
|
|
1638
|
+
* perfectly-inelastic (restitution 0) collision between it and every body struck at that
|
|
1639
|
+
* first contact: all of them are set to their common momentum-conserving normal velocity.
|
|
1640
|
+
* This transfers momentum to the struck bodies and removes the fast body's approach, so
|
|
1641
|
+
* the discrete step resolves a resting (not a plowing) contact — and, at a seam, sharing
|
|
1642
|
+
* the impact across both boxes keeps each below the coupled relax's stability limit. A
|
|
1643
|
+
* static/kinematic target has infinite mass along the normal, so the common velocity is
|
|
1644
|
+
* that surface's (a hard stop). Only the normal component changes; tangential is kept.
|
|
1645
|
+
*/
|
|
1646
|
+
function applyToiResponse(fast, solverBodies, clamp) {
|
|
1647
|
+
fast.p = {
|
|
1648
|
+
x: clamp.prePos.x + clamp.dirX * clamp.maxAdvance,
|
|
1649
|
+
y: clamp.prePos.y + clamp.dirY * clamp.maxAdvance,
|
|
1650
|
+
z: clamp.prePos.z + clamp.dirZ * clamp.maxAdvance,
|
|
1651
|
+
};
|
|
1652
|
+
const nx = clamp.normalX;
|
|
1653
|
+
const ny = clamp.normalY;
|
|
1654
|
+
const nz = clamp.normalZ;
|
|
1655
|
+
const normalMassAlong = (b) => {
|
|
1656
|
+
const k = nx * nx * b.invMassVec.x + ny * ny * b.invMassVec.y + nz * nz * b.invMassVec.z;
|
|
1657
|
+
return k > 0 ? 1 / k : Infinity; // effective mass along the contact normal
|
|
1658
|
+
};
|
|
1659
|
+
const setNormalVelocity = (b, vc) => {
|
|
1660
|
+
const vn = b.v.x * nx + b.v.y * ny + b.v.z * nz;
|
|
1661
|
+
const d = vc - vn;
|
|
1662
|
+
b.v = { x: b.v.x + nx * d, y: b.v.y + ny * d, z: b.v.z + nz * d };
|
|
1663
|
+
};
|
|
1664
|
+
const fastMass = normalMassAlong(fast); // dynamic ⇒ finite
|
|
1665
|
+
let sumMass = fastMass;
|
|
1666
|
+
let sumMomentum = fastMass * (fast.v.x * nx + fast.v.y * ny + fast.v.z * nz);
|
|
1667
|
+
let anyImmovable = false;
|
|
1668
|
+
let immovableVn = 0;
|
|
1669
|
+
for (const ti of clamp.targetIndices) {
|
|
1670
|
+
const t = solverBodies[ti];
|
|
1671
|
+
const tm = normalMassAlong(t);
|
|
1672
|
+
if (tm === Infinity) {
|
|
1673
|
+
anyImmovable = true;
|
|
1674
|
+
immovableVn = t.v.x * nx + t.v.y * ny + t.v.z * nz; // static/kinematic ⇒ its surface velocity
|
|
1675
|
+
}
|
|
1676
|
+
else {
|
|
1677
|
+
sumMass += tm;
|
|
1678
|
+
sumMomentum += tm * (t.v.x * nx + t.v.y * ny + t.v.z * nz);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
// Common post-collision normal velocity: a struck static/kinematic body pins it.
|
|
1682
|
+
const vc = anyImmovable ? immovableVn : sumMomentum / sumMass;
|
|
1683
|
+
// Only resolve if the fast body is actually approaching the contact (moving into it).
|
|
1684
|
+
if ((fast.v.x * nx + fast.v.y * ny + fast.v.z * nz) - vc >= 0) {
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
setNormalVelocity(fast, vc);
|
|
1688
|
+
for (const ti of clamp.targetIndices) {
|
|
1689
|
+
const t = solverBodies[ti];
|
|
1690
|
+
if (normalMassAlong(t) !== Infinity) {
|
|
1691
|
+
setNormalVelocity(t, vc);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Clamp a fast body's POSITION so it advances no farther than its first TOI along its
|
|
1697
|
+
* intended motion. Velocity is set by the pre-solve TOI response and the discrete solver;
|
|
1698
|
+
* this only removes any residual along-`dir` overshoot past the contact.
|
|
1699
|
+
*/
|
|
1700
|
+
function clampFastToToi(sb, clamp) {
|
|
1701
|
+
const s = (sb.p.x - clamp.prePos.x) * clamp.dirX
|
|
1702
|
+
+ (sb.p.y - clamp.prePos.y) * clamp.dirY
|
|
1703
|
+
+ (sb.p.z - clamp.prePos.z) * clamp.dirZ;
|
|
1704
|
+
if (s <= clamp.maxAdvance) {
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
const excess = s - clamp.maxAdvance;
|
|
1708
|
+
sb.p = {
|
|
1709
|
+
x: sb.p.x - clamp.dirX * excess,
|
|
1710
|
+
y: sb.p.y - clamp.dirY * excess,
|
|
1711
|
+
z: sb.p.z - clamp.dirZ * excess,
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
function runRigidSolver(world, dt, gravityY, velocityIterations, substeps, joints, pairs) {
|
|
1443
1715
|
const source = world.bodies;
|
|
1444
1716
|
const indexById = new Map(source.map((body, index) => [body.id, index]));
|
|
1445
1717
|
const solverBodies = source.map((body, index) => buildSolverBody(body, index));
|
|
@@ -1462,19 +1734,61 @@ function runRigidSolver(world, dt, gravityY, velocityIterations, joints) {
|
|
|
1462
1734
|
})
|
|
1463
1735
|
.filter((key) => key !== undefined);
|
|
1464
1736
|
const noCollidePairs = new Set(noCollideKeys);
|
|
1737
|
+
// Extreme-speed ccd (box3d model): flag fast bodies and find each one's exact first
|
|
1738
|
+
// time-of-impact. The discrete solve runs with a small speculative margin (fast ccd
|
|
1739
|
+
// bodies plow through during the step, staying finite); afterward each fast body's
|
|
1740
|
+
// POSITION is clamped to its TOI so it never tunnels, while its VELOCITY is left to
|
|
1741
|
+
// the solver so the next step's contact transfers momentum. Empty set ⇒ no clamp.
|
|
1742
|
+
const fastClamps = computeFastCcdClamps(world, source, solverBodies, dt, indexById, noCollidePairs);
|
|
1743
|
+
// Pre-solve TOI response (box3d-style, in stable body-index order): advance each fast
|
|
1744
|
+
// body to its exact first contact and apply a one-shot inelastic normal impulse that
|
|
1745
|
+
// transfers momentum to the struck body and arrests the approach, so the discrete step
|
|
1746
|
+
// never sees the fast body plow through a coupled cluster.
|
|
1747
|
+
for (const [index, clamp] of fastClamps) {
|
|
1748
|
+
applyToiResponse(solverBodies[index], solverBodies, clamp);
|
|
1749
|
+
}
|
|
1465
1750
|
const result = (0, physics3d_solver_js_1.solveRigidStep)(solverBodies, {
|
|
1466
1751
|
dt,
|
|
1467
1752
|
gravityY,
|
|
1468
1753
|
velocityIterations,
|
|
1754
|
+
substeps,
|
|
1469
1755
|
noCollidePairs: noCollidePairs.size > 0 ? noCollidePairs : undefined,
|
|
1470
1756
|
warm,
|
|
1757
|
+
pairs,
|
|
1471
1758
|
joints: solverJoints,
|
|
1472
1759
|
});
|
|
1760
|
+
// Post-solve safety clamp: no fast body may end past its TOI along its motion.
|
|
1761
|
+
for (const [index, clamp] of fastClamps) {
|
|
1762
|
+
clampFastToToi(solverBodies[index], clamp);
|
|
1763
|
+
}
|
|
1473
1764
|
const bodies = source.map((body, index) => {
|
|
1474
|
-
if (body.kind
|
|
1765
|
+
if (body.kind === 'static') {
|
|
1475
1766
|
return body;
|
|
1476
1767
|
}
|
|
1477
1768
|
const sb = solverBodies[index];
|
|
1769
|
+
if (body.kind === 'kinematic') {
|
|
1770
|
+
// Author-driven: the solver advanced the pose from the authored velocity
|
|
1771
|
+
// and never touched the velocity, mass, or sleep state.
|
|
1772
|
+
const moved = {
|
|
1773
|
+
...body,
|
|
1774
|
+
x: (0, physics3d_shared_js_1.quantizeDistance)(sb.p.x),
|
|
1775
|
+
y: (0, physics3d_shared_js_1.quantizeDistance)(sb.p.y),
|
|
1776
|
+
z: (0, physics3d_shared_js_1.quantizeDistance)(sb.p.z),
|
|
1777
|
+
};
|
|
1778
|
+
if (body.orientation === undefined) {
|
|
1779
|
+
// legacy quarter-turn decoration, same as the dynamic path
|
|
1780
|
+
return { ...moved, ...integrateAngularState(body, dt) };
|
|
1781
|
+
}
|
|
1782
|
+
return {
|
|
1783
|
+
...moved,
|
|
1784
|
+
orientation: {
|
|
1785
|
+
x: (0, physics3d_shared_js_1.quantizeDistance)(sb.q.x),
|
|
1786
|
+
y: (0, physics3d_shared_js_1.quantizeDistance)(sb.q.y),
|
|
1787
|
+
z: (0, physics3d_shared_js_1.quantizeDistance)(sb.q.z),
|
|
1788
|
+
w: (0, physics3d_shared_js_1.quantizeDistance)(sb.q.w),
|
|
1789
|
+
},
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1478
1792
|
const mapped = {
|
|
1479
1793
|
...body,
|
|
1480
1794
|
x: (0, physics3d_shared_js_1.quantizeDistance)(sb.p.x),
|
|
@@ -1577,7 +1891,7 @@ function detectContacts(bodies, keepPair) {
|
|
|
1577
1891
|
return contacts.sort((left, right) => compareStrings(left.pair, right.pair));
|
|
1578
1892
|
}
|
|
1579
1893
|
function solveConstraints3D(bodies, constraints) {
|
|
1580
|
-
const next = bodies
|
|
1894
|
+
const next = [...bodies];
|
|
1581
1895
|
let corrections = 0;
|
|
1582
1896
|
for (const constraint of [...constraints].sort((left, right) => compareStrings(left.id, right.id))) {
|
|
1583
1897
|
const aIndex = next.findIndex((body) => body.id === constraint.a);
|
|
@@ -1736,9 +2050,21 @@ function validatePhysicsBody3D(body) {
|
|
|
1736
2050
|
throw new Error(`physics3d body ${id}: orientation quaternion must have non-zero length`);
|
|
1737
2051
|
}
|
|
1738
2052
|
}
|
|
2053
|
+
if (body.shape.type === 'mesh' && body.kind === 'dynamic') {
|
|
2054
|
+
throw new Error(`physics3d body ${id}: mesh collider must be static or kinematic — cook a concave dynamic body into a compound of convex hulls`);
|
|
2055
|
+
}
|
|
1739
2056
|
validatePhysicsShape3D(id, body.shape);
|
|
1740
2057
|
}
|
|
1741
2058
|
function validatePhysicsShape3D(id, shape) {
|
|
2059
|
+
if (shape.type === 'hull') {
|
|
2060
|
+
try {
|
|
2061
|
+
(0, physics3d_hull_js_1.buildSolverHull)(shape.vertices);
|
|
2062
|
+
}
|
|
2063
|
+
catch (error) {
|
|
2064
|
+
throw new Error(`physics3d body ${id}: ${error.message}`);
|
|
2065
|
+
}
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
1742
2068
|
if (shape.type === 'box') {
|
|
1743
2069
|
for (const [name, value] of [['halfX', shape.halfX], ['halfY', shape.halfY], ['halfZ', shape.halfZ]]) {
|
|
1744
2070
|
if (!isFiniteNumber(value) || value <= 0) {
|
|
@@ -1847,6 +2173,9 @@ function shapePoseBounds(pose) {
|
|
|
1847
2173
|
}
|
|
1848
2174
|
return boundsFromPoints(boxLocalCorners(shape).map((point) => translateRotatedPoint(point, pose)));
|
|
1849
2175
|
}
|
|
2176
|
+
if (shape.type === 'hull') {
|
|
2177
|
+
return boundsFromPoints(shape.vertices.map((vertex) => translateRotatedPoint(vertex, pose)));
|
|
2178
|
+
}
|
|
1850
2179
|
if (shape.type === 'mesh') {
|
|
1851
2180
|
if (shape.vertices.length < 3 || shape.vertices.length % 3 !== 0) {
|
|
1852
2181
|
throw new Error('3D mesh collider requires triangle-list vertices');
|
|
@@ -1972,6 +2301,14 @@ function angularGeometryProof() {
|
|
|
1972
2301
|
&& meshHit.some((hit) => hit.bodyId === 'rotated-mesh')
|
|
1973
2302
|
&& overlap.includes('rotated-box'));
|
|
1974
2303
|
}
|
|
2304
|
+
function poseToSolverGeom(pose, index) {
|
|
2305
|
+
return {
|
|
2306
|
+
index,
|
|
2307
|
+
p: { x: pose.x, y: pose.y, z: pose.z },
|
|
2308
|
+
q: (0, physics3d_shared_js_1.quaternionFromQuarterTurns)({ x: pose.rx ?? 0, y: pose.ry ?? 0, z: pose.rz ?? 0 }),
|
|
2309
|
+
shape: toSolverShape(pose.shape),
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
1975
2312
|
function shapesOverlap3D(left, right) {
|
|
1976
2313
|
if (left.shape.type === 'compound') {
|
|
1977
2314
|
return compoundChildPoses(left).some((childPose) => shapesOverlap3D(childPose, right));
|
|
@@ -1979,6 +2316,11 @@ function shapesOverlap3D(left, right) {
|
|
|
1979
2316
|
if (right.shape.type === 'compound') {
|
|
1980
2317
|
return compoundChildPoses(right).some((childPose) => shapesOverlap3D(left, childPose));
|
|
1981
2318
|
}
|
|
2319
|
+
if (left.shape.type === 'hull' || right.shape.type === 'hull') {
|
|
2320
|
+
// Exact, orientation-aware: build solver geometry from each pose (mesh is
|
|
2321
|
+
// one-sided and handled by generateManifold's mesh dispatch).
|
|
2322
|
+
return (0, physics3d_solver_js_1.generateManifold)(poseToSolverGeom(left, 0), poseToSolverGeom(right, 1)) !== undefined;
|
|
2323
|
+
}
|
|
1982
2324
|
if (left.shape.type === 'mesh') {
|
|
1983
2325
|
return meshOverlapsShape(left, right);
|
|
1984
2326
|
}
|
|
@@ -2411,8 +2753,42 @@ function rayBodyDistance(body, query) {
|
|
|
2411
2753
|
if (body.shape.type === 'mesh') {
|
|
2412
2754
|
return rayMeshDistance(meshPose(bodyPose(body), body.shape), query);
|
|
2413
2755
|
}
|
|
2756
|
+
if (body.shape.type === 'hull') {
|
|
2757
|
+
return rayHullBodyDistance(body, body.shape, query);
|
|
2758
|
+
}
|
|
2414
2759
|
return rayAabbDistance(toBounds(body), query);
|
|
2415
2760
|
}
|
|
2761
|
+
function rayHullBodyDistance(body, shape, query) {
|
|
2762
|
+
// Oriented bodies already recursed with orientation cleared. The remaining
|
|
2763
|
+
// rotation is legacy quarter-turns; fold it into the ray so we can reuse the
|
|
2764
|
+
// cached (identity-space) hull half-planes rather than rebuilding per query.
|
|
2765
|
+
const hull = (0, physics3d_hull_js_1.buildSolverHull)(shape.vertices);
|
|
2766
|
+
const local = inverseRotateQuarterTurns({ x: query.x - body.x, y: query.y - body.y, z: query.z - body.z }, body.rx ?? 0, body.ry ?? 0, body.rz ?? 0);
|
|
2767
|
+
const localDir = inverseRotateQuarterTurns({ x: query.dx, y: query.dy, z: query.dz }, body.rx ?? 0, body.ry ?? 0, body.rz ?? 0);
|
|
2768
|
+
const interval = (0, physics3d_hull_js_1.rayHullIntervalLocal)(local, localDir, hull);
|
|
2769
|
+
if (interval === undefined) {
|
|
2770
|
+
return undefined;
|
|
2771
|
+
}
|
|
2772
|
+
const t = Math.max(0, interval.tEnter);
|
|
2773
|
+
if (t > query.maxDistance) {
|
|
2774
|
+
return undefined;
|
|
2775
|
+
}
|
|
2776
|
+
return (0, physics3d_shared_js_1.quantizeDistance)(t);
|
|
2777
|
+
}
|
|
2778
|
+
function inverseRotateQuarterTurns(point, rx, ry, rz) {
|
|
2779
|
+
// Inverse of rotateQuarterTurns: apply the opposite turns in reverse order.
|
|
2780
|
+
let rotated = point;
|
|
2781
|
+
for (let index = 0; index < (4 - quarterTurns(rz)) % 4; index += 1) {
|
|
2782
|
+
rotated = { x: -rotated.y, y: rotated.x, z: rotated.z };
|
|
2783
|
+
}
|
|
2784
|
+
for (let index = 0; index < (4 - quarterTurns(ry)) % 4; index += 1) {
|
|
2785
|
+
rotated = { x: rotated.z, y: rotated.y, z: -rotated.x };
|
|
2786
|
+
}
|
|
2787
|
+
for (let index = 0; index < (4 - quarterTurns(rx)) % 4; index += 1) {
|
|
2788
|
+
rotated = { x: rotated.x, y: -rotated.z, z: rotated.y };
|
|
2789
|
+
}
|
|
2790
|
+
return rotated;
|
|
2791
|
+
}
|
|
2416
2792
|
function shapeCastBodyDistance(body, query) {
|
|
2417
2793
|
if (body.orientation !== undefined) {
|
|
2418
2794
|
// Transform the cast into the body's local frame (isometry preserves the hit
|
|
@@ -2676,7 +3052,12 @@ function expandBoundsByBounds(bounds, extents) {
|
|
|
2676
3052
|
maxZ: bounds.maxZ - extents.minZ,
|
|
2677
3053
|
};
|
|
2678
3054
|
}
|
|
3055
|
+
let countCandidatePairsCalls = 0;
|
|
3056
|
+
function __countCandidatePairsCalls() {
|
|
3057
|
+
return countCandidatePairsCalls;
|
|
3058
|
+
}
|
|
2679
3059
|
function countCandidatePairs(bodies) {
|
|
3060
|
+
countCandidatePairsCalls += 1;
|
|
2680
3061
|
return broadphaseCandidatePairs(bodies).length;
|
|
2681
3062
|
}
|
|
2682
3063
|
function detailedHitForBody(body, query, distance, castRadius) {
|