@series-inc/rundot-syncplay 5.23.0 → 5.24.0-beta.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/README.md +53 -12
- package/dist/authority-room.d.ts +11 -0
- package/dist/authority-room.js +43 -0
- package/dist/browser.d.ts +11 -1
- package/dist/browser.js +5 -0
- package/dist/checksum-basis.d.ts +4 -0
- package/dist/checksum-basis.js +17 -0
- package/dist/cjs/authority-room.js +43 -0
- package/dist/cjs/browser.js +18 -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-complete.js +1 -4
- package/dist/cjs/engine-parity-matrix.js +41 -9
- package/dist/cjs/engine-parity.js +2 -2
- package/dist/cjs/index.js +24 -2
- package/dist/cjs/input-authority.js +34 -2
- package/dist/cjs/instant-replay.js +4 -9
- package/dist/cjs/local-authority-server-runner.js +3 -1
- package/dist/cjs/local-authority-wire.js +20 -1
- package/dist/cjs/match-log.js +27 -0
- package/dist/cjs/movement3d.js +17 -1
- package/dist/cjs/networked-client.js +70 -20
- package/dist/cjs/node.js +23 -1
- package/dist/cjs/offline-session.js +52 -24
- package/dist/cjs/physics-cert.js +8 -3
- package/dist/cjs/physics3d-heightfield.js +51 -0
- package/dist/cjs/physics3d-hull.js +381 -0
- package/dist/cjs/physics3d-joints.js +255 -49
- package/dist/cjs/physics3d-mass.js +131 -0
- package/dist/cjs/physics3d-recipes.js +197 -0
- package/dist/cjs/physics3d-shared.js +11 -1
- package/dist/cjs/physics3d-solver.js +845 -98
- package/dist/cjs/physics3d-stacking.js +154 -0
- package/dist/cjs/physics3d-toi.js +274 -0
- package/dist/cjs/physics3d-vehicle.js +35 -10
- package/dist/cjs/physics3d.js +601 -38
- package/dist/cjs/replay.js +25 -1
- package/dist/cjs/runtime-support.js +71 -0
- package/dist/cjs/sdk-offline.js +9 -1
- package/dist/cjs/sdk-session.js +8 -0
- package/dist/cjs/secret-authority.js +647 -0
- package/dist/cjs/secret-client.js +615 -0
- package/dist/cjs/secret-command-classification.js +8 -0
- package/dist/cjs/secret-config.js +211 -0
- package/dist/cjs/secret-crypto.js +208 -0
- package/dist/cjs/secret-protocol.js +238 -0
- package/dist/cjs/session-build.js +7 -4
- package/dist/cjs/session-promotion.js +5 -5
- package/dist/cjs/synctest.js +49 -0
- package/dist/cjs/testing.js +4 -1
- package/dist/cjs/tools/static-checker.js +61 -13
- package/dist/cli.js +193 -48
- package/dist/collider-cooking.d.ts +141 -0
- package/dist/collider-cooking.js +829 -0
- package/dist/deterministic-primitives.d.ts +1 -0
- package/dist/deterministic-session.js +2 -0
- package/dist/engine-complete.js +1 -4
- package/dist/engine-parity-matrix.js +41 -9
- package/dist/engine-parity.js +2 -2
- package/dist/index.d.ts +12 -1
- package/dist/index.js +6 -0
- package/dist/input-authority.d.ts +3 -0
- package/dist/input-authority.js +35 -3
- package/dist/instant-replay.d.ts +2 -2
- package/dist/instant-replay.js +4 -9
- package/dist/local-authority-server-runner.d.ts +1 -0
- package/dist/local-authority-server-runner.js +2 -1
- package/dist/local-authority-wire.js +20 -1
- package/dist/match-log.d.ts +4 -1
- package/dist/match-log.js +27 -0
- package/dist/movement3d.js +18 -2
- package/dist/networked-client.d.ts +8 -0
- package/dist/networked-client.js +70 -20
- package/dist/node.d.ts +7 -0
- package/dist/node.js +6 -0
- package/dist/offline-session.js +53 -25
- package/dist/physics-cert.js +8 -3
- package/dist/physics3d-heightfield.d.ts +17 -0
- package/dist/physics3d-heightfield.js +47 -0
- package/dist/physics3d-hull.d.ts +53 -0
- package/dist/physics3d-hull.js +376 -0
- package/dist/physics3d-joints.d.ts +13 -1
- package/dist/physics3d-joints.js +256 -50
- package/dist/physics3d-mass.d.ts +11 -0
- package/dist/physics3d-mass.js +126 -0
- package/dist/physics3d-recipes.d.ts +29 -0
- package/dist/physics3d-recipes.js +193 -0
- package/dist/physics3d-shared.d.ts +1 -0
- package/dist/physics3d-shared.js +10 -1
- package/dist/physics3d-solver.d.ts +78 -2
- package/dist/physics3d-solver.js +842 -98
- package/dist/physics3d-stacking.d.ts +26 -0
- package/dist/physics3d-stacking.js +147 -0
- package/dist/physics3d-toi.d.ts +79 -0
- package/dist/physics3d-toi.js +267 -0
- package/dist/physics3d-vehicle.js +35 -10
- package/dist/physics3d.d.ts +28 -3
- package/dist/physics3d.js +601 -40
- package/dist/replay.d.ts +5 -0
- package/dist/replay.js +25 -1
- package/dist/reports.d.ts +6 -1
- package/dist/runtime-support.d.ts +60 -0
- package/dist/runtime-support.js +66 -0
- package/dist/sdk-offline.d.ts +4 -0
- package/dist/sdk-offline.js +9 -1
- package/dist/sdk-session.d.ts +4 -0
- package/dist/sdk-session.js +8 -0
- package/dist/secret-authority.d.ts +90 -0
- package/dist/secret-authority.js +643 -0
- package/dist/secret-client.d.ts +89 -0
- package/dist/secret-client.js +611 -0
- package/dist/secret-command-classification.d.ts +1 -0
- package/dist/secret-command-classification.js +5 -0
- package/dist/secret-config.d.ts +45 -0
- package/dist/secret-config.js +206 -0
- package/dist/secret-crypto.d.ts +29 -0
- package/dist/secret-crypto.js +190 -0
- package/dist/secret-protocol.d.ts +181 -0
- package/dist/secret-protocol.js +230 -0
- package/dist/session-build.d.ts +16 -0
- package/dist/session-build.js +7 -4
- 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/tools/static-checker.d.ts +11 -0
- package/dist/tools/static-checker.js +60 -13
- package/dist/types.d.ts +21 -2
- package/package.json +8 -3
|
@@ -14,8 +14,13 @@
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.__solverInternals = exports.WARM_KEY_BASE = exports.SOLVER_CONSTANTS = void 0;
|
|
16
16
|
exports.generateManifold = generateManifold;
|
|
17
|
+
exports.combinedContactFriction = combinedContactFriction;
|
|
17
18
|
exports.solveRigidStep = solveRigidStep;
|
|
19
|
+
exports.minExtentOf = minExtentOf;
|
|
20
|
+
exports.boundingRadiusOf = boundingRadiusOf;
|
|
18
21
|
const physics3d_shared_js_1 = require("./physics3d-shared.js");
|
|
22
|
+
const physics3d_hull_js_1 = require("./physics3d-hull.js");
|
|
23
|
+
const physics3d_heightfield_js_1 = require("./physics3d-heightfield.js");
|
|
19
24
|
const ZERO = { x: 0, y: 0, z: 0 };
|
|
20
25
|
/** Expand a compound body into primitive sub-bodies in world space. */
|
|
21
26
|
function primitiveViews(body) {
|
|
@@ -29,7 +34,7 @@ function primitiveViews(body) {
|
|
|
29
34
|
}));
|
|
30
35
|
}
|
|
31
36
|
/* istanbul ignore next -- mesh bodies are routed to meshManifold before primitiveViews is ever reached; this branch only satisfies the SolverShape union. */
|
|
32
|
-
if (shape.kind === 'mesh') {
|
|
37
|
+
if (shape.kind === 'mesh' || shape.kind === 'heightfield') {
|
|
33
38
|
return [];
|
|
34
39
|
}
|
|
35
40
|
return [{ index: body.index, p: body.p, q: body.q, shape }];
|
|
@@ -43,7 +48,7 @@ function primitiveViews(body) {
|
|
|
43
48
|
*/
|
|
44
49
|
function asPrimitive(shape) {
|
|
45
50
|
/* istanbul ignore next -- callers guard compound, and mesh bodies are routed to meshManifold first; type guard only. */
|
|
46
|
-
if (shape.kind === 'compound' || shape.kind === 'mesh') {
|
|
51
|
+
if (shape.kind === 'compound' || shape.kind === 'mesh' || shape.kind === 'heightfield') {
|
|
47
52
|
throw new Error('asPrimitive requires a primitive shape');
|
|
48
53
|
}
|
|
49
54
|
return shape;
|
|
@@ -51,7 +56,7 @@ function asPrimitive(shape) {
|
|
|
51
56
|
function generateManifold(a, b, speculativeMargin = 0) {
|
|
52
57
|
// mesh is one-sided static geometry; the other shape's primitives collide
|
|
53
58
|
// against each triangle.
|
|
54
|
-
if (a.shape.kind === 'mesh' || b.shape.kind === 'mesh') {
|
|
59
|
+
if (a.shape.kind === 'mesh' || b.shape.kind === 'mesh' || a.shape.kind === 'heightfield' || b.shape.kind === 'heightfield') {
|
|
55
60
|
return meshManifold(a, b, speculativeMargin);
|
|
56
61
|
}
|
|
57
62
|
if (a.shape.kind !== 'compound' && b.shape.kind !== 'compound') {
|
|
@@ -128,17 +133,38 @@ function collidePrimitives(a, b, centerA, centerB, speculativeMargin, featureBas
|
|
|
128
133
|
if (ka === 'capsule' && kb === 'box') {
|
|
129
134
|
return capsuleBox(a, b, centerA, centerB, speculativeMargin, featureBase, false);
|
|
130
135
|
}
|
|
131
|
-
/* istanbul ignore next -- when a box operand reaches this point kb is necessarily 'capsule' (box-box/box-sphere returned earlier), so the second condition is always true. */
|
|
132
136
|
if (ka === 'box' && kb === 'capsule') {
|
|
133
137
|
return capsuleBox(b, a, centerB, centerA, speculativeMargin, featureBase, true);
|
|
134
138
|
}
|
|
135
|
-
|
|
139
|
+
if (ka === 'hull' && kb === 'hull') {
|
|
140
|
+
return collideHulls(a, b, centerA, centerB, speculativeMargin, featureBase);
|
|
141
|
+
}
|
|
142
|
+
if (ka === 'hull' && kb === 'box') {
|
|
143
|
+
return collideHulls(a, boxAsHull(b), centerA, centerB, speculativeMargin, featureBase);
|
|
144
|
+
}
|
|
145
|
+
if (ka === 'box' && kb === 'hull') {
|
|
146
|
+
return collideHulls(boxAsHull(a), b, centerA, centerB, speculativeMargin, featureBase);
|
|
147
|
+
}
|
|
148
|
+
if (ka === 'hull' && kb === 'sphere') {
|
|
149
|
+
return sphereHull(b, a, centerB, centerA, speculativeMargin, featureBase, true);
|
|
150
|
+
}
|
|
151
|
+
if (ka === 'sphere' && kb === 'hull') {
|
|
152
|
+
return sphereHull(a, b, centerA, centerB, speculativeMargin, featureBase, false);
|
|
153
|
+
}
|
|
154
|
+
if (ka === 'hull' && kb === 'capsule') {
|
|
155
|
+
return capsuleHull(b, a, centerB, centerA, speculativeMargin, featureBase, true);
|
|
156
|
+
}
|
|
157
|
+
/* istanbul ignore else -- every primitive shape pairing is enumerated above; the fallthrough is an exhaustiveness guard. */
|
|
158
|
+
if (ka === 'capsule' && kb === 'hull') {
|
|
159
|
+
return capsuleHull(a, b, centerA, centerB, speculativeMargin, featureBase, false);
|
|
160
|
+
}
|
|
161
|
+
/* istanbul ignore next -- unreachable exhaustiveness guard. */
|
|
136
162
|
return undefined;
|
|
137
163
|
}
|
|
138
164
|
// ---------- sphere / capsule closed forms ----------
|
|
139
165
|
function radiusOf(shape) {
|
|
140
|
-
/* istanbul ignore next -- radiusOf is only called for sphere/capsule operands; the
|
|
141
|
-
return shape.kind === '
|
|
166
|
+
/* istanbul ignore next -- radiusOf is only called for sphere/capsule operands; the non-radius arm is defensive. */
|
|
167
|
+
return shape.kind === 'sphere' || shape.kind === 'capsule' ? shape.radius : 0;
|
|
142
168
|
}
|
|
143
169
|
/**
|
|
144
170
|
* Build a single contact point from two world points on the surfaces of two
|
|
@@ -617,6 +643,7 @@ function collideBoxes(A, B, centerA, centerB, speculativeMargin, featureBase) {
|
|
|
617
643
|
swap = poly;
|
|
618
644
|
poly = spare;
|
|
619
645
|
spare = swap;
|
|
646
|
+
/* istanbul ignore next -- SAT already proved the faces overlap, so the Sutherland-Hodgman clip against the reference face's own side planes always leaves at least one vertex (20k-pair box/hull fuzz: never hit). */
|
|
620
647
|
if (polyCount === 0) {
|
|
621
648
|
return undefined;
|
|
622
649
|
}
|
|
@@ -648,12 +675,411 @@ function collideBoxes(A, B, centerA, centerB, speculativeMargin, featureBase) {
|
|
|
648
675
|
}
|
|
649
676
|
return { a: 0, b: 0, normal, points };
|
|
650
677
|
}
|
|
678
|
+
// ---------- convex hull SAT + face clipping ----------
|
|
679
|
+
/** Outward bias on reference side planes so boundary-incident corners survive fp clipping. */
|
|
680
|
+
const CLIP_BOUNDARY_TOL = 1e-4;
|
|
681
|
+
// Box-as-hull view: the 8 corners of a box are a valid convex hull, so hull×box
|
|
682
|
+
// reuses the generalized hull path. Keyed by the box shape object identity (pure
|
|
683
|
+
// derived data, never canonical state — same rationale as quatMat3Cache).
|
|
684
|
+
// rdm-ignore-next-line determinism/no-module-mutable-state: pure derived box-hull view keyed by immutable box shape identity
|
|
685
|
+
const boxHullViewCache = new WeakMap();
|
|
686
|
+
function boxAsHull(box) {
|
|
687
|
+
const shape = box.shape;
|
|
688
|
+
/* istanbul ignore next -- boxAsHull is only called for box operands; type guard only. */
|
|
689
|
+
if (shape.kind !== 'box') {
|
|
690
|
+
throw new Error('boxAsHull requires a box');
|
|
691
|
+
}
|
|
692
|
+
let data = boxHullViewCache.get(shape);
|
|
693
|
+
if (data === undefined) {
|
|
694
|
+
const h = shape.half;
|
|
695
|
+
const verts = [];
|
|
696
|
+
for (const sx of [-1, 1]) {
|
|
697
|
+
for (const sy of [-1, 1]) {
|
|
698
|
+
for (const sz of [-1, 1]) {
|
|
699
|
+
verts.push({ x: sx * h.x, y: sy * h.y, z: sz * h.z });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
data = (0, physics3d_hull_js_1.buildSolverHull)(verts);
|
|
704
|
+
boxHullViewCache.set(shape, data);
|
|
705
|
+
}
|
|
706
|
+
return { index: box.index, p: box.p, q: box.q, shape: { kind: 'hull', data } };
|
|
707
|
+
}
|
|
708
|
+
function hullData(shape) {
|
|
709
|
+
/* istanbul ignore next -- callers guard hull; type guard only. */
|
|
710
|
+
if (shape.kind !== 'hull') {
|
|
711
|
+
throw new Error('hullData requires a hull');
|
|
712
|
+
}
|
|
713
|
+
return shape.data;
|
|
714
|
+
}
|
|
715
|
+
/** World-space hull vertices (support samples for mesh + queries). */
|
|
716
|
+
function hullWorldVertices(hull) {
|
|
717
|
+
const data = hullData(hull.shape);
|
|
718
|
+
return data.vertices.map((v) => (0, physics3d_shared_js_1.addVec3)(hull.p, (0, physics3d_shared_js_1.rotatePointByQuat)(hull.q, v)));
|
|
719
|
+
}
|
|
720
|
+
function hullWorldFaces(hull, worldVerts) {
|
|
721
|
+
const data = hullData(hull.shape);
|
|
722
|
+
const rot = quatToMat3Cached(hull.q);
|
|
723
|
+
return data.faces.map((face) => {
|
|
724
|
+
const nx = rot[0] * face.normal.x + rot[1] * face.normal.y + rot[2] * face.normal.z;
|
|
725
|
+
const ny = rot[3] * face.normal.x + rot[4] * face.normal.y + rot[5] * face.normal.z;
|
|
726
|
+
const nz = rot[6] * face.normal.x + rot[7] * face.normal.y + rot[8] * face.normal.z;
|
|
727
|
+
const v0 = worldVerts[face.verts[0]];
|
|
728
|
+
const offset = nx * v0.x + ny * v0.y + nz * v0.z;
|
|
729
|
+
return { nx, ny, nz, offset, verts: face.verts };
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
function hullWorldEdgeDirs(hull) {
|
|
733
|
+
const data = hullData(hull.shape);
|
|
734
|
+
return data.edgeDirs.map((d) => (0, physics3d_shared_js_1.rotatePointByQuat)(hull.q, d));
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Project a hull (given its world vertices) onto an axis, returning the max
|
|
738
|
+
* support value dot(n, v). Used for SAT separation along arbitrary axes.
|
|
739
|
+
*/
|
|
740
|
+
function hullSupportMax(worldVerts, nx, ny, nz) {
|
|
741
|
+
let max = -Infinity;
|
|
742
|
+
for (const v of worldVerts) {
|
|
743
|
+
const d = nx * v.x + ny * v.y + nz * v.z;
|
|
744
|
+
if (d > max) {
|
|
745
|
+
max = d;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return max;
|
|
749
|
+
}
|
|
750
|
+
function hullSupportMin(worldVerts, nx, ny, nz) {
|
|
751
|
+
let min = Infinity;
|
|
752
|
+
for (const v of worldVerts) {
|
|
753
|
+
const d = nx * v.x + ny * v.y + nz * v.z;
|
|
754
|
+
if (d < min) {
|
|
755
|
+
min = d;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return min;
|
|
759
|
+
}
|
|
760
|
+
/** Separation of B beyond A along axis n (positive = separated). */
|
|
761
|
+
function axisSeparation(vertsA, vertsB, nx, ny, nz) {
|
|
762
|
+
const maxA = hullSupportMax(vertsA, nx, ny, nz);
|
|
763
|
+
const minB = hullSupportMin(vertsB, nx, ny, nz);
|
|
764
|
+
return minB - maxA;
|
|
765
|
+
}
|
|
766
|
+
function collideHulls(A, B, centerA, centerB, speculativeMargin, featureBase) {
|
|
767
|
+
const vertsA = hullWorldVertices(A);
|
|
768
|
+
const vertsB = hullWorldVertices(B);
|
|
769
|
+
const facesA = hullWorldFaces(A, vertsA);
|
|
770
|
+
const facesB = hullWorldFaces(B, vertsB);
|
|
771
|
+
const dx = B.p.x - A.p.x;
|
|
772
|
+
const dy = B.p.y - A.p.y;
|
|
773
|
+
const dz = B.p.z - A.p.z;
|
|
774
|
+
// Face SAT: use each face's outward normal directly (already oriented).
|
|
775
|
+
let faceSep = -Infinity;
|
|
776
|
+
let faceRefIsA = true;
|
|
777
|
+
let faceIndex = -1;
|
|
778
|
+
let faceNx = 0;
|
|
779
|
+
let faceNy = 0;
|
|
780
|
+
let faceNz = 0;
|
|
781
|
+
for (let i = 0; i < facesA.length; i += 1) {
|
|
782
|
+
const f = facesA[i];
|
|
783
|
+
// Reference face outward normal points away from A. All A verts satisfy
|
|
784
|
+
// dot(n,v) <= offset; separation = min_B(dot(n,v)) - offset (>0 = gap).
|
|
785
|
+
// The best reference face maximizes separation (shallowest penetration).
|
|
786
|
+
// Strict-greater keeps the lowest sorted-index face on near-ties (stable).
|
|
787
|
+
const s = hullSupportMin(vertsB, f.nx, f.ny, f.nz) - f.offset;
|
|
788
|
+
if (s > faceSep + (i === 0 ? 0 : 1e-9)) {
|
|
789
|
+
faceSep = s;
|
|
790
|
+
faceRefIsA = true;
|
|
791
|
+
faceIndex = i;
|
|
792
|
+
faceNx = f.nx;
|
|
793
|
+
faceNy = f.ny;
|
|
794
|
+
faceNz = f.nz;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
for (let i = 0; i < facesB.length; i += 1) {
|
|
798
|
+
const f = facesB[i];
|
|
799
|
+
const s = hullSupportMin(vertsA, f.nx, f.ny, f.nz) - f.offset;
|
|
800
|
+
// Hysteresis: B's face wins only when strictly deeper, so a resting stack
|
|
801
|
+
// keeps a stable reference frame-to-frame (warm-start anchors don't jump).
|
|
802
|
+
if (s > faceSep + 1e-9) {
|
|
803
|
+
faceSep = s;
|
|
804
|
+
faceRefIsA = false;
|
|
805
|
+
faceIndex = i;
|
|
806
|
+
faceNx = f.nx;
|
|
807
|
+
faceNy = f.ny;
|
|
808
|
+
faceNz = f.nz;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (faceSep > speculativeMargin) {
|
|
812
|
+
return undefined;
|
|
813
|
+
}
|
|
814
|
+
// Edge SAT: cross products of edge-direction pairs.
|
|
815
|
+
const edgesA = hullWorldEdgeDirs(A);
|
|
816
|
+
const edgesB = hullWorldEdgeDirs(B);
|
|
817
|
+
let edgeSep = -Infinity;
|
|
818
|
+
let edgeNx = 0;
|
|
819
|
+
let edgeNy = 0;
|
|
820
|
+
let edgeNz = 0;
|
|
821
|
+
let edgeFound = false;
|
|
822
|
+
let edgePairIndex = -1;
|
|
823
|
+
for (let i = 0; i < edgesA.length; i += 1) {
|
|
824
|
+
const ea = edgesA[i];
|
|
825
|
+
for (let j = 0; j < edgesB.length; j += 1) {
|
|
826
|
+
const eb = edgesB[j];
|
|
827
|
+
let cx = ea.y * eb.z - ea.z * eb.y;
|
|
828
|
+
let cy = ea.z * eb.x - ea.x * eb.z;
|
|
829
|
+
let cz = ea.x * eb.y - ea.y * eb.x;
|
|
830
|
+
const l2 = cx * cx + cy * cy + cz * cz;
|
|
831
|
+
if (l2 < 1e-8) {
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
const inv = 1 / (0, physics3d_shared_js_1.solverSqrt)(l2);
|
|
835
|
+
cx *= inv;
|
|
836
|
+
cy *= inv;
|
|
837
|
+
cz *= inv;
|
|
838
|
+
// orient axis from A toward B
|
|
839
|
+
if (cx * dx + cy * dy + cz * dz < 0) {
|
|
840
|
+
cx = -cx;
|
|
841
|
+
cy = -cy;
|
|
842
|
+
cz = -cz;
|
|
843
|
+
}
|
|
844
|
+
const sep = axisSeparation(vertsA, vertsB, cx, cy, cz);
|
|
845
|
+
if (sep > speculativeMargin) {
|
|
846
|
+
return undefined;
|
|
847
|
+
}
|
|
848
|
+
if (sep > edgeSep + 1e-9) {
|
|
849
|
+
edgeSep = sep;
|
|
850
|
+
edgeNx = cx;
|
|
851
|
+
edgeNy = cy;
|
|
852
|
+
edgeNz = cz;
|
|
853
|
+
edgeFound = true;
|
|
854
|
+
edgePairIndex = i * edgesB.length + j;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
// qu3e-style hysteresis: prefer the edge axis only when decisively shallower
|
|
859
|
+
// separation (deeper penetration) than the face axis, and only for real overlaps.
|
|
860
|
+
const facePen = -faceSep;
|
|
861
|
+
const edgePen = -edgeSep;
|
|
862
|
+
const edgePreferred = edgeFound && facePen > 0 && edgePen < facePen * 0.9 - 5e-3;
|
|
863
|
+
if (edgePreferred) {
|
|
864
|
+
return hullEdgeContact(A, B, edgesA, edgesB, edgePairIndex, edgeNx, edgeNy, edgeNz, edgePen, centerA, centerB, featureBase);
|
|
865
|
+
}
|
|
866
|
+
return hullFaceContact(faceRefIsA ? A : B, faceRefIsA ? B : A, faceRefIsA ? vertsA : vertsB, faceRefIsA ? vertsB : vertsA, faceRefIsA ? facesA : facesB, faceRefIsA ? facesB : facesA, faceIndex, faceNx, faceNy, faceNz, faceRefIsA, speculativeMargin, centerA, centerB, featureBase);
|
|
867
|
+
}
|
|
868
|
+
function hullEdgeContact(A, B, edgesA, edgesB, pairIndex, nx, ny, nz, penetration, centerA, centerB, featureBase) {
|
|
869
|
+
const i = Math.trunc(pairIndex / edgesB.length);
|
|
870
|
+
const j = pairIndex % edgesB.length;
|
|
871
|
+
const vertsA = hullWorldVertices(A);
|
|
872
|
+
const vertsB = hullWorldVertices(B);
|
|
873
|
+
// support vertices of A along +n and B along -n approximate the contacting edges
|
|
874
|
+
const pA = supportVertex(vertsA, nx, ny, nz);
|
|
875
|
+
const pB = supportVertex(vertsB, -nx, -ny, -nz);
|
|
876
|
+
const eA = edgesA[i];
|
|
877
|
+
const eB = edgesB[j];
|
|
878
|
+
const seg = closestSegmentSegment((0, physics3d_shared_js_1.subVec3)(pA, (0, physics3d_shared_js_1.scaleVec3)(eA, 1)), (0, physics3d_shared_js_1.addVec3)(pA, (0, physics3d_shared_js_1.scaleVec3)(eA, 1)), (0, physics3d_shared_js_1.subVec3)(pB, (0, physics3d_shared_js_1.scaleVec3)(eB, 1)), (0, physics3d_shared_js_1.addVec3)(pB, (0, physics3d_shared_js_1.scaleVec3)(eB, 1)));
|
|
879
|
+
const mid = (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.addVec3)(seg.c1, seg.c2), 0.5);
|
|
880
|
+
const normal = { x: nx, y: ny, z: nz };
|
|
881
|
+
return {
|
|
882
|
+
a: 0,
|
|
883
|
+
b: 0,
|
|
884
|
+
normal,
|
|
885
|
+
points: [{ rA: (0, physics3d_shared_js_1.subVec3)(mid, centerA), rB: (0, physics3d_shared_js_1.subVec3)(mid, centerB), penetration, feature: featureBase + 4096 + pairIndex }],
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function supportVertex(worldVerts, nx, ny, nz) {
|
|
889
|
+
let best = worldVerts[0];
|
|
890
|
+
let bestD = -Infinity;
|
|
891
|
+
for (const v of worldVerts) {
|
|
892
|
+
const d = nx * v.x + ny * v.y + nz * v.z;
|
|
893
|
+
if (d > bestD) {
|
|
894
|
+
bestD = d;
|
|
895
|
+
best = v;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return best;
|
|
899
|
+
}
|
|
900
|
+
function hullFaceContact(ref, inc, refVerts, incVerts, refFaces, incFaces, refFaceIndex, nx, ny, nz, refIsA, speculativeMargin, centerA, centerB, featureBase) {
|
|
901
|
+
void ref;
|
|
902
|
+
void inc;
|
|
903
|
+
const refFace = refFaces[refFaceIndex];
|
|
904
|
+
// Incident face = the incident-hull face most anti-parallel to the reference normal.
|
|
905
|
+
let incFaceIndex = 0;
|
|
906
|
+
let minDot = Infinity;
|
|
907
|
+
for (let i = 0; i < incFaces.length; i += 1) {
|
|
908
|
+
const f = incFaces[i];
|
|
909
|
+
const d = f.nx * nx + f.ny * ny + f.nz * nz;
|
|
910
|
+
// Strict-less keeps the lowest sorted-index incident face on near-ties, so
|
|
911
|
+
// the clipped polygon (and its feature ids) stay stable frame-to-frame.
|
|
912
|
+
if (d < minDot - (i === 0 ? Infinity : 1e-9)) {
|
|
913
|
+
minDot = d;
|
|
914
|
+
incFaceIndex = i;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const incFace = incFaces[incFaceIndex];
|
|
918
|
+
// Load incident face polygon into scratch.
|
|
919
|
+
ensureClipSlots(clipScratch.a, incFace.verts.length);
|
|
920
|
+
let poly = clipScratch.a;
|
|
921
|
+
let spare = clipScratch.b;
|
|
922
|
+
let polyCount = 0;
|
|
923
|
+
for (const vi of incFace.verts) {
|
|
924
|
+
const v = incVerts[vi];
|
|
925
|
+
const slot = poly[polyCount];
|
|
926
|
+
slot.x = v.x;
|
|
927
|
+
slot.y = v.y;
|
|
928
|
+
slot.z = v.z;
|
|
929
|
+
slot.id = featureBase + refFaceIndex * 64 + polyCount + 1;
|
|
930
|
+
polyCount += 1;
|
|
931
|
+
}
|
|
932
|
+
// Clip the incident polygon against each reference-face side plane. Each side
|
|
933
|
+
// plane's normal must point OUTWARD from the reference polygon so clipPlaneScratch
|
|
934
|
+
// (keeps dc <= 0) retains interior points. Orientation is fixed against the
|
|
935
|
+
// face centroid rather than assuming a winding convention.
|
|
936
|
+
const rv = refFace.verts;
|
|
937
|
+
let rcx = 0;
|
|
938
|
+
let rcy = 0;
|
|
939
|
+
let rcz = 0;
|
|
940
|
+
for (const vi of rv) {
|
|
941
|
+
rcx += refVerts[vi].x;
|
|
942
|
+
rcy += refVerts[vi].y;
|
|
943
|
+
rcz += refVerts[vi].z;
|
|
944
|
+
}
|
|
945
|
+
const rInv = 1 / rv.length;
|
|
946
|
+
rcx *= rInv;
|
|
947
|
+
rcy *= rInv;
|
|
948
|
+
rcz *= rInv;
|
|
949
|
+
for (let e = 0; e < rv.length; e += 1) {
|
|
950
|
+
const a = refVerts[rv[e]];
|
|
951
|
+
const b = refVerts[rv[(e + 1) % rv.length]];
|
|
952
|
+
const edge = (0, physics3d_shared_js_1.subVec3)(b, a);
|
|
953
|
+
let pnx = ny * edge.z - nz * edge.y;
|
|
954
|
+
let pny = nz * edge.x - nx * edge.z;
|
|
955
|
+
let pnz = nx * edge.y - ny * edge.x;
|
|
956
|
+
const l2 = pnx * pnx + pny * pny + pnz * pnz;
|
|
957
|
+
/* istanbul ignore next -- a reference-face edge is never degenerate: buildSolverHull dedupes vertices and box faces are axis-aligned quads (20k-pair fuzz: never hit). */
|
|
958
|
+
if (l2 < 1e-12) {
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
const invl = 1 / (0, physics3d_shared_js_1.solverSqrt)(l2);
|
|
962
|
+
pnx *= invl;
|
|
963
|
+
pny *= invl;
|
|
964
|
+
pnz *= invl;
|
|
965
|
+
let offset = pnx * a.x + pny * a.y + pnz * a.z;
|
|
966
|
+
// Flip to outward: the centroid (interior) must lie on the negative side.
|
|
967
|
+
/* istanbul ignore else -- reference faces are wound CCW about their outward normal, so `n x edge` always points inward and the flip always runs (20k-pair box/hull fuzz: 48532 flips, 0 skips). The guard stays as a winding assertion. */
|
|
968
|
+
if (pnx * rcx + pny * rcy + pnz * rcz > offset) {
|
|
969
|
+
pnx = -pnx;
|
|
970
|
+
pny = -pny;
|
|
971
|
+
pnz = -pnz;
|
|
972
|
+
offset = -offset;
|
|
973
|
+
}
|
|
974
|
+
// Bias the side plane slightly outward so incident-face corners that lie
|
|
975
|
+
// exactly on a reference-face boundary survive floating-point clipping
|
|
976
|
+
// (dropping them mid-settle collapses a resting face manifold to 1-3 points
|
|
977
|
+
// on one side and injects drift). Box clipping avoids this via exact axis
|
|
978
|
+
// alignment; the generic hull path needs the tolerance.
|
|
979
|
+
offset += CLIP_BOUNDARY_TOL;
|
|
980
|
+
polyCount = clipPlaneScratch(poly, polyCount, pnx, pny, pnz, offset, e + 1, spare);
|
|
981
|
+
const swap = poly;
|
|
982
|
+
poly = spare;
|
|
983
|
+
spare = swap;
|
|
984
|
+
if (polyCount === 0) {
|
|
985
|
+
return undefined;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const refFaceDist = refFace.offset;
|
|
989
|
+
const points = [];
|
|
990
|
+
for (let i = 0; i < polyCount; i += 1) {
|
|
991
|
+
const vert = poly[i];
|
|
992
|
+
const depth = refFaceDist - (nx * vert.x + ny * vert.y + nz * vert.z);
|
|
993
|
+
if (depth >= -speculativeMargin) {
|
|
994
|
+
const onRefX = vert.x + nx * depth;
|
|
995
|
+
const onRefY = vert.y + ny * depth;
|
|
996
|
+
const onRefZ = vert.z + nz * depth;
|
|
997
|
+
const midX = (vert.x + onRefX) * 0.5;
|
|
998
|
+
const midY = (vert.y + onRefY) * 0.5;
|
|
999
|
+
const midZ = (vert.z + onRefZ) * 0.5;
|
|
1000
|
+
points.push({
|
|
1001
|
+
rA: { x: midX - centerA.x, y: midY - centerA.y, z: midZ - centerA.z },
|
|
1002
|
+
rB: { x: midX - centerB.x, y: midY - centerB.y, z: midZ - centerB.z },
|
|
1003
|
+
penetration: depth,
|
|
1004
|
+
feature: vert.id,
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
/* istanbul ignore next -- SAT overlap guarantees the incident face's deepest surviving vertex is within the speculative margin (20k-pair fuzz: never hit). */
|
|
1009
|
+
if (points.length === 0) {
|
|
1010
|
+
return undefined;
|
|
1011
|
+
}
|
|
1012
|
+
// Normal in caller A->B frame: reference face outward normal points ref->inc.
|
|
1013
|
+
// If ref is A, normal ref->inc = A->B = +n. If ref is B, A->B = -n.
|
|
1014
|
+
const normal = refIsA ? { x: nx, y: ny, z: nz } : { x: -nx, y: -ny, z: -nz };
|
|
1015
|
+
return { a: 0, b: 0, normal, points: reducePoints(points) };
|
|
1016
|
+
}
|
|
1017
|
+
function sphereHull(sphere, hull, cSphere, cHull, margin, feature, flip) {
|
|
1018
|
+
/* istanbul ignore next -- dispatch guarantees a (sphere, hull) pair; type guard only. */
|
|
1019
|
+
if (sphere.shape.kind !== 'sphere' || hull.shape.kind !== 'hull') {
|
|
1020
|
+
return undefined;
|
|
1021
|
+
}
|
|
1022
|
+
const radius = sphere.shape.radius;
|
|
1023
|
+
const data = hull.shape.data;
|
|
1024
|
+
const localCenter = (0, physics3d_shared_js_1.rotatePointByQuatInverse)(hull.q, (0, physics3d_shared_js_1.subVec3)(sphere.p, hull.p));
|
|
1025
|
+
const closest = (0, physics3d_hull_js_1.closestPointOnHullLocal)(localCenter, data);
|
|
1026
|
+
if (closest.inside) {
|
|
1027
|
+
// sphere center inside the hull: push out along the least-violated face normal.
|
|
1028
|
+
const nLocal = closest.normal;
|
|
1029
|
+
const nWorld = (0, physics3d_shared_js_1.rotatePointByQuat)(hull.q, nLocal); // outward from hull
|
|
1030
|
+
const penetration = -closest.distance + radius;
|
|
1031
|
+
const nSphereToHull = (0, physics3d_shared_js_1.scaleVec3)(nWorld, -1);
|
|
1032
|
+
return emitSingle(nSphereToHull, sphere.p, penetration, cSphere, cHull, feature, flip);
|
|
1033
|
+
}
|
|
1034
|
+
const closestWorld = (0, physics3d_shared_js_1.addVec3)(hull.p, (0, physics3d_shared_js_1.rotatePointByQuat)(hull.q, closest.point));
|
|
1035
|
+
const delta = (0, physics3d_shared_js_1.subVec3)(sphere.p, closestWorld);
|
|
1036
|
+
const dist = (0, physics3d_shared_js_1.solverSqrt)((0, physics3d_shared_js_1.dotVec3)(delta, delta));
|
|
1037
|
+
const penetration = radius - dist;
|
|
1038
|
+
if (penetration < -margin) {
|
|
1039
|
+
return undefined;
|
|
1040
|
+
}
|
|
1041
|
+
/* istanbul ignore next -- a sphere centre coincident with its closest hull point is `inside` and returned above, so dist is positive here. */
|
|
1042
|
+
const nHullToSphere = dist > 1e-9 ? (0, physics3d_shared_js_1.scaleVec3)(delta, 1 / dist) : (0, physics3d_shared_js_1.rotatePointByQuat)(hull.q, closest.normal);
|
|
1043
|
+
const nSphereToHull = (0, physics3d_shared_js_1.scaleVec3)(nHullToSphere, -1);
|
|
1044
|
+
const mid = (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.addVec3)(closestWorld, (0, physics3d_shared_js_1.subVec3)(sphere.p, (0, physics3d_shared_js_1.scaleVec3)(nHullToSphere, radius))), 0.5);
|
|
1045
|
+
return emitSingle(nSphereToHull, mid, penetration, cSphere, cHull, feature, flip);
|
|
1046
|
+
}
|
|
1047
|
+
function capsuleHull(capsule, hull, cCapsule, cHull, margin, feature, flip) {
|
|
1048
|
+
const seg = capsuleSegment(capsule);
|
|
1049
|
+
const samples = [seg.start, seg.end, (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.addVec3)(seg.start, seg.end), 0.5)];
|
|
1050
|
+
const points = [];
|
|
1051
|
+
let normal;
|
|
1052
|
+
let deepest = -Infinity;
|
|
1053
|
+
samples.forEach((sample, i) => {
|
|
1054
|
+
const sphere = { index: capsule.index, p: sample, q: capsule.q, shape: { kind: 'sphere', radius: seg.radius } };
|
|
1055
|
+
const sub = sphereHull(sphere, hull, cCapsule, cHull, margin, feature + i, flip);
|
|
1056
|
+
if (sub === undefined) {
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
for (const point of sub.points) {
|
|
1060
|
+
points.push(point);
|
|
1061
|
+
}
|
|
1062
|
+
const d = sub.points[0].penetration;
|
|
1063
|
+
if (d > deepest) {
|
|
1064
|
+
deepest = d;
|
|
1065
|
+
normal = sub.normal;
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
if (points.length === 0 || normal === undefined) {
|
|
1069
|
+
return undefined;
|
|
1070
|
+
}
|
|
1071
|
+
return { a: 0, b: 0, normal, points };
|
|
1072
|
+
}
|
|
1073
|
+
function hullSupportSamples(hull) {
|
|
1074
|
+
return hullWorldVertices(hull);
|
|
1075
|
+
}
|
|
651
1076
|
// ---------- mesh (one-sided static triangle) ----------
|
|
652
1077
|
function meshManifold(a, b, speculativeMargin) {
|
|
653
|
-
const meshIsA = a.shape.kind === 'mesh';
|
|
1078
|
+
const meshIsA = a.shape.kind === 'mesh' || a.shape.kind === 'heightfield';
|
|
654
1079
|
const meshBody = meshIsA ? a : b;
|
|
655
1080
|
const otherBody = meshIsA ? b : a;
|
|
656
|
-
if (meshBody.shape.kind !== 'mesh'
|
|
1081
|
+
if ((meshBody.shape.kind !== 'mesh' && meshBody.shape.kind !== 'heightfield')
|
|
1082
|
+
|| otherBody.shape.kind === 'mesh' || otherBody.shape.kind === 'heightfield') {
|
|
657
1083
|
return undefined;
|
|
658
1084
|
}
|
|
659
1085
|
const prims = primitiveViews(otherBody);
|
|
@@ -661,35 +1087,71 @@ function meshManifold(a, b, speculativeMargin) {
|
|
|
661
1087
|
let normal;
|
|
662
1088
|
let deepest = -Infinity;
|
|
663
1089
|
let feature = 0;
|
|
664
|
-
const
|
|
1090
|
+
const otherHalf = shapeHalfExtent(otherBody.shape, otherBody.q);
|
|
1091
|
+
const localTriangles = meshBody.shape.kind === 'mesh'
|
|
1092
|
+
? meshBody.shape.triangles
|
|
1093
|
+
: (0, physics3d_heightfield_js_1.heightfieldLocalTriangles)(meshBody.shape, {
|
|
1094
|
+
minX: otherBody.p.x - otherHalf.x - speculativeMargin - meshBody.p.x,
|
|
1095
|
+
maxX: otherBody.p.x + otherHalf.x + speculativeMargin - meshBody.p.x,
|
|
1096
|
+
minY: otherBody.p.y - otherHalf.y - speculativeMargin - meshBody.p.y,
|
|
1097
|
+
maxY: otherBody.p.y + otherHalf.y + speculativeMargin - meshBody.p.y,
|
|
1098
|
+
minZ: otherBody.p.z - otherHalf.z - speculativeMargin - meshBody.p.z,
|
|
1099
|
+
maxZ: otherBody.p.z + otherHalf.z + speculativeMargin - meshBody.p.z,
|
|
1100
|
+
});
|
|
1101
|
+
const triangles = localTriangles.map((tri) => [
|
|
665
1102
|
(0, physics3d_shared_js_1.addVec3)(meshBody.p, (0, physics3d_shared_js_1.rotatePointByQuat)(meshBody.q, tri[0])),
|
|
666
1103
|
(0, physics3d_shared_js_1.addVec3)(meshBody.p, (0, physics3d_shared_js_1.rotatePointByQuat)(meshBody.q, tri[1])),
|
|
667
1104
|
(0, physics3d_shared_js_1.addVec3)(meshBody.p, (0, physics3d_shared_js_1.rotatePointByQuat)(meshBody.q, tri[2])),
|
|
668
1105
|
]);
|
|
669
1106
|
for (const prim of prims) {
|
|
670
|
-
/* istanbul ignore next -- primitiveViews only yields box/sphere/capsule sub-bodies
|
|
671
|
-
if (prim.shape.kind !== 'sphere' && prim.shape.kind !== 'capsule' && prim.shape.kind !== 'box') {
|
|
1107
|
+
/* istanbul ignore next -- primitiveViews only yields box/sphere/capsule/hull sub-bodies; this skip is unreachable. */
|
|
1108
|
+
if (prim.shape.kind !== 'sphere' && prim.shape.kind !== 'capsule' && prim.shape.kind !== 'box' && prim.shape.kind !== 'hull') {
|
|
672
1109
|
continue;
|
|
673
1110
|
}
|
|
674
|
-
const radius = prim.shape.kind === '
|
|
675
|
-
?
|
|
676
|
-
:
|
|
677
|
-
//
|
|
678
|
-
//
|
|
1111
|
+
const radius = prim.shape.kind === 'sphere' || prim.shape.kind === 'capsule'
|
|
1112
|
+
? prim.shape.radius
|
|
1113
|
+
: 0;
|
|
1114
|
+
// Boxes/hulls sample their support vertices against triangles (like dynamic
|
|
1115
|
+
// box-vs-mesh); sphere/capsule use center/segment samples.
|
|
679
1116
|
const samplePoints = prim.shape.kind === 'box'
|
|
680
1117
|
? boxSupportSamples(prim)
|
|
681
|
-
: prim.shape.kind === '
|
|
682
|
-
?
|
|
683
|
-
:
|
|
1118
|
+
: prim.shape.kind === 'hull'
|
|
1119
|
+
? hullSupportSamples(prim)
|
|
1120
|
+
: prim.shape.kind === 'capsule'
|
|
1121
|
+
? capsuleSamples(prim)
|
|
1122
|
+
: [prim.p];
|
|
684
1123
|
for (const triangle of triangles) {
|
|
685
1124
|
for (const sample of samplePoints) {
|
|
686
1125
|
const closest = closestPointOnTriangle(sample, triangle[0], triangle[1], triangle[2]);
|
|
687
1126
|
const delta = (0, physics3d_shared_js_1.subVec3)(sample, closest);
|
|
688
|
-
const dist = (0, physics3d_shared_js_1.solverSqrt)((0, physics3d_shared_js_1.dotVec3)(delta, delta));
|
|
689
1127
|
const triNormal = triangleNormal(triangle[0], triangle[1], triangle[2]);
|
|
690
|
-
const
|
|
691
|
-
const
|
|
692
|
-
|
|
1128
|
+
const planeDist = (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.subVec3)(sample, triangle[0]), triNormal);
|
|
1129
|
+
const centerPlaneDist = (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.subVec3)(prim.p, triangle[0]), triNormal);
|
|
1130
|
+
const relativeNormalVelocity = (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.subVec3)(otherBody.v ?? ZERO, meshBody.v ?? ZERO), triNormal);
|
|
1131
|
+
// delta = perp + planeDist·n, so perp² = |delta|² - planeDist². A zero
|
|
1132
|
+
// perp means the sample projects onto the face interior (or an edge),
|
|
1133
|
+
// where the signed plane distance — not the unsigned closest-point
|
|
1134
|
+
// distance — carries the penetration. Zero-radius shapes (box, hull)
|
|
1135
|
+
// have no other source of depth, so this is the branch that lets them
|
|
1136
|
+
// rest on and be pushed by a mesh at all.
|
|
1137
|
+
const delta2 = (0, physics3d_shared_js_1.dotVec3)(delta, delta);
|
|
1138
|
+
const perp2 = delta2 - planeDist * planeDist;
|
|
1139
|
+
let penetration;
|
|
1140
|
+
if (perp2 <= exports.SOLVER_CONSTANTS.MESH_FACE_PROJECTION_TOL2) {
|
|
1141
|
+
if (planeDist < -exports.SOLVER_CONSTANTS.MESH_BEHIND_FACE_RECOVERY
|
|
1142
|
+
&& (centerPlaneDist < 0 || relativeNormalVelocity > 0)) {
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
penetration = radius - planeDist;
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
// Edge/vertex region: one-sided, front-face only.
|
|
1149
|
+
if (planeDist < 0) {
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
penetration = radius - (0, physics3d_shared_js_1.solverSqrt)(delta2);
|
|
1153
|
+
}
|
|
1154
|
+
if (penetration < -speculativeMargin) {
|
|
693
1155
|
continue;
|
|
694
1156
|
}
|
|
695
1157
|
const contactNormal = triNormal; // one-sided: mesh -> other
|
|
@@ -787,7 +1249,7 @@ function closestPointOnTriangle(p, a, b, c) {
|
|
|
787
1249
|
// Sequential-impulse velocity solver + split impulse + islands + sleep
|
|
788
1250
|
// ============================================================================
|
|
789
1251
|
exports.SOLVER_CONSTANTS = {
|
|
790
|
-
BAUMGARTE: 0.2,
|
|
1252
|
+
BAUMGARTE: 0.2, // joints only (split impulse) — contacts now soft
|
|
791
1253
|
SLOP: 0.005,
|
|
792
1254
|
WAKE_VEL2: 0.005,
|
|
793
1255
|
SLEEP_VEL2: 0.005,
|
|
@@ -796,7 +1258,50 @@ exports.SOLVER_CONSTANTS = {
|
|
|
796
1258
|
WARM_MATCH_TOL2: 0.01,
|
|
797
1259
|
LINEAR_DAMPING: 0.995,
|
|
798
1260
|
ANGULAR_DAMPING: 0.98,
|
|
1261
|
+
SUBSTEPS: 6,
|
|
1262
|
+
/**
|
|
1263
|
+
* Relax passes per substep = RELAX_ITERATION_SCALE × velocityIterations. The
|
|
1264
|
+
* bias-free relax must drain the warm-start over-push that multiple bias-solve
|
|
1265
|
+
* iterations accumulate into `accN`; under-draining lets weak-gravity stacks
|
|
1266
|
+
* ratchet apart. 2× fully settles the tall-stack / mass-ratio batteries.
|
|
1267
|
+
*/
|
|
1268
|
+
RELAX_ITERATION_SCALE: 2,
|
|
1269
|
+
CONTACT_ZETA: 10,
|
|
1270
|
+
/** hertz = CONTACT_HERTZ_FRACTION · substeps/dt (dimensionless ω·h = π/2). */
|
|
1271
|
+
CONTACT_HERTZ_FRACTION: 0.25,
|
|
1272
|
+
/** Static/kinematic-involved pairs solve 2× stiffer (box2d staticSoftness). */
|
|
1273
|
+
STATIC_HERTZ_SCALE: 2,
|
|
1274
|
+
/** Clamp on soft-bias velocity, length units per tick. */
|
|
1275
|
+
MAX_CONTACT_PUSHOUT: 0.05,
|
|
1276
|
+
/**
|
|
1277
|
+
* Squared in-plane distance under which a mesh support sample counts as
|
|
1278
|
+
* projecting onto the triangle face rather than its edge/vertex region.
|
|
1279
|
+
* This absolute tolerance equals the squared distance quantum and is reliable
|
|
1280
|
+
* for the supported game-coordinate range below 1e4.
|
|
1281
|
+
*/
|
|
1282
|
+
MESH_FACE_PROJECTION_TOL2: 1e-12,
|
|
1283
|
+
/** Maximum solver-creep depth recoverable behind a one-sided mesh face. */
|
|
1284
|
+
MESH_BEHIND_FACE_RECOVERY: 0.02,
|
|
1285
|
+
/**
|
|
1286
|
+
* A ccd body is "fast" (TOI-bound: pre-advanced to first contact with its approach
|
|
1287
|
+
* velocity arrested) when its intended per-step center motion exceeds this fraction
|
|
1288
|
+
* of its smallest world half-extent (box3d b3_isFast). Below it, the in-loop
|
|
1289
|
+
* speculative margin already suffices.
|
|
1290
|
+
*/
|
|
1291
|
+
CCD_FAST_MOTION_FRACTION: 0.5,
|
|
1292
|
+
/**
|
|
1293
|
+
* box3d B3_SPECULATIVE_DISTANCE (4·SLOP): the small fixed speculative margin for ccd
|
|
1294
|
+
* contacts. It only bridges the exact-TOI clamp's residual separation so a clamped
|
|
1295
|
+
* fast body forms its contact next step; a wide velocity-scaled margin instead
|
|
1296
|
+
* generates diverging far-gap contacts (#2821). Fast bodies are kept from tunneling
|
|
1297
|
+
* by the exact-TOI position clamp (runRigidSolver), not by a wide margin.
|
|
1298
|
+
*/
|
|
1299
|
+
CCD_SPECULATIVE_MARGIN: 0.02,
|
|
799
1300
|
};
|
|
1301
|
+
const TWO_PI = 6.283185307179586;
|
|
1302
|
+
function combinedContactFriction(frictionA10, frictionB10) {
|
|
1303
|
+
return (0, physics3d_shared_js_1.solverSqrt)((frictionA10 / 10) * (frictionB10 / 10));
|
|
1304
|
+
}
|
|
800
1305
|
/**
|
|
801
1306
|
* Warm-cache maps pack the canonical index pair (a<b) into one exact integer
|
|
802
1307
|
* `a * WARM_KEY_BASE + b` — string pair keys were measurable churn on large piles.
|
|
@@ -805,6 +1310,14 @@ exports.WARM_KEY_BASE = 2 ** 20;
|
|
|
805
1310
|
function solid(b) {
|
|
806
1311
|
return b.isDynamic && !b.sleeping;
|
|
807
1312
|
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Bodies whose pose advances this step. Kinematic bodies integrate but are
|
|
1315
|
+
* never solved: `solid` still gates every impulse, gravity, damping and sleep
|
|
1316
|
+
* path, so a kinematic body's authored velocity survives the step untouched.
|
|
1317
|
+
*/
|
|
1318
|
+
function integrates(b) {
|
|
1319
|
+
return b.isKinematic || solid(b);
|
|
1320
|
+
}
|
|
808
1321
|
function worldInvInertiaLocked(b) {
|
|
809
1322
|
if (!solid(b)) {
|
|
810
1323
|
return [0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
@@ -871,11 +1384,12 @@ function takeConstraint() {
|
|
|
871
1384
|
if (constraintPool.high === constraintPool.slots.length) {
|
|
872
1385
|
constraintPool.slots.push({
|
|
873
1386
|
manifoldIndex: 0, a: 0, b: 0,
|
|
874
|
-
normal: ZERO, t1: ZERO, t2: ZERO, rA: ZERO, rB: ZERO,
|
|
875
|
-
penetration: 0,
|
|
876
|
-
massN: 0, massT1: 0, massT2: 0,
|
|
877
|
-
|
|
878
|
-
|
|
1387
|
+
normal: ZERO, t1: ZERO, t2: ZERO, rA: ZERO, rB: ZERO, rA0: ZERO,
|
|
1388
|
+
penetration: 0,
|
|
1389
|
+
massN: 0, massT1: 0, massT2: 0,
|
|
1390
|
+
biasRate: 0, massScale: 1, impulseScale: 0, vn0: 0,
|
|
1391
|
+
accN: 0, accT1: 0, accT2: 0, accR1: 0, accR2: 0, accNStep: 0,
|
|
1392
|
+
mu: 0, rolling: 0, restitution: 0,
|
|
879
1393
|
});
|
|
880
1394
|
}
|
|
881
1395
|
const slot = constraintPool.slots[constraintPool.high];
|
|
@@ -890,19 +1404,30 @@ function solveRigidStep(bodies, options) {
|
|
|
890
1404
|
}
|
|
891
1405
|
constraintPool.high = 0;
|
|
892
1406
|
const dt = options.dt;
|
|
893
|
-
|
|
894
|
-
// v/w
|
|
895
|
-
//
|
|
896
|
-
// path mutates these objects in place.
|
|
1407
|
+
const h = dt / options.substeps;
|
|
1408
|
+
// 1. reset pseudo-velocities; copy v/w so the solver owns them (callers may
|
|
1409
|
+
// pass aliased vectors — canonical angularVel, shared zero constants — and the
|
|
1410
|
+
// hot scalar impulse path mutates these objects in place). Gravity + damping +
|
|
1411
|
+
// position integration now happen inside the sub-step loop. Locked axes are
|
|
1412
|
+
// zeroed here so residual input velocity on a locked axis never integrates.
|
|
897
1413
|
for (const b of bodies) {
|
|
898
1414
|
b.vb = { x: 0, y: 0, z: 0 };
|
|
899
1415
|
b.wb = { x: 0, y: 0, z: 0 };
|
|
900
|
-
b.v = { x: b.v.x, y: b.v.y, z: b.v.z };
|
|
901
|
-
b.w = { x: b.w.x, y: b.w.y, z: b.w.z };
|
|
902
1416
|
if (!solid(b)) {
|
|
1417
|
+
b.v = { x: b.v.x, y: b.v.y, z: b.v.z };
|
|
1418
|
+
b.w = { x: b.w.x, y: b.w.y, z: b.w.z };
|
|
903
1419
|
continue;
|
|
904
1420
|
}
|
|
905
|
-
b.v
|
|
1421
|
+
b.v = {
|
|
1422
|
+
x: b.invMassVec.x === 0 ? 0 : b.v.x,
|
|
1423
|
+
y: b.invMassVec.y === 0 ? 0 : b.v.y,
|
|
1424
|
+
z: b.invMassVec.z === 0 ? 0 : b.v.z,
|
|
1425
|
+
};
|
|
1426
|
+
b.w = {
|
|
1427
|
+
x: b.angularLock.x === 0 ? 0 : b.w.x,
|
|
1428
|
+
y: b.angularLock.y === 0 ? 0 : b.w.y,
|
|
1429
|
+
z: b.angularLock.z === 0 ? 0 : b.w.z,
|
|
1430
|
+
};
|
|
906
1431
|
}
|
|
907
1432
|
// 2. narrow-phase manifolds (canonical order i<j).
|
|
908
1433
|
const manifolds = [];
|
|
@@ -921,7 +1446,7 @@ function solveRigidStep(bodies, options) {
|
|
|
921
1446
|
if ((A.mask & B.layer) === 0 || (B.mask & A.layer) === 0) {
|
|
922
1447
|
return;
|
|
923
1448
|
}
|
|
924
|
-
const margin = A.ccd || B.ccd ?
|
|
1449
|
+
const margin = A.ccd || B.ccd ? exports.SOLVER_CONSTANTS.CCD_SPECULATIVE_MARGIN : 0;
|
|
925
1450
|
const manifold = generateManifold(toGeom(A), toGeom(B), margin);
|
|
926
1451
|
if (manifold !== undefined) {
|
|
927
1452
|
manifolds.push(manifold);
|
|
@@ -984,11 +1509,40 @@ function solveRigidStep(bodies, options) {
|
|
|
984
1509
|
const A = bodies[m.a];
|
|
985
1510
|
const B = bodies[m.b];
|
|
986
1511
|
const [t1, t2] = tangentBasis(m.normal);
|
|
987
|
-
const combinedMu =
|
|
1512
|
+
const combinedMu = combinedContactFriction(toInt10(A.matFriction), toInt10(B.matFriction));
|
|
988
1513
|
const combinedRest = Math.max(toInt10(A.matRestitution), toInt10(B.matRestitution)) / 10;
|
|
1514
|
+
const combinedRolling = Math.max(A.matRolling, B.matRolling);
|
|
1515
|
+
// Soft-constraint coefficients (box2d SoftStep): static/kinematic-involved
|
|
1516
|
+
// pairs solve stiffer. hertz = fraction · substeps/dt keeps ω·h = π/2.
|
|
1517
|
+
const softStatic = !A.isDynamic || !B.isDynamic;
|
|
1518
|
+
const hertz = (exports.SOLVER_CONSTANTS.CONTACT_HERTZ_FRACTION * options.substeps / dt)
|
|
1519
|
+
* (softStatic ? exports.SOLVER_CONSTANTS.STATIC_HERTZ_SCALE : 1);
|
|
1520
|
+
const omega = TWO_PI * hertz;
|
|
1521
|
+
const a1 = 2 * exports.SOLVER_CONSTANTS.CONTACT_ZETA + omega * h;
|
|
1522
|
+
const a2 = h * omega * a1;
|
|
1523
|
+
const a3 = 1 / (1 + a2);
|
|
1524
|
+
const biasRate = omega / a1;
|
|
1525
|
+
const massScale = a2 * a3;
|
|
1526
|
+
const impulseScale = a3;
|
|
989
1527
|
const warmPts = options.warm.get(m.a * exports.WARM_KEY_BASE + m.b);
|
|
990
1528
|
for (const pt of m.points) {
|
|
991
|
-
|
|
1529
|
+
// A separated (speculative) contact anchors at the gap midpoint, so a wide margin
|
|
1530
|
+
// over a large gap yields anchors far from BOTH body centers (moment arms ≈ half
|
|
1531
|
+
// the gap), whose ω×r terms blow up the coupled relax at extreme speed (the #2821
|
|
1532
|
+
// cluster divergence). Pull each anchor back to its own body's surface witness
|
|
1533
|
+
// point along the normal by half the separation; a touching/penetrating contact
|
|
1534
|
+
// (penetration ≥ 0) is untouched, so resting stacks and the golden batteries are
|
|
1535
|
+
// byte-identical.
|
|
1536
|
+
let rA = pt.rA;
|
|
1537
|
+
let rB = pt.rB;
|
|
1538
|
+
if (pt.penetration < 0) {
|
|
1539
|
+
const half = -pt.penetration * 0.5;
|
|
1540
|
+
rA = { x: pt.rA.x - m.normal.x * half, y: pt.rA.y - m.normal.y * half, z: pt.rA.z - m.normal.z * half };
|
|
1541
|
+
rB = { x: pt.rB.x + m.normal.x * half, y: pt.rB.y + m.normal.y * half, z: pt.rB.z + m.normal.z * half };
|
|
1542
|
+
}
|
|
1543
|
+
rA = (0, physics3d_shared_js_1.subVec3)(rA, (0, physics3d_shared_js_1.rotatePointByQuat)(A.q, A.localCenter));
|
|
1544
|
+
rB = (0, physics3d_shared_js_1.subVec3)(rB, (0, physics3d_shared_js_1.rotatePointByQuat)(B.q, B.localCenter));
|
|
1545
|
+
const matched = matchWarm(warmPts, rA);
|
|
992
1546
|
const constraint = takeConstraint();
|
|
993
1547
|
constraint.manifoldIndex = manifoldIndex;
|
|
994
1548
|
constraint.a = m.a;
|
|
@@ -996,22 +1550,29 @@ function solveRigidStep(bodies, options) {
|
|
|
996
1550
|
constraint.normal = m.normal;
|
|
997
1551
|
constraint.t1 = t1;
|
|
998
1552
|
constraint.t2 = t2;
|
|
999
|
-
|
|
1000
|
-
|
|
1553
|
+
// Working anchors are fresh copies (rotated per substep); rA0 keeps the
|
|
1554
|
+
// frame-start anchor for the cache.
|
|
1555
|
+
constraint.rA = { x: rA.x, y: rA.y, z: rA.z };
|
|
1556
|
+
constraint.rB = { x: rB.x, y: rB.y, z: rB.z };
|
|
1557
|
+
constraint.rA0 = rA;
|
|
1001
1558
|
constraint.penetration = pt.penetration;
|
|
1002
|
-
constraint.
|
|
1003
|
-
constraint.
|
|
1004
|
-
constraint.
|
|
1005
|
-
constraint.
|
|
1006
|
-
constraint.
|
|
1559
|
+
constraint.massN = effectiveMass(m.normal, rA, rB, invMass[m.a], invMass[m.b], invInertia[m.a], invInertia[m.b]);
|
|
1560
|
+
constraint.massT1 = effectiveMass(t1, rA, rB, invMass[m.a], invMass[m.b], invInertia[m.a], invInertia[m.b]);
|
|
1561
|
+
constraint.massT2 = effectiveMass(t2, rA, rB, invMass[m.a], invMass[m.b], invInertia[m.a], invInertia[m.b]);
|
|
1562
|
+
constraint.biasRate = biasRate;
|
|
1563
|
+
constraint.massScale = massScale;
|
|
1564
|
+
constraint.impulseScale = impulseScale;
|
|
1565
|
+
// Approach velocity captured pre-substep (before any gravity) for restitution.
|
|
1566
|
+
constraint.vn0 = relativeVelocityAlong(A, B, rA, rB, m.normal.x, m.normal.y, m.normal.z);
|
|
1007
1567
|
constraint.accN = matched?.normalImpulse ?? 0;
|
|
1008
1568
|
constraint.accT1 = matched?.tangentImpulse1 ?? 0;
|
|
1009
1569
|
constraint.accT2 = matched?.tangentImpulse2 ?? 0;
|
|
1010
|
-
constraint.
|
|
1570
|
+
constraint.accR1 = 0;
|
|
1571
|
+
constraint.accR2 = 0;
|
|
1572
|
+
constraint.accNStep = 0;
|
|
1011
1573
|
constraint.mu = combinedMu;
|
|
1574
|
+
constraint.rolling = combinedRolling;
|
|
1012
1575
|
constraint.restitution = combinedRest;
|
|
1013
|
-
// warm-start impulse, scalar form of normal*accN + (t1*accT1 + t2*accT2)
|
|
1014
|
-
applyImpulseScalar(bodies, invMass, invInertia, constraint.a, constraint.b, constraint.rA, constraint.rB, m.normal.x * constraint.accN + (t1.x * constraint.accT1 + t2.x * constraint.accT2), m.normal.y * constraint.accN + (t1.y * constraint.accT1 + t2.y * constraint.accT2), m.normal.z * constraint.accN + (t1.z * constraint.accT1 + t2.z * constraint.accT2), false);
|
|
1015
1576
|
constraints.push(constraint);
|
|
1016
1577
|
}
|
|
1017
1578
|
});
|
|
@@ -1020,7 +1581,7 @@ function solveRigidStep(bodies, options) {
|
|
|
1020
1581
|
bodies,
|
|
1021
1582
|
invMass,
|
|
1022
1583
|
invInertia,
|
|
1023
|
-
dt,
|
|
1584
|
+
dt: h,
|
|
1024
1585
|
apply: (a, b, rA, rB, impulse) => applyImpulse(bodies, invMass, invInertia, a, b, rA, rB, impulse),
|
|
1025
1586
|
applyAngular: (a, b, angularImpulse) => {
|
|
1026
1587
|
const A = bodies[a];
|
|
@@ -1043,17 +1604,90 @@ function solveRigidStep(bodies, options) {
|
|
|
1043
1604
|
applyBias: (a, b, rA, rB, impulse) => applyBiasImpulseAt(bodies, invMass, invInertia, a, b, rA, rB, impulse),
|
|
1044
1605
|
biasRelativeVelocity: (a, b, rA, rB) => relativeBiasVelocity(bodies[a], bodies[b], rA, rB),
|
|
1045
1606
|
};
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1607
|
+
// 5. TGS-Soft sub-step loop: gravity(h) → joint prepare + contact warm-start →
|
|
1608
|
+
// soft-bias iterations → position integration(h) + anchor/penetration update →
|
|
1609
|
+
// bias-free relax → per-substep joint-angle integration. One restitution pass
|
|
1610
|
+
// follows all substeps, driven by the pre-step approach velocity.
|
|
1611
|
+
for (let substep = 0; substep < options.substeps; substep += 1) {
|
|
1612
|
+
for (const b of bodies) {
|
|
1613
|
+
if (solid(b) && b.invMassVec.y !== 0) {
|
|
1614
|
+
b.v.y = b.v.y + options.gravityY * b.gravityScale * h;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1050
1617
|
for (const joint of joints) {
|
|
1051
|
-
joint.
|
|
1618
|
+
joint.prepare(jointCtx);
|
|
1619
|
+
}
|
|
1620
|
+
for (const c of constraints) {
|
|
1621
|
+
applyImpulseScalar(bodies, invMass, invInertia, c.a, c.b, c.rA, c.rB, c.normal.x * c.accN + (c.t1.x * c.accT1 + c.t2.x * c.accT2), c.normal.y * c.accN + (c.t1.y * c.accT1 + c.t2.y * c.accT2), c.normal.z * c.accN + (c.t1.z * c.accT1 + c.t2.z * c.accT2));
|
|
1622
|
+
}
|
|
1623
|
+
for (let iter = 0; iter < options.velocityIterations; iter += 1) {
|
|
1624
|
+
for (const joint of joints) {
|
|
1625
|
+
joint.iterate(jointCtx);
|
|
1626
|
+
}
|
|
1627
|
+
for (const c of constraints) {
|
|
1628
|
+
solveContactSoft(bodies, invMass, invInertia, c, h, true);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
for (const b of bodies) {
|
|
1632
|
+
if (!integrates(b)) {
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
b.c = (0, physics3d_shared_js_1.addVec3)(b.c, (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.addVec3)(b.v, b.vb), h));
|
|
1636
|
+
if (b.hasOrientation) {
|
|
1637
|
+
b.q = (0, physics3d_shared_js_1.integrateQuat)(b.q, (0, physics3d_shared_js_1.addVec3)(b.w, b.wb), h);
|
|
1638
|
+
}
|
|
1639
|
+
b.p = (0, physics3d_shared_js_1.subVec3)(b.c, (0, physics3d_shared_js_1.rotatePointByQuat)(b.q, b.localCenter));
|
|
1052
1640
|
}
|
|
1053
1641
|
for (const c of constraints) {
|
|
1054
|
-
|
|
1642
|
+
const A = bodies[c.a];
|
|
1643
|
+
const B = bodies[c.b];
|
|
1644
|
+
const dpA = integrates(A) ? (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.addVec3)(A.v, A.vb), h) : ZERO;
|
|
1645
|
+
const dpB = integrates(B) ? (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.addVec3)(B.v, B.vb), h) : ZERO;
|
|
1646
|
+
const rotA = integrates(A) ? (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.crossVec3)((0, physics3d_shared_js_1.addVec3)(A.w, A.wb), c.rA), h) : ZERO;
|
|
1647
|
+
const rotB = integrates(B) ? (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.crossVec3)((0, physics3d_shared_js_1.addVec3)(B.w, B.wb), c.rB), h) : ZERO;
|
|
1648
|
+
const rel = (0, physics3d_shared_js_1.subVec3)((0, physics3d_shared_js_1.addVec3)(dpB, rotB), (0, physics3d_shared_js_1.addVec3)(dpA, rotA));
|
|
1649
|
+
c.penetration -= (0, physics3d_shared_js_1.dotVec3)(rel, c.normal);
|
|
1650
|
+
c.rA = (0, physics3d_shared_js_1.addVec3)(c.rA, rotA);
|
|
1651
|
+
c.rB = (0, physics3d_shared_js_1.addVec3)(c.rB, rotB);
|
|
1652
|
+
}
|
|
1653
|
+
for (const b of bodies) {
|
|
1654
|
+
b.vb = { x: 0, y: 0, z: 0 };
|
|
1655
|
+
b.wb = { x: 0, y: 0, z: 0 };
|
|
1656
|
+
}
|
|
1657
|
+
const relaxIterations = options.velocityIterations * exports.SOLVER_CONSTANTS.RELAX_ITERATION_SCALE;
|
|
1658
|
+
for (const c of constraints) {
|
|
1659
|
+
c.accR1 = 0;
|
|
1660
|
+
c.accR2 = 0;
|
|
1661
|
+
}
|
|
1662
|
+
for (let iter = 0; iter < relaxIterations; iter += 1) {
|
|
1663
|
+
for (const c of constraints) {
|
|
1664
|
+
solveContactSoft(bodies, invMass, invInertia, c, h, false);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
// Accumulate the per-substep normal impulse so contact events report the
|
|
1668
|
+
// full-step reaction (the raw accN converges to the sustaining per-substep value).
|
|
1669
|
+
for (const c of constraints) {
|
|
1670
|
+
c.accNStep += c.accN;
|
|
1671
|
+
}
|
|
1672
|
+
for (const joint of joints) {
|
|
1673
|
+
joint.postSubstep?.(jointCtx);
|
|
1055
1674
|
}
|
|
1056
1675
|
}
|
|
1676
|
+
// 5b. restitution: replay the pre-step approach velocity for bouncy contacts.
|
|
1677
|
+
for (const c of constraints) {
|
|
1678
|
+
if (c.restitution <= 0 || c.vn0 >= -exports.SOLVER_CONSTANTS.RESTITUTION_THRESHOLD || c.accN <= 0) {
|
|
1679
|
+
continue;
|
|
1680
|
+
}
|
|
1681
|
+
const A = bodies[c.a];
|
|
1682
|
+
const B = bodies[c.b];
|
|
1683
|
+
const vn = relativeVelocityAlong(A, B, c.rA, c.rB, c.normal.x, c.normal.y, c.normal.z);
|
|
1684
|
+
let dLambda = c.massN * (-c.restitution * c.vn0 - vn);
|
|
1685
|
+
const newAcc = Math.max(0, c.accN + dLambda);
|
|
1686
|
+
dLambda = newAcc - c.accN;
|
|
1687
|
+
c.accN = newAcc;
|
|
1688
|
+
c.accNStep += dLambda;
|
|
1689
|
+
applyImpulseScalar(bodies, invMass, invInertia, c.a, c.b, c.rA, c.rB, c.normal.x * dLambda, c.normal.y * dLambda, c.normal.z * dLambda);
|
|
1690
|
+
}
|
|
1057
1691
|
// 6. store warm cache (index-pair keyed, deterministic order).
|
|
1058
1692
|
const warm = new Map();
|
|
1059
1693
|
for (const c of constraints) {
|
|
@@ -1063,12 +1697,13 @@ function solveRigidStep(bodies, options) {
|
|
|
1063
1697
|
list = [];
|
|
1064
1698
|
warm.set(key, list);
|
|
1065
1699
|
}
|
|
1066
|
-
list.push({ anchor: c.
|
|
1700
|
+
list.push({ anchor: c.rA0, normalImpulse: c.accN, tangentImpulse1: c.accT1, tangentImpulse2: c.accT2 });
|
|
1067
1701
|
}
|
|
1068
1702
|
// `jointBodies` (built at the wake pass) keeps joint members awake — a motor or
|
|
1069
1703
|
// grab can drive them below the sleep velocity threshold, and joints are step
|
|
1070
1704
|
// inputs the sleeper can't see.
|
|
1071
|
-
// 7. damping +
|
|
1705
|
+
// 7. damping (once per step) + axis-lock zeroing + sleep counters. Positions
|
|
1706
|
+
// were already integrated per substep in step 5.
|
|
1072
1707
|
for (const b of bodies) {
|
|
1073
1708
|
if (!solid(b)) {
|
|
1074
1709
|
continue;
|
|
@@ -1087,10 +1722,6 @@ function solveRigidStep(bodies, options) {
|
|
|
1087
1722
|
y: b.angularLock.y === 0 ? 0 : b.w.y,
|
|
1088
1723
|
z: b.angularLock.z === 0 ? 0 : b.w.z,
|
|
1089
1724
|
};
|
|
1090
|
-
b.p = (0, physics3d_shared_js_1.addVec3)(b.p, (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.addVec3)(b.v, b.vb), dt));
|
|
1091
|
-
if (b.hasOrientation) {
|
|
1092
|
-
b.q = (0, physics3d_shared_js_1.integrateQuat)(b.q, (0, physics3d_shared_js_1.addVec3)(b.w, b.wb), dt);
|
|
1093
|
-
}
|
|
1094
1725
|
const speed2 = (0, physics3d_shared_js_1.dotVec3)(b.v, b.v) + (0, physics3d_shared_js_1.dotVec3)(b.w, b.w);
|
|
1095
1726
|
b.lowVelFrames = jointBodies.has(b.index) || speed2 >= exports.SOLVER_CONSTANTS.SLEEP_VEL2 ? 0 : b.lowVelFrames + 1;
|
|
1096
1727
|
}
|
|
@@ -1099,7 +1730,7 @@ function solveRigidStep(bodies, options) {
|
|
|
1099
1730
|
// 9. per-manifold accumulated normal impulse for contact events.
|
|
1100
1731
|
const impulseByManifold = new Array(manifolds.length).fill(0);
|
|
1101
1732
|
for (const c of constraints) {
|
|
1102
|
-
impulseByManifold[c.manifoldIndex] += c.
|
|
1733
|
+
impulseByManifold[c.manifoldIndex] += c.accNStep;
|
|
1103
1734
|
}
|
|
1104
1735
|
const contactImpulses = manifolds.map((m, index) => ({
|
|
1105
1736
|
a: m.a,
|
|
@@ -1115,6 +1746,17 @@ function toInt10(unit) {
|
|
|
1115
1746
|
}
|
|
1116
1747
|
/** World AABB of a solver body's shape (orientation-aware), swept by v·dt when ccd. */
|
|
1117
1748
|
function solverBodyAabb(body, dt) {
|
|
1749
|
+
if (body.shape.kind === 'heightfield') {
|
|
1750
|
+
const bounds = (0, physics3d_heightfield_js_1.heightfieldLocalBounds)(body.shape);
|
|
1751
|
+
return {
|
|
1752
|
+
minX: body.p.x + bounds.minX,
|
|
1753
|
+
maxX: body.p.x + bounds.maxX,
|
|
1754
|
+
minY: body.p.y + bounds.minY,
|
|
1755
|
+
maxY: body.p.y + bounds.maxY,
|
|
1756
|
+
minZ: body.p.z + bounds.minZ,
|
|
1757
|
+
maxZ: body.p.z + bounds.maxZ,
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1118
1760
|
const half = shapeHalfExtent(body.shape, body.q);
|
|
1119
1761
|
let minX = body.p.x - half.x;
|
|
1120
1762
|
let maxX = body.p.x + half.x;
|
|
@@ -1151,6 +1793,14 @@ function shapeHalfExtent(shape, q) {
|
|
|
1151
1793
|
const reach = shape.radius + shape.halfHeight;
|
|
1152
1794
|
return { x: reach, y: reach, z: reach };
|
|
1153
1795
|
}
|
|
1796
|
+
if (shape.kind === 'hull') {
|
|
1797
|
+
let ext = { x: 0, y: 0, z: 0 };
|
|
1798
|
+
for (const vertex of shape.data.vertices) {
|
|
1799
|
+
const rotated = (0, physics3d_shared_js_1.rotatePointByQuat)(q, vertex);
|
|
1800
|
+
ext = { x: Math.max(ext.x, Math.abs(rotated.x)), y: Math.max(ext.y, Math.abs(rotated.y)), z: Math.max(ext.z, Math.abs(rotated.z)) };
|
|
1801
|
+
}
|
|
1802
|
+
return ext;
|
|
1803
|
+
}
|
|
1154
1804
|
if (shape.kind === 'mesh') {
|
|
1155
1805
|
let ext = { x: 0, y: 0, z: 0 };
|
|
1156
1806
|
for (const tri of shape.triangles) {
|
|
@@ -1161,6 +1811,14 @@ function shapeHalfExtent(shape, q) {
|
|
|
1161
1811
|
}
|
|
1162
1812
|
return ext;
|
|
1163
1813
|
}
|
|
1814
|
+
if (shape.kind === 'heightfield') {
|
|
1815
|
+
const bounds = (0, physics3d_heightfield_js_1.heightfieldLocalBounds)(shape);
|
|
1816
|
+
return {
|
|
1817
|
+
x: Math.max(Math.abs(bounds.minX), Math.abs(bounds.maxX)),
|
|
1818
|
+
y: Math.max(Math.abs(bounds.minY), Math.abs(bounds.maxY)),
|
|
1819
|
+
z: Math.max(Math.abs(bounds.minZ), Math.abs(bounds.maxZ)),
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1164
1822
|
// compound: union of child extents about the body center.
|
|
1165
1823
|
let ext = { x: 0, y: 0, z: 0 };
|
|
1166
1824
|
for (const child of shape.children) {
|
|
@@ -1174,15 +1832,68 @@ function shapeHalfExtent(shape, q) {
|
|
|
1174
1832
|
}
|
|
1175
1833
|
return ext;
|
|
1176
1834
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1835
|
+
/**
|
|
1836
|
+
* Smallest world half-extent of a shape (the box3d fast-flag denominator). Uses
|
|
1837
|
+
* the orientation-aware `shapeHalfExtent`, so a rotated thin box reports its true
|
|
1838
|
+
* thin axis. Returns 0 for a degenerate (flat/zero-extent) shape; callers treat a
|
|
1839
|
+
* non-positive minExtent as "never fast" (Req 10, no divide).
|
|
1840
|
+
*/
|
|
1841
|
+
function minExtentOf(shape, q) {
|
|
1842
|
+
const h = shapeHalfExtent(shape, q);
|
|
1843
|
+
return Math.min(h.x, h.y, h.z);
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Rotation-invariant bounding radius (max distance from the shape center to any
|
|
1847
|
+
* surface point). Used as a conservative sphere for the fast-body TOI cast: a
|
|
1848
|
+
* bounding sphere contacts a target no later than the true shape would, so the
|
|
1849
|
+
* cast never lets a fast body tunnel (it stops it short, never past first contact).
|
|
1850
|
+
*/
|
|
1851
|
+
function boundingRadiusOf(shape) {
|
|
1852
|
+
if (shape.kind === 'box') {
|
|
1853
|
+
return (0, physics3d_shared_js_1.solverSqrt)(shape.half.x * shape.half.x + shape.half.y * shape.half.y + shape.half.z * shape.half.z);
|
|
1854
|
+
}
|
|
1855
|
+
if (shape.kind === 'sphere') {
|
|
1856
|
+
return shape.radius;
|
|
1857
|
+
}
|
|
1858
|
+
if (shape.kind === 'capsule') {
|
|
1859
|
+
return shape.radius + shape.halfHeight;
|
|
1860
|
+
}
|
|
1861
|
+
if (shape.kind === 'hull') {
|
|
1862
|
+
let max2 = 0;
|
|
1863
|
+
for (const v of shape.data.vertices) {
|
|
1864
|
+
max2 = Math.max(max2, v.x * v.x + v.y * v.y + v.z * v.z);
|
|
1865
|
+
}
|
|
1866
|
+
return (0, physics3d_shared_js_1.solverSqrt)(max2);
|
|
1867
|
+
}
|
|
1868
|
+
if (shape.kind === 'mesh') {
|
|
1869
|
+
let max2 = 0;
|
|
1870
|
+
for (const tri of shape.triangles) {
|
|
1871
|
+
for (const v of tri) {
|
|
1872
|
+
max2 = Math.max(max2, v.x * v.x + v.y * v.y + v.z * v.z);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return (0, physics3d_shared_js_1.solverSqrt)(max2);
|
|
1876
|
+
}
|
|
1877
|
+
if (shape.kind === 'heightfield') {
|
|
1878
|
+
const bounds = (0, physics3d_heightfield_js_1.heightfieldLocalBounds)(shape);
|
|
1879
|
+
return (0, physics3d_shared_js_1.solverSqrt)(Math.max(bounds.minX * bounds.minX, bounds.maxX * bounds.maxX)
|
|
1880
|
+
+ Math.max(bounds.minY * bounds.minY, bounds.maxY * bounds.maxY)
|
|
1881
|
+
+ Math.max(bounds.minZ * bounds.minZ, bounds.maxZ * bounds.maxZ));
|
|
1882
|
+
}
|
|
1883
|
+
let max = 0;
|
|
1884
|
+
for (const child of shape.children) {
|
|
1885
|
+
const offset = (0, physics3d_shared_js_1.solverSqrt)(child.offset.x * child.offset.x + child.offset.y * child.offset.y + child.offset.z * child.offset.z);
|
|
1886
|
+
max = Math.max(max, offset + boundingRadiusOf(child.shape));
|
|
1887
|
+
}
|
|
1888
|
+
return max;
|
|
1180
1889
|
}
|
|
1181
1890
|
function toGeom(b) {
|
|
1182
|
-
return { index: b.index, p: b.p, q: b.q, shape: b.shape };
|
|
1891
|
+
return { index: b.index, p: b.p, q: b.q, shape: b.shape, v: b.v };
|
|
1183
1892
|
}
|
|
1184
1893
|
function wakeIfContacted(sleeper, other) {
|
|
1185
|
-
|
|
1894
|
+
// A kinematic mover carries no impulse but does displace what it touches, so
|
|
1895
|
+
// it has to wake a sleeping pile the same way an awake dynamic neighbour does.
|
|
1896
|
+
if (sleeper.isDynamic && sleeper.sleeping && (other.isDynamic || other.isKinematic) && !other.sleeping
|
|
1186
1897
|
&& (0, physics3d_shared_js_1.dotVec3)(other.v, other.v) + (0, physics3d_shared_js_1.dotVec3)(other.w, other.w) > exports.SOLVER_CONSTANTS.WAKE_VEL2) {
|
|
1187
1898
|
sleeper.sleeping = false;
|
|
1188
1899
|
sleeper.lowVelFrames = 0;
|
|
@@ -1223,7 +1934,7 @@ function relativeBiasVelocity(A, B, rA, rB) {
|
|
|
1223
1934
|
* order as the vector-helper form (bit-identical results), with zero
|
|
1224
1935
|
* intermediate allocations. This runs per contact per velocity iteration.
|
|
1225
1936
|
*/
|
|
1226
|
-
function applyImpulseScalar(bodies, invMass, invInertia, a, b, rA, rB, px, py, pz
|
|
1937
|
+
function applyImpulseScalar(bodies, invMass, invInertia, a, b, rA, rB, px, py, pz) {
|
|
1227
1938
|
const A = bodies[a];
|
|
1228
1939
|
const B = bodies[b];
|
|
1229
1940
|
if (solid(A)) {
|
|
@@ -1232,8 +1943,8 @@ function applyImpulseScalar(bodies, invMass, invInertia, a, b, rA, rB, px, py, p
|
|
|
1232
1943
|
const cz = rA.x * py - rA.y * px;
|
|
1233
1944
|
const m = invInertia[a];
|
|
1234
1945
|
const inv = invMass[a];
|
|
1235
|
-
const lin =
|
|
1236
|
-
const ang =
|
|
1946
|
+
const lin = A.v;
|
|
1947
|
+
const ang = A.w;
|
|
1237
1948
|
lin.x = lin.x - px * inv.x;
|
|
1238
1949
|
lin.y = lin.y - py * inv.y;
|
|
1239
1950
|
lin.z = lin.z - pz * inv.z;
|
|
@@ -1247,8 +1958,8 @@ function applyImpulseScalar(bodies, invMass, invInertia, a, b, rA, rB, px, py, p
|
|
|
1247
1958
|
const cz = rB.x * py - rB.y * px;
|
|
1248
1959
|
const m = invInertia[b];
|
|
1249
1960
|
const inv = invMass[b];
|
|
1250
|
-
const lin =
|
|
1251
|
-
const ang =
|
|
1961
|
+
const lin = B.v;
|
|
1962
|
+
const ang = B.w;
|
|
1252
1963
|
lin.x = lin.x + px * inv.x;
|
|
1253
1964
|
lin.y = lin.y + py * inv.y;
|
|
1254
1965
|
lin.z = lin.z + pz * inv.z;
|
|
@@ -1269,42 +1980,78 @@ function solveTangentImpulse(bodies, invMass, invInertia, c, A, B, t, mass, acc,
|
|
|
1269
1980
|
let dF = mass * -vt;
|
|
1270
1981
|
const clamped = (0, physics3d_shared_js_1.clampNumber)(acc + dF, -maxF, maxF);
|
|
1271
1982
|
dF = clamped - acc;
|
|
1272
|
-
applyImpulseScalar(bodies, invMass, invInertia, c.a, c.b, c.rA, c.rB, t.x * dF, t.y * dF, t.z * dF
|
|
1983
|
+
applyImpulseScalar(bodies, invMass, invInertia, c.a, c.b, c.rA, c.rB, t.x * dF, t.y * dF, t.z * dF);
|
|
1273
1984
|
return clamped;
|
|
1274
1985
|
}
|
|
1275
|
-
function
|
|
1986
|
+
function angularEffectiveMass(direction, invA, invB) {
|
|
1987
|
+
const response = (0, physics3d_shared_js_1.addVec3)((0, physics3d_shared_js_1.mulMat3Vec)(invA, direction), (0, physics3d_shared_js_1.mulMat3Vec)(invB, direction));
|
|
1988
|
+
const inverseMass = (0, physics3d_shared_js_1.dotVec3)(direction, response);
|
|
1989
|
+
return inverseMass > 0 ? 1 / inverseMass : 0;
|
|
1990
|
+
}
|
|
1991
|
+
function solveRollingResistance(bodies, invInertia, c, A, B) {
|
|
1992
|
+
const maxImpulse = c.rolling * c.accN;
|
|
1993
|
+
if (maxImpulse <= 0) {
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
const relative = (0, physics3d_shared_js_1.subVec3)(B.w, A.w);
|
|
1997
|
+
const massR1 = angularEffectiveMass(c.t1, invInertia[c.a], invInertia[c.b]);
|
|
1998
|
+
const massR2 = angularEffectiveMass(c.t2, invInertia[c.a], invInertia[c.b]);
|
|
1999
|
+
let nextR1 = c.accR1 - massR1 * (0, physics3d_shared_js_1.dotVec3)(relative, c.t1);
|
|
2000
|
+
let nextR2 = c.accR2 - massR2 * (0, physics3d_shared_js_1.dotVec3)(relative, c.t2);
|
|
2001
|
+
const magnitude = (0, physics3d_shared_js_1.solverSqrt)(nextR1 * nextR1 + nextR2 * nextR2);
|
|
2002
|
+
if (magnitude > maxImpulse) {
|
|
2003
|
+
const scale = maxImpulse / magnitude;
|
|
2004
|
+
nextR1 *= scale;
|
|
2005
|
+
nextR2 *= scale;
|
|
2006
|
+
}
|
|
2007
|
+
const deltaR1 = nextR1 - c.accR1;
|
|
2008
|
+
const deltaR2 = nextR2 - c.accR2;
|
|
2009
|
+
c.accR1 = nextR1;
|
|
2010
|
+
c.accR2 = nextR2;
|
|
2011
|
+
const impulse = (0, physics3d_shared_js_1.addVec3)((0, physics3d_shared_js_1.scaleVec3)(c.t1, deltaR1), (0, physics3d_shared_js_1.scaleVec3)(c.t2, deltaR2));
|
|
2012
|
+
if (solid(A)) {
|
|
2013
|
+
A.w = (0, physics3d_shared_js_1.subVec3)(A.w, (0, physics3d_shared_js_1.mulMat3Vec)(invInertia[c.a], impulse));
|
|
2014
|
+
}
|
|
2015
|
+
if (solid(B)) {
|
|
2016
|
+
B.w = (0, physics3d_shared_js_1.addVec3)(B.w, (0, physics3d_shared_js_1.mulMat3Vec)(invInertia[c.b], impulse));
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* One soft-constraint contact solve (box2d SoftStep). `useBias` on the bias pass
|
|
2021
|
+
* folds the soft position bias into real velocity; the bias-free relax pass
|
|
2022
|
+
* (`useBias=false`) removes that transient energy after position integration.
|
|
2023
|
+
*/
|
|
2024
|
+
function solveContactSoft(bodies, invMass, invInertia, c, h, useBias) {
|
|
1276
2025
|
const A = bodies[c.a];
|
|
1277
2026
|
const B = bodies[c.b];
|
|
1278
|
-
// normal (no Baumgarte — position error handled by split impulse)
|
|
1279
2027
|
const vn = relativeVelocityAlong(A, B, c.rA, c.rB, c.normal.x, c.normal.y, c.normal.z);
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
2028
|
+
let bias = 0;
|
|
2029
|
+
let massScale = 1;
|
|
2030
|
+
let impulseScale = 0;
|
|
2031
|
+
if (c.penetration < 0) {
|
|
2032
|
+
// speculative / separated: allow closing exactly up to the gap, never push apart.
|
|
2033
|
+
bias = c.penetration / h;
|
|
2034
|
+
}
|
|
2035
|
+
else if (useBias) {
|
|
2036
|
+
const raw = c.biasRate * (c.penetration - exports.SOLVER_CONSTANTS.SLOP);
|
|
2037
|
+
bias = (0, physics3d_shared_js_1.clampNumber)(raw, 0, exports.SOLVER_CONSTANTS.MAX_CONTACT_PUSHOUT / h);
|
|
2038
|
+
massScale = c.massScale;
|
|
2039
|
+
impulseScale = c.impulseScale;
|
|
2040
|
+
}
|
|
2041
|
+
let dLambda = massScale * c.massN * (-(vn) + bias) - impulseScale * c.accN;
|
|
1285
2042
|
const newAcc = Math.max(0, c.accN + dLambda);
|
|
1286
2043
|
dLambda = newAcc - c.accN;
|
|
1287
2044
|
c.accN = newAcc;
|
|
1288
|
-
applyImpulseScalar(bodies, invMass, invInertia, c.a, c.b, c.rA, c.rB, c.normal.x * dLambda, c.normal.y * dLambda, c.normal.z * dLambda
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
const relbX = (B.vb.x + (B.wb.y * c.rB.z - B.wb.z * c.rB.y)) - (A.vb.x + (A.wb.y * c.rA.z - A.wb.z * c.rA.y));
|
|
1292
|
-
const relbY = (B.vb.y + (B.wb.z * c.rB.x - B.wb.x * c.rB.z)) - (A.vb.y + (A.wb.z * c.rA.x - A.wb.x * c.rA.z));
|
|
1293
|
-
const relbZ = (B.vb.z + (B.wb.x * c.rB.y - B.wb.y * c.rB.x)) - (A.vb.z + (A.wb.x * c.rA.y - A.wb.y * c.rA.x));
|
|
1294
|
-
const vnb = relbX * c.normal.x + relbY * c.normal.y + relbZ * c.normal.z;
|
|
1295
|
-
let dLb = c.massN * (c.bias - vnb);
|
|
1296
|
-
const newAccNb = Math.max(0, c.accNb + dLb);
|
|
1297
|
-
dLb = newAccNb - c.accNb;
|
|
1298
|
-
c.accNb = newAccNb;
|
|
1299
|
-
applyImpulseScalar(bodies, invMass, invInertia, c.a, c.b, c.rA, c.rB, c.normal.x * dLb, c.normal.y * dLb, c.normal.z * dLb, true);
|
|
1300
|
-
}
|
|
1301
|
-
// friction (skip for pure speculative points).
|
|
1302
|
-
if (c.speculative) {
|
|
1303
|
-
return;
|
|
2045
|
+
applyImpulseScalar(bodies, invMass, invInertia, c.a, c.b, c.rA, c.rB, c.normal.x * dLambda, c.normal.y * dLambda, c.normal.z * dLambda);
|
|
2046
|
+
if (c.penetration < 0) {
|
|
2047
|
+
return; // friction skipped for separated/speculative points (unchanged policy)
|
|
1304
2048
|
}
|
|
1305
2049
|
const maxF = c.mu * c.accN;
|
|
1306
2050
|
c.accT1 = solveTangentImpulse(bodies, invMass, invInertia, c, A, B, c.t1, c.massT1, c.accT1, maxF);
|
|
1307
2051
|
c.accT2 = solveTangentImpulse(bodies, invMass, invInertia, c, A, B, c.t2, c.massT2, c.accT2, maxF);
|
|
2052
|
+
if (!useBias && c.rolling > 0) {
|
|
2053
|
+
solveRollingResistance(bodies, invInertia, c, A, B);
|
|
2054
|
+
}
|
|
1308
2055
|
}
|
|
1309
2056
|
function islandSleep(bodies, manifolds, joints) {
|
|
1310
2057
|
const parent = new Map();
|