@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
|
@@ -15,7 +15,10 @@ 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
17
|
exports.solveRigidStep = solveRigidStep;
|
|
18
|
+
exports.minExtentOf = minExtentOf;
|
|
19
|
+
exports.boundingRadiusOf = boundingRadiusOf;
|
|
18
20
|
const physics3d_shared_js_1 = require("./physics3d-shared.js");
|
|
21
|
+
const physics3d_hull_js_1 = require("./physics3d-hull.js");
|
|
19
22
|
const ZERO = { x: 0, y: 0, z: 0 };
|
|
20
23
|
/** Expand a compound body into primitive sub-bodies in world space. */
|
|
21
24
|
function primitiveViews(body) {
|
|
@@ -128,17 +131,38 @@ function collidePrimitives(a, b, centerA, centerB, speculativeMargin, featureBas
|
|
|
128
131
|
if (ka === 'capsule' && kb === 'box') {
|
|
129
132
|
return capsuleBox(a, b, centerA, centerB, speculativeMargin, featureBase, false);
|
|
130
133
|
}
|
|
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
134
|
if (ka === 'box' && kb === 'capsule') {
|
|
133
135
|
return capsuleBox(b, a, centerB, centerA, speculativeMargin, featureBase, true);
|
|
134
136
|
}
|
|
135
|
-
|
|
137
|
+
if (ka === 'hull' && kb === 'hull') {
|
|
138
|
+
return collideHulls(a, b, centerA, centerB, speculativeMargin, featureBase);
|
|
139
|
+
}
|
|
140
|
+
if (ka === 'hull' && kb === 'box') {
|
|
141
|
+
return collideHulls(a, boxAsHull(b), centerA, centerB, speculativeMargin, featureBase);
|
|
142
|
+
}
|
|
143
|
+
if (ka === 'box' && kb === 'hull') {
|
|
144
|
+
return collideHulls(boxAsHull(a), b, centerA, centerB, speculativeMargin, featureBase);
|
|
145
|
+
}
|
|
146
|
+
if (ka === 'hull' && kb === 'sphere') {
|
|
147
|
+
return sphereHull(b, a, centerB, centerA, speculativeMargin, featureBase, true);
|
|
148
|
+
}
|
|
149
|
+
if (ka === 'sphere' && kb === 'hull') {
|
|
150
|
+
return sphereHull(a, b, centerA, centerB, speculativeMargin, featureBase, false);
|
|
151
|
+
}
|
|
152
|
+
if (ka === 'hull' && kb === 'capsule') {
|
|
153
|
+
return capsuleHull(b, a, centerB, centerA, speculativeMargin, featureBase, true);
|
|
154
|
+
}
|
|
155
|
+
/* istanbul ignore else -- every primitive shape pairing is enumerated above; the fallthrough is an exhaustiveness guard. */
|
|
156
|
+
if (ka === 'capsule' && kb === 'hull') {
|
|
157
|
+
return capsuleHull(a, b, centerA, centerB, speculativeMargin, featureBase, false);
|
|
158
|
+
}
|
|
159
|
+
/* istanbul ignore next -- unreachable exhaustiveness guard. */
|
|
136
160
|
return undefined;
|
|
137
161
|
}
|
|
138
162
|
// ---------- sphere / capsule closed forms ----------
|
|
139
163
|
function radiusOf(shape) {
|
|
140
|
-
/* istanbul ignore next -- radiusOf is only called for sphere/capsule operands; the
|
|
141
|
-
return shape.kind === '
|
|
164
|
+
/* istanbul ignore next -- radiusOf is only called for sphere/capsule operands; the non-radius arm is defensive. */
|
|
165
|
+
return shape.kind === 'sphere' || shape.kind === 'capsule' ? shape.radius : 0;
|
|
142
166
|
}
|
|
143
167
|
/**
|
|
144
168
|
* Build a single contact point from two world points on the surfaces of two
|
|
@@ -617,6 +641,7 @@ function collideBoxes(A, B, centerA, centerB, speculativeMargin, featureBase) {
|
|
|
617
641
|
swap = poly;
|
|
618
642
|
poly = spare;
|
|
619
643
|
spare = swap;
|
|
644
|
+
/* 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
645
|
if (polyCount === 0) {
|
|
621
646
|
return undefined;
|
|
622
647
|
}
|
|
@@ -648,6 +673,404 @@ function collideBoxes(A, B, centerA, centerB, speculativeMargin, featureBase) {
|
|
|
648
673
|
}
|
|
649
674
|
return { a: 0, b: 0, normal, points };
|
|
650
675
|
}
|
|
676
|
+
// ---------- convex hull SAT + face clipping ----------
|
|
677
|
+
/** Outward bias on reference side planes so boundary-incident corners survive fp clipping. */
|
|
678
|
+
const CLIP_BOUNDARY_TOL = 1e-4;
|
|
679
|
+
// Box-as-hull view: the 8 corners of a box are a valid convex hull, so hull×box
|
|
680
|
+
// reuses the generalized hull path. Keyed by the box shape object identity (pure
|
|
681
|
+
// derived data, never canonical state — same rationale as quatMat3Cache).
|
|
682
|
+
// rdm-ignore-next-line determinism/no-module-mutable-state: pure derived box-hull view keyed by immutable box shape identity
|
|
683
|
+
const boxHullViewCache = new WeakMap();
|
|
684
|
+
function boxAsHull(box) {
|
|
685
|
+
const shape = box.shape;
|
|
686
|
+
/* istanbul ignore next -- boxAsHull is only called for box operands; type guard only. */
|
|
687
|
+
if (shape.kind !== 'box') {
|
|
688
|
+
throw new Error('boxAsHull requires a box');
|
|
689
|
+
}
|
|
690
|
+
let data = boxHullViewCache.get(shape);
|
|
691
|
+
if (data === undefined) {
|
|
692
|
+
const h = shape.half;
|
|
693
|
+
const verts = [];
|
|
694
|
+
for (const sx of [-1, 1]) {
|
|
695
|
+
for (const sy of [-1, 1]) {
|
|
696
|
+
for (const sz of [-1, 1]) {
|
|
697
|
+
verts.push({ x: sx * h.x, y: sy * h.y, z: sz * h.z });
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
data = (0, physics3d_hull_js_1.buildSolverHull)(verts);
|
|
702
|
+
boxHullViewCache.set(shape, data);
|
|
703
|
+
}
|
|
704
|
+
return { index: box.index, p: box.p, q: box.q, shape: { kind: 'hull', data } };
|
|
705
|
+
}
|
|
706
|
+
function hullData(shape) {
|
|
707
|
+
/* istanbul ignore next -- callers guard hull; type guard only. */
|
|
708
|
+
if (shape.kind !== 'hull') {
|
|
709
|
+
throw new Error('hullData requires a hull');
|
|
710
|
+
}
|
|
711
|
+
return shape.data;
|
|
712
|
+
}
|
|
713
|
+
/** World-space hull vertices (support samples for mesh + queries). */
|
|
714
|
+
function hullWorldVertices(hull) {
|
|
715
|
+
const data = hullData(hull.shape);
|
|
716
|
+
return data.vertices.map((v) => (0, physics3d_shared_js_1.addVec3)(hull.p, (0, physics3d_shared_js_1.rotatePointByQuat)(hull.q, v)));
|
|
717
|
+
}
|
|
718
|
+
function hullWorldFaces(hull, worldVerts) {
|
|
719
|
+
const data = hullData(hull.shape);
|
|
720
|
+
const rot = quatToMat3Cached(hull.q);
|
|
721
|
+
return data.faces.map((face) => {
|
|
722
|
+
const nx = rot[0] * face.normal.x + rot[1] * face.normal.y + rot[2] * face.normal.z;
|
|
723
|
+
const ny = rot[3] * face.normal.x + rot[4] * face.normal.y + rot[5] * face.normal.z;
|
|
724
|
+
const nz = rot[6] * face.normal.x + rot[7] * face.normal.y + rot[8] * face.normal.z;
|
|
725
|
+
const v0 = worldVerts[face.verts[0]];
|
|
726
|
+
const offset = nx * v0.x + ny * v0.y + nz * v0.z;
|
|
727
|
+
return { nx, ny, nz, offset, verts: face.verts };
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
function hullWorldEdgeDirs(hull) {
|
|
731
|
+
const data = hullData(hull.shape);
|
|
732
|
+
return data.edgeDirs.map((d) => (0, physics3d_shared_js_1.rotatePointByQuat)(hull.q, d));
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Project a hull (given its world vertices) onto an axis, returning the max
|
|
736
|
+
* support value dot(n, v). Used for SAT separation along arbitrary axes.
|
|
737
|
+
*/
|
|
738
|
+
function hullSupportMax(worldVerts, nx, ny, nz) {
|
|
739
|
+
let max = -Infinity;
|
|
740
|
+
for (const v of worldVerts) {
|
|
741
|
+
const d = nx * v.x + ny * v.y + nz * v.z;
|
|
742
|
+
if (d > max) {
|
|
743
|
+
max = d;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return max;
|
|
747
|
+
}
|
|
748
|
+
function hullSupportMin(worldVerts, nx, ny, nz) {
|
|
749
|
+
let min = Infinity;
|
|
750
|
+
for (const v of worldVerts) {
|
|
751
|
+
const d = nx * v.x + ny * v.y + nz * v.z;
|
|
752
|
+
if (d < min) {
|
|
753
|
+
min = d;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return min;
|
|
757
|
+
}
|
|
758
|
+
/** Separation of B beyond A along axis n (positive = separated). */
|
|
759
|
+
function axisSeparation(vertsA, vertsB, nx, ny, nz) {
|
|
760
|
+
const maxA = hullSupportMax(vertsA, nx, ny, nz);
|
|
761
|
+
const minB = hullSupportMin(vertsB, nx, ny, nz);
|
|
762
|
+
return minB - maxA;
|
|
763
|
+
}
|
|
764
|
+
function collideHulls(A, B, centerA, centerB, speculativeMargin, featureBase) {
|
|
765
|
+
const vertsA = hullWorldVertices(A);
|
|
766
|
+
const vertsB = hullWorldVertices(B);
|
|
767
|
+
const facesA = hullWorldFaces(A, vertsA);
|
|
768
|
+
const facesB = hullWorldFaces(B, vertsB);
|
|
769
|
+
const dx = B.p.x - A.p.x;
|
|
770
|
+
const dy = B.p.y - A.p.y;
|
|
771
|
+
const dz = B.p.z - A.p.z;
|
|
772
|
+
// Face SAT: use each face's outward normal directly (already oriented).
|
|
773
|
+
let faceSep = -Infinity;
|
|
774
|
+
let faceRefIsA = true;
|
|
775
|
+
let faceIndex = -1;
|
|
776
|
+
let faceNx = 0;
|
|
777
|
+
let faceNy = 0;
|
|
778
|
+
let faceNz = 0;
|
|
779
|
+
for (let i = 0; i < facesA.length; i += 1) {
|
|
780
|
+
const f = facesA[i];
|
|
781
|
+
// Reference face outward normal points away from A. All A verts satisfy
|
|
782
|
+
// dot(n,v) <= offset; separation = min_B(dot(n,v)) - offset (>0 = gap).
|
|
783
|
+
// The best reference face maximizes separation (shallowest penetration).
|
|
784
|
+
// Strict-greater keeps the lowest sorted-index face on near-ties (stable).
|
|
785
|
+
const s = hullSupportMin(vertsB, f.nx, f.ny, f.nz) - f.offset;
|
|
786
|
+
if (s > faceSep + (i === 0 ? 0 : 1e-9)) {
|
|
787
|
+
faceSep = s;
|
|
788
|
+
faceRefIsA = true;
|
|
789
|
+
faceIndex = i;
|
|
790
|
+
faceNx = f.nx;
|
|
791
|
+
faceNy = f.ny;
|
|
792
|
+
faceNz = f.nz;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
for (let i = 0; i < facesB.length; i += 1) {
|
|
796
|
+
const f = facesB[i];
|
|
797
|
+
const s = hullSupportMin(vertsA, f.nx, f.ny, f.nz) - f.offset;
|
|
798
|
+
// Hysteresis: B's face wins only when strictly deeper, so a resting stack
|
|
799
|
+
// keeps a stable reference frame-to-frame (warm-start anchors don't jump).
|
|
800
|
+
if (s > faceSep + 1e-9) {
|
|
801
|
+
faceSep = s;
|
|
802
|
+
faceRefIsA = false;
|
|
803
|
+
faceIndex = i;
|
|
804
|
+
faceNx = f.nx;
|
|
805
|
+
faceNy = f.ny;
|
|
806
|
+
faceNz = f.nz;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (faceSep > speculativeMargin) {
|
|
810
|
+
return undefined;
|
|
811
|
+
}
|
|
812
|
+
// Edge SAT: cross products of edge-direction pairs.
|
|
813
|
+
const edgesA = hullWorldEdgeDirs(A);
|
|
814
|
+
const edgesB = hullWorldEdgeDirs(B);
|
|
815
|
+
let edgeSep = -Infinity;
|
|
816
|
+
let edgeNx = 0;
|
|
817
|
+
let edgeNy = 0;
|
|
818
|
+
let edgeNz = 0;
|
|
819
|
+
let edgeFound = false;
|
|
820
|
+
let edgePairIndex = -1;
|
|
821
|
+
for (let i = 0; i < edgesA.length; i += 1) {
|
|
822
|
+
const ea = edgesA[i];
|
|
823
|
+
for (let j = 0; j < edgesB.length; j += 1) {
|
|
824
|
+
const eb = edgesB[j];
|
|
825
|
+
let cx = ea.y * eb.z - ea.z * eb.y;
|
|
826
|
+
let cy = ea.z * eb.x - ea.x * eb.z;
|
|
827
|
+
let cz = ea.x * eb.y - ea.y * eb.x;
|
|
828
|
+
const l2 = cx * cx + cy * cy + cz * cz;
|
|
829
|
+
if (l2 < 1e-8) {
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const inv = 1 / (0, physics3d_shared_js_1.solverSqrt)(l2);
|
|
833
|
+
cx *= inv;
|
|
834
|
+
cy *= inv;
|
|
835
|
+
cz *= inv;
|
|
836
|
+
// orient axis from A toward B
|
|
837
|
+
if (cx * dx + cy * dy + cz * dz < 0) {
|
|
838
|
+
cx = -cx;
|
|
839
|
+
cy = -cy;
|
|
840
|
+
cz = -cz;
|
|
841
|
+
}
|
|
842
|
+
const sep = axisSeparation(vertsA, vertsB, cx, cy, cz);
|
|
843
|
+
if (sep > speculativeMargin) {
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
if (sep > edgeSep + 1e-9) {
|
|
847
|
+
edgeSep = sep;
|
|
848
|
+
edgeNx = cx;
|
|
849
|
+
edgeNy = cy;
|
|
850
|
+
edgeNz = cz;
|
|
851
|
+
edgeFound = true;
|
|
852
|
+
edgePairIndex = i * edgesB.length + j;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
// qu3e-style hysteresis: prefer the edge axis only when decisively shallower
|
|
857
|
+
// separation (deeper penetration) than the face axis, and only for real overlaps.
|
|
858
|
+
const facePen = -faceSep;
|
|
859
|
+
const edgePen = -edgeSep;
|
|
860
|
+
const edgePreferred = edgeFound && facePen > 0 && edgePen < facePen * 0.9 - 5e-3;
|
|
861
|
+
if (edgePreferred) {
|
|
862
|
+
return hullEdgeContact(A, B, edgesA, edgesB, edgePairIndex, edgeNx, edgeNy, edgeNz, edgePen, centerA, centerB, featureBase);
|
|
863
|
+
}
|
|
864
|
+
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);
|
|
865
|
+
}
|
|
866
|
+
function hullEdgeContact(A, B, edgesA, edgesB, pairIndex, nx, ny, nz, penetration, centerA, centerB, featureBase) {
|
|
867
|
+
const i = Math.trunc(pairIndex / edgesB.length);
|
|
868
|
+
const j = pairIndex % edgesB.length;
|
|
869
|
+
const vertsA = hullWorldVertices(A);
|
|
870
|
+
const vertsB = hullWorldVertices(B);
|
|
871
|
+
// support vertices of A along +n and B along -n approximate the contacting edges
|
|
872
|
+
const pA = supportVertex(vertsA, nx, ny, nz);
|
|
873
|
+
const pB = supportVertex(vertsB, -nx, -ny, -nz);
|
|
874
|
+
const eA = edgesA[i];
|
|
875
|
+
const eB = edgesB[j];
|
|
876
|
+
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)));
|
|
877
|
+
const mid = (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.addVec3)(seg.c1, seg.c2), 0.5);
|
|
878
|
+
const normal = { x: nx, y: ny, z: nz };
|
|
879
|
+
return {
|
|
880
|
+
a: 0,
|
|
881
|
+
b: 0,
|
|
882
|
+
normal,
|
|
883
|
+
points: [{ rA: (0, physics3d_shared_js_1.subVec3)(mid, centerA), rB: (0, physics3d_shared_js_1.subVec3)(mid, centerB), penetration, feature: featureBase + 4096 + pairIndex }],
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function supportVertex(worldVerts, nx, ny, nz) {
|
|
887
|
+
let best = worldVerts[0];
|
|
888
|
+
let bestD = -Infinity;
|
|
889
|
+
for (const v of worldVerts) {
|
|
890
|
+
const d = nx * v.x + ny * v.y + nz * v.z;
|
|
891
|
+
if (d > bestD) {
|
|
892
|
+
bestD = d;
|
|
893
|
+
best = v;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return best;
|
|
897
|
+
}
|
|
898
|
+
function hullFaceContact(ref, inc, refVerts, incVerts, refFaces, incFaces, refFaceIndex, nx, ny, nz, refIsA, speculativeMargin, centerA, centerB, featureBase) {
|
|
899
|
+
void ref;
|
|
900
|
+
void inc;
|
|
901
|
+
const refFace = refFaces[refFaceIndex];
|
|
902
|
+
// Incident face = the incident-hull face most anti-parallel to the reference normal.
|
|
903
|
+
let incFaceIndex = 0;
|
|
904
|
+
let minDot = Infinity;
|
|
905
|
+
for (let i = 0; i < incFaces.length; i += 1) {
|
|
906
|
+
const f = incFaces[i];
|
|
907
|
+
const d = f.nx * nx + f.ny * ny + f.nz * nz;
|
|
908
|
+
// Strict-less keeps the lowest sorted-index incident face on near-ties, so
|
|
909
|
+
// the clipped polygon (and its feature ids) stay stable frame-to-frame.
|
|
910
|
+
if (d < minDot - (i === 0 ? Infinity : 1e-9)) {
|
|
911
|
+
minDot = d;
|
|
912
|
+
incFaceIndex = i;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const incFace = incFaces[incFaceIndex];
|
|
916
|
+
// Load incident face polygon into scratch.
|
|
917
|
+
ensureClipSlots(clipScratch.a, incFace.verts.length);
|
|
918
|
+
let poly = clipScratch.a;
|
|
919
|
+
let spare = clipScratch.b;
|
|
920
|
+
let polyCount = 0;
|
|
921
|
+
for (const vi of incFace.verts) {
|
|
922
|
+
const v = incVerts[vi];
|
|
923
|
+
const slot = poly[polyCount];
|
|
924
|
+
slot.x = v.x;
|
|
925
|
+
slot.y = v.y;
|
|
926
|
+
slot.z = v.z;
|
|
927
|
+
slot.id = featureBase + refFaceIndex * 64 + polyCount + 1;
|
|
928
|
+
polyCount += 1;
|
|
929
|
+
}
|
|
930
|
+
// Clip the incident polygon against each reference-face side plane. Each side
|
|
931
|
+
// plane's normal must point OUTWARD from the reference polygon so clipPlaneScratch
|
|
932
|
+
// (keeps dc <= 0) retains interior points. Orientation is fixed against the
|
|
933
|
+
// face centroid rather than assuming a winding convention.
|
|
934
|
+
const rv = refFace.verts;
|
|
935
|
+
let rcx = 0;
|
|
936
|
+
let rcy = 0;
|
|
937
|
+
let rcz = 0;
|
|
938
|
+
for (const vi of rv) {
|
|
939
|
+
rcx += refVerts[vi].x;
|
|
940
|
+
rcy += refVerts[vi].y;
|
|
941
|
+
rcz += refVerts[vi].z;
|
|
942
|
+
}
|
|
943
|
+
const rInv = 1 / rv.length;
|
|
944
|
+
rcx *= rInv;
|
|
945
|
+
rcy *= rInv;
|
|
946
|
+
rcz *= rInv;
|
|
947
|
+
for (let e = 0; e < rv.length; e += 1) {
|
|
948
|
+
const a = refVerts[rv[e]];
|
|
949
|
+
const b = refVerts[rv[(e + 1) % rv.length]];
|
|
950
|
+
const edge = (0, physics3d_shared_js_1.subVec3)(b, a);
|
|
951
|
+
let pnx = ny * edge.z - nz * edge.y;
|
|
952
|
+
let pny = nz * edge.x - nx * edge.z;
|
|
953
|
+
let pnz = nx * edge.y - ny * edge.x;
|
|
954
|
+
const l2 = pnx * pnx + pny * pny + pnz * pnz;
|
|
955
|
+
/* 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). */
|
|
956
|
+
if (l2 < 1e-12) {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
const invl = 1 / (0, physics3d_shared_js_1.solverSqrt)(l2);
|
|
960
|
+
pnx *= invl;
|
|
961
|
+
pny *= invl;
|
|
962
|
+
pnz *= invl;
|
|
963
|
+
let offset = pnx * a.x + pny * a.y + pnz * a.z;
|
|
964
|
+
// Flip to outward: the centroid (interior) must lie on the negative side.
|
|
965
|
+
/* 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. */
|
|
966
|
+
if (pnx * rcx + pny * rcy + pnz * rcz > offset) {
|
|
967
|
+
pnx = -pnx;
|
|
968
|
+
pny = -pny;
|
|
969
|
+
pnz = -pnz;
|
|
970
|
+
offset = -offset;
|
|
971
|
+
}
|
|
972
|
+
// Bias the side plane slightly outward so incident-face corners that lie
|
|
973
|
+
// exactly on a reference-face boundary survive floating-point clipping
|
|
974
|
+
// (dropping them mid-settle collapses a resting face manifold to 1-3 points
|
|
975
|
+
// on one side and injects drift). Box clipping avoids this via exact axis
|
|
976
|
+
// alignment; the generic hull path needs the tolerance.
|
|
977
|
+
offset += CLIP_BOUNDARY_TOL;
|
|
978
|
+
polyCount = clipPlaneScratch(poly, polyCount, pnx, pny, pnz, offset, e + 1, spare);
|
|
979
|
+
const swap = poly;
|
|
980
|
+
poly = spare;
|
|
981
|
+
spare = swap;
|
|
982
|
+
if (polyCount === 0) {
|
|
983
|
+
return undefined;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
const refFaceDist = refFace.offset;
|
|
987
|
+
const points = [];
|
|
988
|
+
for (let i = 0; i < polyCount; i += 1) {
|
|
989
|
+
const vert = poly[i];
|
|
990
|
+
const depth = refFaceDist - (nx * vert.x + ny * vert.y + nz * vert.z);
|
|
991
|
+
if (depth >= -speculativeMargin) {
|
|
992
|
+
const onRefX = vert.x + nx * depth;
|
|
993
|
+
const onRefY = vert.y + ny * depth;
|
|
994
|
+
const onRefZ = vert.z + nz * depth;
|
|
995
|
+
const midX = (vert.x + onRefX) * 0.5;
|
|
996
|
+
const midY = (vert.y + onRefY) * 0.5;
|
|
997
|
+
const midZ = (vert.z + onRefZ) * 0.5;
|
|
998
|
+
points.push({
|
|
999
|
+
rA: { x: midX - centerA.x, y: midY - centerA.y, z: midZ - centerA.z },
|
|
1000
|
+
rB: { x: midX - centerB.x, y: midY - centerB.y, z: midZ - centerB.z },
|
|
1001
|
+
penetration: depth,
|
|
1002
|
+
feature: vert.id,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/* istanbul ignore next -- SAT overlap guarantees the incident face's deepest surviving vertex is within the speculative margin (20k-pair fuzz: never hit). */
|
|
1007
|
+
if (points.length === 0) {
|
|
1008
|
+
return undefined;
|
|
1009
|
+
}
|
|
1010
|
+
// Normal in caller A->B frame: reference face outward normal points ref->inc.
|
|
1011
|
+
// If ref is A, normal ref->inc = A->B = +n. If ref is B, A->B = -n.
|
|
1012
|
+
const normal = refIsA ? { x: nx, y: ny, z: nz } : { x: -nx, y: -ny, z: -nz };
|
|
1013
|
+
return { a: 0, b: 0, normal, points: reducePoints(points) };
|
|
1014
|
+
}
|
|
1015
|
+
function sphereHull(sphere, hull, cSphere, cHull, margin, feature, flip) {
|
|
1016
|
+
/* istanbul ignore next -- dispatch guarantees a (sphere, hull) pair; type guard only. */
|
|
1017
|
+
if (sphere.shape.kind !== 'sphere' || hull.shape.kind !== 'hull') {
|
|
1018
|
+
return undefined;
|
|
1019
|
+
}
|
|
1020
|
+
const radius = sphere.shape.radius;
|
|
1021
|
+
const data = hull.shape.data;
|
|
1022
|
+
const localCenter = (0, physics3d_shared_js_1.rotatePointByQuatInverse)(hull.q, (0, physics3d_shared_js_1.subVec3)(sphere.p, hull.p));
|
|
1023
|
+
const closest = (0, physics3d_hull_js_1.closestPointOnHullLocal)(localCenter, data);
|
|
1024
|
+
if (closest.inside) {
|
|
1025
|
+
// sphere center inside the hull: push out along the least-violated face normal.
|
|
1026
|
+
const nLocal = closest.normal;
|
|
1027
|
+
const nWorld = (0, physics3d_shared_js_1.rotatePointByQuat)(hull.q, nLocal); // outward from hull
|
|
1028
|
+
const penetration = -closest.distance + radius;
|
|
1029
|
+
const nSphereToHull = (0, physics3d_shared_js_1.scaleVec3)(nWorld, -1);
|
|
1030
|
+
return emitSingle(nSphereToHull, sphere.p, penetration, cSphere, cHull, feature, flip);
|
|
1031
|
+
}
|
|
1032
|
+
const closestWorld = (0, physics3d_shared_js_1.addVec3)(hull.p, (0, physics3d_shared_js_1.rotatePointByQuat)(hull.q, closest.point));
|
|
1033
|
+
const delta = (0, physics3d_shared_js_1.subVec3)(sphere.p, closestWorld);
|
|
1034
|
+
const dist = (0, physics3d_shared_js_1.solverSqrt)((0, physics3d_shared_js_1.dotVec3)(delta, delta));
|
|
1035
|
+
const penetration = radius - dist;
|
|
1036
|
+
if (penetration < -margin) {
|
|
1037
|
+
return undefined;
|
|
1038
|
+
}
|
|
1039
|
+
/* istanbul ignore next -- a sphere centre coincident with its closest hull point is `inside` and returned above, so dist is positive here. */
|
|
1040
|
+
const nHullToSphere = dist > 1e-9 ? (0, physics3d_shared_js_1.scaleVec3)(delta, 1 / dist) : (0, physics3d_shared_js_1.rotatePointByQuat)(hull.q, closest.normal);
|
|
1041
|
+
const nSphereToHull = (0, physics3d_shared_js_1.scaleVec3)(nHullToSphere, -1);
|
|
1042
|
+
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);
|
|
1043
|
+
return emitSingle(nSphereToHull, mid, penetration, cSphere, cHull, feature, flip);
|
|
1044
|
+
}
|
|
1045
|
+
function capsuleHull(capsule, hull, cCapsule, cHull, margin, feature, flip) {
|
|
1046
|
+
const seg = capsuleSegment(capsule);
|
|
1047
|
+
const samples = [seg.start, seg.end, (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.addVec3)(seg.start, seg.end), 0.5)];
|
|
1048
|
+
const points = [];
|
|
1049
|
+
let normal;
|
|
1050
|
+
let deepest = -Infinity;
|
|
1051
|
+
samples.forEach((sample, i) => {
|
|
1052
|
+
const sphere = { index: capsule.index, p: sample, q: capsule.q, shape: { kind: 'sphere', radius: seg.radius } };
|
|
1053
|
+
const sub = sphereHull(sphere, hull, cCapsule, cHull, margin, feature + i, flip);
|
|
1054
|
+
if (sub === undefined) {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
for (const point of sub.points) {
|
|
1058
|
+
points.push(point);
|
|
1059
|
+
}
|
|
1060
|
+
const d = sub.points[0].penetration;
|
|
1061
|
+
if (d > deepest) {
|
|
1062
|
+
deepest = d;
|
|
1063
|
+
normal = sub.normal;
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
if (points.length === 0 || normal === undefined) {
|
|
1067
|
+
return undefined;
|
|
1068
|
+
}
|
|
1069
|
+
return { a: 0, b: 0, normal, points };
|
|
1070
|
+
}
|
|
1071
|
+
function hullSupportSamples(hull) {
|
|
1072
|
+
return hullWorldVertices(hull);
|
|
1073
|
+
}
|
|
651
1074
|
// ---------- mesh (one-sided static triangle) ----------
|
|
652
1075
|
function meshManifold(a, b, speculativeMargin) {
|
|
653
1076
|
const meshIsA = a.shape.kind === 'mesh';
|
|
@@ -667,29 +1090,55 @@ function meshManifold(a, b, speculativeMargin) {
|
|
|
667
1090
|
(0, physics3d_shared_js_1.addVec3)(meshBody.p, (0, physics3d_shared_js_1.rotatePointByQuat)(meshBody.q, tri[2])),
|
|
668
1091
|
]);
|
|
669
1092
|
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') {
|
|
1093
|
+
/* istanbul ignore next -- primitiveViews only yields box/sphere/capsule/hull sub-bodies; this skip is unreachable. */
|
|
1094
|
+
if (prim.shape.kind !== 'sphere' && prim.shape.kind !== 'capsule' && prim.shape.kind !== 'box' && prim.shape.kind !== 'hull') {
|
|
672
1095
|
continue;
|
|
673
1096
|
}
|
|
674
|
-
const radius = prim.shape.kind === '
|
|
675
|
-
?
|
|
676
|
-
:
|
|
677
|
-
//
|
|
678
|
-
//
|
|
1097
|
+
const radius = prim.shape.kind === 'sphere' || prim.shape.kind === 'capsule'
|
|
1098
|
+
? prim.shape.radius
|
|
1099
|
+
: 0;
|
|
1100
|
+
// Boxes/hulls sample their support vertices against triangles (like dynamic
|
|
1101
|
+
// box-vs-mesh); sphere/capsule use center/segment samples.
|
|
679
1102
|
const samplePoints = prim.shape.kind === 'box'
|
|
680
1103
|
? boxSupportSamples(prim)
|
|
681
|
-
: prim.shape.kind === '
|
|
682
|
-
?
|
|
683
|
-
:
|
|
1104
|
+
: prim.shape.kind === 'hull'
|
|
1105
|
+
? hullSupportSamples(prim)
|
|
1106
|
+
: prim.shape.kind === 'capsule'
|
|
1107
|
+
? capsuleSamples(prim)
|
|
1108
|
+
: [prim.p];
|
|
1109
|
+
// A support point that crossed behind the face plane still has to be pushed
|
|
1110
|
+
// back out, but a shape that fully passed the surface must not be yanked
|
|
1111
|
+
// back; cap the depth we will claim at the sample's own shape diameter.
|
|
1112
|
+
const maxPenetration = radius + 2 * boundingRadiusOf(prim.shape);
|
|
684
1113
|
for (const triangle of triangles) {
|
|
685
1114
|
for (const sample of samplePoints) {
|
|
686
1115
|
const closest = closestPointOnTriangle(sample, triangle[0], triangle[1], triangle[2]);
|
|
687
1116
|
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
1117
|
const triNormal = triangleNormal(triangle[0], triangle[1], triangle[2]);
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
1118
|
+
const planeDist = (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.subVec3)(sample, triangle[0]), triNormal);
|
|
1119
|
+
// delta = perp + planeDist·n, so perp² = |delta|² - planeDist². A zero
|
|
1120
|
+
// perp means the sample projects onto the face interior (or an edge),
|
|
1121
|
+
// where the signed plane distance — not the unsigned closest-point
|
|
1122
|
+
// distance — carries the penetration. Zero-radius shapes (box, hull)
|
|
1123
|
+
// have no other source of depth, so this is the branch that lets them
|
|
1124
|
+
// rest on and be pushed by a mesh at all.
|
|
1125
|
+
const delta2 = (0, physics3d_shared_js_1.dotVec3)(delta, delta);
|
|
1126
|
+
const perp2 = delta2 - planeDist * planeDist;
|
|
1127
|
+
let penetration;
|
|
1128
|
+
if (perp2 <= exports.SOLVER_CONSTANTS.MESH_FACE_PROJECTION_TOL2) {
|
|
1129
|
+
penetration = radius - planeDist;
|
|
1130
|
+
if (penetration > maxPenetration) {
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
// Edge/vertex region: one-sided, front-face only.
|
|
1136
|
+
if (planeDist < 0) {
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
penetration = radius - (0, physics3d_shared_js_1.solverSqrt)(delta2);
|
|
1140
|
+
}
|
|
1141
|
+
if (penetration < -speculativeMargin) {
|
|
693
1142
|
continue;
|
|
694
1143
|
}
|
|
695
1144
|
const contactNormal = triNormal; // one-sided: mesh -> other
|
|
@@ -787,7 +1236,7 @@ function closestPointOnTriangle(p, a, b, c) {
|
|
|
787
1236
|
// Sequential-impulse velocity solver + split impulse + islands + sleep
|
|
788
1237
|
// ============================================================================
|
|
789
1238
|
exports.SOLVER_CONSTANTS = {
|
|
790
|
-
BAUMGARTE: 0.2,
|
|
1239
|
+
BAUMGARTE: 0.2, // joints only (split impulse) — contacts now soft
|
|
791
1240
|
SLOP: 0.005,
|
|
792
1241
|
WAKE_VEL2: 0.005,
|
|
793
1242
|
SLEEP_VEL2: 0.005,
|
|
@@ -796,7 +1245,45 @@ exports.SOLVER_CONSTANTS = {
|
|
|
796
1245
|
WARM_MATCH_TOL2: 0.01,
|
|
797
1246
|
LINEAR_DAMPING: 0.995,
|
|
798
1247
|
ANGULAR_DAMPING: 0.98,
|
|
1248
|
+
SUBSTEPS: 6,
|
|
1249
|
+
/**
|
|
1250
|
+
* Relax passes per substep = RELAX_ITERATION_SCALE × velocityIterations. The
|
|
1251
|
+
* bias-free relax must drain the warm-start over-push that multiple bias-solve
|
|
1252
|
+
* iterations accumulate into `accN`; under-draining lets weak-gravity stacks
|
|
1253
|
+
* ratchet apart. 2× fully settles the tall-stack / mass-ratio batteries.
|
|
1254
|
+
*/
|
|
1255
|
+
RELAX_ITERATION_SCALE: 2,
|
|
1256
|
+
CONTACT_ZETA: 10,
|
|
1257
|
+
/** hertz = CONTACT_HERTZ_FRACTION · substeps/dt (dimensionless ω·h = π/2). */
|
|
1258
|
+
CONTACT_HERTZ_FRACTION: 0.25,
|
|
1259
|
+
/** Static/kinematic-involved pairs solve 2× stiffer (box2d staticSoftness). */
|
|
1260
|
+
STATIC_HERTZ_SCALE: 2,
|
|
1261
|
+
/** Clamp on soft-bias velocity, length units per tick. */
|
|
1262
|
+
MAX_CONTACT_PUSHOUT: 0.05,
|
|
1263
|
+
/**
|
|
1264
|
+
* Squared in-plane distance under which a mesh support sample counts as
|
|
1265
|
+
* projecting onto the triangle face rather than its edge/vertex region.
|
|
1266
|
+
* Sized well below the distance quantum so an exact face projection (perp²
|
|
1267
|
+
* accumulates only rounding error) never falls into the edge branch.
|
|
1268
|
+
*/
|
|
1269
|
+
MESH_FACE_PROJECTION_TOL2: 1e-12,
|
|
1270
|
+
/**
|
|
1271
|
+
* A ccd body is "fast" (TOI-bound: pre-advanced to first contact with its approach
|
|
1272
|
+
* velocity arrested) when its intended per-step center motion exceeds this fraction
|
|
1273
|
+
* of its smallest world half-extent (box3d b3_isFast). Below it, the in-loop
|
|
1274
|
+
* speculative margin already suffices.
|
|
1275
|
+
*/
|
|
1276
|
+
CCD_FAST_MOTION_FRACTION: 0.5,
|
|
1277
|
+
/**
|
|
1278
|
+
* box3d B3_SPECULATIVE_DISTANCE (4·SLOP): the small fixed speculative margin for ccd
|
|
1279
|
+
* contacts. It only bridges the exact-TOI clamp's residual separation so a clamped
|
|
1280
|
+
* fast body forms its contact next step; a wide velocity-scaled margin instead
|
|
1281
|
+
* generates diverging far-gap contacts (#2821). Fast bodies are kept from tunneling
|
|
1282
|
+
* by the exact-TOI position clamp (runRigidSolver), not by a wide margin.
|
|
1283
|
+
*/
|
|
1284
|
+
CCD_SPECULATIVE_MARGIN: 0.02,
|
|
799
1285
|
};
|
|
1286
|
+
const TWO_PI = 6.283185307179586;
|
|
800
1287
|
/**
|
|
801
1288
|
* Warm-cache maps pack the canonical index pair (a<b) into one exact integer
|
|
802
1289
|
* `a * WARM_KEY_BASE + b` — string pair keys were measurable churn on large piles.
|
|
@@ -805,6 +1292,14 @@ exports.WARM_KEY_BASE = 2 ** 20;
|
|
|
805
1292
|
function solid(b) {
|
|
806
1293
|
return b.isDynamic && !b.sleeping;
|
|
807
1294
|
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Bodies whose pose advances this step. Kinematic bodies integrate but are
|
|
1297
|
+
* never solved: `solid` still gates every impulse, gravity, damping and sleep
|
|
1298
|
+
* path, so a kinematic body's authored velocity survives the step untouched.
|
|
1299
|
+
*/
|
|
1300
|
+
function integrates(b) {
|
|
1301
|
+
return b.isKinematic || solid(b);
|
|
1302
|
+
}
|
|
808
1303
|
function worldInvInertiaLocked(b) {
|
|
809
1304
|
if (!solid(b)) {
|
|
810
1305
|
return [0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
@@ -871,10 +1366,11 @@ function takeConstraint() {
|
|
|
871
1366
|
if (constraintPool.high === constraintPool.slots.length) {
|
|
872
1367
|
constraintPool.slots.push({
|
|
873
1368
|
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
|
-
|
|
1369
|
+
normal: ZERO, t1: ZERO, t2: ZERO, rA: ZERO, rB: ZERO, rA0: ZERO,
|
|
1370
|
+
penetration: 0,
|
|
1371
|
+
massN: 0, massT1: 0, massT2: 0,
|
|
1372
|
+
biasRate: 0, massScale: 1, impulseScale: 0, vn0: 0,
|
|
1373
|
+
accN: 0, accT1: 0, accT2: 0, accNStep: 0,
|
|
878
1374
|
mu: 0, restitution: 0,
|
|
879
1375
|
});
|
|
880
1376
|
}
|
|
@@ -890,19 +1386,30 @@ function solveRigidStep(bodies, options) {
|
|
|
890
1386
|
}
|
|
891
1387
|
constraintPool.high = 0;
|
|
892
1388
|
const dt = options.dt;
|
|
893
|
-
|
|
894
|
-
// v/w
|
|
895
|
-
//
|
|
896
|
-
// path mutates these objects in place.
|
|
1389
|
+
const h = dt / options.substeps;
|
|
1390
|
+
// 1. reset pseudo-velocities; copy v/w so the solver owns them (callers may
|
|
1391
|
+
// pass aliased vectors — canonical angularVel, shared zero constants — and the
|
|
1392
|
+
// hot scalar impulse path mutates these objects in place). Gravity + damping +
|
|
1393
|
+
// position integration now happen inside the sub-step loop. Locked axes are
|
|
1394
|
+
// zeroed here so residual input velocity on a locked axis never integrates.
|
|
897
1395
|
for (const b of bodies) {
|
|
898
1396
|
b.vb = { x: 0, y: 0, z: 0 };
|
|
899
1397
|
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
1398
|
if (!solid(b)) {
|
|
1399
|
+
b.v = { x: b.v.x, y: b.v.y, z: b.v.z };
|
|
1400
|
+
b.w = { x: b.w.x, y: b.w.y, z: b.w.z };
|
|
903
1401
|
continue;
|
|
904
1402
|
}
|
|
905
|
-
b.v
|
|
1403
|
+
b.v = {
|
|
1404
|
+
x: b.invMassVec.x === 0 ? 0 : b.v.x,
|
|
1405
|
+
y: b.invMassVec.y === 0 ? 0 : b.v.y,
|
|
1406
|
+
z: b.invMassVec.z === 0 ? 0 : b.v.z,
|
|
1407
|
+
};
|
|
1408
|
+
b.w = {
|
|
1409
|
+
x: b.angularLock.x === 0 ? 0 : b.w.x,
|
|
1410
|
+
y: b.angularLock.y === 0 ? 0 : b.w.y,
|
|
1411
|
+
z: b.angularLock.z === 0 ? 0 : b.w.z,
|
|
1412
|
+
};
|
|
906
1413
|
}
|
|
907
1414
|
// 2. narrow-phase manifolds (canonical order i<j).
|
|
908
1415
|
const manifolds = [];
|
|
@@ -921,7 +1428,7 @@ function solveRigidStep(bodies, options) {
|
|
|
921
1428
|
if ((A.mask & B.layer) === 0 || (B.mask & A.layer) === 0) {
|
|
922
1429
|
return;
|
|
923
1430
|
}
|
|
924
|
-
const margin = A.ccd || B.ccd ?
|
|
1431
|
+
const margin = A.ccd || B.ccd ? exports.SOLVER_CONSTANTS.CCD_SPECULATIVE_MARGIN : 0;
|
|
925
1432
|
const manifold = generateManifold(toGeom(A), toGeom(B), margin);
|
|
926
1433
|
if (manifold !== undefined) {
|
|
927
1434
|
manifolds.push(manifold);
|
|
@@ -986,9 +1493,35 @@ function solveRigidStep(bodies, options) {
|
|
|
986
1493
|
const [t1, t2] = tangentBasis(m.normal);
|
|
987
1494
|
const combinedMu = Math.floor((toInt10(A.matFriction) + toInt10(B.matFriction)) / 2) / 10;
|
|
988
1495
|
const combinedRest = Math.max(toInt10(A.matRestitution), toInt10(B.matRestitution)) / 10;
|
|
1496
|
+
// Soft-constraint coefficients (box2d SoftStep): static/kinematic-involved
|
|
1497
|
+
// pairs solve stiffer. hertz = fraction · substeps/dt keeps ω·h = π/2.
|
|
1498
|
+
const softStatic = !A.isDynamic || !B.isDynamic;
|
|
1499
|
+
const hertz = (exports.SOLVER_CONSTANTS.CONTACT_HERTZ_FRACTION * options.substeps / dt)
|
|
1500
|
+
* (softStatic ? exports.SOLVER_CONSTANTS.STATIC_HERTZ_SCALE : 1);
|
|
1501
|
+
const omega = TWO_PI * hertz;
|
|
1502
|
+
const a1 = 2 * exports.SOLVER_CONSTANTS.CONTACT_ZETA + omega * h;
|
|
1503
|
+
const a2 = h * omega * a1;
|
|
1504
|
+
const a3 = 1 / (1 + a2);
|
|
1505
|
+
const biasRate = omega / a1;
|
|
1506
|
+
const massScale = a2 * a3;
|
|
1507
|
+
const impulseScale = a3;
|
|
989
1508
|
const warmPts = options.warm.get(m.a * exports.WARM_KEY_BASE + m.b);
|
|
990
1509
|
for (const pt of m.points) {
|
|
991
|
-
|
|
1510
|
+
// A separated (speculative) contact anchors at the gap midpoint, so a wide margin
|
|
1511
|
+
// over a large gap yields anchors far from BOTH body centers (moment arms ≈ half
|
|
1512
|
+
// the gap), whose ω×r terms blow up the coupled relax at extreme speed (the #2821
|
|
1513
|
+
// cluster divergence). Pull each anchor back to its own body's surface witness
|
|
1514
|
+
// point along the normal by half the separation; a touching/penetrating contact
|
|
1515
|
+
// (penetration ≥ 0) is untouched, so resting stacks and the golden batteries are
|
|
1516
|
+
// byte-identical.
|
|
1517
|
+
let rA = pt.rA;
|
|
1518
|
+
let rB = pt.rB;
|
|
1519
|
+
if (pt.penetration < 0) {
|
|
1520
|
+
const half = -pt.penetration * 0.5;
|
|
1521
|
+
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 };
|
|
1522
|
+
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 };
|
|
1523
|
+
}
|
|
1524
|
+
const matched = matchWarm(warmPts, rA);
|
|
992
1525
|
const constraint = takeConstraint();
|
|
993
1526
|
constraint.manifoldIndex = manifoldIndex;
|
|
994
1527
|
constraint.a = m.a;
|
|
@@ -996,22 +1529,26 @@ function solveRigidStep(bodies, options) {
|
|
|
996
1529
|
constraint.normal = m.normal;
|
|
997
1530
|
constraint.t1 = t1;
|
|
998
1531
|
constraint.t2 = t2;
|
|
999
|
-
|
|
1000
|
-
|
|
1532
|
+
// Working anchors are fresh copies (rotated per substep); rA0 keeps the
|
|
1533
|
+
// frame-start anchor for the cache.
|
|
1534
|
+
constraint.rA = { x: rA.x, y: rA.y, z: rA.z };
|
|
1535
|
+
constraint.rB = { x: rB.x, y: rB.y, z: rB.z };
|
|
1536
|
+
constraint.rA0 = rA;
|
|
1001
1537
|
constraint.penetration = pt.penetration;
|
|
1002
|
-
constraint.
|
|
1003
|
-
constraint.
|
|
1004
|
-
constraint.
|
|
1005
|
-
constraint.
|
|
1006
|
-
constraint.
|
|
1538
|
+
constraint.massN = effectiveMass(m.normal, rA, rB, invMass[m.a], invMass[m.b], invInertia[m.a], invInertia[m.b]);
|
|
1539
|
+
constraint.massT1 = effectiveMass(t1, rA, rB, invMass[m.a], invMass[m.b], invInertia[m.a], invInertia[m.b]);
|
|
1540
|
+
constraint.massT2 = effectiveMass(t2, rA, rB, invMass[m.a], invMass[m.b], invInertia[m.a], invInertia[m.b]);
|
|
1541
|
+
constraint.biasRate = biasRate;
|
|
1542
|
+
constraint.massScale = massScale;
|
|
1543
|
+
constraint.impulseScale = impulseScale;
|
|
1544
|
+
// Approach velocity captured pre-substep (before any gravity) for restitution.
|
|
1545
|
+
constraint.vn0 = relativeVelocityAlong(A, B, rA, rB, m.normal.x, m.normal.y, m.normal.z);
|
|
1007
1546
|
constraint.accN = matched?.normalImpulse ?? 0;
|
|
1008
1547
|
constraint.accT1 = matched?.tangentImpulse1 ?? 0;
|
|
1009
1548
|
constraint.accT2 = matched?.tangentImpulse2 ?? 0;
|
|
1010
|
-
constraint.
|
|
1549
|
+
constraint.accNStep = 0;
|
|
1011
1550
|
constraint.mu = combinedMu;
|
|
1012
1551
|
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
1552
|
constraints.push(constraint);
|
|
1016
1553
|
}
|
|
1017
1554
|
});
|
|
@@ -1020,7 +1557,7 @@ function solveRigidStep(bodies, options) {
|
|
|
1020
1557
|
bodies,
|
|
1021
1558
|
invMass,
|
|
1022
1559
|
invInertia,
|
|
1023
|
-
dt,
|
|
1560
|
+
dt: h,
|
|
1024
1561
|
apply: (a, b, rA, rB, impulse) => applyImpulse(bodies, invMass, invInertia, a, b, rA, rB, impulse),
|
|
1025
1562
|
applyAngular: (a, b, angularImpulse) => {
|
|
1026
1563
|
const A = bodies[a];
|
|
@@ -1043,17 +1580,85 @@ function solveRigidStep(bodies, options) {
|
|
|
1043
1580
|
applyBias: (a, b, rA, rB, impulse) => applyBiasImpulseAt(bodies, invMass, invInertia, a, b, rA, rB, impulse),
|
|
1044
1581
|
biasRelativeVelocity: (a, b, rA, rB) => relativeBiasVelocity(bodies[a], bodies[b], rA, rB),
|
|
1045
1582
|
};
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1583
|
+
// 5. TGS-Soft sub-step loop: gravity(h) → joint prepare + contact warm-start →
|
|
1584
|
+
// soft-bias iterations → position integration(h) + anchor/penetration update →
|
|
1585
|
+
// bias-free relax → per-substep joint-angle integration. One restitution pass
|
|
1586
|
+
// follows all substeps, driven by the pre-step approach velocity.
|
|
1587
|
+
for (let substep = 0; substep < options.substeps; substep += 1) {
|
|
1588
|
+
for (const b of bodies) {
|
|
1589
|
+
if (solid(b) && b.invMassVec.y !== 0) {
|
|
1590
|
+
b.v.y = b.v.y + options.gravityY * b.gravityScale * h;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1050
1593
|
for (const joint of joints) {
|
|
1051
|
-
joint.
|
|
1594
|
+
joint.prepare(jointCtx);
|
|
1595
|
+
}
|
|
1596
|
+
for (const c of constraints) {
|
|
1597
|
+
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));
|
|
1598
|
+
}
|
|
1599
|
+
for (let iter = 0; iter < options.velocityIterations; iter += 1) {
|
|
1600
|
+
for (const joint of joints) {
|
|
1601
|
+
joint.iterate(jointCtx);
|
|
1602
|
+
}
|
|
1603
|
+
for (const c of constraints) {
|
|
1604
|
+
solveContactSoft(bodies, invMass, invInertia, c, h, true);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
for (const b of bodies) {
|
|
1608
|
+
if (!integrates(b)) {
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
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), h));
|
|
1612
|
+
if (b.hasOrientation) {
|
|
1613
|
+
b.q = (0, physics3d_shared_js_1.integrateQuat)(b.q, (0, physics3d_shared_js_1.addVec3)(b.w, b.wb), h);
|
|
1614
|
+
}
|
|
1052
1615
|
}
|
|
1053
1616
|
for (const c of constraints) {
|
|
1054
|
-
|
|
1617
|
+
const A = bodies[c.a];
|
|
1618
|
+
const B = bodies[c.b];
|
|
1619
|
+
const dpA = integrates(A) ? (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.addVec3)(A.v, A.vb), h) : ZERO;
|
|
1620
|
+
const dpB = integrates(B) ? (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.addVec3)(B.v, B.vb), h) : ZERO;
|
|
1621
|
+
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;
|
|
1622
|
+
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;
|
|
1623
|
+
const rel = (0, physics3d_shared_js_1.subVec3)((0, physics3d_shared_js_1.addVec3)(dpB, rotB), (0, physics3d_shared_js_1.addVec3)(dpA, rotA));
|
|
1624
|
+
c.penetration -= (0, physics3d_shared_js_1.dotVec3)(rel, c.normal);
|
|
1625
|
+
c.rA = (0, physics3d_shared_js_1.addVec3)(c.rA, rotA);
|
|
1626
|
+
c.rB = (0, physics3d_shared_js_1.addVec3)(c.rB, rotB);
|
|
1627
|
+
}
|
|
1628
|
+
for (const b of bodies) {
|
|
1629
|
+
b.vb = { x: 0, y: 0, z: 0 };
|
|
1630
|
+
b.wb = { x: 0, y: 0, z: 0 };
|
|
1631
|
+
}
|
|
1632
|
+
const relaxIterations = options.velocityIterations * exports.SOLVER_CONSTANTS.RELAX_ITERATION_SCALE;
|
|
1633
|
+
for (let iter = 0; iter < relaxIterations; iter += 1) {
|
|
1634
|
+
for (const c of constraints) {
|
|
1635
|
+
solveContactSoft(bodies, invMass, invInertia, c, h, false);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
// Accumulate the per-substep normal impulse so contact events report the
|
|
1639
|
+
// full-step reaction (the raw accN converges to the sustaining per-substep value).
|
|
1640
|
+
for (const c of constraints) {
|
|
1641
|
+
c.accNStep += c.accN;
|
|
1642
|
+
}
|
|
1643
|
+
for (const joint of joints) {
|
|
1644
|
+
joint.postSubstep?.(jointCtx);
|
|
1055
1645
|
}
|
|
1056
1646
|
}
|
|
1647
|
+
// 5b. restitution: replay the pre-step approach velocity for bouncy contacts.
|
|
1648
|
+
for (const c of constraints) {
|
|
1649
|
+
if (c.restitution <= 0 || c.vn0 >= -exports.SOLVER_CONSTANTS.RESTITUTION_THRESHOLD || c.accN <= 0) {
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
const A = bodies[c.a];
|
|
1653
|
+
const B = bodies[c.b];
|
|
1654
|
+
const vn = relativeVelocityAlong(A, B, c.rA, c.rB, c.normal.x, c.normal.y, c.normal.z);
|
|
1655
|
+
let dLambda = c.massN * (-c.restitution * c.vn0 - vn);
|
|
1656
|
+
const newAcc = Math.max(0, c.accN + dLambda);
|
|
1657
|
+
dLambda = newAcc - c.accN;
|
|
1658
|
+
c.accN = newAcc;
|
|
1659
|
+
c.accNStep += dLambda;
|
|
1660
|
+
applyImpulseScalar(bodies, invMass, invInertia, c.a, c.b, c.rA, c.rB, c.normal.x * dLambda, c.normal.y * dLambda, c.normal.z * dLambda);
|
|
1661
|
+
}
|
|
1057
1662
|
// 6. store warm cache (index-pair keyed, deterministic order).
|
|
1058
1663
|
const warm = new Map();
|
|
1059
1664
|
for (const c of constraints) {
|
|
@@ -1063,12 +1668,13 @@ function solveRigidStep(bodies, options) {
|
|
|
1063
1668
|
list = [];
|
|
1064
1669
|
warm.set(key, list);
|
|
1065
1670
|
}
|
|
1066
|
-
list.push({ anchor: c.
|
|
1671
|
+
list.push({ anchor: c.rA0, normalImpulse: c.accN, tangentImpulse1: c.accT1, tangentImpulse2: c.accT2 });
|
|
1067
1672
|
}
|
|
1068
1673
|
// `jointBodies` (built at the wake pass) keeps joint members awake — a motor or
|
|
1069
1674
|
// grab can drive them below the sleep velocity threshold, and joints are step
|
|
1070
1675
|
// inputs the sleeper can't see.
|
|
1071
|
-
// 7. damping +
|
|
1676
|
+
// 7. damping (once per step) + axis-lock zeroing + sleep counters. Positions
|
|
1677
|
+
// were already integrated per substep in step 5.
|
|
1072
1678
|
for (const b of bodies) {
|
|
1073
1679
|
if (!solid(b)) {
|
|
1074
1680
|
continue;
|
|
@@ -1087,10 +1693,6 @@ function solveRigidStep(bodies, options) {
|
|
|
1087
1693
|
y: b.angularLock.y === 0 ? 0 : b.w.y,
|
|
1088
1694
|
z: b.angularLock.z === 0 ? 0 : b.w.z,
|
|
1089
1695
|
};
|
|
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
1696
|
const speed2 = (0, physics3d_shared_js_1.dotVec3)(b.v, b.v) + (0, physics3d_shared_js_1.dotVec3)(b.w, b.w);
|
|
1095
1697
|
b.lowVelFrames = jointBodies.has(b.index) || speed2 >= exports.SOLVER_CONSTANTS.SLEEP_VEL2 ? 0 : b.lowVelFrames + 1;
|
|
1096
1698
|
}
|
|
@@ -1099,7 +1701,7 @@ function solveRigidStep(bodies, options) {
|
|
|
1099
1701
|
// 9. per-manifold accumulated normal impulse for contact events.
|
|
1100
1702
|
const impulseByManifold = new Array(manifolds.length).fill(0);
|
|
1101
1703
|
for (const c of constraints) {
|
|
1102
|
-
impulseByManifold[c.manifoldIndex] += c.
|
|
1704
|
+
impulseByManifold[c.manifoldIndex] += c.accNStep;
|
|
1103
1705
|
}
|
|
1104
1706
|
const contactImpulses = manifolds.map((m, index) => ({
|
|
1105
1707
|
a: m.a,
|
|
@@ -1151,6 +1753,14 @@ function shapeHalfExtent(shape, q) {
|
|
|
1151
1753
|
const reach = shape.radius + shape.halfHeight;
|
|
1152
1754
|
return { x: reach, y: reach, z: reach };
|
|
1153
1755
|
}
|
|
1756
|
+
if (shape.kind === 'hull') {
|
|
1757
|
+
let ext = { x: 0, y: 0, z: 0 };
|
|
1758
|
+
for (const vertex of shape.data.vertices) {
|
|
1759
|
+
const rotated = (0, physics3d_shared_js_1.rotatePointByQuat)(q, vertex);
|
|
1760
|
+
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)) };
|
|
1761
|
+
}
|
|
1762
|
+
return ext;
|
|
1763
|
+
}
|
|
1154
1764
|
if (shape.kind === 'mesh') {
|
|
1155
1765
|
let ext = { x: 0, y: 0, z: 0 };
|
|
1156
1766
|
for (const tri of shape.triangles) {
|
|
@@ -1174,15 +1784,62 @@ function shapeHalfExtent(shape, q) {
|
|
|
1174
1784
|
}
|
|
1175
1785
|
return ext;
|
|
1176
1786
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1787
|
+
/**
|
|
1788
|
+
* Smallest world half-extent of a shape (the box3d fast-flag denominator). Uses
|
|
1789
|
+
* the orientation-aware `shapeHalfExtent`, so a rotated thin box reports its true
|
|
1790
|
+
* thin axis. Returns 0 for a degenerate (flat/zero-extent) shape; callers treat a
|
|
1791
|
+
* non-positive minExtent as "never fast" (Req 10, no divide).
|
|
1792
|
+
*/
|
|
1793
|
+
function minExtentOf(shape, q) {
|
|
1794
|
+
const h = shapeHalfExtent(shape, q);
|
|
1795
|
+
return Math.min(h.x, h.y, h.z);
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Rotation-invariant bounding radius (max distance from the shape center to any
|
|
1799
|
+
* surface point). Used as a conservative sphere for the fast-body TOI cast: a
|
|
1800
|
+
* bounding sphere contacts a target no later than the true shape would, so the
|
|
1801
|
+
* cast never lets a fast body tunnel (it stops it short, never past first contact).
|
|
1802
|
+
*/
|
|
1803
|
+
function boundingRadiusOf(shape) {
|
|
1804
|
+
if (shape.kind === 'box') {
|
|
1805
|
+
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);
|
|
1806
|
+
}
|
|
1807
|
+
if (shape.kind === 'sphere') {
|
|
1808
|
+
return shape.radius;
|
|
1809
|
+
}
|
|
1810
|
+
if (shape.kind === 'capsule') {
|
|
1811
|
+
return shape.radius + shape.halfHeight;
|
|
1812
|
+
}
|
|
1813
|
+
if (shape.kind === 'hull') {
|
|
1814
|
+
let max2 = 0;
|
|
1815
|
+
for (const v of shape.data.vertices) {
|
|
1816
|
+
max2 = Math.max(max2, v.x * v.x + v.y * v.y + v.z * v.z);
|
|
1817
|
+
}
|
|
1818
|
+
return (0, physics3d_shared_js_1.solverSqrt)(max2);
|
|
1819
|
+
}
|
|
1820
|
+
if (shape.kind === 'mesh') {
|
|
1821
|
+
let max2 = 0;
|
|
1822
|
+
for (const tri of shape.triangles) {
|
|
1823
|
+
for (const v of tri) {
|
|
1824
|
+
max2 = Math.max(max2, v.x * v.x + v.y * v.y + v.z * v.z);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
return (0, physics3d_shared_js_1.solverSqrt)(max2);
|
|
1828
|
+
}
|
|
1829
|
+
let max = 0;
|
|
1830
|
+
for (const child of shape.children) {
|
|
1831
|
+
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);
|
|
1832
|
+
max = Math.max(max, offset + boundingRadiusOf(child.shape));
|
|
1833
|
+
}
|
|
1834
|
+
return max;
|
|
1180
1835
|
}
|
|
1181
1836
|
function toGeom(b) {
|
|
1182
1837
|
return { index: b.index, p: b.p, q: b.q, shape: b.shape };
|
|
1183
1838
|
}
|
|
1184
1839
|
function wakeIfContacted(sleeper, other) {
|
|
1185
|
-
|
|
1840
|
+
// A kinematic mover carries no impulse but does displace what it touches, so
|
|
1841
|
+
// it has to wake a sleeping pile the same way an awake dynamic neighbour does.
|
|
1842
|
+
if (sleeper.isDynamic && sleeper.sleeping && (other.isDynamic || other.isKinematic) && !other.sleeping
|
|
1186
1843
|
&& (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
1844
|
sleeper.sleeping = false;
|
|
1188
1845
|
sleeper.lowVelFrames = 0;
|
|
@@ -1223,7 +1880,7 @@ function relativeBiasVelocity(A, B, rA, rB) {
|
|
|
1223
1880
|
* order as the vector-helper form (bit-identical results), with zero
|
|
1224
1881
|
* intermediate allocations. This runs per contact per velocity iteration.
|
|
1225
1882
|
*/
|
|
1226
|
-
function applyImpulseScalar(bodies, invMass, invInertia, a, b, rA, rB, px, py, pz
|
|
1883
|
+
function applyImpulseScalar(bodies, invMass, invInertia, a, b, rA, rB, px, py, pz) {
|
|
1227
1884
|
const A = bodies[a];
|
|
1228
1885
|
const B = bodies[b];
|
|
1229
1886
|
if (solid(A)) {
|
|
@@ -1232,8 +1889,8 @@ function applyImpulseScalar(bodies, invMass, invInertia, a, b, rA, rB, px, py, p
|
|
|
1232
1889
|
const cz = rA.x * py - rA.y * px;
|
|
1233
1890
|
const m = invInertia[a];
|
|
1234
1891
|
const inv = invMass[a];
|
|
1235
|
-
const lin =
|
|
1236
|
-
const ang =
|
|
1892
|
+
const lin = A.v;
|
|
1893
|
+
const ang = A.w;
|
|
1237
1894
|
lin.x = lin.x - px * inv.x;
|
|
1238
1895
|
lin.y = lin.y - py * inv.y;
|
|
1239
1896
|
lin.z = lin.z - pz * inv.z;
|
|
@@ -1247,8 +1904,8 @@ function applyImpulseScalar(bodies, invMass, invInertia, a, b, rA, rB, px, py, p
|
|
|
1247
1904
|
const cz = rB.x * py - rB.y * px;
|
|
1248
1905
|
const m = invInertia[b];
|
|
1249
1906
|
const inv = invMass[b];
|
|
1250
|
-
const lin =
|
|
1251
|
-
const ang =
|
|
1907
|
+
const lin = B.v;
|
|
1908
|
+
const ang = B.w;
|
|
1252
1909
|
lin.x = lin.x + px * inv.x;
|
|
1253
1910
|
lin.y = lin.y + py * inv.y;
|
|
1254
1911
|
lin.z = lin.z + pz * inv.z;
|
|
@@ -1269,38 +1926,38 @@ function solveTangentImpulse(bodies, invMass, invInertia, c, A, B, t, mass, acc,
|
|
|
1269
1926
|
let dF = mass * -vt;
|
|
1270
1927
|
const clamped = (0, physics3d_shared_js_1.clampNumber)(acc + dF, -maxF, maxF);
|
|
1271
1928
|
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
|
|
1929
|
+
applyImpulseScalar(bodies, invMass, invInertia, c.a, c.b, c.rA, c.rB, t.x * dF, t.y * dF, t.z * dF);
|
|
1273
1930
|
return clamped;
|
|
1274
1931
|
}
|
|
1275
|
-
|
|
1932
|
+
/**
|
|
1933
|
+
* One soft-constraint contact solve (box2d SoftStep). `useBias` on the bias pass
|
|
1934
|
+
* folds the soft position bias into real velocity; the bias-free relax pass
|
|
1935
|
+
* (`useBias=false`) removes that transient energy after position integration.
|
|
1936
|
+
*/
|
|
1937
|
+
function solveContactSoft(bodies, invMass, invInertia, c, h, useBias) {
|
|
1276
1938
|
const A = bodies[c.a];
|
|
1277
1939
|
const B = bodies[c.b];
|
|
1278
|
-
// normal (no Baumgarte — position error handled by split impulse)
|
|
1279
1940
|
const vn = relativeVelocityAlong(A, B, c.rA, c.rB, c.normal.x, c.normal.y, c.normal.z);
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1941
|
+
let bias = 0;
|
|
1942
|
+
let massScale = 1;
|
|
1943
|
+
let impulseScale = 0;
|
|
1944
|
+
if (c.penetration < 0) {
|
|
1945
|
+
// speculative / separated: allow closing exactly up to the gap, never push apart.
|
|
1946
|
+
bias = c.penetration / h;
|
|
1947
|
+
}
|
|
1948
|
+
else if (useBias) {
|
|
1949
|
+
const raw = c.biasRate * (c.penetration - exports.SOLVER_CONSTANTS.SLOP);
|
|
1950
|
+
bias = (0, physics3d_shared_js_1.clampNumber)(raw, 0, exports.SOLVER_CONSTANTS.MAX_CONTACT_PUSHOUT / h);
|
|
1951
|
+
massScale = c.massScale;
|
|
1952
|
+
impulseScale = c.impulseScale;
|
|
1953
|
+
}
|
|
1954
|
+
let dLambda = massScale * c.massN * (-(vn) + bias) - impulseScale * c.accN;
|
|
1285
1955
|
const newAcc = Math.max(0, c.accN + dLambda);
|
|
1286
1956
|
dLambda = newAcc - c.accN;
|
|
1287
1957
|
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;
|
|
1958
|
+
applyImpulseScalar(bodies, invMass, invInertia, c.a, c.b, c.rA, c.rB, c.normal.x * dLambda, c.normal.y * dLambda, c.normal.z * dLambda);
|
|
1959
|
+
if (c.penetration < 0) {
|
|
1960
|
+
return; // friction skipped for separated/speculative points (unchanged policy)
|
|
1304
1961
|
}
|
|
1305
1962
|
const maxF = c.mu * c.accN;
|
|
1306
1963
|
c.accT1 = solveTangentImpulse(bodies, invMass, invInertia, c, A, B, c.t1, c.massT1, c.accT1, maxF);
|