reze-engine 0.33.2 → 0.35.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 +13 -8
- package/dist/engine.d.ts +22 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +40 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/physics/contact.d.ts +3 -0
- package/dist/physics/contact.d.ts.map +1 -1
- package/dist/physics/contact.js +386 -19
- package/dist/physics/manifold.d.ts +13 -0
- package/dist/physics/manifold.d.ts.map +1 -0
- package/dist/physics/manifold.js +68 -0
- package/dist/physics/physics.d.ts +4 -0
- package/dist/physics/physics.d.ts.map +1 -1
- package/dist/physics/physics.js +7 -0
- package/dist/physics/solver.d.ts.map +1 -1
- package/dist/physics/solver.js +64 -4
- package/dist/physics/world.d.ts +33 -0
- package/dist/physics/world.d.ts.map +1 -1
- package/dist/physics/world.js +57 -8
- package/package.json +1 -1
- package/src/engine.ts +44 -0
- package/src/index.ts +2 -1
- package/src/physics/contact.ts +410 -15
- package/src/physics/physics.ts +8 -1
- package/src/physics/solver.ts +64 -3
- package/src/physics/world.ts +84 -3
package/src/physics/contact.ts
CHANGED
|
@@ -4,16 +4,23 @@
|
|
|
4
4
|
// positive normal impulse pushes B away from A. `rA` / `rB` are world-space
|
|
5
5
|
// lever arms from each CG to the contact point. Depth is positive when
|
|
6
6
|
// shapes overlap, ≤ 0 for speculative contacts inside the margin band.
|
|
7
|
-
// Box-box
|
|
7
|
+
// Box-box is SAT + face clipping (see detectBoxBox) — MMD dress rigs are built
|
|
8
|
+
// from box panels, and it is the majority of collidable pairs on those models.
|
|
8
9
|
|
|
9
10
|
import { RigidbodyShape } from "./types"
|
|
10
11
|
import type { RigidBodyStore } from "./body"
|
|
11
12
|
|
|
12
13
|
// Speculative contact range. Depth is reported relative to the un-inflated
|
|
13
14
|
// surface, so values 0 ≥ depth ≥ −CONTACT_MARGIN cover the "near touch but
|
|
14
|
-
// not overlapping yet" case.
|
|
15
|
-
// until actual overlap, but they prevent fast bodies from crossing a thin
|
|
15
|
+
// not overlapping yet" case. They exist so a fast body cannot cross a thin
|
|
16
16
|
// surface in one substep without ever generating a contact.
|
|
17
|
+
//
|
|
18
|
+
// What keeps them inert until the body would actually arrive is the solver's
|
|
19
|
+
// `allowedApproachVel` (gap / dt), NOT the push-only impulse clamp — the clamp
|
|
20
|
+
// only forbids a negative (pulling) impulse and does nothing to stop a large
|
|
21
|
+
// positive one from stopping a body dead in mid-air. This comment used to
|
|
22
|
+
// claim otherwise, and the bug it hid was worth 88% of speculative rows firing
|
|
23
|
+
// on a dress rig. See setupContactRow.
|
|
17
24
|
export const CONTACT_MARGIN = 0.04
|
|
18
25
|
|
|
19
26
|
export interface Contact {
|
|
@@ -44,6 +51,11 @@ export interface Contact {
|
|
|
44
51
|
cBxN: number; cByN: number; cBzN: number // rB × n
|
|
45
52
|
jacInvN: number
|
|
46
53
|
bounceVel: number // restitution reference, captured at setup from initial relVelN
|
|
54
|
+
/** Per-contact relaxation gain, 1/max(rows on A, rows on B). See CONTACT_SOR_MIN. */
|
|
55
|
+
sorGain: number
|
|
56
|
+
// Approach speed this row is allowed to leave alone: gap / dt for a
|
|
57
|
+
// speculative row, 0 once the shapes actually touch. See setupContactRow.
|
|
58
|
+
allowedApproachVel: number
|
|
47
59
|
// Friction tangent 1:
|
|
48
60
|
t1x: number; t1y: number; t1z: number
|
|
49
61
|
cAxT1: number; cAyT1: number; cAzT1: number
|
|
@@ -72,6 +84,8 @@ function makeContact(): Contact {
|
|
|
72
84
|
cBxN: 0, cByN: 0, cBzN: 0,
|
|
73
85
|
jacInvN: 0,
|
|
74
86
|
bounceVel: 0,
|
|
87
|
+
sorGain: 1,
|
|
88
|
+
allowedApproachVel: 0,
|
|
75
89
|
t1x: 0, t1y: 0, t1z: 0,
|
|
76
90
|
cAxT1: 0, cAyT1: 0, cAzT1: 0,
|
|
77
91
|
cBxT1: 0, cByT1: 0, cBzT1: 0,
|
|
@@ -731,9 +745,17 @@ function detectSphereBox(store: RigidBodyStore, a: number, b: number, pool: Cont
|
|
|
731
745
|
|
|
732
746
|
// --- Capsule–box -----------------------------------------------------------
|
|
733
747
|
// Walk the capsule's segment (in box-local space) toward the box, sample
|
|
734
|
-
// sphere-box at the converged parameter plus both endpoints
|
|
735
|
-
// samples catch caps grazing a face when the
|
|
736
|
-
// at one end of the segment.
|
|
748
|
+
// sphere-box at the converged parameter plus both endpoints, and keep the
|
|
749
|
+
// DEEPEST sample. Endpoint samples catch caps grazing a face when the
|
|
750
|
+
// closest-point parameter sits at one end of the segment.
|
|
751
|
+
//
|
|
752
|
+
// Deliberately one contact, not a manifold along the touching segment. A line
|
|
753
|
+
// of contacts here does stop a panel pivoting into a leg, and it was tried:
|
|
754
|
+
// penetration fell, but idle jitter on a 688-body rig rose because every extra
|
|
755
|
+
// contact is another shove from the position-correction pass, which the joint
|
|
756
|
+
// springs hand straight back. Cloth that buzzes reads worse than cloth that
|
|
757
|
+
// clips, so this keeps a little 穿模 in exchange for stillness. Box-box is a
|
|
758
|
+
// different case — those panels had NO collision at all, so it is pure gain.
|
|
737
759
|
function detectCapsuleBox(store: RigidBodyStore, a: number, b: number, pool: ContactPool): void {
|
|
738
760
|
const pos = store.positions,
|
|
739
761
|
sz = store.size
|
|
@@ -914,6 +936,372 @@ function detectCapsuleBox(store: RigidBodyStore, a: number, b: number, pool: Con
|
|
|
914
936
|
combineMaterials(store, a, b, c)
|
|
915
937
|
}
|
|
916
938
|
|
|
939
|
+
// --- Box–box ---------------------------------------------------------------
|
|
940
|
+
// SAT over the 15 candidate axes, then face clipping for a multi-point
|
|
941
|
+
// manifold. Everything below happens in A's local frame: B's centre and axes
|
|
942
|
+
// are transformed in once, so the 15 tests and the clipping all read as plain
|
|
943
|
+
// vector maths instead of repeated quaternion work.
|
|
944
|
+
//
|
|
945
|
+
// Why this exists at all: MMD dress rigs are built from flat box PANELS, and
|
|
946
|
+
// panel-against-panel is the collision that keeps skirt layers out of each
|
|
947
|
+
// other. Measured across seven shipped models, box-box is 45–70% of every
|
|
948
|
+
// collidable pair on models whose riggers left skirt self-collision enabled —
|
|
949
|
+
// all of it silently dropped before this. MMD's own physics is Bullet, which
|
|
950
|
+
// dispatches box-box through btBoxBoxDetector (ODE's dBoxBox: this same
|
|
951
|
+
// 15-axis SAT plus face clipping), so rigs are authored assuming it works.
|
|
952
|
+
//
|
|
953
|
+
// The pair count is not the frame cost: the AABB pass upstream filters first,
|
|
954
|
+
// and in a rest pose only ~220 of 诗蔻蒂's 60k box-box candidates reach here.
|
|
955
|
+
|
|
956
|
+
// Scratch, module-level so a frame of narrowphase allocates nothing.
|
|
957
|
+
const _bbBax = new Float32Array(9) // B's axes in A's frame, row j = axis j
|
|
958
|
+
const _bbC = new Float32Array(3) // B's centre in A's frame
|
|
959
|
+
const _bbAxis = new Float32Array(3) // best separating axis, A's frame
|
|
960
|
+
const _bbClip = new Float32Array(24) // clip buffer: up to 8 points
|
|
961
|
+
const _bbClip2 = new Float32Array(24)
|
|
962
|
+
const _bbDepth = new Float32Array(8)
|
|
963
|
+
const _bbTmp = new Float32Array(3)
|
|
964
|
+
// A's axes in A's own frame are the identity; kept as a constant so the
|
|
965
|
+
// reference/incident selection can treat both boxes through one code path
|
|
966
|
+
// without allocating a basis per call.
|
|
967
|
+
const _bbIdent = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1])
|
|
968
|
+
const _bbRefH = new Float32Array(3)
|
|
969
|
+
const _bbIncH = new Float32Array(3)
|
|
970
|
+
const _bbPA = new Float32Array(3)
|
|
971
|
+
const _bbHA = new Float32Array(3)
|
|
972
|
+
const _bbHB = new Float32Array(3)
|
|
973
|
+
|
|
974
|
+
// A face axis has to lose by a real margin before an edge axis wins. Near
|
|
975
|
+
// ties are common between two flat panels lying against each other, and an
|
|
976
|
+
// edge axis there yields one point where a face yields four — the manifold
|
|
977
|
+
// would flicker between them frame to frame and the panel would rock.
|
|
978
|
+
const EDGE_AXIS_BIAS = 1.05
|
|
979
|
+
|
|
980
|
+
function detectBoxBox(store: RigidBodyStore, a: number, b: number, pool: ContactPool): void {
|
|
981
|
+
const ai = a * 3,
|
|
982
|
+
bi = b * 3
|
|
983
|
+
const sz = store.size
|
|
984
|
+
const hAx = sz[ai + 0], hAy = sz[ai + 1], hAz = sz[ai + 2]
|
|
985
|
+
const hBx = sz[bi + 0], hBy = sz[bi + 1], hBz = sz[bi + 2]
|
|
986
|
+
|
|
987
|
+
// B's centre, in A's frame. The body index here is A, not B — transforming
|
|
988
|
+
// B's own centre by B's own transform yields the origin every time, which
|
|
989
|
+
// makes every SAT distance zero and every pair maximally overlapping.
|
|
990
|
+
worldToBodyLocal(store, a, store.positions[bi + 0], store.positions[bi + 1], store.positions[bi + 2], _bbC)
|
|
991
|
+
const cx = _bbC[0], cy = _bbC[1], cz = _bbC[2]
|
|
992
|
+
|
|
993
|
+
// B's three axes, in A's frame: RAᵀ · RB. loadBodyRot writes one body at a
|
|
994
|
+
// time, so read B's columns out before loading A over the top of them.
|
|
995
|
+
loadBodyRot(store, b)
|
|
996
|
+
const b00 = _rot[0], b01 = _rot[1], b02 = _rot[2]
|
|
997
|
+
const b10 = _rot[3], b11 = _rot[4], b12 = _rot[5]
|
|
998
|
+
const b20 = _rot[6], b21 = _rot[7], b22 = _rot[8]
|
|
999
|
+
loadBodyRot(store, a)
|
|
1000
|
+
const a00 = _rot[0], a01 = _rot[1], a02 = _rot[2]
|
|
1001
|
+
const a10 = _rot[3], a11 = _rot[4], a12 = _rot[5]
|
|
1002
|
+
const a20 = _rot[6], a21 = _rot[7], a22 = _rot[8]
|
|
1003
|
+
// Column j of RB is B's axis j in world; RAᵀ · that is it in A's frame.
|
|
1004
|
+
for (let j = 0; j < 3; j++) {
|
|
1005
|
+
const wx = j === 0 ? b00 : j === 1 ? b01 : b02
|
|
1006
|
+
const wy = j === 0 ? b10 : j === 1 ? b11 : b12
|
|
1007
|
+
const wz = j === 0 ? b20 : j === 1 ? b21 : b22
|
|
1008
|
+
_bbBax[j * 3 + 0] = a00 * wx + a10 * wy + a20 * wz
|
|
1009
|
+
_bbBax[j * 3 + 1] = a01 * wx + a11 * wy + a21 * wz
|
|
1010
|
+
_bbBax[j * 3 + 2] = a02 * wx + a12 * wy + a22 * wz
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// --- SAT. Track the axis of MINIMUM overlap; that is the shallowest way out
|
|
1014
|
+
// and therefore the contact normal.
|
|
1015
|
+
let bestOverlap = Infinity
|
|
1016
|
+
let bestAxis = -1 // 0-2 = A's faces, 3-5 = B's faces, 6-14 = edge crosses
|
|
1017
|
+
let bestNx = 0, bestNy = 0, bestNz = 0
|
|
1018
|
+
|
|
1019
|
+
// `scale` lets the edge axes be judged slightly harder — see EDGE_AXIS_BIAS.
|
|
1020
|
+
const test = (nx: number, ny: number, nz: number, id: number, scale: number): boolean => {
|
|
1021
|
+
const len2 = nx * nx + ny * ny + nz * nz
|
|
1022
|
+
// Degenerate cross product: the two edges are parallel, so this axis adds
|
|
1023
|
+
// nothing that the face axes have not already covered.
|
|
1024
|
+
if (len2 < 1e-12) return true
|
|
1025
|
+
const inv = 1 / Math.sqrt(len2)
|
|
1026
|
+
const ux = nx * inv, uy = ny * inv, uz = nz * inv
|
|
1027
|
+
const projA = hAx * Math.abs(ux) + hAy * Math.abs(uy) + hAz * Math.abs(uz)
|
|
1028
|
+
const projB =
|
|
1029
|
+
hBx * Math.abs(ux * _bbBax[0] + uy * _bbBax[1] + uz * _bbBax[2]) +
|
|
1030
|
+
hBy * Math.abs(ux * _bbBax[3] + uy * _bbBax[4] + uz * _bbBax[5]) +
|
|
1031
|
+
hBz * Math.abs(ux * _bbBax[6] + uy * _bbBax[7] + uz * _bbBax[8])
|
|
1032
|
+
const dist = Math.abs(ux * cx + uy * cy + uz * cz)
|
|
1033
|
+
const overlap = projA + projB - dist
|
|
1034
|
+
// A gap wider than the speculative band: no contact, and no need to test
|
|
1035
|
+
// the rest — one separating axis is proof.
|
|
1036
|
+
if (overlap < -CONTACT_MARGIN) return false
|
|
1037
|
+
if (overlap * scale < bestOverlap) {
|
|
1038
|
+
bestOverlap = overlap * scale
|
|
1039
|
+
bestAxis = id
|
|
1040
|
+
bestNx = ux; bestNy = uy; bestNz = uz
|
|
1041
|
+
}
|
|
1042
|
+
return true
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (!test(1, 0, 0, 0, 1)) return
|
|
1046
|
+
if (!test(0, 1, 0, 1, 1)) return
|
|
1047
|
+
if (!test(0, 0, 1, 2, 1)) return
|
|
1048
|
+
for (let j = 0; j < 3; j++) {
|
|
1049
|
+
if (!test(_bbBax[j * 3 + 0], _bbBax[j * 3 + 1], _bbBax[j * 3 + 2], 3 + j, 1)) return
|
|
1050
|
+
}
|
|
1051
|
+
for (let i = 0; i < 3; i++) {
|
|
1052
|
+
const axi = i === 0 ? 1 : 0, ayi = i === 1 ? 1 : 0, azi = i === 2 ? 1 : 0
|
|
1053
|
+
for (let j = 0; j < 3; j++) {
|
|
1054
|
+
const bx = _bbBax[j * 3 + 0], by = _bbBax[j * 3 + 1], bz = _bbBax[j * 3 + 2]
|
|
1055
|
+
if (!test(ayi * bz - azi * by, azi * bx - axi * bz, axi * by - ayi * bx, 6 + i * 3 + j, EDGE_AXIS_BIAS)) return
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (bestAxis < 0) return
|
|
1059
|
+
|
|
1060
|
+
// Orient the normal A → B, matching the contact convention.
|
|
1061
|
+
if (bestNx * cx + bestNy * cy + bestNz * cz < 0) {
|
|
1062
|
+
bestNx = -bestNx; bestNy = -bestNy; bestNz = -bestNz
|
|
1063
|
+
}
|
|
1064
|
+
_bbAxis[0] = bestNx; _bbAxis[1] = bestNy; _bbAxis[2] = bestNz
|
|
1065
|
+
|
|
1066
|
+
if (bestAxis >= 6) {
|
|
1067
|
+
emitBoxEdgeContact(store, a, b, bestOverlap / EDGE_AXIS_BIAS, (bestAxis - 6) / 3 | 0, (bestAxis - 6) % 3,
|
|
1068
|
+
hAx, hAy, hAz, hBx, hBy, hBz, cx, cy, cz, pool)
|
|
1069
|
+
return
|
|
1070
|
+
}
|
|
1071
|
+
emitBoxFaceManifold(store, a, b, bestAxis, hAx, hAy, hAz, hBx, hBy, hBz, cx, cy, cz, pool)
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Write one contact from a point given in A's LOCAL frame, with the manifold's
|
|
1075
|
+
// shared world normal. Both lever arms come from the same world point: the
|
|
1076
|
+
// clipped point sits on the incident face, within CONTACT_MARGIN of the
|
|
1077
|
+
// reference face, so splitting them would be false precision.
|
|
1078
|
+
function emitBoxContact(
|
|
1079
|
+
store: RigidBodyStore,
|
|
1080
|
+
a: number,
|
|
1081
|
+
b: number,
|
|
1082
|
+
lx: number, ly: number, lz: number,
|
|
1083
|
+
depth: number,
|
|
1084
|
+
pool: ContactPool,
|
|
1085
|
+
): void {
|
|
1086
|
+
const ai = a * 3, bi = b * 3
|
|
1087
|
+
bodyLocalToWorldDir(store, a, lx, ly, lz, _bbTmp)
|
|
1088
|
+
const wx = _bbTmp[0] + store.positions[ai + 0]
|
|
1089
|
+
const wy = _bbTmp[1] + store.positions[ai + 1]
|
|
1090
|
+
const wz = _bbTmp[2] + store.positions[ai + 2]
|
|
1091
|
+
bodyLocalToWorldDir(store, a, _bbAxis[0], _bbAxis[1], _bbAxis[2], _bbTmp)
|
|
1092
|
+
const c = pool.acquire()
|
|
1093
|
+
c.bodyA = a
|
|
1094
|
+
c.bodyB = b
|
|
1095
|
+
c.nx = _bbTmp[0]; c.ny = _bbTmp[1]; c.nz = _bbTmp[2]
|
|
1096
|
+
c.depth = depth
|
|
1097
|
+
c.rAx = wx - store.positions[ai + 0]
|
|
1098
|
+
c.rAy = wy - store.positions[ai + 1]
|
|
1099
|
+
c.rAz = wz - store.positions[ai + 2]
|
|
1100
|
+
c.rBx = wx - store.positions[bi + 0]
|
|
1101
|
+
c.rBy = wy - store.positions[bi + 1]
|
|
1102
|
+
c.rBz = wz - store.positions[bi + 2]
|
|
1103
|
+
combineMaterials(store, a, b, c)
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Clip a polygon against the plane dot(p, t) ≤ offset (Sutherland–Hodgman).
|
|
1107
|
+
// Points are xyz triples packed into `src`; returns the new count.
|
|
1108
|
+
function clipPolyByPlane(
|
|
1109
|
+
src: Float32Array, n: number,
|
|
1110
|
+
tx: number, ty: number, tz: number, offset: number,
|
|
1111
|
+
dst: Float32Array,
|
|
1112
|
+
): number {
|
|
1113
|
+
let out = 0
|
|
1114
|
+
for (let i = 0; i < n; i++) {
|
|
1115
|
+
const j = (i + 1) % n
|
|
1116
|
+
const px = src[i * 3], py = src[i * 3 + 1], pz = src[i * 3 + 2]
|
|
1117
|
+
const qx = src[j * 3], qy = src[j * 3 + 1], qz = src[j * 3 + 2]
|
|
1118
|
+
const dp = px * tx + py * ty + pz * tz - offset
|
|
1119
|
+
const dq = qx * tx + qy * ty + qz * tz - offset
|
|
1120
|
+
if (dp <= 0) {
|
|
1121
|
+
dst[out * 3] = px; dst[out * 3 + 1] = py; dst[out * 3 + 2] = pz
|
|
1122
|
+
out++
|
|
1123
|
+
}
|
|
1124
|
+
// Sign change: the edge crosses the plane, so the crossing point joins the
|
|
1125
|
+
// polygon. Guard the divide — a denominator this small means the edge lies
|
|
1126
|
+
// in the plane, and both endpoints are already handled by the tests above.
|
|
1127
|
+
if ((dp < 0 && dq > 0) || (dp > 0 && dq < 0)) {
|
|
1128
|
+
const den = dp - dq
|
|
1129
|
+
if (Math.abs(den) > 1e-12 && out < 8) {
|
|
1130
|
+
const s = dp / den
|
|
1131
|
+
dst[out * 3] = px + (qx - px) * s
|
|
1132
|
+
dst[out * 3 + 1] = py + (qy - py) * s
|
|
1133
|
+
dst[out * 3 + 2] = pz + (qz - pz) * s
|
|
1134
|
+
out++
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
if (out >= 8) break
|
|
1138
|
+
}
|
|
1139
|
+
return out
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Face-vs-face: clip the incident face against the reference face's four side
|
|
1143
|
+
// planes, then keep whatever is at or below the reference plane. This is what
|
|
1144
|
+
// yields a multi-point manifold — the reason a flat panel resting on another
|
|
1145
|
+
// stops pivoting about a single point.
|
|
1146
|
+
function emitBoxFaceManifold(
|
|
1147
|
+
store: RigidBodyStore,
|
|
1148
|
+
a: number, b: number,
|
|
1149
|
+
bestAxis: number,
|
|
1150
|
+
hAx: number, hAy: number, hAz: number,
|
|
1151
|
+
hBx: number, hBy: number, hBz: number,
|
|
1152
|
+
cx: number, cy: number, cz: number,
|
|
1153
|
+
pool: ContactPool,
|
|
1154
|
+
): void {
|
|
1155
|
+
const refIsA = bestAxis < 3
|
|
1156
|
+
const refAxisIdx = refIsA ? bestAxis : bestAxis - 3
|
|
1157
|
+
// Reference basis, half extents and centre — all in A's frame. When A is the
|
|
1158
|
+
// reference its axes ARE the frame, hence the identity rows.
|
|
1159
|
+
const refAx = refIsA ? _bbIdent : _bbBax
|
|
1160
|
+
const incAx = refIsA ? _bbBax : _bbIdent
|
|
1161
|
+
const refH = _bbRefH, incH = _bbIncH
|
|
1162
|
+
refH[0] = refIsA ? hAx : hBx; refH[1] = refIsA ? hAy : hBy; refH[2] = refIsA ? hAz : hBz
|
|
1163
|
+
incH[0] = refIsA ? hBx : hAx; incH[1] = refIsA ? hBy : hAy; incH[2] = refIsA ? hBz : hAz
|
|
1164
|
+
const refCx = refIsA ? 0 : cx, refCy = refIsA ? 0 : cy, refCz = refIsA ? 0 : cz
|
|
1165
|
+
const incCx = refIsA ? cx : 0, incCy = refIsA ? cy : 0, incCz = refIsA ? cz : 0
|
|
1166
|
+
|
|
1167
|
+
// Outward normal of the reference face, pointing at the incident box.
|
|
1168
|
+
// _bbAxis runs A → B, so B-as-reference faces the other way.
|
|
1169
|
+
const sgn = refIsA ? 1 : -1
|
|
1170
|
+
const nx = _bbAxis[0] * sgn, ny = _bbAxis[1] * sgn, nz = _bbAxis[2] * sgn
|
|
1171
|
+
|
|
1172
|
+
// Incident face: the one whose outward normal is most opposed to n.
|
|
1173
|
+
let incIdx = 0, incDot = Infinity, incSign = 1
|
|
1174
|
+
for (let k = 0; k < 3; k++) {
|
|
1175
|
+
const d = incAx[k * 3] * nx + incAx[k * 3 + 1] * ny + incAx[k * 3 + 2] * nz
|
|
1176
|
+
const s = d > 0 ? -1 : 1
|
|
1177
|
+
const v = d * s
|
|
1178
|
+
if (v < incDot) { incDot = v; incIdx = k; incSign = s }
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// Its four corners, from the face centre along the two remaining axes.
|
|
1182
|
+
const u = (incIdx + 1) % 3, v = (incIdx + 2) % 3
|
|
1183
|
+
const fx = incCx + incAx[incIdx * 3] * incSign * incH[incIdx]
|
|
1184
|
+
const fy = incCy + incAx[incIdx * 3 + 1] * incSign * incH[incIdx]
|
|
1185
|
+
const fz = incCz + incAx[incIdx * 3 + 2] * incSign * incH[incIdx]
|
|
1186
|
+
let n0 = 0
|
|
1187
|
+
for (let iu = 0; iu < 2; iu++) {
|
|
1188
|
+
const su = iu === 0 ? 1 : -1
|
|
1189
|
+
for (let iv = 0; iv < 2; iv++) {
|
|
1190
|
+
const sv = iv === 0 ? 1 : -1
|
|
1191
|
+
// Wound consistently (++, +−, −−, −+) so the clip walks a real quad.
|
|
1192
|
+
const s2 = su === 1 ? sv : -sv
|
|
1193
|
+
_bbClip[n0 * 3] = fx + incAx[u * 3] * incH[u] * su + incAx[v * 3] * incH[v] * s2
|
|
1194
|
+
_bbClip[n0 * 3 + 1] = fy + incAx[u * 3 + 1] * incH[u] * su + incAx[v * 3 + 1] * incH[v] * s2
|
|
1195
|
+
_bbClip[n0 * 3 + 2] = fz + incAx[u * 3 + 2] * incH[u] * su + incAx[v * 3 + 2] * incH[v] * s2
|
|
1196
|
+
n0++
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Clip against the reference face's four side planes.
|
|
1201
|
+
const ru = (refAxisIdx + 1) % 3, rv = (refAxisIdx + 2) % 3
|
|
1202
|
+
let src = _bbClip, dst = _bbClip2, cnt = n0
|
|
1203
|
+
for (let plane = 0; plane < 4; plane++) {
|
|
1204
|
+
const ax = plane < 2 ? ru : rv
|
|
1205
|
+
const sgn2 = plane % 2 === 0 ? 1 : -1
|
|
1206
|
+
const tx = refAx[ax * 3] * sgn2, ty = refAx[ax * 3 + 1] * sgn2, tz = refAx[ax * 3 + 2] * sgn2
|
|
1207
|
+
const offset = refCx * tx + refCy * ty + refCz * tz + refH[ax]
|
|
1208
|
+
cnt = clipPolyByPlane(src, cnt, tx, ty, tz, offset, dst)
|
|
1209
|
+
const t = src; src = dst; dst = t
|
|
1210
|
+
if (cnt === 0) return
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Keep what is at or below the reference face plane.
|
|
1214
|
+
const planeD = (refCx + nx * refH[refAxisIdx]) * nx + (refCy + ny * refH[refAxisIdx]) * ny +
|
|
1215
|
+
(refCz + nz * refH[refAxisIdx]) * nz
|
|
1216
|
+
let kept = 0
|
|
1217
|
+
for (let i = 0; i < cnt; i++) {
|
|
1218
|
+
const sep = src[i * 3] * nx + src[i * 3 + 1] * ny + src[i * 3 + 2] * nz - planeD
|
|
1219
|
+
if (sep > CONTACT_MARGIN) continue
|
|
1220
|
+
src[kept * 3] = src[i * 3]
|
|
1221
|
+
src[kept * 3 + 1] = src[i * 3 + 1]
|
|
1222
|
+
src[kept * 3 + 2] = src[i * 3 + 2]
|
|
1223
|
+
_bbDepth[kept] = -sep
|
|
1224
|
+
kept++
|
|
1225
|
+
}
|
|
1226
|
+
if (kept === 0) return
|
|
1227
|
+
|
|
1228
|
+
// Cap the manifold at Bullet's four. Clipping a quad by four planes can
|
|
1229
|
+
// reach eight points, and every extra one is another solver row for a
|
|
1230
|
+
// patch the deepest four already describe. Deepest-first so the points
|
|
1231
|
+
// that matter survive the cut.
|
|
1232
|
+
if (kept > 4) {
|
|
1233
|
+
for (let i = 1; i < kept; i++) {
|
|
1234
|
+
const d = _bbDepth[i]
|
|
1235
|
+
const px = src[i * 3], py = src[i * 3 + 1], pz = src[i * 3 + 2]
|
|
1236
|
+
let j = i - 1
|
|
1237
|
+
while (j >= 0 && _bbDepth[j] < d) {
|
|
1238
|
+
_bbDepth[j + 1] = _bbDepth[j]
|
|
1239
|
+
src[(j + 1) * 3] = src[j * 3]
|
|
1240
|
+
src[(j + 1) * 3 + 1] = src[j * 3 + 1]
|
|
1241
|
+
src[(j + 1) * 3 + 2] = src[j * 3 + 2]
|
|
1242
|
+
j--
|
|
1243
|
+
}
|
|
1244
|
+
_bbDepth[j + 1] = d
|
|
1245
|
+
src[(j + 1) * 3] = px; src[(j + 1) * 3 + 1] = py; src[(j + 1) * 3 + 2] = pz
|
|
1246
|
+
}
|
|
1247
|
+
kept = 4
|
|
1248
|
+
}
|
|
1249
|
+
for (let i = 0; i < kept; i++) {
|
|
1250
|
+
emitBoxContact(store, a, b, src[i * 3], src[i * 3 + 1], src[i * 3 + 2], _bbDepth[i], pool)
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Edge-vs-edge: one point, at the midpoint of the closest approach between the
|
|
1255
|
+
// two supporting edges. Single-point is correct here — two crossed edges touch
|
|
1256
|
+
// at a point, unlike two faces.
|
|
1257
|
+
function emitBoxEdgeContact(
|
|
1258
|
+
store: RigidBodyStore,
|
|
1259
|
+
a: number, b: number,
|
|
1260
|
+
depth: number,
|
|
1261
|
+
i: number, j: number,
|
|
1262
|
+
hAx: number, hAy: number, hAz: number,
|
|
1263
|
+
hBx: number, hBy: number, hBz: number,
|
|
1264
|
+
cx: number, cy: number, cz: number,
|
|
1265
|
+
pool: ContactPool,
|
|
1266
|
+
): void {
|
|
1267
|
+
const hA = _bbHA, hB = _bbHB
|
|
1268
|
+
hA[0] = hAx; hA[1] = hAy; hA[2] = hAz
|
|
1269
|
+
hB[0] = hBx; hB[1] = hBy; hB[2] = hBz
|
|
1270
|
+
const nx = _bbAxis[0], ny = _bbAxis[1], nz = _bbAxis[2]
|
|
1271
|
+
|
|
1272
|
+
// A's supporting edge: offset along the two axes that are NOT the edge
|
|
1273
|
+
// direction, each toward B.
|
|
1274
|
+
const pA = _bbPA
|
|
1275
|
+
pA[0] = 0; pA[1] = 0; pA[2] = 0
|
|
1276
|
+
for (let k = 0; k < 3; k++) {
|
|
1277
|
+
if (k === i) continue
|
|
1278
|
+
const d = k === 0 ? nx : k === 1 ? ny : nz
|
|
1279
|
+
pA[k] = hA[k] * (d >= 0 ? 1 : -1)
|
|
1280
|
+
}
|
|
1281
|
+
const dAx = i === 0 ? 1 : 0, dAy = i === 1 ? 1 : 0, dAz = i === 2 ? 1 : 0
|
|
1282
|
+
|
|
1283
|
+
// B's, offset the other way — its edge faces back toward A.
|
|
1284
|
+
let pBx = cx, pBy = cy, pBz = cz
|
|
1285
|
+
for (let k = 0; k < 3; k++) {
|
|
1286
|
+
if (k === j) continue
|
|
1287
|
+
const ax = _bbBax[k * 3], ay = _bbBax[k * 3 + 1], az = _bbBax[k * 3 + 2]
|
|
1288
|
+
const s = ax * nx + ay * ny + az * nz >= 0 ? -1 : 1
|
|
1289
|
+
pBx += ax * hB[k] * s; pBy += ay * hB[k] * s; pBz += az * hB[k] * s
|
|
1290
|
+
}
|
|
1291
|
+
const dBx = _bbBax[j * 3], dBy = _bbBax[j * 3 + 1], dBz = _bbBax[j * 3 + 2]
|
|
1292
|
+
|
|
1293
|
+
closestPointsTwoSegments(
|
|
1294
|
+
pA[0] - dAx * hA[i], pA[1] - dAy * hA[i], pA[2] - dAz * hA[i],
|
|
1295
|
+
pA[0] + dAx * hA[i], pA[1] + dAy * hA[i], pA[2] + dAz * hA[i],
|
|
1296
|
+
pBx - dBx * hB[j], pBy - dBy * hB[j], pBz - dBz * hB[j],
|
|
1297
|
+
pBx + dBx * hB[j], pBy + dBy * hB[j], pBz + dBz * hB[j],
|
|
1298
|
+
_cpA, _cpB,
|
|
1299
|
+
)
|
|
1300
|
+
emitBoxContact(store, a, b,
|
|
1301
|
+
(_cpA[0] + _cpB[0]) * 0.5, (_cpA[1] + _cpB[1]) * 0.5, (_cpA[2] + _cpB[2]) * 0.5,
|
|
1302
|
+
depth, pool)
|
|
1303
|
+
}
|
|
1304
|
+
|
|
917
1305
|
// Dispatch a pair to the matching narrowphase. Caller has already done
|
|
918
1306
|
// broadphase + group/mask filtering. Some shape pairs (sphere-A capsule-B
|
|
919
1307
|
// etc.) reuse a canonical implementation via swap + flipLastNormal.
|
|
@@ -935,7 +1323,7 @@ export function generateContacts(store: RigidBodyStore, a: number, b: number, po
|
|
|
935
1323
|
// end of the pool — pushing those two bodies together instead of apart.
|
|
936
1324
|
const before = pool.count
|
|
937
1325
|
detectSphereCapsule(store, b, a, pool)
|
|
938
|
-
|
|
1326
|
+
flipNormalsFrom(pool, before)
|
|
939
1327
|
return
|
|
940
1328
|
}
|
|
941
1329
|
if (sA === RigidbodyShape.Capsule && sB === RigidbodyShape.Capsule) {
|
|
@@ -949,7 +1337,7 @@ export function generateContacts(store: RigidBodyStore, a: number, b: number, po
|
|
|
949
1337
|
if (sA === RigidbodyShape.Box && sB === RigidbodyShape.Sphere) {
|
|
950
1338
|
const before = pool.count
|
|
951
1339
|
detectSphereBox(store, b, a, pool)
|
|
952
|
-
|
|
1340
|
+
flipNormalsFrom(pool, before)
|
|
953
1341
|
return
|
|
954
1342
|
}
|
|
955
1343
|
if (sA === RigidbodyShape.Capsule && sB === RigidbodyShape.Box) {
|
|
@@ -959,17 +1347,24 @@ export function generateContacts(store: RigidBodyStore, a: number, b: number, po
|
|
|
959
1347
|
if (sA === RigidbodyShape.Box && sB === RigidbodyShape.Capsule) {
|
|
960
1348
|
const before = pool.count
|
|
961
1349
|
detectCapsuleBox(store, b, a, pool)
|
|
962
|
-
|
|
1350
|
+
flipNormalsFrom(pool, before)
|
|
963
1351
|
return
|
|
964
1352
|
}
|
|
965
|
-
|
|
1353
|
+
if (sA === RigidbodyShape.Box && sB === RigidbodyShape.Box) {
|
|
1354
|
+
detectBoxBox(store, a, b, pool)
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// After a swapped detect* call, the produced contacts' normals point the wrong
|
|
1359
|
+
// way and lever arms are mismatched. Flip and re-anchor EVERY contact the call
|
|
1360
|
+
// emitted, not just the last: capsule-box now returns up to three, and flipping
|
|
1361
|
+
// one of them would leave the others pulling the pair together instead of
|
|
1362
|
+
// pushing it apart.
|
|
1363
|
+
function flipNormalsFrom(pool: ContactPool, from: number): void {
|
|
1364
|
+
for (let i = from; i < pool.count; i++) flipOneNormal(pool.get(i))
|
|
966
1365
|
}
|
|
967
1366
|
|
|
968
|
-
|
|
969
|
-
// way and lever arms are mismatched. Flip and re-anchor.
|
|
970
|
-
function flipLastNormal(pool: ContactPool): void {
|
|
971
|
-
if (pool.count === 0) return
|
|
972
|
-
const c = pool.get(pool.count - 1)
|
|
1367
|
+
function flipOneNormal(c: Contact): void {
|
|
973
1368
|
const ta = c.bodyA
|
|
974
1369
|
c.bodyA = c.bodyB
|
|
975
1370
|
c.bodyB = ta
|
package/src/physics/physics.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Vec3, Quat, Mat4 } from "../math"
|
|
|
2
2
|
import type { Rigidbody, Joint } from "./types"
|
|
3
3
|
import { RigidbodyType, RigidbodyShape } from "./types"
|
|
4
4
|
import { RigidBodyStore } from "./body"
|
|
5
|
-
import { World } from "./world"
|
|
5
|
+
import { World, type WindOptions } from "./world"
|
|
6
6
|
import { buildConstraints, type SixDofSpringConstraint } from "./constraint"
|
|
7
7
|
import { SolverCache } from "./solver"
|
|
8
8
|
import { ContactPool } from "./contact"
|
|
@@ -166,6 +166,13 @@ export class RezePhysics {
|
|
|
166
166
|
getGravity(): Vec3 {
|
|
167
167
|
return this.world.gravity
|
|
168
168
|
}
|
|
169
|
+
/** World-wide air movement. See {@link WindOptions}; null is still air. */
|
|
170
|
+
setWind(wind: WindOptions | null): void {
|
|
171
|
+
this.world.setWind(wind)
|
|
172
|
+
}
|
|
173
|
+
getWind(): WindOptions | null {
|
|
174
|
+
return this.world.getWind()
|
|
175
|
+
}
|
|
169
176
|
getRigidbodies(): Rigidbody[] {
|
|
170
177
|
return this.rigidbodies
|
|
171
178
|
}
|
package/src/physics/solver.ts
CHANGED
|
@@ -19,6 +19,31 @@ import type { Contact, ContactPool } from "./contact"
|
|
|
19
19
|
|
|
20
20
|
const BOUNCE_THRESHOLD = 2.0
|
|
21
21
|
|
|
22
|
+
// Successive over-relaxation factor on CONTACT rows only (joints untouched).
|
|
23
|
+
// Each contact row independently drives the relative velocity at its own point
|
|
24
|
+
// to zero; when a dress panel carries up to 43 of them at once, they all
|
|
25
|
+
// correct the same motion and the body is over-braked, differently every
|
|
26
|
+
// substep. Scaling each row's step damps that without changing the fixed
|
|
27
|
+
// point — the accumulated impulse still converges to the same answer, just
|
|
28
|
+
// approached rather than overshot.
|
|
29
|
+
//
|
|
30
|
+
// The gain is PER CONTACT, scaled by how contended its two bodies are, not a
|
|
31
|
+
// flat constant. A flat factor was measured first and is wrong: it also slows
|
|
32
|
+
// the well-conditioned rows, and a body resting on a SINGLE contact can then
|
|
33
|
+
// no longer cancel its approach velocity within the iteration budget, so it
|
|
34
|
+
// creeps forever instead of settling (托特 peak speed at 15 s: 0.11 at gain
|
|
35
|
+
// 1.0, 0.46 at 0.5, 0.72 at 0.3 — a permanent limit cycle on a one-contact
|
|
36
|
+
// body). Over-constraint is a local property, so the remedy has to be local.
|
|
37
|
+
//
|
|
38
|
+
// gain = 1 / max(rows on A, rows on B), floored. One contact → 1.0, exact and
|
|
39
|
+
// settling preserved; a dress panel sharing 20 rows → 0.05, damped. This is
|
|
40
|
+
// the standard mass-splitting blend toward Jacobi in the contended cluster.
|
|
41
|
+
const CONTACT_SOR_MIN = 0.12
|
|
42
|
+
|
|
43
|
+
// Per-body contact-row counts for the gain above; grown on demand, refilled
|
|
44
|
+
// each substep. Module-level so the solve allocates nothing.
|
|
45
|
+
let _rowCount = new Int32Array(0)
|
|
46
|
+
|
|
22
47
|
// Ceilings on limit-correction velocity. In normal operation limit errors are
|
|
23
48
|
// tiny; a large error only appears after a discontinuity (teleport, stall,
|
|
24
49
|
// deep penetration), and feeding err·ERP/dt to the solver unclamped then
|
|
@@ -93,8 +118,23 @@ export function solveConstraints(
|
|
|
93
118
|
for (let c = 0; c < constraints.length; c++) {
|
|
94
119
|
setupConstraint(constraints[c], c, cache, store, dt, invDt)
|
|
95
120
|
}
|
|
121
|
+
// How many contact rows each body carries this substep — the per-contact
|
|
122
|
+
// relaxation gain below is a function of the more contended of its two
|
|
123
|
+
// bodies. Counting is O(contacts), done once, before any setup.
|
|
124
|
+
if (_rowCount.length < store.count) _rowCount = new Int32Array(store.count)
|
|
125
|
+
else _rowCount.fill(0, 0, store.count)
|
|
126
|
+
// Only DYNAMIC bodies are counted. A static body — above all the ground —
|
|
127
|
+
// collects a row from every body resting on it, and letting that inflate the
|
|
128
|
+
// count crushes the gain of each of those contacts to the floor, so nothing
|
|
129
|
+
// resting on the ground can build enough impulse and it creeps instead of
|
|
130
|
+
// settling. A body with invMass 0 cannot be over-braked in the first place.
|
|
96
131
|
for (let ci = 0; ci < contacts.count; ci++) {
|
|
97
|
-
|
|
132
|
+
const c = contacts.get(ci)
|
|
133
|
+
if (invMass[c.bodyA] > 0) _rowCount[c.bodyA]++
|
|
134
|
+
if (invMass[c.bodyB] > 0) _rowCount[c.bodyB]++
|
|
135
|
+
}
|
|
136
|
+
for (let ci = 0; ci < contacts.count; ci++) {
|
|
137
|
+
setupContactRow(contacts.get(ci), lv, av, invMass, W, invDt)
|
|
98
138
|
}
|
|
99
139
|
|
|
100
140
|
for (let iter = 0; iter < iterations; iter++) {
|
|
@@ -705,6 +745,7 @@ function setupContactRow(
|
|
|
705
745
|
av: Float32Array,
|
|
706
746
|
invMass: Float32Array,
|
|
707
747
|
W: Float32Array,
|
|
748
|
+
invDt: number,
|
|
708
749
|
): void {
|
|
709
750
|
const ai = c.bodyA * 3
|
|
710
751
|
const bi = c.bodyB * 3
|
|
@@ -748,6 +789,26 @@ function setupContactRow(
|
|
|
748
789
|
? -c.restitution * relVelN0
|
|
749
790
|
: 0
|
|
750
791
|
|
|
792
|
+
// Speculative rows (depth < 0 — the shapes are inside the margin band but
|
|
793
|
+
// NOT touching) must not brake a body that hasn't arrived yet. Their whole
|
|
794
|
+
// job is to stop it crossing the surface within this substep, so the
|
|
795
|
+
// approach speed they leave alone is exactly the one that closes the
|
|
796
|
+
// remaining gap in dt; only the excess above that is cancelled.
|
|
797
|
+
//
|
|
798
|
+
// Without this the row targets relVelN = 0 like a touching contact and
|
|
799
|
+
// stops approaching bodies dead up to CONTACT_MARGIN away from anything.
|
|
800
|
+
// The push-only clamp does NOT prevent that — it only forbids a negative
|
|
801
|
+
// (pulling) impulse, not a large positive one on a body in mid-air. On a
|
|
802
|
+
// dress rig half of all contact rows are speculative and 88% of them fire,
|
|
803
|
+
// which is the field of invisible brakes the cloth was shaking against.
|
|
804
|
+
c.allowedApproachVel = c.depth < 0 ? -c.depth * invDt : 0
|
|
805
|
+
|
|
806
|
+
// Relaxation gain, from the more contended of the two bodies (see
|
|
807
|
+
// CONTACT_SOR_MIN). A lone contact keeps gain 1.0 and stays exact.
|
|
808
|
+
const contended = _rowCount[c.bodyA] > _rowCount[c.bodyB] ? _rowCount[c.bodyA] : _rowCount[c.bodyB]
|
|
809
|
+
const gain = contended > 1 ? 1 / contended : 1
|
|
810
|
+
c.sorGain = gain < CONTACT_SOR_MIN ? CONTACT_SOR_MIN : gain
|
|
811
|
+
|
|
751
812
|
// Friction tangent basis. Pick the axis least aligned with n.
|
|
752
813
|
let t1x: number, t1y: number, t1z: number
|
|
753
814
|
if (Math.abs(nx) < 0.7071) { t1x = 0; t1y = -nz; t1z = ny }
|
|
@@ -837,7 +898,7 @@ function iterateContactRow(
|
|
|
837
898
|
if (jacInvN > 0) {
|
|
838
899
|
const nx = c.nx, ny = c.ny, nz = c.nz
|
|
839
900
|
const relVelN = dvx * nx + dvy * ny + dvz * nz
|
|
840
|
-
let dImpN = (c.bounceVel - relVelN) * jacInvN
|
|
901
|
+
let dImpN = (c.bounceVel - c.allowedApproachVel - relVelN) * jacInvN * c.sorGain
|
|
841
902
|
const oldN = c.appliedNormalImpulse
|
|
842
903
|
let newN = oldN + dImpN
|
|
843
904
|
if (newN < 0) { newN = 0; dImpN = -oldN }
|
|
@@ -907,7 +968,7 @@ function applyFrictionTangent(
|
|
|
907
968
|
): void {
|
|
908
969
|
if (jacInv <= 0) return
|
|
909
970
|
const relVel = dvx * tx + dvy * ty + dvz * tz
|
|
910
|
-
let dImp = -relVel * jacInv
|
|
971
|
+
let dImp = -relVel * jacInv * c.sorGain
|
|
911
972
|
const old = slot === 1 ? c.appliedFrictionImpulse1 : c.appliedFrictionImpulse2
|
|
912
973
|
let next = old + dImp
|
|
913
974
|
if (next < -muNormal) { next = -muNormal; dImp = next - old }
|