reze-engine 0.36.0 → 0.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/physics/contact.d.ts +12 -3
- package/dist/physics/contact.d.ts.map +1 -1
- package/dist/physics/contact.js +5 -2
- package/dist/physics/manifold.d.ts +32 -11
- package/dist/physics/manifold.d.ts.map +1 -1
- package/dist/physics/manifold.js +137 -57
- package/dist/physics/solver.d.ts +7 -1
- package/dist/physics/solver.d.ts.map +1 -1
- package/dist/physics/solver.js +536 -182
- package/dist/physics/world.d.ts +2 -0
- package/dist/physics/world.d.ts.map +1 -1
- package/dist/physics/world.js +16 -43
- package/package.json +1 -1
- package/src/physics/contact.ts +17 -7
- package/src/physics/manifold.ts +151 -0
- package/src/physics/solver.ts +520 -184
- package/src/physics/world.ts +17 -43
package/dist/physics/solver.js
CHANGED
|
@@ -13,29 +13,63 @@
|
|
|
13
13
|
import { Mat4 } from "../math";
|
|
14
14
|
import { STOP_ERP } from "./constraint";
|
|
15
15
|
const BOUNCE_THRESHOLD = 2.0;
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// correct the same motion and the body is over-braked, differently every
|
|
20
|
-
// substep. Scaling each row's step damps that without changing the fixed
|
|
21
|
-
// point — the accumulated impulse still converges to the same answer, just
|
|
22
|
-
// approached rather than overshot.
|
|
16
|
+
// Contact error-reduction parameter — btContactSolverInfo::m_erp in Bullet
|
|
17
|
+
// 2.75, the build MMD's own physics runs, at its stock 0.2. PMX rigs are
|
|
18
|
+
// authored against exactly this response.
|
|
23
19
|
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// the well-conditioned rows, and a body resting on a SINGLE contact can then
|
|
27
|
-
// no longer cancel its approach velocity within the iteration budget, so it
|
|
28
|
-
// creeps forever instead of settling (托特 peak speed at 15 s: 0.11 at gain
|
|
29
|
-
// 1.0, 0.46 at 0.5, 0.72 at 0.3 — a permanent limit cycle on a one-contact
|
|
30
|
-
// body). Over-constraint is a local property, so the remedy has to be local.
|
|
20
|
+
// Bullet folds penetration recovery into the CONTACT VELOCITY ROW rather than
|
|
21
|
+
// translating positions (m_splitImpulse defaults to false in 2.75):
|
|
31
22
|
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
23
|
+
// penetration = cp.getDistance() + m_linearSlop // < 0 when overlapping
|
|
24
|
+
// positionalError = -penetration * m_erp / m_timeStep
|
|
25
|
+
// velocityError = restitution - rel_vel
|
|
26
|
+
// m_rhs = (positionalError + velocityError) * m_jacDiagABInv
|
|
27
|
+
//
|
|
28
|
+
// Our `depth` is the negation of Bullet's distance, so positionalError is just
|
|
29
|
+
// depth·ERP/dt — one expression covering both cases. Penetrating (depth > 0) it
|
|
30
|
+
// pushes apart; separated (depth < 0, a speculative row) it ALLOWS approach at
|
|
31
|
+
// the rate that closes the gap, which is what keeps those rows inert until the
|
|
32
|
+
// body would really arrive.
|
|
33
|
+
const CONTACT_ERP = 0.2;
|
|
34
|
+
// btContactSolverInfo's SOLVER_RANDMIZE_ORDER. Gauss-Seidel is order-biased —
|
|
35
|
+
// rows solved first win — which on a cross-linked lattice shows up as a lean or
|
|
36
|
+
// a period-2 chatter. Bullet ships the flag off; it is here to be measured.
|
|
37
|
+
const RANDOMIZE_ORDER = true;
|
|
38
|
+
// btSequentialImpulseConstraintSolver::btRand2, so the shuffle is reproducible
|
|
39
|
+
// and a take replays identically.
|
|
40
|
+
let _seed = 0;
|
|
41
|
+
function rand2() {
|
|
42
|
+
_seed = (1664525 * _seed + 1013904223) & 0xffffffff;
|
|
43
|
+
return _seed >>> 0;
|
|
44
|
+
}
|
|
45
|
+
let _order = new Int32Array(0);
|
|
46
|
+
// btContactSolverInfo::m_warmstartingFactor. Seeded impulses are scaled by
|
|
47
|
+
// this so a stale cache decays instead of compounding.
|
|
48
|
+
const WARMSTARTING_FACTOR = 0.85;
|
|
49
|
+
// btContactSolverInfo::m_restingContactRestitutionThreshold — past this many
|
|
50
|
+
// substeps a contact is "resting" and stops bouncing.
|
|
51
|
+
const RESTING_CONTACT_AGE = 2;
|
|
52
|
+
// btContactSolverInfo::m_linearSlop is 0 in 2.75 — no allowance, the row aims
|
|
53
|
+
// at exactly touching. Kept named so the divergence is visible if it ever moves.
|
|
54
|
+
const LINEAR_SLOP = 0;
|
|
55
|
+
// btContactSolverInfo::m_splitImpulse. False in 2.75 (only the demos turn it
|
|
56
|
+
// on), but TRUE by default from Bullet 2.8x onward — which is what babylon-mmd
|
|
57
|
+
// runs today. MMD is closed-source so its own setting cannot be read, but it is
|
|
58
|
+
// widely reported to enable the flag, and our own measurements agree
|
|
59
|
+
// independently: turning it on lowered the velocity-reversal rate on every rig
|
|
60
|
+
// tested (伊邪那美 0.118 → 0.098, 诗蔻蒂 0.098 → 0.074, 托特 0.054 → 0.028).
|
|
61
|
+
// With it on, the penetration term is moved out of the real
|
|
62
|
+
// velocity row into a pseudo-velocity ("push") channel that is solved
|
|
63
|
+
// separately and integrated straight into the transform, so recovering overlap
|
|
64
|
+
// never adds momentum the joint springs can hand back.
|
|
65
|
+
const SPLIT_IMPULSE = true;
|
|
66
|
+
// btContactSolverInfo::m_splitImpulsePenetrationThreshold. Bullet's sign: only
|
|
67
|
+
// contacts DEEPER than this go to the split channel; shallower ones keep the
|
|
68
|
+
// bias in the real row. Our depth is the negation, hence the flipped compare.
|
|
69
|
+
const SPLIT_PENETRATION_THRESHOLD = 0.02;
|
|
70
|
+
// Pseudo-velocity accumulators for the split channel, zeroed each substep.
|
|
71
|
+
let _pushLin = new Float32Array(0);
|
|
72
|
+
let _pushAng = new Float32Array(0);
|
|
39
73
|
// Ceilings on limit-correction velocity. In normal operation limit errors are
|
|
40
74
|
// tiny; a large error only appears after a discontinuity (teleport, stall,
|
|
41
75
|
// deep penetration), and feeding err·ERP/dt to the solver unclamped then
|
|
@@ -57,7 +91,44 @@ const LIMIT_SOFTNESS_ANGULAR = 0.5;
|
|
|
57
91
|
// error), so resting chains rang forever — the "static dress slowly boils"
|
|
58
92
|
// bug. c is derived per row from a fixed damping ratio against the row's
|
|
59
93
|
// effective mass: c = 2ζ√(k·m_eff).
|
|
60
|
-
|
|
94
|
+
// btGeneric6DofSpringConstraint drives a spring as a MOTOR, not as a
|
|
95
|
+
// spring-damper: stiffness becomes a velocity target with a force cap.
|
|
96
|
+
//
|
|
97
|
+
// velFactor = fps · damping / numIterations
|
|
98
|
+
// targetVelocity = velFactor · force (force = err · k)
|
|
99
|
+
// maxMotorForce = |force| / fps
|
|
100
|
+
//
|
|
101
|
+
// m_springDamping defaults to 1.0 and PMX carries no damping field, so every
|
|
102
|
+
// MMD spring runs at exactly that — which is what these rigs were authored
|
|
103
|
+
// against. This replaces an implicit spring-damper with an invented damping
|
|
104
|
+
// ratio (ζ = 0.7); ported in isolation the motor form measured 0.0015 mean
|
|
105
|
+
// reversal at jerk 0.5, the calmest joint-side result recorded.
|
|
106
|
+
const SPRING_DAMPING = 1.0;
|
|
107
|
+
// btConstraintInfo2::erp, from infoGlobal.m_erp — used only by getMotorFactor.
|
|
108
|
+
const INFO_ERP = 0.2;
|
|
109
|
+
// btTypedConstraint::getMotorFactor. Scales a motor down as it approaches the
|
|
110
|
+
// limit it is driving toward, so a stiff spring cannot drive straight through
|
|
111
|
+
// its own stop and then be fought by the limit row. That fight is a candidate
|
|
112
|
+
// explanation for the period-2 joint chatter seen on bodies carrying NO
|
|
113
|
+
// contacts at all (qh_15_5: accumulated joint impulse alternating 8.4 / 14.7).
|
|
114
|
+
function getMotorFactor(pos, lowLim, uppLim, vel, timeFact) {
|
|
115
|
+
if (lowLim > uppLim)
|
|
116
|
+
return 1;
|
|
117
|
+
if (lowLim === uppLim)
|
|
118
|
+
return 0;
|
|
119
|
+
const deltaMax = vel / timeFact;
|
|
120
|
+
if (deltaMax < 0) {
|
|
121
|
+
if (pos >= lowLim && pos < lowLim - deltaMax)
|
|
122
|
+
return (lowLim - pos) / deltaMax;
|
|
123
|
+
return pos < lowLim ? 0 : 1;
|
|
124
|
+
}
|
|
125
|
+
if (deltaMax > 0) {
|
|
126
|
+
if (pos <= uppLim && pos > uppLim - deltaMax)
|
|
127
|
+
return (uppLim - pos) / deltaMax;
|
|
128
|
+
return pos > uppLim ? 0 : 1;
|
|
129
|
+
}
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
61
132
|
// ERP scale for loop-closing constraints (see buildConstraints: joints that
|
|
62
133
|
// close a cycle in the joint graph, e.g. the horizontal ring welds of
|
|
63
134
|
// cross-linked skirt lattices). A loop over-determines positions — when
|
|
@@ -66,11 +137,6 @@ const SPRING_DAMPING_ZETA = 0.7;
|
|
|
66
137
|
// chatter. Loop edges keep shape at a fraction of the correction rate while
|
|
67
138
|
// the spanning-tree chains stay stiff.
|
|
68
139
|
const LOOP_ERP_SCALE = 1.0;
|
|
69
|
-
// Loop-edge LOCKED axes are converted to force-clamped springs instead of
|
|
70
|
-
// weld rows: an equality row on a cycle fights the other cycle edges at any
|
|
71
|
-
// ERP (the velocity system is over-determined too). A spring bounded by its
|
|
72
|
-
// real force k·|err|·dt holds the ring's shape elastically without fighting.
|
|
73
|
-
const LOOP_SPRING_K = 900;
|
|
74
140
|
// Angular limit violations below this switch to per-axis euler rows; above
|
|
75
141
|
// it, the single geodesic row takes over (see setupConstraint).
|
|
76
142
|
const GEODESIC_THRESHOLD = 0.5; // rad
|
|
@@ -82,7 +148,7 @@ const _bodyMatB = new Float32Array(16);
|
|
|
82
148
|
const _angDiffScratch = new Float32Array(3);
|
|
83
149
|
const _quatScratchA = new Float32Array(4);
|
|
84
150
|
const _quatScratchB = new Float32Array(4);
|
|
85
|
-
export function solveConstraints(store, constraints, cache, contacts, dt, iterations) {
|
|
151
|
+
export function solveConstraints(store, constraints, cache, contacts, dt, iterations, manifolds = null) {
|
|
86
152
|
if (dt <= 0)
|
|
87
153
|
return;
|
|
88
154
|
if (constraints.length === 0 && contacts.count === 0)
|
|
@@ -95,36 +161,189 @@ export function solveConstraints(store, constraints, cache, contacts, dt, iterat
|
|
|
95
161
|
store.updateInvInertiaWorld();
|
|
96
162
|
const W = store.invInertiaWorld;
|
|
97
163
|
for (let c = 0; c < constraints.length; c++) {
|
|
98
|
-
setupConstraint(constraints[c], c, cache, store, dt, invDt);
|
|
99
|
-
}
|
|
100
|
-
// How many contact rows each body carries this substep — the per-contact
|
|
101
|
-
// relaxation gain below is a function of the more contended of its two
|
|
102
|
-
// bodies. Counting is O(contacts), done once, before any setup.
|
|
103
|
-
if (_rowCount.length < store.count)
|
|
104
|
-
_rowCount = new Int32Array(store.count);
|
|
105
|
-
else
|
|
106
|
-
_rowCount.fill(0, 0, store.count);
|
|
107
|
-
// Only DYNAMIC bodies are counted. A static body — above all the ground —
|
|
108
|
-
// collects a row from every body resting on it, and letting that inflate the
|
|
109
|
-
// count crushes the gain of each of those contacts to the floor, so nothing
|
|
110
|
-
// resting on the ground can build enough impulse and it creeps instead of
|
|
111
|
-
// settling. A body with invMass 0 cannot be over-braked in the first place.
|
|
112
|
-
for (let ci = 0; ci < contacts.count; ci++) {
|
|
113
|
-
const c = contacts.get(ci);
|
|
114
|
-
if (invMass[c.bodyA] > 0)
|
|
115
|
-
_rowCount[c.bodyA]++;
|
|
116
|
-
if (invMass[c.bodyB] > 0)
|
|
117
|
-
_rowCount[c.bodyB]++;
|
|
164
|
+
setupConstraint(constraints[c], c, cache, store, dt, invDt, iterations);
|
|
118
165
|
}
|
|
119
166
|
for (let ci = 0; ci < contacts.count; ci++) {
|
|
120
167
|
setupContactRow(contacts.get(ci), lv, av, invMass, W, invDt);
|
|
121
168
|
}
|
|
169
|
+
// Warm start: seed each row from what the same point converged to last
|
|
170
|
+
// substep and APPLY it before iterating, exactly as Bullet does at setup.
|
|
171
|
+
if (manifolds !== null) {
|
|
172
|
+
for (let ci = 0; ci < contacts.count; ci++) {
|
|
173
|
+
warmStartContact(contacts.get(ci), store, manifolds, lv, av, invMass);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Bullet 2.75 solves each iteration in three separate passes, in this order:
|
|
177
|
+
// all joint rows, then all contact normal rows, then all friction rows. The
|
|
178
|
+
// friction bound is read from the contact's accumulated impulse AS IT STANDS
|
|
179
|
+
// after the normal pass, so friction tightens as the normal converges. Doing
|
|
180
|
+
// friction inline per contact (our previous shape) bounds it against a normal
|
|
181
|
+
// impulse that half the manifold has not contributed to yet.
|
|
182
|
+
if (RANDOMIZE_ORDER) {
|
|
183
|
+
// Reset per substep. Bullet keeps the seed on the solver instance; ours is
|
|
184
|
+
// module-level, so without this two worlds in one process share a stream
|
|
185
|
+
// and the same scene stops replaying identically — which the determinism
|
|
186
|
+
// test catches. Re-seeding costs nothing: the point is breaking the order
|
|
187
|
+
// bias BETWEEN iterations, not being unpredictable across frames.
|
|
188
|
+
_seed = 0;
|
|
189
|
+
if (_order.length < contacts.count)
|
|
190
|
+
_order = new Int32Array(contacts.count);
|
|
191
|
+
for (let i = 0; i < contacts.count; i++)
|
|
192
|
+
_order[i] = i;
|
|
193
|
+
}
|
|
122
194
|
for (let iter = 0; iter < iterations; iter++) {
|
|
195
|
+
if (RANDOMIZE_ORDER) {
|
|
196
|
+
// Bullet reshuffles between iterations, not once per substep.
|
|
197
|
+
for (let i = 1; i < contacts.count; i++) {
|
|
198
|
+
const j = rand2() % (i + 1);
|
|
199
|
+
const tmp = _order[i];
|
|
200
|
+
_order[i] = _order[j];
|
|
201
|
+
_order[j] = tmp;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
123
204
|
for (let c = 0; c < constraints.length; c++) {
|
|
124
205
|
iterateConstraint(c, cache, lv, av, invMass);
|
|
125
206
|
}
|
|
126
207
|
for (let ci = 0; ci < contacts.count; ci++) {
|
|
127
|
-
|
|
208
|
+
iterateContactNormal(contacts.get(RANDOMIZE_ORDER ? _order[ci] : ci), lv, av, invMass);
|
|
209
|
+
}
|
|
210
|
+
for (let ci = 0; ci < contacts.count; ci++) {
|
|
211
|
+
iterateContactFriction(contacts.get(ci), lv, av, invMass);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Split-impulse pass: its OWN full set of iterations over the penetration
|
|
215
|
+
// channel only, exactly as Bullet runs it after the main loop.
|
|
216
|
+
if (SPLIT_IMPULSE) {
|
|
217
|
+
const n3 = store.count * 3;
|
|
218
|
+
if (_pushLin.length < n3) {
|
|
219
|
+
_pushLin = new Float32Array(n3);
|
|
220
|
+
_pushAng = new Float32Array(n3);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
_pushLin.fill(0, 0, n3);
|
|
224
|
+
_pushAng.fill(0, 0, n3);
|
|
225
|
+
}
|
|
226
|
+
let any = false;
|
|
227
|
+
for (let ci = 0; ci < contacts.count; ci++)
|
|
228
|
+
if (contacts.get(ci).rhsPenetration !== 0) {
|
|
229
|
+
any = true;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
if (any) {
|
|
233
|
+
if (RANDOMIZE_ORDER) {
|
|
234
|
+
// Reset per substep. Bullet keeps the seed on the solver instance; ours is
|
|
235
|
+
// module-level, so without this two worlds in one process share a stream
|
|
236
|
+
// and the same scene stops replaying identically — which the determinism
|
|
237
|
+
// test catches. Re-seeding costs nothing: the point is breaking the order
|
|
238
|
+
// bias BETWEEN iterations, not being unpredictable across frames.
|
|
239
|
+
_seed = 0;
|
|
240
|
+
if (_order.length < contacts.count)
|
|
241
|
+
_order = new Int32Array(contacts.count);
|
|
242
|
+
for (let i = 0; i < contacts.count; i++)
|
|
243
|
+
_order[i] = i;
|
|
244
|
+
}
|
|
245
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
246
|
+
if (RANDOMIZE_ORDER) {
|
|
247
|
+
// Bullet reshuffles between iterations, not once per substep.
|
|
248
|
+
for (let i = 1; i < contacts.count; i++) {
|
|
249
|
+
const j = rand2() % (i + 1);
|
|
250
|
+
const tmp = _order[i];
|
|
251
|
+
_order[i] = _order[j];
|
|
252
|
+
_order[j] = tmp;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
for (let ci = 0; ci < contacts.count; ci++) {
|
|
256
|
+
resolveSplitPenetration(contacts.get(ci), invMass);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Bullet's resolveSplitPenetrationImpulse: same Jacobian, but reading and
|
|
263
|
+
// writing the pseudo-velocity channel, with its own push-only accumulator.
|
|
264
|
+
function resolveSplitPenetration(c, invMass) {
|
|
265
|
+
if (c.rhsPenetration === 0)
|
|
266
|
+
return;
|
|
267
|
+
const jacInvN = c.jacInvN;
|
|
268
|
+
if (jacInvN <= 0)
|
|
269
|
+
return;
|
|
270
|
+
const imA = invMass[c.bodyA];
|
|
271
|
+
const imB = invMass[c.bodyB];
|
|
272
|
+
if (imA === 0 && imB === 0)
|
|
273
|
+
return;
|
|
274
|
+
const ai = c.bodyA * 3, bi = c.bodyB * 3;
|
|
275
|
+
const nx = c.nx, ny = c.ny, nz = c.nz;
|
|
276
|
+
const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz;
|
|
277
|
+
const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz;
|
|
278
|
+
const vAx = _pushLin[ai + 0] + _pushAng[ai + 1] * rAz - _pushAng[ai + 2] * rAy;
|
|
279
|
+
const vAy = _pushLin[ai + 1] + _pushAng[ai + 2] * rAx - _pushAng[ai + 0] * rAz;
|
|
280
|
+
const vAz = _pushLin[ai + 2] + _pushAng[ai + 0] * rAy - _pushAng[ai + 1] * rAx;
|
|
281
|
+
const vBx = _pushLin[bi + 0] + _pushAng[bi + 1] * rBz - _pushAng[bi + 2] * rBy;
|
|
282
|
+
const vBy = _pushLin[bi + 1] + _pushAng[bi + 2] * rBx - _pushAng[bi + 0] * rBz;
|
|
283
|
+
const vBz = _pushLin[bi + 2] + _pushAng[bi + 0] * rBy - _pushAng[bi + 1] * rBx;
|
|
284
|
+
const relVel = (vBx - vAx) * nx + (vBy - vAy) * ny + (vBz - vAz) * nz;
|
|
285
|
+
let d = (c.rhsPenetration - relVel) * jacInvN;
|
|
286
|
+
const old = c.appliedPushImpulse;
|
|
287
|
+
let sum = old + d;
|
|
288
|
+
if (sum < 0) {
|
|
289
|
+
sum = 0;
|
|
290
|
+
d = -old;
|
|
291
|
+
}
|
|
292
|
+
c.appliedPushImpulse = sum;
|
|
293
|
+
if (d === 0)
|
|
294
|
+
return;
|
|
295
|
+
if (imA > 0) {
|
|
296
|
+
_pushLin[ai + 0] -= d * imA * nx;
|
|
297
|
+
_pushLin[ai + 1] -= d * imA * ny;
|
|
298
|
+
_pushLin[ai + 2] -= d * imA * nz;
|
|
299
|
+
_pushAng[ai + 0] -= d * c.cAxN;
|
|
300
|
+
_pushAng[ai + 1] -= d * c.cAyN;
|
|
301
|
+
_pushAng[ai + 2] -= d * c.cAzN;
|
|
302
|
+
}
|
|
303
|
+
if (imB > 0) {
|
|
304
|
+
_pushLin[bi + 0] += d * imB * nx;
|
|
305
|
+
_pushLin[bi + 1] += d * imB * ny;
|
|
306
|
+
_pushLin[bi + 2] += d * imB * nz;
|
|
307
|
+
_pushAng[bi + 0] += d * c.cBxN;
|
|
308
|
+
_pushAng[bi + 1] += d * c.cByN;
|
|
309
|
+
_pushAng[bi + 2] += d * c.cBzN;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/** Integrate the accumulated push/turn velocity straight into the transform —
|
|
313
|
+
* btSolverBody::writebackVelocity(timeStep). Never touches real momentum. */
|
|
314
|
+
export function applySplitImpulsePush(store, dt) {
|
|
315
|
+
if (!SPLIT_IMPULSE || _pushLin.length === 0)
|
|
316
|
+
return;
|
|
317
|
+
const pos = store.positions;
|
|
318
|
+
const ori = store.orientations;
|
|
319
|
+
const invMass = store.invMass;
|
|
320
|
+
for (let i = 0; i < store.count; i++) {
|
|
321
|
+
if (invMass[i] <= 0)
|
|
322
|
+
continue;
|
|
323
|
+
const i3 = i * 3, i4 = i * 4;
|
|
324
|
+
const px = _pushLin[i3 + 0], py = _pushLin[i3 + 1], pz = _pushLin[i3 + 2];
|
|
325
|
+
const wx = _pushAng[i3 + 0], wy = _pushAng[i3 + 1], wz = _pushAng[i3 + 2];
|
|
326
|
+
if (px === 0 && py === 0 && pz === 0 && wx === 0 && wy === 0 && wz === 0)
|
|
327
|
+
continue;
|
|
328
|
+
pos[i3 + 0] += px * dt;
|
|
329
|
+
pos[i3 + 1] += py * dt;
|
|
330
|
+
pos[i3 + 2] += pz * dt;
|
|
331
|
+
if (wx !== 0 || wy !== 0 || wz !== 0) {
|
|
332
|
+
const qx = ori[i4 + 0], qy = ori[i4 + 1], qz = ori[i4 + 2], qw = ori[i4 + 3];
|
|
333
|
+
const dx = qw * wx + wy * qz - wz * qy;
|
|
334
|
+
const dy = qw * wy + wz * qx - wx * qz;
|
|
335
|
+
const dz = qw * wz + wx * qy - wy * qx;
|
|
336
|
+
const dw = -(wx * qx + wy * qy + wz * qz);
|
|
337
|
+
const h = 0.5 * dt;
|
|
338
|
+
let nx2 = qx + dx * h, ny2 = qy + dy * h, nz2 = qz + dz * h, nw2 = qw + dw * h;
|
|
339
|
+
const l2 = nx2 * nx2 + ny2 * ny2 + nz2 * nz2 + nw2 * nw2;
|
|
340
|
+
if (l2 > 0) {
|
|
341
|
+
const inv = 1 / Math.sqrt(l2);
|
|
342
|
+
ori[i4 + 0] = nx2 * inv;
|
|
343
|
+
ori[i4 + 1] = ny2 * inv;
|
|
344
|
+
ori[i4 + 2] = nz2 * inv;
|
|
345
|
+
ori[i4 + 3] = nw2 * inv;
|
|
346
|
+
}
|
|
128
347
|
}
|
|
129
348
|
}
|
|
130
349
|
}
|
|
@@ -180,7 +399,7 @@ export class SolverCache {
|
|
|
180
399
|
}
|
|
181
400
|
// SETUP: compute everything that doesn't depend on velocities. Caller
|
|
182
401
|
// guarantees pos/ori don't change between this and the iter loop.
|
|
183
|
-
function setupConstraint(con, ci, cache, store, dt, invDt) {
|
|
402
|
+
function setupConstraint(con, ci, cache, store, dt, invDt, iterations) {
|
|
184
403
|
const F = cache.F;
|
|
185
404
|
const I = cache.I;
|
|
186
405
|
const D = cache.D;
|
|
@@ -306,16 +525,20 @@ function setupConstraint(con, ci, cache, store, dt, invDt) {
|
|
|
306
525
|
// iteration (PMX rigs routinely put k=100000 springs on locked axes,
|
|
307
526
|
// which turned welded weight-bodies into energy pumps).
|
|
308
527
|
if (con.springEnabled[i] && denom > 0 && lo !== hi) {
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
528
|
+
// Bullet motor form. Our relVel is (vB − vA)·axis where Bullet's is the
|
|
529
|
+
// negation, so the target carries the opposite sign to Bullet's — which
|
|
530
|
+
// is the same sign the old spring-damper produced, so behaviour keeps its
|
|
531
|
+
// direction and only its magnitude law changes.
|
|
312
532
|
const k = con.springStiffness[i];
|
|
313
533
|
const serr = curr - con.equilibriumPoint[i];
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
534
|
+
const force = serr * k;
|
|
535
|
+
const velFactor = (invDt * SPRING_DAMPING) / iterations;
|
|
536
|
+
const target = -velFactor * force;
|
|
537
|
+
// getMotorFactor's `vel` is Bullet's tag_vel: −targetVelocity for a
|
|
538
|
+
// linear axis, which in our sign convention is the target itself.
|
|
539
|
+
const motFact = getMotorFactor(curr, lo, hi, target, invDt * INFO_ERP);
|
|
540
|
+
F[base + LIN_SPR_TGT + i] = motFact * target;
|
|
541
|
+
F[base + LIN_SPR_MAX + i] = Math.abs(force) * dt;
|
|
319
542
|
F[base + LIN_SPR_IMP + i] = 0;
|
|
320
543
|
I[ib + I_LIN_SPR_ACT + i] = 1;
|
|
321
544
|
}
|
|
@@ -406,16 +629,18 @@ function setupConstraint(con, ci, cache, store, dt, invDt) {
|
|
|
406
629
|
// Springs on locked axes are skipped — the limit row welds those, and
|
|
407
630
|
// double-driving a DOF overshoots every iteration (see the linear loop).
|
|
408
631
|
if (con.springEnabled[idx] && F[angJac + i] > 0 && con.angularMin[i] !== con.angularMax[i]) {
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
// the CFM softness s (not a clamp).
|
|
632
|
+
// Bullet motor form, angular flavour. Bullet's angular force is −err·k
|
|
633
|
+
// and its rel_vel is the negation of our relAv, so the two flips cancel
|
|
634
|
+
// and our target stays positive for positive error, as before.
|
|
413
635
|
const k = con.springStiffness[idx];
|
|
414
636
|
const serr = _angDiffScratch[i] - con.equilibriumPoint[idx];
|
|
415
|
-
const
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
637
|
+
const force = -serr * k;
|
|
638
|
+
const velFactor = (invDt * SPRING_DAMPING) / iterations;
|
|
639
|
+
const target = -velFactor * force;
|
|
640
|
+
// tag_vel for a rotational axis is Bullet's targetVelocity, i.e. −ours.
|
|
641
|
+
const motFact = getMotorFactor(_angDiffScratch[i], con.angularMin[i], con.angularMax[i], -target, invDt * INFO_ERP);
|
|
642
|
+
F[angTgt + i] = motFact * target;
|
|
643
|
+
F[base + ANG_SPR_MAX + i] = Math.abs(force) * dt;
|
|
419
644
|
I[angAct + i] = 1;
|
|
420
645
|
}
|
|
421
646
|
else {
|
|
@@ -595,9 +820,18 @@ function iterateConstraint(ci, cache, lv, av, invMass) {
|
|
|
595
820
|
// the spring off the stale value double-corrects the DOF.
|
|
596
821
|
if (I[sprAct + i]) {
|
|
597
822
|
const relVelNow = j !== 0 ? relVel + j / F[jac + i] : relVel;
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
F[
|
|
823
|
+
// Motor row: drive toward the target, accumulated impulse clamped to the
|
|
824
|
+
// spring's real force over the step (Bullet's m_lowerLimit/m_upperLimit).
|
|
825
|
+
let dImp = (F[sprTgt + i] - relVelNow) * F[jac + i];
|
|
826
|
+
const maxF = F[sprMax + i];
|
|
827
|
+
const oldS = F[sprImp + i];
|
|
828
|
+
let nextS = oldS + dImp;
|
|
829
|
+
if (nextS < -maxF)
|
|
830
|
+
nextS = -maxF;
|
|
831
|
+
else if (nextS > maxF)
|
|
832
|
+
nextS = maxF;
|
|
833
|
+
dImp = nextS - oldS;
|
|
834
|
+
F[sprImp + i] = nextS;
|
|
601
835
|
j += dImp;
|
|
602
836
|
}
|
|
603
837
|
if (j === 0)
|
|
@@ -638,9 +872,16 @@ function iterateConstraint(ci, cache, lv, av, invMass) {
|
|
|
638
872
|
const axx = F[angAxes + o + 0], axy = F[angAxes + o + 1], axz = F[angAxes + o + 2];
|
|
639
873
|
const relAv = dax * axx + day * axy + daz * axz;
|
|
640
874
|
// Implicit spring-damper row (soft constraint, see setup).
|
|
641
|
-
|
|
642
|
-
const
|
|
643
|
-
F[angSprImp + i]
|
|
875
|
+
let j = (F[angTgt + i] - relAv) * F[angJac + i];
|
|
876
|
+
const maxF = F[angSprMax + i];
|
|
877
|
+
const oldS = F[angSprImp + i];
|
|
878
|
+
let nextS = oldS + j;
|
|
879
|
+
if (nextS < -maxF)
|
|
880
|
+
nextS = -maxF;
|
|
881
|
+
else if (nextS > maxF)
|
|
882
|
+
nextS = maxF;
|
|
883
|
+
j = nextS - oldS;
|
|
884
|
+
F[angSprImp + i] = nextS;
|
|
644
885
|
if (j === 0)
|
|
645
886
|
continue;
|
|
646
887
|
if (imA > 0) {
|
|
@@ -774,8 +1015,14 @@ function setupContactRow(c, lv, av, invMass, W, invDt) {
|
|
|
774
1015
|
const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy;
|
|
775
1016
|
const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz;
|
|
776
1017
|
const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx;
|
|
777
|
-
const
|
|
778
|
-
|
|
1018
|
+
const dvx = vBx - vAx;
|
|
1019
|
+
const dvy = vBy - vAy;
|
|
1020
|
+
const dvz = vBz - vAz;
|
|
1021
|
+
const relVelN0 = dvx * nx + dvy * ny + dvz * nz;
|
|
1022
|
+
// Restitution is switched off once a point has been in contact for more than
|
|
1023
|
+
// a couple of substeps (m_restingContactRestitutionThreshold), or resting
|
|
1024
|
+
// cloth keeps being handed a little bounce forever.
|
|
1025
|
+
c.bounceVel = c.restitution > 0 && relVelN0 < -BOUNCE_THRESHOLD && c.age <= RESTING_CONTACT_AGE
|
|
779
1026
|
? -c.restitution * relVelN0
|
|
780
1027
|
: 0;
|
|
781
1028
|
// Speculative rows (depth < 0 — the shapes are inside the margin band but
|
|
@@ -790,39 +1037,66 @@ function setupContactRow(c, lv, av, invMass, W, invDt) {
|
|
|
790
1037
|
// (pulling) impulse, not a large positive one on a body in mid-air. On a
|
|
791
1038
|
// dress rig half of all contact rows are speculative and 88% of them fire,
|
|
792
1039
|
// which is the field of invisible brakes the cloth was shaking against.
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
const gain = contended > 1 ? 1 / contended : 1;
|
|
798
|
-
c.sorGain = gain < CONTACT_SOR_MIN ? CONTACT_SOR_MIN : gain;
|
|
799
|
-
// Friction tangent basis. Pick the axis least aligned with n.
|
|
800
|
-
let t1x, t1y, t1z;
|
|
801
|
-
if (Math.abs(nx) < 0.7071) {
|
|
802
|
-
t1x = 0;
|
|
803
|
-
t1y = -nz;
|
|
804
|
-
t1z = ny;
|
|
1040
|
+
const positionalError = (c.depth - LINEAR_SLOP) * CONTACT_ERP * invDt;
|
|
1041
|
+
if (SPLIT_IMPULSE && c.depth > SPLIT_PENETRATION_THRESHOLD) {
|
|
1042
|
+
c.biasVel = 0;
|
|
1043
|
+
c.rhsPenetration = positionalError;
|
|
805
1044
|
}
|
|
806
1045
|
else {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
t1z = -nx;
|
|
1046
|
+
c.biasVel = positionalError;
|
|
1047
|
+
c.rhsPenetration = 0;
|
|
810
1048
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1049
|
+
// Friction direction, Bullet 2.75 style. The stock solverMode is
|
|
1050
|
+
// SOLVER_USE_WARMSTARTING | SOLVER_SIMD — SOLVER_USE_2_FRICTION_DIRECTIONS is
|
|
1051
|
+
// NOT set, so there is exactly ONE friction row, aligned with the direction
|
|
1052
|
+
// the contact is actually sliding:
|
|
1053
|
+
//
|
|
1054
|
+
// m_lateralFrictionDir1 = vel - normalWorldOnB * rel_vel
|
|
1055
|
+
//
|
|
1056
|
+
// with vel = vA − vB. Our dv is vB − vA, so that direction is the negation of
|
|
1057
|
+
// dv's tangential part; sign is irrelevant to a row with symmetric ±μN bounds.
|
|
1058
|
+
// Below SIMD_EPSILON of sliding Bullet falls back to btPlaneSpace1, ported
|
|
1059
|
+
// exactly. Direction is recomputed every substep — friction-direction caching
|
|
1060
|
+
// is off in the stock mode.
|
|
1061
|
+
let t1x = dvx - nx * relVelN0;
|
|
1062
|
+
let t1y = dvy - ny * relVelN0;
|
|
1063
|
+
let t1z = dvz - nz * relVelN0;
|
|
1064
|
+
let t2x, t2y, t2z;
|
|
1065
|
+
const lat2 = t1x * t1x + t1y * t1y + t1z * t1z;
|
|
1066
|
+
if (lat2 > 1.192092896e-07) {
|
|
1067
|
+
const inv = 1 / Math.sqrt(lat2);
|
|
1068
|
+
t1x *= inv;
|
|
1069
|
+
t1y *= inv;
|
|
1070
|
+
t1z *= inv;
|
|
1071
|
+
// Second tangent is unused (single-direction mode) but kept orthonormal so
|
|
1072
|
+
// the cached Jacobian slots stay well-defined.
|
|
1073
|
+
t2x = ny * t1z - nz * t1y;
|
|
1074
|
+
t2y = nz * t1x - nx * t1z;
|
|
1075
|
+
t2z = nx * t1y - ny * t1x;
|
|
817
1076
|
}
|
|
818
1077
|
else {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1078
|
+
// btPlaneSpace1, verbatim.
|
|
1079
|
+
if (Math.abs(nz) > 0.7071067811865475) {
|
|
1080
|
+
const a = ny * ny + nz * nz;
|
|
1081
|
+
const k = 1 / Math.sqrt(a);
|
|
1082
|
+
t1x = 0;
|
|
1083
|
+
t1y = -nz * k;
|
|
1084
|
+
t1z = ny * k;
|
|
1085
|
+
t2x = a * k;
|
|
1086
|
+
t2y = -nx * t1z;
|
|
1087
|
+
t2z = nx * t1y;
|
|
1088
|
+
}
|
|
1089
|
+
else {
|
|
1090
|
+
const a = nx * nx + ny * ny;
|
|
1091
|
+
const k = 1 / Math.sqrt(a);
|
|
1092
|
+
t1x = -ny * k;
|
|
1093
|
+
t1y = nx * k;
|
|
1094
|
+
t1z = 0;
|
|
1095
|
+
t2x = -nz * t1y;
|
|
1096
|
+
t2y = nz * t1x;
|
|
1097
|
+
t2z = a * k;
|
|
1098
|
+
}
|
|
822
1099
|
}
|
|
823
|
-
const t2x = ny * t1z - nz * t1y;
|
|
824
|
-
const t2y = nz * t1x - nx * t1z;
|
|
825
|
-
const t2z = nx * t1y - ny * t1x;
|
|
826
1100
|
c.t1x = t1x;
|
|
827
1101
|
c.t1y = t1y;
|
|
828
1102
|
c.t1z = t1z;
|
|
@@ -875,10 +1149,13 @@ function setupContactRow(c, lv, av, invMass, W, invDt) {
|
|
|
875
1149
|
c.cBzT2 = wBzT2;
|
|
876
1150
|
c.jacInvT2 = denomT2 > 0 ? 1 / denomT2 : 0;
|
|
877
1151
|
}
|
|
878
|
-
// ITER
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
function
|
|
1152
|
+
// ITER, normal row only — Bullet resolveSingleConstraintRowLowerLimit:
|
|
1153
|
+
// deltaImpulse = (rhs - relVel) * jacDiagABInv, accumulated impulse clamped at
|
|
1154
|
+
// the lower limit of 0 (push-only). `rhs` here is restitution + Baumgarte bias.
|
|
1155
|
+
function iterateContactNormal(c, lv, av, invMass) {
|
|
1156
|
+
const jacInvN = c.jacInvN;
|
|
1157
|
+
if (jacInvN <= 0)
|
|
1158
|
+
return;
|
|
882
1159
|
const imA = invMass[c.bodyA];
|
|
883
1160
|
const imB = invMass[c.bodyB];
|
|
884
1161
|
if (imA === 0 && imB === 0)
|
|
@@ -886,102 +1163,92 @@ function iterateContactRow(c, lv, av, invMass) {
|
|
|
886
1163
|
const ai = c.bodyA * 3, bi = c.bodyB * 3;
|
|
887
1164
|
const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz;
|
|
888
1165
|
const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz;
|
|
1166
|
+
const nx = c.nx, ny = c.ny, nz = c.nz;
|
|
889
1167
|
const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy;
|
|
890
1168
|
const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz;
|
|
891
1169
|
const vAz = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx;
|
|
892
1170
|
const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy;
|
|
893
1171
|
const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz;
|
|
894
1172
|
const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx;
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
c.
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
const cBxN = c.cBxN, cByN = c.cByN, cBzN = c.cBzN;
|
|
914
|
-
if (imA > 0) {
|
|
915
|
-
lv[ai + 0] -= dImpN * imA * nx;
|
|
916
|
-
lv[ai + 1] -= dImpN * imA * ny;
|
|
917
|
-
lv[ai + 2] -= dImpN * imA * nz;
|
|
918
|
-
av[ai + 0] -= dImpN * cAxN;
|
|
919
|
-
av[ai + 1] -= dImpN * cAyN;
|
|
920
|
-
av[ai + 2] -= dImpN * cAzN;
|
|
921
|
-
}
|
|
922
|
-
if (imB > 0) {
|
|
923
|
-
lv[bi + 0] += dImpN * imB * nx;
|
|
924
|
-
lv[bi + 1] += dImpN * imB * ny;
|
|
925
|
-
lv[bi + 2] += dImpN * imB * nz;
|
|
926
|
-
av[bi + 0] += dImpN * cBxN;
|
|
927
|
-
av[bi + 1] += dImpN * cByN;
|
|
928
|
-
av[bi + 2] += dImpN * cBzN;
|
|
929
|
-
}
|
|
930
|
-
}
|
|
1173
|
+
const relVelN = (vBx - vAx) * nx + (vBy - vAy) * ny + (vBz - vAz) * nz;
|
|
1174
|
+
let dImpN = (c.bounceVel + c.biasVel - relVelN) * jacInvN;
|
|
1175
|
+
const oldN = c.appliedNormalImpulse;
|
|
1176
|
+
let newN = oldN + dImpN;
|
|
1177
|
+
if (newN < 0) {
|
|
1178
|
+
newN = 0;
|
|
1179
|
+
dImpN = -oldN;
|
|
1180
|
+
}
|
|
1181
|
+
c.appliedNormalImpulse = newN;
|
|
1182
|
+
if (dImpN === 0)
|
|
1183
|
+
return;
|
|
1184
|
+
if (imA > 0) {
|
|
1185
|
+
lv[ai + 0] -= dImpN * imA * nx;
|
|
1186
|
+
lv[ai + 1] -= dImpN * imA * ny;
|
|
1187
|
+
lv[ai + 2] -= dImpN * imA * nz;
|
|
1188
|
+
av[ai + 0] -= dImpN * c.cAxN;
|
|
1189
|
+
av[ai + 1] -= dImpN * c.cAyN;
|
|
1190
|
+
av[ai + 2] -= dImpN * c.cAzN;
|
|
931
1191
|
}
|
|
932
|
-
|
|
1192
|
+
if (imB > 0) {
|
|
1193
|
+
lv[bi + 0] += dImpN * imB * nx;
|
|
1194
|
+
lv[bi + 1] += dImpN * imB * ny;
|
|
1195
|
+
lv[bi + 2] += dImpN * imB * nz;
|
|
1196
|
+
av[bi + 0] += dImpN * c.cBxN;
|
|
1197
|
+
av[bi + 1] += dImpN * c.cByN;
|
|
1198
|
+
av[bi + 2] += dImpN * c.cBzN;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
// ITER, friction pass. One Coulomb row (stock Bullet has
|
|
1202
|
+
// SOLVER_USE_2_FRICTION_DIRECTIONS off), bounded by ±friction · the normal
|
|
1203
|
+
// impulse accumulated so far. Bullet skips the row entirely while that is
|
|
1204
|
+
// still zero.
|
|
1205
|
+
function iterateContactFriction(c, lv, av, invMass) {
|
|
933
1206
|
const muNormal = c.friction * c.appliedNormalImpulse;
|
|
934
1207
|
if (muNormal <= 0)
|
|
935
1208
|
return;
|
|
936
|
-
|
|
937
|
-
const vAx2 = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy;
|
|
938
|
-
const vAy2 = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz;
|
|
939
|
-
const vAz2 = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx;
|
|
940
|
-
const vBx2 = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy;
|
|
941
|
-
const vBy2 = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz;
|
|
942
|
-
const vBz2 = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx;
|
|
943
|
-
const dvx2 = vBx2 - vAx2;
|
|
944
|
-
const dvy2 = vBy2 - vAy2;
|
|
945
|
-
const dvz2 = vBz2 - vAz2;
|
|
946
|
-
applyFrictionTangent(c, ai, bi, dvx2, dvy2, dvz2, c.t1x, c.t1y, c.t1z, c.cAxT1, c.cAyT1, c.cAzT1, c.cBxT1, c.cByT1, c.cBzT1, c.jacInvT1, muNormal, imA, imB, lv, av, 1);
|
|
947
|
-
applyFrictionTangent(c, ai, bi, dvx2, dvy2, dvz2, c.t2x, c.t2y, c.t2z, c.cAxT2, c.cAyT2, c.cAzT2, c.cBxT2, c.cByT2, c.cBzT2, c.jacInvT2, muNormal, imA, imB, lv, av, 2);
|
|
948
|
-
}
|
|
949
|
-
function applyFrictionTangent(c, ai, bi, dvx, dvy, dvz, tx, ty, tz, cAx, cAy, cAz, cBx, cBy, cBz, jacInv, muNormal, imA, imB, lv, av, slot) {
|
|
950
|
-
if (jacInv <= 0)
|
|
1209
|
+
if (c.jacInvT1 <= 0)
|
|
951
1210
|
return;
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
1211
|
+
const imA = invMass[c.bodyA];
|
|
1212
|
+
const imB = invMass[c.bodyB];
|
|
1213
|
+
if (imA === 0 && imB === 0)
|
|
1214
|
+
return;
|
|
1215
|
+
const ai = c.bodyA * 3, bi = c.bodyB * 3;
|
|
1216
|
+
const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz;
|
|
1217
|
+
const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz;
|
|
1218
|
+
const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy;
|
|
1219
|
+
const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz;
|
|
1220
|
+
const vAz = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx;
|
|
1221
|
+
const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy;
|
|
1222
|
+
const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz;
|
|
1223
|
+
const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx;
|
|
1224
|
+
const dvx = vBx - vAx, dvy = vBy - vAy, dvz = vBz - vAz;
|
|
1225
|
+
const relVel = dvx * c.t1x + dvy * c.t1y + dvz * c.t1z;
|
|
1226
|
+
let dImp = -relVel * c.jacInvT1;
|
|
1227
|
+
const old = c.appliedFrictionImpulse1;
|
|
955
1228
|
let next = old + dImp;
|
|
956
|
-
if (next < -muNormal)
|
|
1229
|
+
if (next < -muNormal)
|
|
957
1230
|
next = -muNormal;
|
|
958
|
-
|
|
959
|
-
}
|
|
960
|
-
else if (next > muNormal) {
|
|
1231
|
+
else if (next > muNormal)
|
|
961
1232
|
next = muNormal;
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
if (slot === 1)
|
|
965
|
-
c.appliedFrictionImpulse1 = next;
|
|
966
|
-
else
|
|
967
|
-
c.appliedFrictionImpulse2 = next;
|
|
1233
|
+
dImp = next - old;
|
|
1234
|
+
c.appliedFrictionImpulse1 = next;
|
|
968
1235
|
if (dImp === 0)
|
|
969
1236
|
return;
|
|
970
1237
|
if (imA > 0) {
|
|
971
|
-
lv[ai + 0] -= dImp * imA *
|
|
972
|
-
lv[ai + 1] -= dImp * imA *
|
|
973
|
-
lv[ai + 2] -= dImp * imA *
|
|
974
|
-
av[ai + 0] -= dImp *
|
|
975
|
-
av[ai + 1] -= dImp *
|
|
976
|
-
av[ai + 2] -= dImp *
|
|
1238
|
+
lv[ai + 0] -= dImp * imA * c.t1x;
|
|
1239
|
+
lv[ai + 1] -= dImp * imA * c.t1y;
|
|
1240
|
+
lv[ai + 2] -= dImp * imA * c.t1z;
|
|
1241
|
+
av[ai + 0] -= dImp * c.cAxT1;
|
|
1242
|
+
av[ai + 1] -= dImp * c.cAyT1;
|
|
1243
|
+
av[ai + 2] -= dImp * c.cAzT1;
|
|
977
1244
|
}
|
|
978
1245
|
if (imB > 0) {
|
|
979
|
-
lv[bi + 0] += dImp * imB *
|
|
980
|
-
lv[bi + 1] += dImp * imB *
|
|
981
|
-
lv[bi + 2] += dImp * imB *
|
|
982
|
-
av[bi + 0] += dImp *
|
|
983
|
-
av[bi + 1] += dImp *
|
|
984
|
-
av[bi + 2] += dImp *
|
|
1246
|
+
lv[bi + 0] += dImp * imB * c.t1x;
|
|
1247
|
+
lv[bi + 1] += dImp * imB * c.t1y;
|
|
1248
|
+
lv[bi + 2] += dImp * imB * c.t1z;
|
|
1249
|
+
av[bi + 0] += dImp * c.cBxT1;
|
|
1250
|
+
av[bi + 1] += dImp * c.cByT1;
|
|
1251
|
+
av[bi + 2] += dImp * c.cBzT1;
|
|
985
1252
|
}
|
|
986
1253
|
}
|
|
987
1254
|
function buildBodyMat(store, i, out) {
|
|
@@ -999,6 +1266,19 @@ function eulerXYZQuatInto(x, y, z, out) {
|
|
|
999
1266
|
out[3] = cx * cy * cz - sx * sy * sz;
|
|
1000
1267
|
}
|
|
1001
1268
|
// Euler XYZ from a 3×3 rotation matrix (row-major elements).
|
|
1269
|
+
//
|
|
1270
|
+
// ⚠ TRANSPOSED RELATIVE TO BULLET. Bullet's matrixToEulerXYZ (btGeneric6DofConstraint.cpp)
|
|
1271
|
+
// reads asin(r02) and atan2(-r12, r22); this reads asin(r20) and atan2(-r21, r22).
|
|
1272
|
+
// Every angular DOF here is therefore the NEGATION of Bullet's, and the code
|
|
1273
|
+
// compensates by driving angular rows with +k where Bullet uses −k, and by
|
|
1274
|
+
// treating a low violation as Bullet's high one. The two flips cancel, so
|
|
1275
|
+
// nothing is wrong — but they must be flipped TOGETHER.
|
|
1276
|
+
//
|
|
1277
|
+
// Porting Bullet's sign without also swapping the violation enum makes every
|
|
1278
|
+
// limit drive its joint further out of range: measured mean velocity-reversal
|
|
1279
|
+
// 0.59 with jerk 1912 (an explosion), improving to 0.40 and then 0.137 as each
|
|
1280
|
+
// half was corrected. If you ever port more Bullet joint code, either flip both
|
|
1281
|
+
// or change this function to Bullet's convention and flip every consumer.
|
|
1002
1282
|
function matrixToEulerXYZ(r00, r01, r10, r11, r20, r21, r22, out) {
|
|
1003
1283
|
if (r20 < 1) {
|
|
1004
1284
|
if (r20 > -1) {
|
|
@@ -1018,3 +1298,77 @@ function matrixToEulerXYZ(r00, r01, r10, r11, r20, r21, r22, out) {
|
|
|
1018
1298
|
out[2] = 0;
|
|
1019
1299
|
}
|
|
1020
1300
|
}
|
|
1301
|
+
// Seed a row from the persistent manifold and apply that impulse up front —
|
|
1302
|
+
// Bullet's setup does this inline (m_appliedImpulse = cp.m_appliedImpulse *
|
|
1303
|
+
// m_warmstartingFactor, then applyImpulse). Contact points are matched in body
|
|
1304
|
+
// A's local frame so the identity survives the bodies moving.
|
|
1305
|
+
function warmStartContact(c, store, manifolds, lv, av, invMass) {
|
|
1306
|
+
const imA = invMass[c.bodyA];
|
|
1307
|
+
const imB = invMass[c.bodyB];
|
|
1308
|
+
if (imA === 0 && imB === 0)
|
|
1309
|
+
return;
|
|
1310
|
+
worldToLocal(store, c.bodyA, c.rAx, c.rAy, c.rAz, _mfA);
|
|
1311
|
+
const prev = manifolds.find(c.bodyA, c.bodyB, _mfA[0], _mfA[1], _mfA[2]);
|
|
1312
|
+
if (prev === null) {
|
|
1313
|
+
c.age = 0;
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
c.age = prev.age + 1;
|
|
1317
|
+
const n = prev.normalImpulse * WARMSTARTING_FACTOR;
|
|
1318
|
+
const f1 = prev.frictionImpulse1 * WARMSTARTING_FACTOR;
|
|
1319
|
+
const f2 = prev.frictionImpulse2 * WARMSTARTING_FACTOR;
|
|
1320
|
+
c.appliedNormalImpulse = n;
|
|
1321
|
+
c.appliedFrictionImpulse1 = f1;
|
|
1322
|
+
c.appliedFrictionImpulse2 = f2;
|
|
1323
|
+
applyContactImpulse(c, lv, av, imA, imB, c.nx, c.ny, c.nz, c.cAxN, c.cAyN, c.cAzN, c.cBxN, c.cByN, c.cBzN, n);
|
|
1324
|
+
if (c.jacInvT1 > 0)
|
|
1325
|
+
applyContactImpulse(c, lv, av, imA, imB, c.t1x, c.t1y, c.t1z, c.cAxT1, c.cAyT1, c.cAzT1, c.cBxT1, c.cByT1, c.cBzT1, f1);
|
|
1326
|
+
if (c.jacInvT2 > 0)
|
|
1327
|
+
applyContactImpulse(c, lv, av, imA, imB, c.t2x, c.t2y, c.t2z, c.cAxT2, c.cAyT2, c.cAzT2, c.cBxT2, c.cByT2, c.cBzT2, f2);
|
|
1328
|
+
}
|
|
1329
|
+
function applyContactImpulse(c, lv, av, imA, imB, dx, dy, dz, cAx, cAy, cAz, cBx, cBy, cBz, j) {
|
|
1330
|
+
if (j === 0)
|
|
1331
|
+
return;
|
|
1332
|
+
const ai = c.bodyA * 3, bi = c.bodyB * 3;
|
|
1333
|
+
if (imA > 0) {
|
|
1334
|
+
lv[ai + 0] -= j * imA * dx;
|
|
1335
|
+
lv[ai + 1] -= j * imA * dy;
|
|
1336
|
+
lv[ai + 2] -= j * imA * dz;
|
|
1337
|
+
av[ai + 0] -= j * cAx;
|
|
1338
|
+
av[ai + 1] -= j * cAy;
|
|
1339
|
+
av[ai + 2] -= j * cAz;
|
|
1340
|
+
}
|
|
1341
|
+
if (imB > 0) {
|
|
1342
|
+
lv[bi + 0] += j * imB * dx;
|
|
1343
|
+
lv[bi + 1] += j * imB * dy;
|
|
1344
|
+
lv[bi + 2] += j * imB * dz;
|
|
1345
|
+
av[bi + 0] += j * cBx;
|
|
1346
|
+
av[bi + 1] += j * cBy;
|
|
1347
|
+
av[bi + 2] += j * cBz;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
/** Store every solved row back into the manifold for the next substep. */
|
|
1351
|
+
export function saveContactImpulses(store, contacts, manifolds) {
|
|
1352
|
+
for (let ci = 0; ci < contacts.count; ci++) {
|
|
1353
|
+
const c = contacts.get(ci);
|
|
1354
|
+
worldToLocal(store, c.bodyA, c.rAx, c.rAy, c.rAz, _mfA);
|
|
1355
|
+
worldToLocal(store, c.bodyB, c.rBx, c.rBy, c.rBz, _mfB);
|
|
1356
|
+
manifolds.store(c.bodyA, c.bodyB, _mfA[0], _mfA[1], _mfA[2], _mfB[0], _mfB[1], _mfB[2], c.appliedNormalImpulse, c.appliedFrictionImpulse1, c.appliedFrictionImpulse2, c.age);
|
|
1357
|
+
}
|
|
1358
|
+
manifolds.endStep();
|
|
1359
|
+
}
|
|
1360
|
+
const _mfA = new Float32Array(3);
|
|
1361
|
+
const _mfB = new Float32Array(3);
|
|
1362
|
+
// Rotate a world-space lever arm into the body's local frame (R^T · v).
|
|
1363
|
+
function worldToLocal(store, i, x, y, z, out) {
|
|
1364
|
+
const i4 = i * 4;
|
|
1365
|
+
const qx = store.orientations[i4 + 0], qy = store.orientations[i4 + 1];
|
|
1366
|
+
const qz = store.orientations[i4 + 2], qw = store.orientations[i4 + 3];
|
|
1367
|
+
// v' = conj(q) * v * q
|
|
1368
|
+
const tx = 2 * (qy * z - qz * y);
|
|
1369
|
+
const ty = 2 * (qz * x - qx * z);
|
|
1370
|
+
const tz = 2 * (qx * y - qy * x);
|
|
1371
|
+
out[0] = x - qw * tx + (qy * tz - qz * ty);
|
|
1372
|
+
out[1] = y - qw * ty + (qz * tx - qx * tz);
|
|
1373
|
+
out[2] = z - qw * tz + (qx * ty - qy * tx);
|
|
1374
|
+
}
|