reze-engine 0.16.2 → 0.17.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.
Files changed (80) hide show
  1. package/dist/engine.d.ts +3 -1
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +165 -26
  4. package/dist/model.d.ts +5 -1
  5. package/dist/model.d.ts.map +1 -1
  6. package/dist/model.js +14 -1
  7. package/dist/physics/body.d.ts +4 -1
  8. package/dist/physics/body.d.ts.map +1 -1
  9. package/dist/physics/body.js +86 -20
  10. package/dist/physics/constraint.d.ts +15 -2
  11. package/dist/physics/constraint.d.ts.map +1 -1
  12. package/dist/physics/constraint.js +49 -2
  13. package/dist/physics/physics.d.ts +3 -0
  14. package/dist/physics/physics.d.ts.map +1 -1
  15. package/dist/physics/physics.js +69 -1
  16. package/dist/physics/solver.d.ts.map +1 -1
  17. package/dist/physics/solver.js +289 -113
  18. package/dist/physics/types.d.ts +2 -1
  19. package/dist/physics/types.d.ts.map +1 -1
  20. package/dist/physics/types.js +5 -0
  21. package/dist/physics/world.d.ts +3 -0
  22. package/dist/physics/world.d.ts.map +1 -1
  23. package/dist/physics/world.js +19 -3
  24. package/dist/pmx-loader.d.ts +2 -0
  25. package/dist/pmx-loader.d.ts.map +1 -1
  26. package/dist/pmx-loader.js +38 -20
  27. package/dist/shaders/materials/body.d.ts +1 -1
  28. package/dist/shaders/materials/body.d.ts.map +1 -1
  29. package/dist/shaders/materials/body.js +90 -85
  30. package/dist/shaders/materials/cloth_rough.d.ts +1 -1
  31. package/dist/shaders/materials/cloth_rough.d.ts.map +1 -1
  32. package/dist/shaders/materials/cloth_rough.js +1 -1
  33. package/dist/shaders/materials/cloth_smooth.d.ts +1 -1
  34. package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -1
  35. package/dist/shaders/materials/cloth_smooth.js +2 -2
  36. package/dist/shaders/materials/common.d.ts +1 -1
  37. package/dist/shaders/materials/common.d.ts.map +1 -1
  38. package/dist/shaders/materials/common.js +137 -112
  39. package/dist/shaders/materials/default.d.ts +1 -1
  40. package/dist/shaders/materials/default.d.ts.map +1 -1
  41. package/dist/shaders/materials/default.js +37 -32
  42. package/dist/shaders/materials/eye.d.ts +1 -1
  43. package/dist/shaders/materials/eye.d.ts.map +1 -1
  44. package/dist/shaders/materials/eye.js +54 -35
  45. package/dist/shaders/materials/face.d.ts +1 -1
  46. package/dist/shaders/materials/face.d.ts.map +1 -1
  47. package/dist/shaders/materials/face.js +90 -85
  48. package/dist/shaders/materials/hair.d.ts +1 -1
  49. package/dist/shaders/materials/hair.d.ts.map +1 -1
  50. package/dist/shaders/materials/hair.js +85 -75
  51. package/dist/shaders/materials/metal.d.ts +1 -1
  52. package/dist/shaders/materials/metal.d.ts.map +1 -1
  53. package/dist/shaders/materials/metal.js +1 -1
  54. package/dist/shaders/materials/mmd_classic.d.ts +2 -0
  55. package/dist/shaders/materials/mmd_classic.d.ts.map +1 -0
  56. package/dist/shaders/materials/mmd_classic.js +66 -0
  57. package/dist/shaders/materials/stockings.d.ts +1 -1
  58. package/dist/shaders/materials/stockings.d.ts.map +1 -1
  59. package/dist/shaders/materials/stockings.js +1 -1
  60. package/package.json +1 -1
  61. package/src/engine.ts +4198 -4030
  62. package/src/model.ts +21 -1
  63. package/src/physics/body.ts +87 -24
  64. package/src/physics/constraint.ts +80 -6
  65. package/src/physics/physics.ts +68 -1
  66. package/src/physics/solver.ts +915 -744
  67. package/src/physics/types.ts +10 -2
  68. package/src/physics/world.ts +20 -3
  69. package/src/pmx-loader.ts +39 -20
  70. package/src/shaders/materials/body.ts +97 -92
  71. package/src/shaders/materials/cloth_rough.ts +1 -1
  72. package/src/shaders/materials/cloth_smooth.ts +2 -2
  73. package/src/shaders/materials/common.ts +204 -179
  74. package/src/shaders/materials/default.ts +45 -40
  75. package/src/shaders/materials/eye.ts +62 -43
  76. package/src/shaders/materials/face.ts +97 -92
  77. package/src/shaders/materials/hair.ts +92 -82
  78. package/src/shaders/materials/metal.ts +1 -1
  79. package/src/shaders/materials/mmd_classic.ts +68 -0
  80. package/src/shaders/materials/stockings.ts +1 -1
@@ -24,6 +24,32 @@ const MAX_ANGULAR_CORRECTION_VEL = 30; // rad/s
24
24
  // instead of as a hard velocity snap.
25
25
  const LIMIT_SOFTNESS_LINEAR = 0.7;
26
26
  const LIMIT_SOFTNESS_ANGULAR = 0.5;
