reze-engine 0.35.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/engine.js +2 -2
- package/dist/model.d.ts +0 -4
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +0 -16
- 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/engine.ts +2 -2
- package/src/model.ts +0 -20
- 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/src/physics/solver.ts
CHANGED
|
@@ -16,33 +16,72 @@ import type { RigidBodyStore } from "./body"
|
|
|
16
16
|
import type { SixDofSpringConstraint } from "./constraint"
|
|
17
17
|
import { STOP_ERP } from "./constraint"
|
|
18
18
|
import type { Contact, ContactPool } from "./contact"
|
|
19
|
+
import type { ManifoldCache } from "./manifold"
|
|
19
20
|
|
|
20
21
|
const BOUNCE_THRESHOLD = 2.0
|
|
21
22
|
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
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.
|
|
23
|
+
// Contact error-reduction parameter — btContactSolverInfo::m_erp in Bullet
|
|
24
|
+
// 2.75, the build MMD's own physics runs, at its stock 0.2. PMX rigs are
|
|
25
|
+
// authored against exactly this response.
|
|
29
26
|
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
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.
|
|
27
|
+
// Bullet folds penetration recovery into the CONTACT VELOCITY ROW rather than
|
|
28
|
+
// translating positions (m_splitImpulse defaults to false in 2.75):
|
|
37
29
|
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
30
|
+
// penetration = cp.getDistance() + m_linearSlop // < 0 when overlapping
|
|
31
|
+
// positionalError = -penetration * m_erp / m_timeStep
|
|
32
|
+
// velocityError = restitution - rel_vel
|
|
33
|
+
// m_rhs = (positionalError + velocityError) * m_jacDiagABInv
|
|
34
|
+
//
|
|
35
|
+
// Our `depth` is the negation of Bullet's distance, so positionalError is just
|
|
36
|
+
// depth·ERP/dt — one expression covering both cases. Penetrating (depth > 0) it
|
|
37
|
+
// pushes apart; separated (depth < 0, a speculative row) it ALLOWS approach at
|
|
38
|
+
// the rate that closes the gap, which is what keeps those rows inert until the
|
|
39
|
+
// body would really arrive.
|
|
40
|
+
const CONTACT_ERP = 0.2
|
|
41
|
+
|
|
42
|
+
// btContactSolverInfo's SOLVER_RANDMIZE_ORDER. Gauss-Seidel is order-biased —
|
|
43
|
+
// rows solved first win — which on a cross-linked lattice shows up as a lean or
|
|
44
|
+
// a period-2 chatter. Bullet ships the flag off; it is here to be measured.
|
|
45
|
+
const RANDOMIZE_ORDER = true
|
|
46
|
+
// btSequentialImpulseConstraintSolver::btRand2, so the shuffle is reproducible
|
|
47
|
+
// and a take replays identically.
|
|
48
|
+
let _seed = 0
|
|
49
|
+
function rand2(): number {
|
|
50
|
+
_seed = (1664525 * _seed + 1013904223) & 0xffffffff
|
|
51
|
+
return _seed >>> 0
|
|
52
|
+
}
|
|
53
|
+
let _order = new Int32Array(0)
|
|
54
|
+
|
|
55
|
+
// btContactSolverInfo::m_warmstartingFactor. Seeded impulses are scaled by
|
|
56
|
+
// this so a stale cache decays instead of compounding.
|
|
57
|
+
const WARMSTARTING_FACTOR = 0.85
|
|
58
|
+
// btContactSolverInfo::m_restingContactRestitutionThreshold — past this many
|
|
59
|
+
// substeps a contact is "resting" and stops bouncing.
|
|
60
|
+
const RESTING_CONTACT_AGE = 2
|
|
61
|
+
|
|
62
|
+
// btContactSolverInfo::m_linearSlop is 0 in 2.75 — no allowance, the row aims
|
|
63
|
+
// at exactly touching. Kept named so the divergence is visible if it ever moves.
|
|
64
|
+
const LINEAR_SLOP = 0
|
|
42
65
|
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
66
|
+
// btContactSolverInfo::m_splitImpulse. False in 2.75 (only the demos turn it
|
|
67
|
+
// on), but TRUE by default from Bullet 2.8x onward — which is what babylon-mmd
|
|
68
|
+
// runs today. MMD is closed-source so its own setting cannot be read, but it is
|
|
69
|
+
// widely reported to enable the flag, and our own measurements agree
|
|
70
|
+
// independently: turning it on lowered the velocity-reversal rate on every rig
|
|
71
|
+
// tested (伊邪那美 0.118 → 0.098, 诗蔻蒂 0.098 → 0.074, 托特 0.054 → 0.028).
|
|
72
|
+
// With it on, the penetration term is moved out of the real
|
|
73
|
+
// velocity row into a pseudo-velocity ("push") channel that is solved
|
|
74
|
+
// separately and integrated straight into the transform, so recovering overlap
|
|
75
|
+
// never adds momentum the joint springs can hand back.
|
|
76
|
+
const SPLIT_IMPULSE = true
|
|
77
|
+
// btContactSolverInfo::m_splitImpulsePenetrationThreshold. Bullet's sign: only
|
|
78
|
+
// contacts DEEPER than this go to the split channel; shallower ones keep the
|
|
79
|
+
// bias in the real row. Our depth is the negation, hence the flipped compare.
|
|
80
|
+
const SPLIT_PENETRATION_THRESHOLD = 0.02
|
|
81
|
+
|
|
82
|
+
// Pseudo-velocity accumulators for the split channel, zeroed each substep.
|
|
83
|
+
let _pushLin = new Float32Array(0)
|
|
84
|
+
let _pushAng = new Float32Array(0)
|
|
46
85
|
|
|
47
86
|
// Ceilings on limit-correction velocity. In normal operation limit errors are
|
|
48
87
|
// tiny; a large error only appears after a discontinuity (teleport, stall,
|
|
@@ -67,7 +106,41 @@ const LIMIT_SOFTNESS_ANGULAR = 0.5
|
|
|
67
106
|
// error), so resting chains rang forever — the "static dress slowly boils"
|
|
68
107
|
// bug. c is derived per row from a fixed damping ratio against the row's
|
|
69
108
|
// effective mass: c = 2ζ√(k·m_eff).
|
|
70
|
-
|
|
109
|
+
// btGeneric6DofSpringConstraint drives a spring as a MOTOR, not as a
|
|
110
|
+
// spring-damper: stiffness becomes a velocity target with a force cap.
|
|
111
|
+
//
|
|
112
|
+
// velFactor = fps · damping / numIterations
|
|
113
|
+
// targetVelocity = velFactor · force (force = err · k)
|
|
114
|
+
// maxMotorForce = |force| / fps
|
|
115
|
+
//
|
|
116
|
+
// m_springDamping defaults to 1.0 and PMX carries no damping field, so every
|
|
117
|
+
// MMD spring runs at exactly that — which is what these rigs were authored
|
|
118
|
+
// against. This replaces an implicit spring-damper with an invented damping
|
|
119
|
+
// ratio (ζ = 0.7); ported in isolation the motor form measured 0.0015 mean
|
|
120
|
+
// reversal at jerk 0.5, the calmest joint-side result recorded.
|
|
121
|
+
const SPRING_DAMPING = 1.0
|
|
122
|
+
// btConstraintInfo2::erp, from infoGlobal.m_erp — used only by getMotorFactor.
|
|
123
|
+
const INFO_ERP = 0.2
|
|
124
|
+
|
|
125
|
+
// btTypedConstraint::getMotorFactor. Scales a motor down as it approaches the
|
|
126
|
+
// limit it is driving toward, so a stiff spring cannot drive straight through
|
|
127
|
+
// its own stop and then be fought by the limit row. That fight is a candidate
|
|
128
|
+
// explanation for the period-2 joint chatter seen on bodies carrying NO
|
|
129
|
+
// contacts at all (qh_15_5: accumulated joint impulse alternating 8.4 / 14.7).
|
|
130
|
+
function getMotorFactor(pos: number, lowLim: number, uppLim: number, vel: number, timeFact: number): number {
|
|
131
|
+
if (lowLim > uppLim) return 1
|
|
132
|
+
if (lowLim === uppLim) return 0
|
|
133
|
+
const deltaMax = vel / timeFact
|
|
134
|
+
if (deltaMax < 0) {
|
|
135
|
+
if (pos >= lowLim && pos < lowLim - deltaMax) return (lowLim - pos) / deltaMax
|
|
136
|
+
return pos < lowLim ? 0 : 1
|
|
137
|
+
}
|
|
138
|
+
if (deltaMax > 0) {
|
|
139
|
+
if (pos <= uppLim && pos > uppLim - deltaMax) return (uppLim - pos) / deltaMax
|
|
140
|
+
return pos > uppLim ? 0 : 1
|
|
141
|
+
}
|
|
142
|
+
return 0
|
|
143
|
+
}
|
|
71
144
|
|
|
72
145
|
// ERP scale for loop-closing constraints (see buildConstraints: joints that
|
|
73
146
|
// close a cycle in the joint graph, e.g. the horizontal ring welds of
|
|
@@ -77,11 +150,6 @@ const SPRING_DAMPING_ZETA = 0.7
|
|
|
77
150
|
// chatter. Loop edges keep shape at a fraction of the correction rate while
|
|
78
151
|
// the spanning-tree chains stay stiff.
|
|
79
152
|
const LOOP_ERP_SCALE = 1.0
|
|
80
|
-
// Loop-edge LOCKED axes are converted to force-clamped springs instead of
|
|
81
|
-
// weld rows: an equality row on a cycle fights the other cycle edges at any
|
|
82
|
-
// ERP (the velocity system is over-determined too). A spring bounded by its
|
|
83
|
-
// real force k·|err|·dt holds the ring's shape elastically without fighting.
|
|
84
|
-
const LOOP_SPRING_K = 900
|
|
85
153
|
// Angular limit violations below this switch to per-axis euler rows; above
|
|
86
154
|
// it, the single geodesic row takes over (see setupConstraint).
|
|
87
155
|
const GEODESIC_THRESHOLD = 0.5 // rad
|
|
@@ -102,6 +170,7 @@ export function solveConstraints(
|
|
|
102
170
|
contacts: ContactPool,
|
|
103
171
|
dt: number,
|
|
104
172
|
iterations: number,
|
|
173
|
+
manifolds: ManifoldCache | null = null,
|
|
105
174
|
): void {
|
|
106
175
|
if (dt <= 0) return
|
|
107
176
|
if (constraints.length === 0 && contacts.count === 0) return
|
|
@@ -116,33 +185,165 @@ export function solveConstraints(
|
|
|
116
185
|
const W = store.invInertiaWorld
|
|
117
186
|
|
|
118
187
|
for (let c = 0; c < constraints.length; c++) {
|
|
119
|
-
setupConstraint(constraints[c], c, cache, store, dt, invDt)
|
|
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.
|
|
131
|
-
for (let ci = 0; ci < contacts.count; ci++) {
|
|
132
|
-
const c = contacts.get(ci)
|
|
133
|
-
if (invMass[c.bodyA] > 0) _rowCount[c.bodyA]++
|
|
134
|
-
if (invMass[c.bodyB] > 0) _rowCount[c.bodyB]++
|
|
188
|
+
setupConstraint(constraints[c], c, cache, store, dt, invDt, iterations)
|
|
135
189
|
}
|
|
136
190
|
for (let ci = 0; ci < contacts.count; ci++) {
|
|
137
191
|
setupContactRow(contacts.get(ci), lv, av, invMass, W, invDt)
|
|
138
192
|
}
|
|
193
|
+
// Warm start: seed each row from what the same point converged to last
|
|
194
|
+
// substep and APPLY it before iterating, exactly as Bullet does at setup.
|
|
195
|
+
if (manifolds !== null) {
|
|
196
|
+
for (let ci = 0; ci < contacts.count; ci++) {
|
|
197
|
+
warmStartContact(contacts.get(ci), store, manifolds, lv, av, invMass)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
139
200
|
|
|
201
|
+
// Bullet 2.75 solves each iteration in three separate passes, in this order:
|
|
202
|
+
// all joint rows, then all contact normal rows, then all friction rows. The
|
|
203
|
+
// friction bound is read from the contact's accumulated impulse AS IT STANDS
|
|
204
|
+
// after the normal pass, so friction tightens as the normal converges. Doing
|
|
205
|
+
// friction inline per contact (our previous shape) bounds it against a normal
|
|
206
|
+
// impulse that half the manifold has not contributed to yet.
|
|
207
|
+
if (RANDOMIZE_ORDER) {
|
|
208
|
+
// Reset per substep. Bullet keeps the seed on the solver instance; ours is
|
|
209
|
+
// module-level, so without this two worlds in one process share a stream
|
|
210
|
+
// and the same scene stops replaying identically — which the determinism
|
|
211
|
+
// test catches. Re-seeding costs nothing: the point is breaking the order
|
|
212
|
+
// bias BETWEEN iterations, not being unpredictable across frames.
|
|
213
|
+
_seed = 0
|
|
214
|
+
if (_order.length < contacts.count) _order = new Int32Array(contacts.count)
|
|
215
|
+
for (let i = 0; i < contacts.count; i++) _order[i] = i
|
|
216
|
+
}
|
|
140
217
|
for (let iter = 0; iter < iterations; iter++) {
|
|
218
|
+
if (RANDOMIZE_ORDER) {
|
|
219
|
+
// Bullet reshuffles between iterations, not once per substep.
|
|
220
|
+
for (let i = 1; i < contacts.count; i++) {
|
|
221
|
+
const j = rand2() % (i + 1)
|
|
222
|
+
const tmp = _order[i]; _order[i] = _order[j]; _order[j] = tmp
|
|
223
|
+
}
|
|
224
|
+
}
|
|
141
225
|
for (let c = 0; c < constraints.length; c++) {
|
|
142
226
|
iterateConstraint(c, cache, lv, av, invMass)
|
|
143
227
|
}
|
|
144
228
|
for (let ci = 0; ci < contacts.count; ci++) {
|
|
145
|
-
|
|
229
|
+
iterateContactNormal(contacts.get(RANDOMIZE_ORDER ? _order[ci] : ci), lv, av, invMass)
|
|
230
|
+
}
|
|
231
|
+
for (let ci = 0; ci < contacts.count; ci++) {
|
|
232
|
+
iterateContactFriction(contacts.get(ci), lv, av, invMass)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Split-impulse pass: its OWN full set of iterations over the penetration
|
|
237
|
+
// channel only, exactly as Bullet runs it after the main loop.
|
|
238
|
+
if (SPLIT_IMPULSE) {
|
|
239
|
+
const n3 = store.count * 3
|
|
240
|
+
if (_pushLin.length < n3) { _pushLin = new Float32Array(n3); _pushAng = new Float32Array(n3) }
|
|
241
|
+
else { _pushLin.fill(0, 0, n3); _pushAng.fill(0, 0, n3) }
|
|
242
|
+
let any = false
|
|
243
|
+
for (let ci = 0; ci < contacts.count; ci++) if (contacts.get(ci).rhsPenetration !== 0) { any = true; break }
|
|
244
|
+
if (any) {
|
|
245
|
+
if (RANDOMIZE_ORDER) {
|
|
246
|
+
// Reset per substep. Bullet keeps the seed on the solver instance; ours is
|
|
247
|
+
// module-level, so without this two worlds in one process share a stream
|
|
248
|
+
// and the same scene stops replaying identically — which the determinism
|
|
249
|
+
// test catches. Re-seeding costs nothing: the point is breaking the order
|
|
250
|
+
// bias BETWEEN iterations, not being unpredictable across frames.
|
|
251
|
+
_seed = 0
|
|
252
|
+
if (_order.length < contacts.count) _order = new Int32Array(contacts.count)
|
|
253
|
+
for (let i = 0; i < contacts.count; i++) _order[i] = i
|
|
254
|
+
}
|
|
255
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
256
|
+
if (RANDOMIZE_ORDER) {
|
|
257
|
+
// Bullet reshuffles between iterations, not once per substep.
|
|
258
|
+
for (let i = 1; i < contacts.count; i++) {
|
|
259
|
+
const j = rand2() % (i + 1)
|
|
260
|
+
const tmp = _order[i]; _order[i] = _order[j]; _order[j] = tmp
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (let ci = 0; ci < contacts.count; ci++) {
|
|
264
|
+
resolveSplitPenetration(contacts.get(ci), invMass)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Bullet's resolveSplitPenetrationImpulse: same Jacobian, but reading and
|
|
272
|
+
// writing the pseudo-velocity channel, with its own push-only accumulator.
|
|
273
|
+
function resolveSplitPenetration(c: Contact, invMass: Float32Array): void {
|
|
274
|
+
if (c.rhsPenetration === 0) return
|
|
275
|
+
const jacInvN = c.jacInvN
|
|
276
|
+
if (jacInvN <= 0) return
|
|
277
|
+
const imA = invMass[c.bodyA]
|
|
278
|
+
const imB = invMass[c.bodyB]
|
|
279
|
+
if (imA === 0 && imB === 0) return
|
|
280
|
+
const ai = c.bodyA * 3, bi = c.bodyB * 3
|
|
281
|
+
const nx = c.nx, ny = c.ny, nz = c.nz
|
|
282
|
+
const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz
|
|
283
|
+
const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz
|
|
284
|
+
|
|
285
|
+
const vAx = _pushLin[ai + 0] + _pushAng[ai + 1] * rAz - _pushAng[ai + 2] * rAy
|
|
286
|
+
const vAy = _pushLin[ai + 1] + _pushAng[ai + 2] * rAx - _pushAng[ai + 0] * rAz
|
|
287
|
+
const vAz = _pushLin[ai + 2] + _pushAng[ai + 0] * rAy - _pushAng[ai + 1] * rAx
|
|
288
|
+
const vBx = _pushLin[bi + 0] + _pushAng[bi + 1] * rBz - _pushAng[bi + 2] * rBy
|
|
289
|
+
const vBy = _pushLin[bi + 1] + _pushAng[bi + 2] * rBx - _pushAng[bi + 0] * rBz
|
|
290
|
+
const vBz = _pushLin[bi + 2] + _pushAng[bi + 0] * rBy - _pushAng[bi + 1] * rBx
|
|
291
|
+
const relVel = (vBx - vAx) * nx + (vBy - vAy) * ny + (vBz - vAz) * nz
|
|
292
|
+
|
|
293
|
+
let d = (c.rhsPenetration - relVel) * jacInvN
|
|
294
|
+
const old = c.appliedPushImpulse
|
|
295
|
+
let sum = old + d
|
|
296
|
+
if (sum < 0) { sum = 0; d = -old }
|
|
297
|
+
c.appliedPushImpulse = sum
|
|
298
|
+
if (d === 0) return
|
|
299
|
+
if (imA > 0) {
|
|
300
|
+
_pushLin[ai + 0] -= d * imA * nx
|
|
301
|
+
_pushLin[ai + 1] -= d * imA * ny
|
|
302
|
+
_pushLin[ai + 2] -= d * imA * nz
|
|
303
|
+
_pushAng[ai + 0] -= d * c.cAxN
|
|
304
|
+
_pushAng[ai + 1] -= d * c.cAyN
|
|
305
|
+
_pushAng[ai + 2] -= d * c.cAzN
|
|
306
|
+
}
|
|
307
|
+
if (imB > 0) {
|
|
308
|
+
_pushLin[bi + 0] += d * imB * nx
|
|
309
|
+
_pushLin[bi + 1] += d * imB * ny
|
|
310
|
+
_pushLin[bi + 2] += d * imB * nz
|
|
311
|
+
_pushAng[bi + 0] += d * c.cBxN
|
|
312
|
+
_pushAng[bi + 1] += d * c.cByN
|
|
313
|
+
_pushAng[bi + 2] += d * c.cBzN
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Integrate the accumulated push/turn velocity straight into the transform —
|
|
318
|
+
* btSolverBody::writebackVelocity(timeStep). Never touches real momentum. */
|
|
319
|
+
export function applySplitImpulsePush(store: RigidBodyStore, dt: number): void {
|
|
320
|
+
if (!SPLIT_IMPULSE || _pushLin.length === 0) return
|
|
321
|
+
const pos = store.positions
|
|
322
|
+
const ori = store.orientations
|
|
323
|
+
const invMass = store.invMass
|
|
324
|
+
for (let i = 0; i < store.count; i++) {
|
|
325
|
+
if (invMass[i] <= 0) continue
|
|
326
|
+
const i3 = i * 3, i4 = i * 4
|
|
327
|
+
const px = _pushLin[i3 + 0], py = _pushLin[i3 + 1], pz = _pushLin[i3 + 2]
|
|
328
|
+
const wx = _pushAng[i3 + 0], wy = _pushAng[i3 + 1], wz = _pushAng[i3 + 2]
|
|
329
|
+
if (px === 0 && py === 0 && pz === 0 && wx === 0 && wy === 0 && wz === 0) continue
|
|
330
|
+
pos[i3 + 0] += px * dt
|
|
331
|
+
pos[i3 + 1] += py * dt
|
|
332
|
+
pos[i3 + 2] += pz * dt
|
|
333
|
+
if (wx !== 0 || wy !== 0 || wz !== 0) {
|
|
334
|
+
const qx = ori[i4 + 0], qy = ori[i4 + 1], qz = ori[i4 + 2], qw = ori[i4 + 3]
|
|
335
|
+
const dx = qw * wx + wy * qz - wz * qy
|
|
336
|
+
const dy = qw * wy + wz * qx - wx * qz
|
|
337
|
+
const dz = qw * wz + wx * qy - wy * qx
|
|
338
|
+
const dw = -(wx * qx + wy * qy + wz * qz)
|
|
339
|
+
const h = 0.5 * dt
|
|
340
|
+
let nx2 = qx + dx * h, ny2 = qy + dy * h, nz2 = qz + dz * h, nw2 = qw + dw * h
|
|
341
|
+
const l2 = nx2 * nx2 + ny2 * ny2 + nz2 * nz2 + nw2 * nw2
|
|
342
|
+
if (l2 > 0) {
|
|
343
|
+
const inv = 1 / Math.sqrt(l2)
|
|
344
|
+
ori[i4 + 0] = nx2 * inv; ori[i4 + 1] = ny2 * inv
|
|
345
|
+
ori[i4 + 2] = nz2 * inv; ori[i4 + 3] = nw2 * inv
|
|
346
|
+
}
|
|
146
347
|
}
|
|
147
348
|
}
|
|
148
349
|
}
|
|
@@ -213,6 +414,7 @@ function setupConstraint(
|
|
|
213
414
|
store: RigidBodyStore,
|
|
214
415
|
dt: number,
|
|
215
416
|
invDt: number,
|
|
417
|
+
iterations: number,
|
|
216
418
|
): void {
|
|
217
419
|
const F = cache.F
|
|
218
420
|
const I = cache.I
|
|
@@ -334,16 +536,20 @@ function setupConstraint(
|
|
|
334
536
|
// iteration (PMX rigs routinely put k=100000 springs on locked axes,
|
|
335
537
|
// which turned welded weight-bodies into energy pumps).
|
|
336
538
|
if (con.springEnabled[i] && denom > 0 && lo !== hi) {
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
539
|
+
// Bullet motor form. Our relVel is (vB − vA)·axis where Bullet's is the
|
|
540
|
+
// negation, so the target carries the opposite sign to Bullet's — which
|
|
541
|
+
// is the same sign the old spring-damper produced, so behaviour keeps its
|
|
542
|
+
// direction and only its magnitude law changes.
|
|
340
543
|
const k = con.springStiffness[i]
|
|
341
544
|
const serr = curr - con.equilibriumPoint[i]
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
545
|
+
const force = serr * k
|
|
546
|
+
const velFactor = (invDt * SPRING_DAMPING) / iterations
|
|
547
|
+
const target = -velFactor * force
|
|
548
|
+
// getMotorFactor's `vel` is Bullet's tag_vel: −targetVelocity for a
|
|
549
|
+
// linear axis, which in our sign convention is the target itself.
|
|
550
|
+
const motFact = getMotorFactor(curr, lo, hi, target, invDt * INFO_ERP)
|
|
551
|
+
F[base + LIN_SPR_TGT + i] = motFact * target
|
|
552
|
+
F[base + LIN_SPR_MAX + i] = Math.abs(force) * dt
|
|
347
553
|
F[base + LIN_SPR_IMP + i] = 0
|
|
348
554
|
I[ib + I_LIN_SPR_ACT + i] = 1
|
|
349
555
|
} else if (!(lo === hi && con.isLoop)) {
|
|
@@ -413,16 +619,18 @@ function setupConstraint(
|
|
|
413
619
|
// Springs on locked axes are skipped — the limit row welds those, and
|
|
414
620
|
// double-driving a DOF overshoots every iteration (see the linear loop).
|
|
415
621
|
if (con.springEnabled[idx] && F[angJac + i] > 0 && con.angularMin[i] !== con.angularMax[i]) {
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
// the CFM softness s (not a clamp).
|
|
622
|
+
// Bullet motor form, angular flavour. Bullet's angular force is −err·k
|
|
623
|
+
// and its rel_vel is the negation of our relAv, so the two flips cancel
|
|
624
|
+
// and our target stays positive for positive error, as before.
|
|
420
625
|
const k = con.springStiffness[idx]
|
|
421
626
|
const serr = _angDiffScratch[i] - con.equilibriumPoint[idx]
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
627
|
+
const force = -serr * k
|
|
628
|
+
const velFactor = (invDt * SPRING_DAMPING) / iterations
|
|
629
|
+
const target = -velFactor * force
|
|
630
|
+
// tag_vel for a rotational axis is Bullet's targetVelocity, i.e. −ours.
|
|
631
|
+
const motFact = getMotorFactor(_angDiffScratch[i], con.angularMin[i], con.angularMax[i], -target, invDt * INFO_ERP)
|
|
632
|
+
F[angTgt + i] = motFact * target
|
|
633
|
+
F[base + ANG_SPR_MAX + i] = Math.abs(force) * dt
|
|
426
634
|
I[angAct + i] = 1
|
|
427
635
|
} else {
|
|
428
636
|
F[angTgt + i] = 0
|
|
@@ -600,9 +808,16 @@ function iterateConstraint(
|
|
|
600
808
|
// the spring off the stale value double-corrects the DOF.
|
|
601
809
|
if (I[sprAct + i]) {
|
|
602
810
|
const relVelNow = j !== 0 ? relVel + j / F[jac + i] : relVel
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
F[
|
|
811
|
+
// Motor row: drive toward the target, accumulated impulse clamped to the
|
|
812
|
+
// spring's real force over the step (Bullet's m_lowerLimit/m_upperLimit).
|
|
813
|
+
let dImp = (F[sprTgt + i] - relVelNow) * F[jac + i]
|
|
814
|
+
const maxF = F[sprMax + i]
|
|
815
|
+
const oldS = F[sprImp + i]
|
|
816
|
+
let nextS = oldS + dImp
|
|
817
|
+
if (nextS < -maxF) nextS = -maxF
|
|
818
|
+
else if (nextS > maxF) nextS = maxF
|
|
819
|
+
dImp = nextS - oldS
|
|
820
|
+
F[sprImp + i] = nextS
|
|
606
821
|
j += dImp
|
|
607
822
|
}
|
|
608
823
|
|
|
@@ -643,9 +858,14 @@ function iterateConstraint(
|
|
|
643
858
|
const axx = F[angAxes + o + 0], axy = F[angAxes + o + 1], axz = F[angAxes + o + 2]
|
|
644
859
|
const relAv = dax * axx + day * axy + daz * axz
|
|
645
860
|
// Implicit spring-damper row (soft constraint, see setup).
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
F[angSprImp + i]
|
|
861
|
+
let j = (F[angTgt + i] - relAv) * F[angJac + i]
|
|
862
|
+
const maxF = F[angSprMax + i]
|
|
863
|
+
const oldS = F[angSprImp + i]
|
|
864
|
+
let nextS = oldS + j
|
|
865
|
+
if (nextS < -maxF) nextS = -maxF
|
|
866
|
+
else if (nextS > maxF) nextS = maxF
|
|
867
|
+
j = nextS - oldS
|
|
868
|
+
F[angSprImp + i] = nextS
|
|
649
869
|
if (j === 0) continue
|
|
650
870
|
if (imA > 0) {
|
|
651
871
|
av[ai + 0] -= j * F[angWAs + o + 0]
|
|
@@ -784,8 +1004,14 @@ function setupContactRow(
|
|
|
784
1004
|
const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy
|
|
785
1005
|
const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz
|
|
786
1006
|
const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx
|
|
787
|
-
const
|
|
788
|
-
|
|
1007
|
+
const dvx = vBx - vAx
|
|
1008
|
+
const dvy = vBy - vAy
|
|
1009
|
+
const dvz = vBz - vAz
|
|
1010
|
+
const relVelN0 = dvx * nx + dvy * ny + dvz * nz
|
|
1011
|
+
// Restitution is switched off once a point has been in contact for more than
|
|
1012
|
+
// a couple of substeps (m_restingContactRestitutionThreshold), or resting
|
|
1013
|
+
// cloth keeps being handed a little bounce forever.
|
|
1014
|
+
c.bounceVel = c.restitution > 0 && relVelN0 < -BOUNCE_THRESHOLD && c.age <= RESTING_CONTACT_AGE
|
|
789
1015
|
? -c.restitution * relVelN0
|
|
790
1016
|
: 0
|
|
791
1017
|
|
|
@@ -801,29 +1027,54 @@ function setupContactRow(
|
|
|
801
1027
|
// (pulling) impulse, not a large positive one on a body in mid-air. On a
|
|
802
1028
|
// dress rig half of all contact rows are speculative and 88% of them fire,
|
|
803
1029
|
// which is the field of invisible brakes the cloth was shaking against.
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
|
|
812
|
-
// Friction tangent basis. Pick the axis least aligned with n.
|
|
813
|
-
let t1x: number, t1y: number, t1z: number
|
|
814
|
-
if (Math.abs(nx) < 0.7071) { t1x = 0; t1y = -nz; t1z = ny }
|
|
815
|
-
else { t1x = nz; t1y = 0; t1z = -nx }
|
|
816
|
-
const tl = Math.hypot(t1x, t1y, t1z)
|
|
817
|
-
if (tl > 1e-8) {
|
|
818
|
-
const tInv = 1 / tl
|
|
819
|
-
t1x *= tInv; t1y *= tInv; t1z *= tInv
|
|
1030
|
+
const positionalError = (c.depth - LINEAR_SLOP) * CONTACT_ERP * invDt
|
|
1031
|
+
if (SPLIT_IMPULSE && c.depth > SPLIT_PENETRATION_THRESHOLD) {
|
|
1032
|
+
c.biasVel = 0
|
|
1033
|
+
c.rhsPenetration = positionalError
|
|
820
1034
|
} else {
|
|
821
|
-
c.
|
|
822
|
-
|
|
1035
|
+
c.biasVel = positionalError
|
|
1036
|
+
c.rhsPenetration = 0
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Friction direction, Bullet 2.75 style. The stock solverMode is
|
|
1040
|
+
// SOLVER_USE_WARMSTARTING | SOLVER_SIMD — SOLVER_USE_2_FRICTION_DIRECTIONS is
|
|
1041
|
+
// NOT set, so there is exactly ONE friction row, aligned with the direction
|
|
1042
|
+
// the contact is actually sliding:
|
|
1043
|
+
//
|
|
1044
|
+
// m_lateralFrictionDir1 = vel - normalWorldOnB * rel_vel
|
|
1045
|
+
//
|
|
1046
|
+
// with vel = vA − vB. Our dv is vB − vA, so that direction is the negation of
|
|
1047
|
+
// dv's tangential part; sign is irrelevant to a row with symmetric ±μN bounds.
|
|
1048
|
+
// Below SIMD_EPSILON of sliding Bullet falls back to btPlaneSpace1, ported
|
|
1049
|
+
// exactly. Direction is recomputed every substep — friction-direction caching
|
|
1050
|
+
// is off in the stock mode.
|
|
1051
|
+
let t1x = dvx - nx * relVelN0
|
|
1052
|
+
let t1y = dvy - ny * relVelN0
|
|
1053
|
+
let t1z = dvz - nz * relVelN0
|
|
1054
|
+
let t2x: number, t2y: number, t2z: number
|
|
1055
|
+
const lat2 = t1x * t1x + t1y * t1y + t1z * t1z
|
|
1056
|
+
if (lat2 > 1.192092896e-07) {
|
|
1057
|
+
const inv = 1 / Math.sqrt(lat2)
|
|
1058
|
+
t1x *= inv; t1y *= inv; t1z *= inv
|
|
1059
|
+
// Second tangent is unused (single-direction mode) but kept orthonormal so
|
|
1060
|
+
// the cached Jacobian slots stay well-defined.
|
|
1061
|
+
t2x = ny * t1z - nz * t1y
|
|
1062
|
+
t2y = nz * t1x - nx * t1z
|
|
1063
|
+
t2z = nx * t1y - ny * t1x
|
|
1064
|
+
} else {
|
|
1065
|
+
// btPlaneSpace1, verbatim.
|
|
1066
|
+
if (Math.abs(nz) > 0.7071067811865475) {
|
|
1067
|
+
const a = ny * ny + nz * nz
|
|
1068
|
+
const k = 1 / Math.sqrt(a)
|
|
1069
|
+
t1x = 0; t1y = -nz * k; t1z = ny * k
|
|
1070
|
+
t2x = a * k; t2y = -nx * t1z; t2z = nx * t1y
|
|
1071
|
+
} else {
|
|
1072
|
+
const a = nx * nx + ny * ny
|
|
1073
|
+
const k = 1 / Math.sqrt(a)
|
|
1074
|
+
t1x = -ny * k; t1y = nx * k; t1z = 0
|
|
1075
|
+
t2x = -nz * t1y; t2y = nz * t1x; t2z = a * k
|
|
1076
|
+
}
|
|
823
1077
|
}
|
|
824
|
-
const t2x = ny * t1z - nz * t1y
|
|
825
|
-
const t2y = nz * t1x - nx * t1z
|
|
826
|
-
const t2z = nx * t1y - ny * t1x
|
|
827
1078
|
c.t1x = t1x; c.t1y = t1y; c.t1z = t1z
|
|
828
1079
|
c.t2x = t2x; c.t2y = t2y; c.t2z = t2z
|
|
829
1080
|
|
|
@@ -867,21 +1118,24 @@ function setupContactRow(
|
|
|
867
1118
|
c.jacInvT2 = denomT2 > 0 ? 1 / denomT2 : 0
|
|
868
1119
|
}
|
|
869
1120
|
|
|
870
|
-
// ITER
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
function
|
|
1121
|
+
// ITER, normal row only — Bullet resolveSingleConstraintRowLowerLimit:
|
|
1122
|
+
// deltaImpulse = (rhs - relVel) * jacDiagABInv, accumulated impulse clamped at
|
|
1123
|
+
// the lower limit of 0 (push-only). `rhs` here is restitution + Baumgarte bias.
|
|
1124
|
+
function iterateContactNormal(
|
|
874
1125
|
c: Contact,
|
|
875
1126
|
lv: Float32Array,
|
|
876
1127
|
av: Float32Array,
|
|
877
1128
|
invMass: Float32Array,
|
|
878
1129
|
): void {
|
|
1130
|
+
const jacInvN = c.jacInvN
|
|
1131
|
+
if (jacInvN <= 0) return
|
|
879
1132
|
const imA = invMass[c.bodyA]
|
|
880
1133
|
const imB = invMass[c.bodyB]
|
|
881
1134
|
if (imA === 0 && imB === 0) return
|
|
882
1135
|
const ai = c.bodyA * 3, bi = c.bodyB * 3
|
|
883
1136
|
const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz
|
|
884
1137
|
const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz
|
|
1138
|
+
const nx = c.nx, ny = c.ny, nz = c.nz
|
|
885
1139
|
|
|
886
1140
|
const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy
|
|
887
1141
|
const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz
|
|
@@ -889,109 +1143,84 @@ function iterateContactRow(
|
|
|
889
1143
|
const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy
|
|
890
1144
|
const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz
|
|
891
1145
|
const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx
|
|
892
|
-
const
|
|
893
|
-
const dvy = vBy - vAy
|
|
894
|
-
const dvz = vBz - vAz
|
|
1146
|
+
const relVelN = (vBx - vAx) * nx + (vBy - vAy) * ny + (vBz - vAz) * nz
|
|
895
1147
|
|
|
896
|
-
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
if (imB > 0) {
|
|
918
|
-
lv[bi + 0] += dImpN * imB * nx
|
|
919
|
-
lv[bi + 1] += dImpN * imB * ny
|
|
920
|
-
lv[bi + 2] += dImpN * imB * nz
|
|
921
|
-
av[bi + 0] += dImpN * cBxN
|
|
922
|
-
av[bi + 1] += dImpN * cByN
|
|
923
|
-
av[bi + 2] += dImpN * cBzN
|
|
924
|
-
}
|
|
925
|
-
}
|
|
1148
|
+
let dImpN = (c.bounceVel + c.biasVel - relVelN) * jacInvN
|
|
1149
|
+
const oldN = c.appliedNormalImpulse
|
|
1150
|
+
let newN = oldN + dImpN
|
|
1151
|
+
if (newN < 0) { newN = 0; dImpN = -oldN }
|
|
1152
|
+
c.appliedNormalImpulse = newN
|
|
1153
|
+
if (dImpN === 0) return
|
|
1154
|
+
if (imA > 0) {
|
|
1155
|
+
lv[ai + 0] -= dImpN * imA * nx
|
|
1156
|
+
lv[ai + 1] -= dImpN * imA * ny
|
|
1157
|
+
lv[ai + 2] -= dImpN * imA * nz
|
|
1158
|
+
av[ai + 0] -= dImpN * c.cAxN
|
|
1159
|
+
av[ai + 1] -= dImpN * c.cAyN
|
|
1160
|
+
av[ai + 2] -= dImpN * c.cAzN
|
|
1161
|
+
}
|
|
1162
|
+
if (imB > 0) {
|
|
1163
|
+
lv[bi + 0] += dImpN * imB * nx
|
|
1164
|
+
lv[bi + 1] += dImpN * imB * ny
|
|
1165
|
+
lv[bi + 2] += dImpN * imB * nz
|
|
1166
|
+
av[bi + 0] += dImpN * c.cBxN
|
|
1167
|
+
av[bi + 1] += dImpN * c.cByN
|
|
1168
|
+
av[bi + 2] += dImpN * c.cBzN
|
|
926
1169
|
}
|
|
1170
|
+
}
|
|
927
1171
|
|
|
928
|
-
|
|
1172
|
+
// ITER, friction pass. One Coulomb row (stock Bullet has
|
|
1173
|
+
// SOLVER_USE_2_FRICTION_DIRECTIONS off), bounded by ±friction · the normal
|
|
1174
|
+
// impulse accumulated so far. Bullet skips the row entirely while that is
|
|
1175
|
+
// still zero.
|
|
1176
|
+
function iterateContactFriction(
|
|
1177
|
+
c: Contact,
|
|
1178
|
+
lv: Float32Array,
|
|
1179
|
+
av: Float32Array,
|
|
1180
|
+
invMass: Float32Array,
|
|
1181
|
+
): void {
|
|
929
1182
|
const muNormal = c.friction * c.appliedNormalImpulse
|
|
930
1183
|
if (muNormal <= 0) return
|
|
1184
|
+
if (c.jacInvT1 <= 0) return
|
|
1185
|
+
const imA = invMass[c.bodyA]
|
|
1186
|
+
const imB = invMass[c.bodyB]
|
|
1187
|
+
if (imA === 0 && imB === 0) return
|
|
1188
|
+
const ai = c.bodyA * 3, bi = c.bodyB * 3
|
|
1189
|
+
const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz
|
|
1190
|
+
const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz
|
|
931
1191
|
|
|
932
|
-
|
|
933
|
-
const
|
|
934
|
-
const
|
|
935
|
-
const
|
|
936
|
-
const
|
|
937
|
-
const
|
|
938
|
-
const
|
|
939
|
-
const dvx2 = vBx2 - vAx2
|
|
940
|
-
const dvy2 = vBy2 - vAy2
|
|
941
|
-
const dvz2 = vBz2 - vAz2
|
|
942
|
-
|
|
943
|
-
applyFrictionTangent(
|
|
944
|
-
c, ai, bi, dvx2, dvy2, dvz2,
|
|
945
|
-
c.t1x, c.t1y, c.t1z,
|
|
946
|
-
c.cAxT1, c.cAyT1, c.cAzT1, c.cBxT1, c.cByT1, c.cBzT1,
|
|
947
|
-
c.jacInvT1, muNormal, imA, imB, lv, av, 1,
|
|
948
|
-
)
|
|
949
|
-
applyFrictionTangent(
|
|
950
|
-
c, ai, bi, dvx2, dvy2, dvz2,
|
|
951
|
-
c.t2x, c.t2y, c.t2z,
|
|
952
|
-
c.cAxT2, c.cAyT2, c.cAzT2, c.cBxT2, c.cByT2, c.cBzT2,
|
|
953
|
-
c.jacInvT2, muNormal, imA, imB, lv, av, 2,
|
|
954
|
-
)
|
|
955
|
-
}
|
|
1192
|
+
const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy
|
|
1193
|
+
const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz
|
|
1194
|
+
const vAz = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx
|
|
1195
|
+
const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy
|
|
1196
|
+
const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz
|
|
1197
|
+
const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx
|
|
1198
|
+
const dvx = vBx - vAx, dvy = vBy - vAy, dvz = vBz - vAz
|
|
956
1199
|
|
|
957
|
-
|
|
958
|
-
c
|
|
959
|
-
|
|
960
|
-
dvx: number, dvy: number, dvz: number,
|
|
961
|
-
tx: number, ty: number, tz: number,
|
|
962
|
-
cAx: number, cAy: number, cAz: number,
|
|
963
|
-
cBx: number, cBy: number, cBz: number,
|
|
964
|
-
jacInv: number, muNormal: number,
|
|
965
|
-
imA: number, imB: number,
|
|
966
|
-
lv: Float32Array, av: Float32Array,
|
|
967
|
-
slot: 1 | 2,
|
|
968
|
-
): void {
|
|
969
|
-
if (jacInv <= 0) return
|
|
970
|
-
const relVel = dvx * tx + dvy * ty + dvz * tz
|
|
971
|
-
let dImp = -relVel * jacInv * c.sorGain
|
|
972
|
-
const old = slot === 1 ? c.appliedFrictionImpulse1 : c.appliedFrictionImpulse2
|
|
1200
|
+
const relVel = dvx * c.t1x + dvy * c.t1y + dvz * c.t1z
|
|
1201
|
+
let dImp = -relVel * c.jacInvT1
|
|
1202
|
+
const old = c.appliedFrictionImpulse1
|
|
973
1203
|
let next = old + dImp
|
|
974
|
-
if (next < -muNormal)
|
|
975
|
-
else if (next > muNormal)
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1204
|
+
if (next < -muNormal) next = -muNormal
|
|
1205
|
+
else if (next > muNormal) next = muNormal
|
|
1206
|
+
dImp = next - old
|
|
1207
|
+
c.appliedFrictionImpulse1 = next
|
|
979
1208
|
if (dImp === 0) return
|
|
980
1209
|
if (imA > 0) {
|
|
981
|
-
lv[ai + 0] -= dImp * imA *
|
|
982
|
-
lv[ai + 1] -= dImp * imA *
|
|
983
|
-
lv[ai + 2] -= dImp * imA *
|
|
984
|
-
av[ai + 0] -= dImp *
|
|
985
|
-
av[ai + 1] -= dImp *
|
|
986
|
-
av[ai + 2] -= dImp *
|
|
1210
|
+
lv[ai + 0] -= dImp * imA * c.t1x
|
|
1211
|
+
lv[ai + 1] -= dImp * imA * c.t1y
|
|
1212
|
+
lv[ai + 2] -= dImp * imA * c.t1z
|
|
1213
|
+
av[ai + 0] -= dImp * c.cAxT1
|
|
1214
|
+
av[ai + 1] -= dImp * c.cAyT1
|
|
1215
|
+
av[ai + 2] -= dImp * c.cAzT1
|
|
987
1216
|
}
|
|
988
1217
|
if (imB > 0) {
|
|
989
|
-
lv[bi + 0] += dImp * imB *
|
|
990
|
-
lv[bi + 1] += dImp * imB *
|
|
991
|
-
lv[bi + 2] += dImp * imB *
|
|
992
|
-
av[bi + 0] += dImp *
|
|
993
|
-
av[bi + 1] += dImp *
|
|
994
|
-
av[bi + 2] += dImp *
|
|
1218
|
+
lv[bi + 0] += dImp * imB * c.t1x
|
|
1219
|
+
lv[bi + 1] += dImp * imB * c.t1y
|
|
1220
|
+
lv[bi + 2] += dImp * imB * c.t1z
|
|
1221
|
+
av[bi + 0] += dImp * c.cBxT1
|
|
1222
|
+
av[bi + 1] += dImp * c.cByT1
|
|
1223
|
+
av[bi + 2] += dImp * c.cBzT1
|
|
995
1224
|
}
|
|
996
1225
|
}
|
|
997
1226
|
|
|
@@ -1016,6 +1245,19 @@ function eulerXYZQuatInto(x: number, y: number, z: number, out: Float32Array): v
|
|
|
1016
1245
|
}
|
|
1017
1246
|
|
|
1018
1247
|
// Euler XYZ from a 3×3 rotation matrix (row-major elements).
|
|
1248
|
+
//
|
|
1249
|
+
// ⚠ TRANSPOSED RELATIVE TO BULLET. Bullet's matrixToEulerXYZ (btGeneric6DofConstraint.cpp)
|
|
1250
|
+
// reads asin(r02) and atan2(-r12, r22); this reads asin(r20) and atan2(-r21, r22).
|
|
1251
|
+
// Every angular DOF here is therefore the NEGATION of Bullet's, and the code
|
|
1252
|
+
// compensates by driving angular rows with +k where Bullet uses −k, and by
|
|
1253
|
+
// treating a low violation as Bullet's high one. The two flips cancel, so
|
|
1254
|
+
// nothing is wrong — but they must be flipped TOGETHER.
|
|
1255
|
+
//
|
|
1256
|
+
// Porting Bullet's sign without also swapping the violation enum makes every
|
|
1257
|
+
// limit drive its joint further out of range: measured mean velocity-reversal
|
|
1258
|
+
// 0.59 with jerk 1912 (an explosion), improving to 0.40 and then 0.137 as each
|
|
1259
|
+
// half was corrected. If you ever port more Bullet joint code, either flip both
|
|
1260
|
+
// or change this function to Bullet's convention and flip every consumer.
|
|
1019
1261
|
function matrixToEulerXYZ(
|
|
1020
1262
|
r00: number, r01: number,
|
|
1021
1263
|
r10: number, r11: number,
|
|
@@ -1038,3 +1280,97 @@ function matrixToEulerXYZ(
|
|
|
1038
1280
|
out[2] = 0
|
|
1039
1281
|
}
|
|
1040
1282
|
}
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
// Seed a row from the persistent manifold and apply that impulse up front —
|
|
1286
|
+
// Bullet's setup does this inline (m_appliedImpulse = cp.m_appliedImpulse *
|
|
1287
|
+
// m_warmstartingFactor, then applyImpulse). Contact points are matched in body
|
|
1288
|
+
// A's local frame so the identity survives the bodies moving.
|
|
1289
|
+
function warmStartContact(
|
|
1290
|
+
c: Contact,
|
|
1291
|
+
store: RigidBodyStore,
|
|
1292
|
+
manifolds: ManifoldCache,
|
|
1293
|
+
lv: Float32Array,
|
|
1294
|
+
av: Float32Array,
|
|
1295
|
+
invMass: Float32Array,
|
|
1296
|
+
): void {
|
|
1297
|
+
const imA = invMass[c.bodyA]
|
|
1298
|
+
const imB = invMass[c.bodyB]
|
|
1299
|
+
if (imA === 0 && imB === 0) return
|
|
1300
|
+
worldToLocal(store, c.bodyA, c.rAx, c.rAy, c.rAz, _mfA)
|
|
1301
|
+
const prev = manifolds.find(c.bodyA, c.bodyB, _mfA[0], _mfA[1], _mfA[2])
|
|
1302
|
+
if (prev === null) {
|
|
1303
|
+
c.age = 0
|
|
1304
|
+
return
|
|
1305
|
+
}
|
|
1306
|
+
c.age = prev.age + 1
|
|
1307
|
+
const n = prev.normalImpulse * WARMSTARTING_FACTOR
|
|
1308
|
+
const f1 = prev.frictionImpulse1 * WARMSTARTING_FACTOR
|
|
1309
|
+
const f2 = prev.frictionImpulse2 * WARMSTARTING_FACTOR
|
|
1310
|
+
c.appliedNormalImpulse = n
|
|
1311
|
+
c.appliedFrictionImpulse1 = f1
|
|
1312
|
+
c.appliedFrictionImpulse2 = f2
|
|
1313
|
+
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)
|
|
1314
|
+
if (c.jacInvT1 > 0)
|
|
1315
|
+
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)
|
|
1316
|
+
if (c.jacInvT2 > 0)
|
|
1317
|
+
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)
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function applyContactImpulse(
|
|
1321
|
+
c: Contact, lv: Float32Array, av: Float32Array,
|
|
1322
|
+
imA: number, imB: number,
|
|
1323
|
+
dx: number, dy: number, dz: number,
|
|
1324
|
+
cAx: number, cAy: number, cAz: number,
|
|
1325
|
+
cBx: number, cBy: number, cBz: number,
|
|
1326
|
+
j: number,
|
|
1327
|
+
): void {
|
|
1328
|
+
if (j === 0) return
|
|
1329
|
+
const ai = c.bodyA * 3, bi = c.bodyB * 3
|
|
1330
|
+
if (imA > 0) {
|
|
1331
|
+
lv[ai + 0] -= j * imA * dx; lv[ai + 1] -= j * imA * dy; lv[ai + 2] -= j * imA * dz
|
|
1332
|
+
av[ai + 0] -= j * cAx; av[ai + 1] -= j * cAy; av[ai + 2] -= j * cAz
|
|
1333
|
+
}
|
|
1334
|
+
if (imB > 0) {
|
|
1335
|
+
lv[bi + 0] += j * imB * dx; lv[bi + 1] += j * imB * dy; lv[bi + 2] += j * imB * dz
|
|
1336
|
+
av[bi + 0] += j * cBx; av[bi + 1] += j * cBy; av[bi + 2] += j * cBz
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/** Store every solved row back into the manifold for the next substep. */
|
|
1341
|
+
export function saveContactImpulses(
|
|
1342
|
+
store: RigidBodyStore,
|
|
1343
|
+
contacts: ContactPool,
|
|
1344
|
+
manifolds: ManifoldCache,
|
|
1345
|
+
): void {
|
|
1346
|
+
for (let ci = 0; ci < contacts.count; ci++) {
|
|
1347
|
+
const c = contacts.get(ci)
|
|
1348
|
+
worldToLocal(store, c.bodyA, c.rAx, c.rAy, c.rAz, _mfA)
|
|
1349
|
+
worldToLocal(store, c.bodyB, c.rBx, c.rBy, c.rBz, _mfB)
|
|
1350
|
+
manifolds.store(
|
|
1351
|
+
c.bodyA, c.bodyB,
|
|
1352
|
+
_mfA[0], _mfA[1], _mfA[2],
|
|
1353
|
+
_mfB[0], _mfB[1], _mfB[2],
|
|
1354
|
+
c.appliedNormalImpulse, c.appliedFrictionImpulse1, c.appliedFrictionImpulse2,
|
|
1355
|
+
c.age,
|
|
1356
|
+
)
|
|
1357
|
+
}
|
|
1358
|
+
manifolds.endStep()
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const _mfA = new Float32Array(3)
|
|
1362
|
+
const _mfB = new Float32Array(3)
|
|
1363
|
+
|
|
1364
|
+
// Rotate a world-space lever arm into the body's local frame (R^T · v).
|
|
1365
|
+
function worldToLocal(store: RigidBodyStore, i: number, x: number, y: number, z: number, out: Float32Array): void {
|
|
1366
|
+
const i4 = i * 4
|
|
1367
|
+
const qx = store.orientations[i4 + 0], qy = store.orientations[i4 + 1]
|
|
1368
|
+
const qz = store.orientations[i4 + 2], qw = store.orientations[i4 + 3]
|
|
1369
|
+
// v' = conj(q) * v * q
|
|
1370
|
+
const tx = 2 * (qy * z - qz * y)
|
|
1371
|
+
const ty = 2 * (qz * x - qx * z)
|
|
1372
|
+
const tz = 2 * (qx * y - qy * x)
|
|
1373
|
+
out[0] = x - qw * tx + (qy * tz - qz * ty)
|
|
1374
|
+
out[1] = y - qw * ty + (qz * tx - qx * tz)
|
|
1375
|
+
out[2] = z - qw * tz + (qx * ty - qy * tx)
|
|
1376
|
+
}
|