27
+ // Bullet spring-motor velocity factor: target velocity = (fps/numIterations)
28
+ // × spring force, with the applied impulse clamped to the real spring force
29
+ // k·|err|·dt. The big target makes the row deliver its full physical force
30
+ // every substep (stiff, MMD-authored hold); the clamp keeps it from ever
31
+ // exceeding it (no energy pumping). Matches btGeneric6DofSpringConstraint
32
+ // with springDamping=1 and the default 10 solver iterations.
33
+ const SPRING_SOLVER_ITERATIONS = 10;
34
+ // Slack on the spring impulse clamp: MMD rigs are authored against Bullet's
35
+ // spring motors, which deliver several times the naive k·err·dt per substep
36
+ // through their solver-row impulse bounds. Bounded slack keeps the hold
37
+ // stiff (breasts/hair rest near the authored pose) while still capping the
38
+ // energy any spring can inject per substep (the slow-boil guard).
39
+ const SPRING_FORCE_SLACK = 4;
40
+ // ERP scale for loop-closing constraints (see buildConstraints: joints that
41
+ // close a cycle in the joint graph, e.g. the horizontal ring welds of
42
+ // cross-linked skirt lattices). A loop over-determines positions — when
43
+ // contacts push the lattice, the ring's errors cannot all reach zero, and
44
+ // full-rate corrections chase each other around the cycle as violent
45
+ // chatter. Loop edges keep shape at a fraction of the correction rate while
46
+ // the spanning-tree chains stay stiff.
47
+ const LOOP_ERP_SCALE = 1.0;
48
+ // Loop-edge LOCKED axes are converted to force-clamped springs instead of
49
+ // weld rows: an equality row on a cycle fights the other cycle edges at any
50
+ // ERP (the velocity system is over-determined too). A spring bounded by its
51
+ // real force k·|err|·dt holds the ring's shape elastically without fighting.
52
+ const LOOP_SPRING_K = 900;
27
53
  // Angular limit violations below this switch to per-axis euler rows; above
28
54
  // it, the single geodesic row takes over (see setupConstraint).
29
55
  const GEODESIC_THRESHOLD = 0.5; // rad
@@ -44,19 +70,21 @@ export function solveConstraints(store, constraints, contacts, dt, iterations) {
44
70
  const lv = store.linearVelocities;
45
71
  const av = store.angularVelocities;
46
72
  const invMass = store.invMass;
47
- const invInertia = store.invInertia;
73
+ // World-space inverse inertia tensors for this substep's poses.
74
+ store.updateInvInertiaWorld();
75
+ const W = store.invInertiaWorld;
48
76
  for (let c = 0; c < constraints.length; c++) {
49
77
  setupConstraint(constraints[c], store, dt, invDt);
50
78
  }
51
79
  for (let ci = 0; ci < contacts.count; ci++) {
52
- setupContactRow(contacts.get(ci), lv, av, invMass, invInertia);
80
+ setupContactRow(contacts.get(ci), lv, av, invMass, W);
53
81
  }
54
82
  for (let iter = 0; iter < iterations; iter++) {
55
83
  for (let c = 0; c < constraints.length; c++) {
56
- iterateConstraint(constraints[c], lv, av, invMass, invInertia);
84
+ iterateConstraint(constraints[c], lv, av, invMass);
57
85
  }
58
86
  for (let ci = 0; ci < contacts.count; ci++) {
59
- iterateContactRow(contacts.get(ci), lv, av, invMass, invInertia);
87
+ iterateContactRow(contacts.get(ci), lv, av, invMass);
60
88
  }
61
89
  }
62
90
  }
@@ -67,11 +95,13 @@ function setupConstraint(con, store, dt, invDt) {
67
95
  const b = con.bodyB;
68
96
  const imA = store.invMass[a];
69
97
  const imB = store.invMass[b];
70
- const iiA = store.invInertia[a];
71
- const iiB = store.invInertia[b];
98
+ const W = store.invInertiaWorld;
99
+ const a9 = a * 9;
100
+ const b9 = b * 9;
72
101
  con.cacheSkip = imA === 0 && imB === 0;
73
102
  if (con.cacheSkip)
74
103
  return;
104
+ const erpScale = con.isLoop ? LOOP_ERP_SCALE : 1.0;
75
105
  buildBodyMat(store, a, _bodyMatA);
76
106
  buildBodyMat(store, b, _bodyMatB);
77
107
  Mat4.multiplyArrays(_bodyMatA, 0, con.frameA, 0, _TA, 0);
@@ -128,15 +158,23 @@ function setupConstraint(con, store, dt, invDt) {
128
158
  const cBx = rBy * axz - rBz * axy;
129
159
  const cBy = rBz * axx - rBx * axz;
130
160
  const cBz = rBx * axy - rBy * axx;
131
- cA[o + 0] = cAx;
132
- cA[o + 1] = cAy;
133
- cA[o + 2] = cAz;
134
- cB[o + 0] = cBx;
135
- cB[o + 1] = cBy;
136
- cB[o + 2] = cBz;
161
+ // Tensor-multiplied lever crosses: cache I⁻¹·(r×ax) for application;
162
+ // denominator = (r×ax)ᵀ·I⁻¹·(r×ax).
163
+ const wAx = W[a9 + 0] * cAx + W[a9 + 1] * cAy + W[a9 + 2] * cAz;
164
+ const wAy = W[a9 + 3] * cAx + W[a9 + 4] * cAy + W[a9 + 5] * cAz;
165
+ const wAz = W[a9 + 6] * cAx + W[a9 + 7] * cAy + W[a9 + 8] * cAz;
166
+ const wBx = W[b9 + 0] * cBx + W[b9 + 1] * cBy + W[b9 + 2] * cBz;
167
+ const wBy = W[b9 + 3] * cBx + W[b9 + 4] * cBy + W[b9 + 5] * cBz;
168
+ const wBz = W[b9 + 6] * cBx + W[b9 + 7] * cBy + W[b9 + 8] * cBz;
169
+ cA[o + 0] = wAx;
170
+ cA[o + 1] = wAy;
171
+ cA[o + 2] = wAz;
172
+ cB[o + 0] = wBx;
173
+ cB[o + 1] = wBy;
174
+ cB[o + 2] = wBz;
137
175
  const denom = imA + imB +
138
- (cAx * cAx + cAy * cAy + cAz * cAz) * iiA +
139
- (cBx * cBx + cBy * cBy + cBz * cBz) * iiB;
176
+ (cAx * wAx + cAy * wAy + cAz * wAz) +
177
+ (cBx * wBx + cBy * wBy + cBz * wBz);
140
178
  jac[i] = denom > 0 ? 1 / denom : 0;
141
179
  const lo = con.linearMin[i];
142
180
  const hi = con.linearMax[i];
@@ -156,7 +194,7 @@ function setupConstraint(con, store, dt, invDt) {
156
194
  else if (err !== 0)
157
195
  active = 2;
158
196
  if (err !== 0) {
159
- target = -err * STOP_ERP * invDt;
197
+ target = -err * STOP_ERP * erpScale * invDt;
160
198
  if (target > MAX_LINEAR_CORRECTION_VEL)
161
199
  target = MAX_LINEAR_CORRECTION_VEL;
162
200
  else if (target < -MAX_LINEAR_CORRECTION_VEL)
@@ -166,17 +204,25 @@ function setupConstraint(con, store, dt, invDt) {
166
204
  tgt[i] = target;
167
205
  act[i] = denom > 0 ? active : 0;
168
206
  con.cacheLinLimitImp[i] = 0;
169
- if (con.springEnabled[i] && denom > 0) {
207
+ // A spring on a locked axis is redundant — the bilateral limit row
208
+ // already welds the DOF, and driving it twice overshoots every
209
+ // iteration (PMX rigs routinely put k=100000 springs on locked axes,
210
+ // which turned welded weight-bodies into energy pumps).
211
+ if (con.springEnabled[i] && denom > 0 && lo !== hi) {
170
212
  // Clamp k to the deadbeat limit: an explicit spring with k·dt² > 1
171
213
  // overshoots equilibrium every step and pumps energy — the classic
172
- // pre-Spring2 Bullet 6dof instability this port inherited. (¼ margin
173
- // for Gauss-Seidel coupling in chains.)
174
- const k = Math.min(con.springStiffness[i], 0.25 * invDt * invDt);
214
+ // pre-Spring2 Bullet 6dof instability this port inherited.
215
+ const k = Math.min(con.springStiffness[i], invDt * invDt);
175
216
  const serr = curr - con.equilibriumPoint[i];
176
217
  con.cacheLinSpringTarget[i] = -k * serr * dt;
218
+ // Boil guard: per-substep spring impulse bounded by (a slack over) the
219
+ // physical spring force — unbounded spring drives pump energy through
220
+ // the euler-axis mis-scale and slowly boil resting cloth.
221
+ con.cacheLinSpringMaxImp[i] = SPRING_FORCE_SLACK * Math.abs(k * serr) * dt;
222
+ con.cacheLinSpringImp[i] = 0;
177
223
  con.cacheLinSpringActive[i] = 1;
178
224
  }
179
- else {
225
+ else if (!(lo === hi && con.isLoop)) {
180
226
  con.cacheLinSpringActive[i] = 0;
181
227
  }
182
228
  }
@@ -231,25 +277,49 @@ function setupConstraint(con, store, dt, invDt) {
231
277
  angAxes[6] = zx;
232
278
  angAxes[7] = zy;
233
279
  angAxes[8] = zz;
234
- const angDenom = iiA + iiB;
235
- con.cacheAngJacInv = angDenom > 0 ? 1 / angDenom : 0;
280
+ // Per-axis angular Jacobians with the full tensors: cache I⁻¹·axis per
281
+ // body plus 1/(axᵀ(I⁻¹A+I⁻¹B)ax) per axis.
282
+ const angJac = con.cacheAngJacInv;
283
+ const angWAs = con.cacheAngWA;
284
+ const angWBs = con.cacheAngWB;
285
+ for (let i = 0; i < 3; i++) {
286
+ const o = i * 3;
287
+ const axx = angAxes[o + 0], axy = angAxes[o + 1], axz = angAxes[o + 2];
288
+ const wAx = W[a9 + 0] * axx + W[a9 + 1] * axy + W[a9 + 2] * axz;
289
+ const wAy = W[a9 + 3] * axx + W[a9 + 4] * axy + W[a9 + 5] * axz;
290
+ const wAz = W[a9 + 6] * axx + W[a9 + 7] * axy + W[a9 + 8] * axz;
291
+ const wBx = W[b9 + 0] * axx + W[b9 + 1] * axy + W[b9 + 2] * axz;
292
+ const wBy = W[b9 + 3] * axx + W[b9 + 4] * axy + W[b9 + 5] * axz;
293
+ const wBz = W[b9 + 6] * axx + W[b9 + 7] * axy + W[b9 + 8] * axz;
294
+ angWAs[o + 0] = wAx;
295
+ angWAs[o + 1] = wAy;
296
+ angWAs[o + 2] = wAz;
297
+ angWBs[o + 0] = wBx;
298
+ angWBs[o + 1] = wBy;
299
+ angWBs[o + 2] = wBz;
300
+ const denom = axx * (wAx + wBx) + axy * (wAy + wBy) + axz * (wAz + wBz);
301
+ angJac[i] = denom > 0 ? 1 / denom : 0;
302
+ }
236
303
  // Per-axis rows carry only the springs. Sign flip vs linear:
237
304
  // d(angDiff)/dt = −(ω_B − ω_A)·ax.
238
305
  const angTgt = con.cacheAngTargetVel;
239
306
  const angAct = con.cacheAngActive;
240
307
  for (let i = 0; i < 3; i++) {
241
308
  const idx = i + 3;
242
- if (con.springEnabled[idx] && angDenom > 0) {
243
- // Same deadbeat clamp as the linear springs.
244
- const k = Math.min(con.springStiffness[idx], 0.25 * invDt * invDt);
309
+ // Springs on locked axes are skipped — the limit row welds those, and
310
+ // double-driving a DOF overshoots every iteration (see the linear loop).
311
+ if (con.springEnabled[idx] && angJac[i] > 0 && con.angularMin[i] !== con.angularMax[i]) {
312
+ const k = Math.min(con.springStiffness[idx], invDt * invDt);
245
313
  const serr = _angDiffScratch[i] - con.equilibriumPoint[idx];
246
314
  angTgt[i] = k * serr * dt;
315
+ con.cacheAngSpringMaxImp[i] = SPRING_FORCE_SLACK * Math.abs(k * serr) * dt;
247
316
  angAct[i] = 1;
248
317
  }
249
318
  else {
250
319
  angTgt[i] = 0;
251
320
  angAct[i] = 0;
252
321
  }
322
+ con.cacheAngSpringImp[i] = 0;
253
323
  }
254
324
  // Angular limit handling is hybrid. Small violations (the resting-cloth
255
325
  // regime) use per-axis euler rows — they converge cleanly and keep resting
@@ -259,7 +329,10 @@ function setupConstraint(con, store, dt, invDt) {
259
329
  // the asin singularity they chase phantom errors and pump angular velocity
260
330
  // into the chain instead of converging.
261
331
  con.cacheAngLimActive = 0;
262
- if (angDenom > 0) {
332
+ con.cacheAngPAActive[0] = 0;
333
+ con.cacheAngPAActive[1] = 0;
334
+ con.cacheAngPAActive[2] = 0;
335
+ if (imA > 0 || imB > 0) {
263
336
  const ex = _angDiffScratch[0], ey = _angDiffScratch[1], ez = _angDiffScratch[2];
264
337
  // Free axes (min > max) follow the current angle, i.e. no correction.
265
338
  let tx = ex, ty = ey, tz = ez;
@@ -272,19 +345,25 @@ function setupConstraint(con, store, dt, invDt) {
272
345
  const errX = ex - tx, errY = ey - ty, errZ = ez - tz;
273
346
  const maxErr = Math.max(Math.abs(errX), Math.abs(errY), Math.abs(errZ));
274
347
  if (maxErr > 0 && maxErr < GEODESIC_THRESHOLD) {
275
- // Per-axis euler limit rows, folded into the per-axis row set (the
276
- // spring impulse clamp must not bound a limit row lift it).
348
+ // Per-axis euler limit rows. Locked axes are bilateral joints; ranged
349
+ // axes are unilateral stops (sign-clamped accumulation)a bilateral
350
+ // row on a ranged axis brakes natural recovery every substep and
351
+ // pumps energy into swinging cloth, and the pump grows WITH solver
352
+ // convergence (more iterations enforce the brake harder).
277
353
  for (let i = 0; i < 3; i++) {
278
354
  const err = i === 0 ? errX : i === 1 ? errY : errZ;
279
- if (err === 0)
355
+ con.cacheAngPAImp[i] = 0;
356
+ if (err === 0) {
357
+ con.cacheAngPAActive[i] = 0;
280
358
  continue;
281
- let target = err * STOP_ERP * invDt;
359
+ }
360
+ let target = err * STOP_ERP * erpScale * invDt;
282
361
  if (target > MAX_ANGULAR_CORRECTION_VEL)
283
362
  target = MAX_ANGULAR_CORRECTION_VEL;
284
363
  else if (target < -MAX_ANGULAR_CORRECTION_VEL)
285
364
  target = -MAX_ANGULAR_CORRECTION_VEL;
286
- angTgt[i] += target;
287
- angAct[i] = 1;
365
+ con.cacheAngPATarget[i] = target;
366
+ con.cacheAngPAActive[i] = con.angularMin[i] === con.angularMax[i] ? 1 : 2;
288
367
  }
289
368
  }
290
369
  else if (maxErr > 0) {
@@ -321,7 +400,17 @@ function setupConstraint(con, store, dt, invDt) {
321
400
  lim[0] = _TA[0] * axx + _TA[4] * axy + _TA[8] * axz;
322
401
  lim[1] = _TA[1] * axx + _TA[5] * axy + _TA[9] * axz;
323
402
  lim[2] = _TA[2] * axx + _TA[6] * axy + _TA[10] * axz;
324
- let target = angle * STOP_ERP * invDt;
403
+ const gWA = con.cacheAngLimWA;
404
+ const gWB = con.cacheAngLimWB;
405
+ gWA[0] = W[a9 + 0] * lim[0] + W[a9 + 1] * lim[1] + W[a9 + 2] * lim[2];
406
+ gWA[1] = W[a9 + 3] * lim[0] + W[a9 + 4] * lim[1] + W[a9 + 5] * lim[2];
407
+ gWA[2] = W[a9 + 6] * lim[0] + W[a9 + 7] * lim[1] + W[a9 + 8] * lim[2];
408
+ gWB[0] = W[b9 + 0] * lim[0] + W[b9 + 1] * lim[1] + W[b9 + 2] * lim[2];
409
+ gWB[1] = W[b9 + 3] * lim[0] + W[b9 + 4] * lim[1] + W[b9 + 5] * lim[2];
410
+ gWB[2] = W[b9 + 6] * lim[0] + W[b9 + 7] * lim[1] + W[b9 + 8] * lim[2];
411
+ const gDenom = lim[0] * (gWA[0] + gWB[0]) + lim[1] * (gWA[1] + gWB[1]) + lim[2] * (gWA[2] + gWB[2]);
412
+ con.cacheAngLimJacInv = gDenom > 0 ? 1 / gDenom : 0;
413
+ let target = angle * STOP_ERP * erpScale * invDt;
325
414
  if (target > MAX_ANGULAR_CORRECTION_VEL)
326
415
  target = MAX_ANGULAR_CORRECTION_VEL;
327
416
  con.cacheAngLimTarget = target;
@@ -332,7 +421,7 @@ function setupConstraint(con, store, dt, invDt) {
332
421
  con.cacheAngLimImp = 0;
333
422
  }
334
423
  // ITER: read cache, compute relVel from current lv/av, apply impulse.
335
- function iterateConstraint(con, lv, av, invMass, invInertia) {
424
+ function iterateConstraint(con, lv, av, invMass) {
336
425
  if (con.cacheSkip)
337
426
  return;
338
427
  const a = con.bodyA;
@@ -341,8 +430,6 @@ function iterateConstraint(con, lv, av, invMass, invInertia) {
341
430
  const bi = b * 3;
342
431
  const imA = invMass[a];
343
432
  const imB = invMass[b];
344
- const iiA = invInertia[a];
345
- const iiB = invInertia[b];
346
433
  // Linear axes — relVel at the offset point: v_pivot = v_CG + ω × r.
347
434
  const lA = con.cacheLeverA;
348
435
  const lB = con.cacheLeverB;
@@ -365,6 +452,8 @@ function iterateConstraint(con, lv, av, invMass, invInertia) {
365
452
  const dvz = vBz - vAz;
366
453
  const sprAct = con.cacheLinSpringActive;
367
454
  const sprTgt = con.cacheLinSpringTarget;
455
+ const sprMax = con.cacheLinSpringMaxImp;
456
+ const sprImp = con.cacheLinSpringImp;
368
457
  const limImp = con.cacheLinLimitImp;
369
458
  for (let i = 0; i < 3; i++) {
370
459
  if (!act[i] && !sprAct[i])
@@ -392,9 +481,25 @@ function iterateConstraint(con, lv, av, invMass, invInertia) {
392
481
  j += dImp;
393
482
  }
394
483
  // Spring row: velocity-target drive; the deadbeat k-clamp at setup
395
- // bounds its aggression.
484
+ // bounds its aggression, and maxImp bounds loop-edge springs by their
485
+ // real force. relVel is refreshed with the limit impulse applied just
486
+ // above (j·denom = j / jac) — driving the spring off the stale value
487
+ // double-corrects the DOF and overshoots every iteration.
396
488
  if (sprAct[i]) {
397
- j += (sprTgt[i] - relVel) * jac[i];
489
+ const relVelNow = j !== 0 ? relVel + j / jac[i] : relVel;
490
+ let dImp = (sprTgt[i] - relVelNow) * jac[i];
491
+ const max = sprMax[i];
492
+ if (max !== Infinity) {
493
+ const old = sprImp[i];
494
+ let next = old + dImp;
495
+ if (next > max)
496
+ next = max;
497
+ else if (next < -max)
498
+ next = -max;
499
+ dImp = next - old;
500
+ sprImp[i] = next;
501
+ }
502
+ j += dImp;
398
503
  }
399
504
  if (j === 0)
400
505
  continue;
@@ -402,49 +507,102 @@ function iterateConstraint(con, lv, av, invMass, invInertia) {
402
507
  lv[ai + 0] -= j * imA * axx;
403
508
  lv[ai + 1] -= j * imA * axy;
404
509
  lv[ai + 2] -= j * imA * axz;
405
- av[ai + 0] -= j * iiA * cA[o + 0];
406
- av[ai + 1] -= j * iiA * cA[o + 1];
407
- av[ai + 2] -= j * iiA * cA[o + 2];
510
+ av[ai + 0] -= j * cA[o + 0];
511
+ av[ai + 1] -= j * cA[o + 1];
512
+ av[ai + 2] -= j * cA[o + 2];
408
513
  }
409
514
  if (imB > 0) {
410
515
  lv[bi + 0] += j * imB * axx;
411
516
  lv[bi + 1] += j * imB * axy;
412
517
  lv[bi + 2] += j * imB * axz;
413
- av[bi + 0] += j * iiB * cB[o + 0];
414
- av[bi + 1] += j * iiB * cB[o + 1];
415
- av[bi + 2] += j * iiB * cB[o + 2];
518
+ av[bi + 0] += j * cB[o + 0];
519
+ av[bi + 1] += j * cB[o + 1];
520
+ av[bi + 2] += j * cB[o + 2];
416
521
  }
417
522
  }
418
523
  // Angular axes — relAv = ω_B − ω_A.
419
- const angJacInv = con.cacheAngJacInv;
420
- if (angJacInv === 0)
421
- return;
422
524
  const angAxes = con.cacheAngAxes;
525
+ const angJac = con.cacheAngJacInv;
526
+ const angWAs = con.cacheAngWA;
527
+ const angWBs = con.cacheAngWB;
423
528
  const angTgt = con.cacheAngTargetVel;
424
529
  const angAct = con.cacheAngActive;
425
530
  const dax = av[bi + 0] - av[ai + 0];
426
531
  const day = av[bi + 1] - av[ai + 1];
427
532
  const daz = av[bi + 2] - av[ai + 2];
533
+ const angSprMax = con.cacheAngSpringMaxImp;
534
+ const angSprImp = con.cacheAngSpringImp;
428
535
  for (let i = 0; i < 3; i++) {
429
536
  if (!angAct[i])
430
537
  continue;
431
538
  const o = i * 3;
432
539
  const axx = angAxes[o + 0], axy = angAxes[o + 1], axz = angAxes[o + 2];
433
540
  const relAv = dax * axx + day * axy + daz * axz;
434
- // Spring drive (deadbeat-clamped at setup) plus, for small violations,
435
- // the folded per-axis limit correction.
436
- const j = (angTgt[i] - relAv) * angJacInv;
541
+ let j = (angTgt[i] - relAv) * angJac[i];
542
+ {
543
+ const max = angSprMax[i];
544
+ const old = angSprImp[i];
545
+ let next = old + j;
546
+ if (next > max)
547
+ next = max;
548
+ else if (next < -max)
549
+ next = -max;
550
+ j = next - old;
551
+ angSprImp[i] = next;
552
+ }
437
553
  if (j === 0)
438
554
  continue;
439
- if (iiA > 0) {
440
- av[ai + 0] -= j * iiA * axx;
441
- av[ai + 1] -= j * iiA * axy;
442
- av[ai + 2] -= j * iiA * axz;
555
+ if (imA > 0) {
556
+ av[ai + 0] -= j * angWAs[o + 0];
557
+ av[ai + 1] -= j * angWAs[o + 1];
558
+ av[ai + 2] -= j * angWAs[o + 2];
443
559
  }
444
- if (iiB > 0) {
445
- av[bi + 0] += j * iiB * axx;
446
- av[bi + 1] += j * iiB * axy;
447
- av[bi + 2] += j * iiB * axz;
560
+ if (imB > 0) {
561
+ av[bi + 0] += j * angWBs[o + 0];
562
+ av[bi + 1] += j * angWBs[o + 1];
563
+ av[bi + 2] += j * angWBs[o + 2];
564
+ }
565
+ }
566
+ // Per-axis angular limit rows (small-violation regime), on the derived
567
+ // euler axes. Sign convention matches the springs: positive target reduces
568
+ // positive error via d(angDiff)/dt = −(ω_B − ω_A)·ax.
569
+ const paAct = con.cacheAngPAActive;
570
+ if (paAct[0] || paAct[1] || paAct[2]) {
571
+ const paTgt = con.cacheAngPATarget;
572
+ const paImp = con.cacheAngPAImp;
573
+ for (let i = 0; i < 3; i++) {
574
+ if (!paAct[i])
575
+ continue;
576
+ const o = i * 3;
577
+ const axx = angAxes[o + 0], axy = angAxes[o + 1], axz = angAxes[o + 2];
578
+ const relAv = (av[bi + 0] - av[ai + 0]) * axx +
579
+ (av[bi + 1] - av[ai + 1]) * axy +
580
+ (av[bi + 2] - av[ai + 2]) * axz;
581
+ const target = paTgt[i];
582
+ // Locked axes (act 1) are welds — full gain, like the 0.16.3 fold;
583
+ // softness only tempers the unilateral stops.
584
+ const soft = paAct[i] === 2 ? LIMIT_SOFTNESS_ANGULAR : 1.0;
585
+ let j = soft * (target - relAv) * angJac[i];
586
+ if (paAct[i] === 2) {
587
+ const old = paImp[i];
588
+ let next = old + j;
589
+ if (target > 0 ? next < 0 : next > 0)
590
+ next = 0;
591
+ j = next - old;
592
+ paImp[i] = next;
593
+ }
594
+ if (j === 0)
595
+ continue;
596
+ if (imA > 0) {
597
+ av[ai + 0] -= j * angWAs[o + 0];
598
+ av[ai + 1] -= j * angWAs[o + 1];
599
+ av[ai + 2] -= j * angWAs[o + 2];
600
+ }
601
+ if (imB > 0) {
602
+ av[bi + 0] += j * angWBs[o + 0];
603
+ av[bi + 1] += j * angWBs[o + 1];
604
+ av[bi + 2] += j * angWBs[o + 2];
605
+ }
448
606
  }
449
607
  }
450
608
  // Geodesic limit row: drive (ω_B − ω_A)·axis toward the correction target.
@@ -457,7 +615,7 @@ function iterateConstraint(con, lv, av, invMass, invInertia) {
457
615
  const relAv = (av[bi + 0] - av[ai + 0]) * nx +
458
616
  (av[bi + 1] - av[ai + 1]) * ny +
459
617
  (av[bi + 2] - av[ai + 2]) * nz;
460
- let j = LIMIT_SOFTNESS_ANGULAR * (con.cacheAngLimTarget - relAv) * angJacInv;
618
+ let j = LIMIT_SOFTNESS_ANGULAR * (con.cacheAngLimTarget - relAv) * con.cacheAngLimJacInv;
461
619
  if (con.cacheAngLimActive === 2) {
462
620
  const old = con.cacheAngLimImp;
463
621
  let next = old + j;
@@ -467,15 +625,17 @@ function iterateConstraint(con, lv, av, invMass, invInertia) {
467
625
  con.cacheAngLimImp = next;
468
626
  }
469
627
  if (j !== 0) {
470
- if (iiA > 0) {
471
- av[ai + 0] -= j * iiA * nx;
472
- av[ai + 1] -= j * iiA * ny;
473
- av[ai + 2] -= j * iiA * nz;
628
+ const gWA = con.cacheAngLimWA;
629
+ const gWB = con.cacheAngLimWB;
630
+ if (imA > 0) {
631
+ av[ai + 0] -= j * gWA[0];
632
+ av[ai + 1] -= j * gWA[1];
633
+ av[ai + 2] -= j * gWA[2];
474
634
  }
475
- if (iiB > 0) {
476
- av[bi + 0] += j * iiB * nx;
477
- av[bi + 1] += j * iiB * ny;
478
- av[bi + 2] += j * iiB * nz;
635
+ if (imB > 0) {
636
+ av[bi + 0] += j * gWB[0];
637
+ av[bi + 1] += j * gWB[1];
638
+ av[bi + 2] += j * gWB[2];
479
639
  }
480
640
  }
481
641
  }
@@ -483,32 +643,38 @@ function iterateConstraint(con, lv, av, invMass, invInertia) {
483
643
  // SETUP: pre-compute Jacobians, friction basis, and the bounce reference
484
644
  // from the *initial* closing velocity (Bullet's pattern — captures restitution
485
645
  // before iter 1 zeroes out the approach).
486
- function setupContactRow(c, lv, av, invMass, invInertia) {
646
+ function setupContactRow(c, lv, av, invMass, W) {
487
647
  const ai = c.bodyA * 3;
488
648
  const bi = c.bodyB * 3;
649
+ const a9 = c.bodyA * 9;
650
+ const b9 = c.bodyB * 9;
489
651
  const imA = invMass[c.bodyA];
490
652
  const imB = invMass[c.bodyB];
491
- const iiA = invInertia[c.bodyA];
492
- const iiB = invInertia[c.bodyB];
493
653
  const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz;
494
654
  const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz;
495
655
  const nx = c.nx, ny = c.ny, nz = c.nz;
496
- // Normal Jacobian.
656
+ // Normal Jacobian. Cached vectors are tensor-multiplied I⁻¹·(r×n).
497
657
  const cAxN = rAy * nz - rAz * ny;
498
658
  const cAyN = rAz * nx - rAx * nz;
499
659
  const cAzN = rAx * ny - rAy * nx;
500
660
  const cBxN = rBy * nz - rBz * ny;
501
661
  const cByN = rBz * nx - rBx * nz;
502
662
  const cBzN = rBx * ny - rBy * nx;
663
+ const wAxN = W[a9 + 0] * cAxN + W[a9 + 1] * cAyN + W[a9 + 2] * cAzN;
664
+ const wAyN = W[a9 + 3] * cAxN + W[a9 + 4] * cAyN + W[a9 + 5] * cAzN;
665
+ const wAzN = W[a9 + 6] * cAxN + W[a9 + 7] * cAyN + W[a9 + 8] * cAzN;
666
+ const wBxN = W[b9 + 0] * cBxN + W[b9 + 1] * cByN + W[b9 + 2] * cBzN;
667
+ const wByN = W[b9 + 3] * cBxN + W[b9 + 4] * cByN + W[b9 + 5] * cBzN;
668
+ const wBzN = W[b9 + 6] * cBxN + W[b9 + 7] * cByN + W[b9 + 8] * cBzN;
503
669
  const denomN = imA + imB +
504
- (cAxN * cAxN + cAyN * cAyN + cAzN * cAzN) * iiA +
505
- (cBxN * cBxN + cByN * cByN + cBzN * cBzN) * iiB;
506
- c.cAxN = cAxN;
507
- c.cAyN = cAyN;
508
- c.cAzN = cAzN;
509
- c.cBxN = cBxN;
510
- c.cByN = cByN;
511
- c.cBzN = cBzN;
670
+ (cAxN * wAxN + cAyN * wAyN + cAzN * wAzN) +
671
+ (cBxN * wBxN + cByN * wByN + cBzN * wBzN);
672
+ c.cAxN = wAxN;
673
+ c.cAyN = wAyN;
674
+ c.cAzN = wAzN;
675
+ c.cBxN = wBxN;
676
+ c.cByN = wByN;
677
+ c.cBzN = wBzN;
512
678
  c.jacInvN = denomN > 0 ? 1 / denomN : 0;
513
679
  // Restitution reference, captured from initial relVelN.
514
680
  const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy;
@@ -561,15 +727,21 @@ function setupContactRow(c, lv, av, invMass, invInertia) {
561
727
  const cBxT1 = rBy * t1z - rBz * t1y;
562
728
  const cByT1 = rBz * t1x - rBx * t1z;
563
729
  const cBzT1 = rBx * t1y - rBy * t1x;
730
+ const wAxT1 = W[a9 + 0] * cAxT1 + W[a9 + 1] * cAyT1 + W[a9 + 2] * cAzT1;
731
+ const wAyT1 = W[a9 + 3] * cAxT1 + W[a9 + 4] * cAyT1 + W[a9 + 5] * cAzT1;
732
+ const wAzT1 = W[a9 + 6] * cAxT1 + W[a9 + 7] * cAyT1 + W[a9 + 8] * cAzT1;
733
+ const wBxT1 = W[b9 + 0] * cBxT1 + W[b9 + 1] * cByT1 + W[b9 + 2] * cBzT1;
734
+ const wByT1 = W[b9 + 3] * cBxT1 + W[b9 + 4] * cByT1 + W[b9 + 5] * cBzT1;
735
+ const wBzT1 = W[b9 + 6] * cBxT1 + W[b9 + 7] * cByT1 + W[b9 + 8] * cBzT1;
564
736
  const denomT1 = imA + imB +
565
- (cAxT1 * cAxT1 + cAyT1 * cAyT1 + cAzT1 * cAzT1) * iiA +
566
- (cBxT1 * cBxT1 + cByT1 * cByT1 + cBzT1 * cBzT1) * iiB;
567
- c.cAxT1 = cAxT1;
568
- c.cAyT1 = cAyT1;
569
- c.cAzT1 = cAzT1;
570
- c.cBxT1 = cBxT1;
571
- c.cByT1 = cByT1;
572
- c.cBzT1 = cBzT1;
737
+ (cAxT1 * wAxT1 + cAyT1 * wAyT1 + cAzT1 * wAzT1) +
738
+ (cBxT1 * wBxT1 + cByT1 * wByT1 + cBzT1 * wBzT1);
739
+ c.cAxT1 = wAxT1;
740
+ c.cAyT1 = wAyT1;
741
+ c.cAzT1 = wAzT1;
742
+ c.cBxT1 = wBxT1;
743
+ c.cByT1 = wByT1;
744
+ c.cBzT1 = wBzT1;
573
745
  c.jacInvT1 = denomT1 > 0 ? 1 / denomT1 : 0;
574
746
  const cAxT2 = rAy * t2z - rAz * t2y;
575
747
  const cAyT2 = rAz * t2x - rAx * t2z;
@@ -577,27 +749,31 @@ function setupContactRow(c, lv, av, invMass, invInertia) {
577
749
  const cBxT2 = rBy * t2z - rBz * t2y;
578
750
  const cByT2 = rBz * t2x - rBx * t2z;
579
751
  const cBzT2 = rBx * t2y - rBy * t2x;
752
+ const wAxT2 = W[a9 + 0] * cAxT2 + W[a9 + 1] * cAyT2 + W[a9 + 2] * cAzT2;
753
+ const wAyT2 = W[a9 + 3] * cAxT2 + W[a9 + 4] * cAyT2 + W[a9 + 5] * cAzT2;
754
+ const wAzT2 = W[a9 + 6] * cAxT2 + W[a9 + 7] * cAyT2 + W[a9 + 8] * cAzT2;
755
+ const wBxT2 = W[b9 + 0] * cBxT2 + W[b9 + 1] * cByT2 + W[b9 + 2] * cBzT2;
756
+ const wByT2 = W[b9 + 3] * cBxT2 + W[b9 + 4] * cByT2 + W[b9 + 5] * cBzT2;
757
+ const wBzT2 = W[b9 + 6] * cBxT2 + W[b9 + 7] * cByT2 + W[b9 + 8] * cBzT2;
580
758
  const denomT2 = imA + imB +
581
- (cAxT2 * cAxT2 + cAyT2 * cAyT2 + cAzT2 * cAzT2) * iiA +
582
- (cBxT2 * cBxT2 + cByT2 * cByT2 + cBzT2 * cBzT2) * iiB;
583
- c.cAxT2 = cAxT2;
584
- c.cAyT2 = cAyT2;
585
- c.cAzT2 = cAzT2;
586
- c.cBxT2 = cBxT2;
587
- c.cByT2 = cByT2;
588
- c.cBzT2 = cBzT2;
759
+ (cAxT2 * wAxT2 + cAyT2 * wAyT2 + cAzT2 * wAzT2) +
760
+ (cBxT2 * wBxT2 + cByT2 * wByT2 + cBzT2 * wBzT2);
761
+ c.cAxT2 = wAxT2;
762
+ c.cAyT2 = wAyT2;
763
+ c.cAzT2 = wAzT2;
764
+ c.cBxT2 = wBxT2;
765
+ c.cByT2 = wByT2;
766
+ c.cBzT2 = wBzT2;
589
767
  c.jacInvT2 = denomT2 > 0 ? 1 / denomT2 : 0;
590
768
  }
591
769
  // ITER: one push-only normal row + two Coulomb friction rows. Friction
592
770
  // bound depends on the *current* applied normal impulse, so it tightens
593
771
  // as the normal row converges.
594
- function iterateContactRow(c, lv, av, invMass, invInertia) {
772
+ function iterateContactRow(c, lv, av, invMass) {
595
773
  const imA = invMass[c.bodyA];
596
774
  const imB = invMass[c.bodyB];
597
775
  if (imA === 0 && imB === 0)
598
776
  return;
599
- const iiA = invInertia[c.bodyA];
600
- const iiB = invInertia[c.bodyB];
601
777
  const ai = c.bodyA * 3, bi = c.bodyB * 3;
602
778
  const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz;
603
779
  const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz;
@@ -630,17 +806,17 @@ function iterateContactRow(c, lv, av, invMass, invInertia) {
630
806
  lv[ai + 0] -= dImpN * imA * nx;
631
807
  lv[ai + 1] -= dImpN * imA * ny;
632
808
  lv[ai + 2] -= dImpN * imA * nz;
633
- av[ai + 0] -= dImpN * iiA * cAxN;
634
- av[ai + 1] -= dImpN * iiA * cAyN;
635
- av[ai + 2] -= dImpN * iiA * cAzN;
809
+ av[ai + 0] -= dImpN * cAxN;
810
+ av[ai + 1] -= dImpN * cAyN;
811
+ av[ai + 2] -= dImpN * cAzN;
636
812
  }
637
813
  if (imB > 0) {
638
814
  lv[bi + 0] += dImpN * imB * nx;
639
815
  lv[bi + 1] += dImpN * imB * ny;
640
816
  lv[bi + 2] += dImpN * imB * nz;
641
- av[bi + 0] += dImpN * iiB * cBxN;
642
- av[bi + 1] += dImpN * iiB * cByN;
643
- av[bi + 2] += dImpN * iiB * cBzN;
817
+ av[bi + 0] += dImpN * cBxN;
818
+ av[bi + 1] += dImpN * cByN;
819
+ av[bi + 2] += dImpN * cBzN;
644
820
  }
645
821
  }
646
822
  }
@@ -658,10 +834,10 @@ function iterateContactRow(c, lv, av, invMass, invInertia) {
658
834
  const dvx2 = vBx2 - vAx2;
659
835
  const dvy2 = vBy2 - vAy2;
660
836
  const dvz2 = vBz2 - vAz2;
661
- 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, iiA, iiB, lv, av, 1);
662
- 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, iiA, iiB, lv, av, 2);
837
+ 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);
838
+ 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);
663
839
  }
664
- function applyFrictionTangent(c, ai, bi, dvx, dvy, dvz, tx, ty, tz, cAx, cAy, cAz, cBx, cBy, cBz, jacInv, muNormal, imA, imB, iiA, iiB, lv, av, slot) {
840
+ function applyFrictionTangent(c, ai, bi, dvx, dvy, dvz, tx, ty, tz, cAx, cAy, cAz, cBx, cBy, cBz, jacInv, muNormal, imA, imB, lv, av, slot) {
665
841
  if (jacInv <= 0)
666
842
  return;
667
843
  const relVel = dvx * tx + dvy * ty + dvz * tz;
@@ -686,17 +862,17 @@ function applyFrictionTangent(c, ai, bi, dvx, dvy, dvz, tx, ty, tz, cAx, cAy, cA
686
862
  lv[ai + 0] -= dImp * imA * tx;
687
863
  lv[ai + 1] -= dImp * imA * ty;
688
864
  lv[ai + 2] -= dImp * imA * tz;
689
- av[ai + 0] -= dImp * iiA * cAx;
690
- av[ai + 1] -= dImp * iiA * cAy;
691
- av[ai + 2] -= dImp * iiA * cAz;
865
+ av[ai + 0] -= dImp * cAx;
866
+ av[ai + 1] -= dImp * cAy;
867
+ av[ai + 2] -= dImp * cAz;
692
868
  }
693
869
  if (imB > 0) {
694
870
  lv[bi + 0] += dImp * imB * tx;
695
871
  lv[bi + 1] += dImp * imB * ty;
696
872
  lv[bi + 2] += dImp * imB * tz;
697
- av[bi + 0] += dImp * iiB * cBx;
698
- av[bi + 1] += dImp * iiB * cBy;
699
- av[bi + 2] += dImp * iiB * cBz;
873
+ av[bi + 0] += dImp * cBx;
874
+ av[bi + 1] += dImp * cBy;
875
+ av[bi + 2] += dImp * cBz;
700
876
  }
701
877
  }
702
878
  function buildBodyMat(store, i, out) {