reze-engine 0.16.1 → 0.16.3

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 (88) hide show
  1. package/README.md +139 -284
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +5 -5
  4. package/dist/model.d.ts +4 -1
  5. package/dist/model.d.ts.map +1 -1
  6. package/dist/model.js +14 -1
  7. package/dist/physics/body.d.ts +1 -0
  8. package/dist/physics/body.d.ts.map +1 -1
  9. package/dist/physics/body.js +2 -0
  10. package/dist/physics/constraint.d.ts +8 -1
  11. package/dist/physics/constraint.d.ts.map +1 -1
  12. package/dist/physics/constraint.js +13 -1
  13. package/dist/physics/physics.d.ts +15 -1
  14. package/dist/physics/physics.d.ts.map +1 -1
  15. package/dist/physics/physics.js +413 -48
  16. package/dist/physics/solver.d.ts.map +1 -1
  17. package/dist/physics/solver.js +223 -39
  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 +31 -4
  24. package/dist/physics-debug.d.ts +30 -0
  25. package/dist/physics-debug.d.ts.map +1 -0
  26. package/dist/physics-debug.js +526 -0
  27. package/dist/pmx-loader.d.ts +2 -0
  28. package/dist/pmx-loader.d.ts.map +1 -1
  29. package/dist/pmx-loader.js +37 -20
  30. package/dist/shaders/materials/body.d.ts +1 -1
  31. package/dist/shaders/materials/body.d.ts.map +1 -1
  32. package/dist/shaders/materials/body.js +90 -85
  33. package/dist/shaders/materials/cloth_rough.d.ts +1 -1
  34. package/dist/shaders/materials/cloth_rough.d.ts.map +1 -1
  35. package/dist/shaders/materials/cloth_rough.js +1 -1
  36. package/dist/shaders/materials/cloth_smooth.d.ts +1 -1
  37. package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -1
  38. package/dist/shaders/materials/cloth_smooth.js +2 -2
  39. package/dist/shaders/materials/common.d.ts +1 -1
  40. package/dist/shaders/materials/common.d.ts.map +1 -1
  41. package/dist/shaders/materials/common.js +10 -0
  42. package/dist/shaders/materials/default.d.ts +1 -1
  43. package/dist/shaders/materials/default.d.ts.map +1 -1
  44. package/dist/shaders/materials/default.js +37 -32
  45. package/dist/shaders/materials/eye.d.ts +1 -1
  46. package/dist/shaders/materials/eye.d.ts.map +1 -1
  47. package/dist/shaders/materials/eye.js +40 -35
  48. package/dist/shaders/materials/face.d.ts +1 -1
  49. package/dist/shaders/materials/face.d.ts.map +1 -1
  50. package/dist/shaders/materials/face.js +90 -85
  51. package/dist/shaders/materials/hair.d.ts +1 -1
  52. package/dist/shaders/materials/hair.d.ts.map +1 -1
  53. package/dist/shaders/materials/hair.js +85 -75
  54. package/dist/shaders/materials/metal.d.ts +1 -1
  55. package/dist/shaders/materials/metal.d.ts.map +1 -1
  56. package/dist/shaders/materials/metal.js +1 -1
  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/dist/shaders/passes/physics-debug.d.ts +2 -0
  61. package/dist/shaders/passes/physics-debug.d.ts.map +1 -0
  62. package/dist/shaders/passes/physics-debug.js +69 -0
  63. package/package.json +1 -1
  64. package/src/engine.ts +36 -21
  65. package/src/model.ts +18 -1
  66. package/src/physics/body.ts +5 -0
  67. package/src/physics/constraint.ts +35 -6
  68. package/src/physics/physics.ts +423 -56
  69. package/src/physics/solver.ts +213 -38
  70. package/src/physics/types.ts +10 -2
  71. package/src/physics/world.ts +33 -4
  72. package/src/pmx-loader.ts +38 -20
  73. package/src/shaders/materials/body.ts +97 -92
  74. package/src/shaders/materials/cloth_rough.ts +1 -1
  75. package/src/shaders/materials/cloth_smooth.ts +2 -2
  76. package/src/shaders/materials/common.ts +10 -0
  77. package/src/shaders/materials/default.ts +45 -40
  78. package/src/shaders/materials/eye.ts +48 -43
  79. package/src/shaders/materials/face.ts +97 -92
  80. package/src/shaders/materials/hair.ts +92 -82
  81. package/src/shaders/materials/metal.ts +1 -1
  82. package/src/shaders/materials/stockings.ts +1 -1
  83. package/dist/gpu-profile.d.ts +0 -19
  84. package/dist/gpu-profile.d.ts.map +0 -1
  85. package/dist/gpu-profile.js +0 -120
  86. package/dist/physics/profile.d.ts +0 -18
  87. package/dist/physics/profile.d.ts.map +0 -1
  88. package/dist/physics/profile.js +0 -44
@@ -16,6 +16,9 @@ export class RezePhysics {
16
16
  this.timeAccum = 0;
17
17
  this.fixedTimeStep = 1 / 60;
18
18
  this.maxSubSteps = 6;
19
+ // Debug counter: frames on which a teleport (scrub/jump discontinuity)
20
+ // was detected and settled.
21
+ this.teleportCount = 0;
19
22
  this.rigidbodies = rigidbodies;
20
23
  this.joints = joints;
21
24
  this.store = new RigidBodyStore(rigidbodies);
@@ -24,6 +27,53 @@ export class RezePhysics {
24
27
  this.contacts = new ContactPool();
25
28
  this.prevPositions = new Float32Array(this.store.count * 3);
26
29
  this.prevOrientations = new Float32Array(this.store.count * 4);
30
+ this.kinTargetPos = new Float32Array(this.store.count * 3);
31
+ this.kinTargetOri = new Float32Array(this.store.count * 4);
32
+ this.kinTargetVel = new Float32Array(this.store.count * 3);
33
+ this.kinTargetAngVel = new Float32Array(this.store.count * 3);
34
+ this.kinRoot = this.buildKinematicRoots();
35
+ this.teleportFlags = new Uint8Array(this.store.count);
36
+ this.alignPinned = new Uint8Array(this.store.count);
37
+ const types = this.store.type;
38
+ for (const c of this.constraints) {
39
+ const tA = types[c.bodyA];
40
+ const tB = types[c.bodyB];
41
+ const aFollows = tA === RigidbodyType.Static || tA === RigidbodyType.Kinematic;
42
+ const bFollows = tB === RigidbodyType.Static || tB === RigidbodyType.Kinematic;
43
+ if (aFollows && this.store.aligned[c.bodyB])
44
+ this.alignPinned[c.bodyB] = 1;
45
+ if (bFollows && this.store.aligned[c.bodyA])
46
+ this.alignPinned[c.bodyA] = 1;
47
+ }
48
+ }
49
+ // BFS over the joint graph from every kinematic body, assigning each
50
+ // reachable dynamic body the kinematic body it (transitively) hangs off.
51
+ buildKinematicRoots() {
52
+ const N = this.store.count;
53
+ const root = new Int32Array(N).fill(-1);
54
+ const types = this.store.type;
55
+ const adj = Array.from({ length: N }, () => []);
56
+ for (const c of this.constraints) {
57
+ adj[c.bodyA].push(c.bodyB);
58
+ adj[c.bodyB].push(c.bodyA);
59
+ }
60
+ const queue = [];
61
+ for (let i = 0; i < N; i++) {
62
+ if (types[i] === RigidbodyType.Static || types[i] === RigidbodyType.Kinematic) {
63
+ root[i] = i;
64
+ queue.push(i);
65
+ }
66
+ }
67
+ for (let h = 0; h < queue.length; h++) {
68
+ const i = queue[h];
69
+ for (const j of adj[i]) {
70
+ if (root[j] !== -1)
71
+ continue;
72
+ root[j] = root[i];
73
+ queue.push(j);
74
+ }
75
+ }
76
+ return root;
27
77
  }
28
78
  // Snapshot the current body pose as the interpolation "previous" state.
29
79
  savePrevState() {
@@ -66,6 +116,13 @@ export class RezePhysics {
66
116
  return;
67
117
  this.snapBodiesToBones(boneWorldMatrices);
68
118
  this.savePrevState(); // prev == curr after a snap, so no interpolation jump
119
+ // Reseed kinematic targets from the snapped pose — otherwise the next
120
+ // step derives the target-trajectory velocity against the pre-reset
121
+ // targets and feeds one frame of enormous anchor velocity to the solver.
122
+ this.kinTargetPos.set(this.store.positions);
123
+ this.kinTargetOri.set(this.store.orientations);
124
+ this.kinTargetVel.fill(0);
125
+ this.kinTargetAngVel.fill(0);
69
126
  this.timeAccum = 0;
70
127
  }
71
128
  step(dt, boneWorldMatrices, boneInverseBindMatrices) {
@@ -75,25 +132,55 @@ export class RezePhysics {
75
132
  // that skip frame 0 don't pop bodies on first step.
76
133
  this.snapBodiesToBones(boneWorldMatrices);
77
134
  this.savePrevState();
135
+ // Seed kinematic targets so the first frame's target-delta velocity
136
+ // is zero instead of a jump from the origin.
137
+ this.kinTargetPos.set(this.store.positions);
138
+ this.kinTargetOri.set(this.store.orientations);
78
139
  this.firstFrame = false;
79
140
  }
80
- // Sync once per render frame; kinematic targets don't change between
81
- // substeps. Render dt is used to derive kinematic velocities from the
82
- // bone-pose delta so joints feel the kinematic motion.
83
- this.syncFromBones(boneWorldMatrices, dt);
141
+ // Compute this frame's kinematic targets from the current bone pose. A
142
+ // target jump beyond what continuous motion can produce (timeline scrub)
143
+ // is a teleport, handled per kinematic root: rigidly carry that root's
144
+ // dynamic chains across the jump with zeroed momentum, snap the root,
145
+ // and keep simulating — dragging cloth through a discontinuity at the
146
+ // raw derived velocity is what used to explode the solver. Chains under
147
+ // unaffected roots keep their momentum untouched.
148
+ if (this.computeKinematicTargets(boneWorldMatrices, dt)) {
149
+ this.teleportCount++;
150
+ this.carryDynamicThroughTeleport();
151
+ this.snapKinematicToTargets(true);
152
+ this.savePrevState(); // prev == curr so interpolation doesn't streak
153
+ }
84
154
  // Fixed-timestep substeps. The maxSubSteps cap prevents runaway after
85
155
  // a long stall (tab backgrounded, etc.). Snapshot the pose before each step so
86
156
  // after the loop prevState is one substep behind the live (current) state.
157
+ // Kinematic bodies split the remaining gap evenly across this frame's
158
+ // substeps (f = 1/substeps-left) and land exactly on the frame's bone
159
+ // pose by the last one: at 60 Hz render that reproduces the classic
160
+ // sync-once-per-frame behavior exactly (no fractional lag trembling
161
+ // against the rendered mesh), while at lower rates the per-substep
162
+ // constraint error stays bounded to one fixed step of bone motion.
87
163
  this.timeAccum += dt;
88
- let sub = 0;
89
- while (this.timeAccum >= this.fixedTimeStep && sub < this.maxSubSteps) {
164
+ let nSub = Math.floor(this.timeAccum / this.fixedTimeStep);
165
+ if (nSub > this.maxSubSteps)
166
+ nSub = this.maxSubSteps;
167
+ for (let k = 0; k < nSub; k++) {
90
168
  this.savePrevState();
169
+ this.advanceKinematicToTargets(1 / (nSub - k));
91
170
  this.world.step(this.store, this.constraints, this.contacts, this.fixedTimeStep);
171
+ this.restoreNonFiniteBodies();
92
172
  this.timeAccum -= this.fixedTimeStep;
93
- sub++;
94
173
  }
95
- if (sub === this.maxSubSteps)
174
+ if (this.timeAccum >= this.fixedTimeStep) {
175
+ // Substep budget exhausted mid-catchup: drop the remaining time and
176
+ // snap kinematic bodies the rest of the way (zero velocity) so they
177
+ // don't start next frame lagging behind their bones.
96
178
  this.timeAccum = 0;
179
+ this.snapKinematicToTargets(false);
180
+ }
181
+ // MMD mode-2 bone alignment for pinned (depth-1) bodies: position
182
+ // re-pins to the animated bone, rotation stays simulated.
183
+ this.alignPinnedBodiesToBones(boneWorldMatrices);
97
184
  // Fraction into the next (not-yet-taken) step; always in [0, 1).
98
185
  const alpha = this.fixedTimeStep > 0 ? this.timeAccum / this.fixedTimeStep : 0;
99
186
  this.applyDynamicsToBones(boneWorldMatrices, alpha);
@@ -130,21 +217,31 @@ export class RezePhysics {
130
217
  av[i3 + 2] = 0;
131
218
  }
132
219
  }
133
- // Pull Static / Kinematic bodies to their bones and derive velocities
134
- // from the bone-pose delta joints attached to fast limbs need to see
135
- // the kinematic motion, not just the position jump, or dependent cloth
136
- // bodies lag behind quick movement.
137
- syncFromBones(boneWorldMatrices, dt) {
220
+ // Fill kinTargetPos/kinTargetOri = boneWorld × bodyOffset for every bone-
221
+ // bound Static/Kinematic body. Returns true if any target is discontinuous
222
+ // with the current body pose farther than continuous motion can carry it
223
+ // in one render frame, or rotated more than 90°.
224
+ computeKinematicTargets(boneWorldMatrices, dt) {
138
225
  const N = this.store.count;
139
226
  const offsets = this.store.bodyOffsetMatrix;
140
227
  const positions = this.store.positions;
141
228
  const orientations = this.store.orientations;
142
- const lv = this.store.linearVelocities;
143
- const av = this.store.angularVelocities;
144
229
  const types = this.store.type;
145
230
  const boneIdx = this.store.boneIndex;
231
+ const tp = this.kinTargetPos;
232
+ const to = this.kinTargetOri;
233
+ // 250 units/s scaled by frame time, floored at 4 units: far above the
234
+ // fastest limb motion (a false positive freezes that chain's momentum
235
+ // for a frame, which reads as stutter), far below any real scrub jump.
236
+ const maxJump = Math.max(4, 250 * dt);
237
+ const maxJumpSq = maxJump * maxJump;
238
+ const flags = this.teleportFlags;
239
+ let teleport = false;
240
+ const tv = this.kinTargetVel;
241
+ const tav = this.kinTargetAngVel;
146
242
  const invDt = dt > 0 ? 1 / dt : 0;
147
243
  for (let i = 0; i < N; i++) {
244
+ flags[i] = 0;
148
245
  const t = types[i];
149
246
  if (t !== RigidbodyType.Static && t !== RigidbodyType.Kinematic)
150
247
  continue;
@@ -154,45 +251,297 @@ export class RezePhysics {
154
251
  Mat4.multiplyArrays(boneWorldMatrices[b].values, 0, offsets, i * 16, _bodyMat, 0);
155
252
  const i3 = i * 3;
156
253
  const i4 = i * 4;
157
- // Save previous transform for the velocity diff. invDt = 0 (first
158
- // frame / reset) skips the diff and zeros velocities.
159
- const oldPx = positions[i3 + 0], oldPy = positions[i3 + 1], oldPz = positions[i3 + 2];
254
+ // Previous frame's target the reference for the trajectory velocity.
255
+ const oldTx = tp[i3 + 0], oldTy = tp[i3 + 1], oldTz = tp[i3 + 2];
256
+ const oldOx = to[i4 + 0], oldOy = to[i4 + 1], oldOz = to[i4 + 2], oldOw = to[i4 + 3];
257
+ tp[i3 + 0] = _bodyMat[12];
258
+ tp[i3 + 1] = _bodyMat[13];
259
+ tp[i3 + 2] = _bodyMat[14];
260
+ Mat4.toQuatFromArrayInto(_bodyMat, 0, _scratchQuat);
261
+ const nOx = _scratchQuat.x, nOy = _scratchQuat.y, nOz = _scratchQuat.z, nOw = _scratchQuat.w;
262
+ to[i4 + 0] = nOx;
263
+ to[i4 + 1] = nOy;
264
+ to[i4 + 2] = nOz;
265
+ to[i4 + 3] = nOw;
266
+ const dx = tp[i3 + 0] - positions[i3 + 0];
267
+ const dy = tp[i3 + 1] - positions[i3 + 1];
268
+ const dz = tp[i3 + 2] - positions[i3 + 2];
269
+ // |q·q'| = cos(θ/2); below cos(45°) the body turned more than 90°.
270
+ const dot = to[i4 + 0] * orientations[i4 + 0] +
271
+ to[i4 + 1] * orientations[i4 + 1] +
272
+ to[i4 + 2] * orientations[i4 + 2] +
273
+ to[i4 + 3] * orientations[i4 + 3];
274
+ if (dx * dx + dy * dy + dz * dz > maxJumpSq || Math.abs(dot) < 0.7071) {
275
+ flags[i] = 1;
276
+ teleport = true;
277
+ tv[i3 + 0] = 0;
278
+ tv[i3 + 1] = 0;
279
+ tv[i3 + 2] = 0;
280
+ tav[i3 + 0] = 0;
281
+ tav[i3 + 1] = 0;
282
+ tav[i3 + 2] = 0;
283
+ continue;
284
+ }
285
+ tv[i3 + 0] = (tp[i3 + 0] - oldTx) * invDt;
286
+ tv[i3 + 1] = (tp[i3 + 1] - oldTy) * invDt;
287
+ tv[i3 + 2] = (tp[i3 + 2] - oldTz) * invDt;
288
+ // ω ≈ 2 · qDiff.xyz / dt with qDiff = qNew · conj(qOld). Shortest-arc
289
+ // sign keeps qDiff and −qDiff (same rotation) from doubling ω.
290
+ const cox = -oldOx, coy = -oldOy, coz = -oldOz, cow = oldOw;
291
+ const qdx = nOw * cox + nOx * cow + nOy * coz - nOz * coy;
292
+ const qdy = nOw * coy - nOx * coz + nOy * cow + nOz * cox;
293
+ const qdz = nOw * coz + nOx * coy - nOy * cox + nOz * cow;
294
+ const qdw = nOw * cow - nOx * cox - nOy * coy - nOz * coz;
295
+ const sign = qdw < 0 ? -1 : 1;
296
+ tav[i3 + 0] = 2 * sign * qdx * invDt;
297
+ tav[i3 + 1] = 2 * sign * qdy * invDt;
298
+ tav[i3 + 2] = 2 * sign * qdz * invDt;
299
+ }
300
+ return teleport;
301
+ }
302
+ // Move kinematic bodies fraction f of the way to the frame target with the
303
+ // target-trajectory velocity, so joints see continuous anchor motion (and a
304
+ // smooth velocity signal) instead of a frame-sized jump on substep 1.
305
+ advanceKinematicToTargets(f) {
306
+ const N = this.store.count;
307
+ const positions = this.store.positions;
308
+ const orientations = this.store.orientations;
309
+ const lv = this.store.linearVelocities;
310
+ const av = this.store.angularVelocities;
311
+ const types = this.store.type;
312
+ const boneIdx = this.store.boneIndex;
313
+ const tp = this.kinTargetPos;
314
+ const to = this.kinTargetOri;
315
+ const tv = this.kinTargetVel;
316
+ const tav = this.kinTargetAngVel;
317
+ for (let i = 0; i < N; i++) {
318
+ const t = types[i];
319
+ if (t !== RigidbodyType.Static && t !== RigidbodyType.Kinematic)
320
+ continue;
321
+ const b = boneIdx[i];
322
+ if (b < 0)
323
+ continue;
324
+ const i3 = i * 3;
325
+ const i4 = i * 4;
326
+ positions[i3 + 0] += (tp[i3 + 0] - positions[i3 + 0]) * f;
327
+ positions[i3 + 1] += (tp[i3 + 1] - positions[i3 + 1]) * f;
328
+ positions[i3 + 2] += (tp[i3 + 2] - positions[i3 + 2]) * f;
329
+ lv[i3 + 0] = tv[i3 + 0];
330
+ lv[i3 + 1] = tv[i3 + 1];
331
+ lv[i3 + 2] = tv[i3 + 2];
332
+ av[i3 + 0] = tav[i3 + 0];
333
+ av[i3 + 1] = tav[i3 + 1];
334
+ av[i3 + 2] = tav[i3 + 2];
335
+ // Shortest-arc nlerp toward the target orientation.
160
336
  const oldOx = orientations[i4 + 0], oldOy = orientations[i4 + 1];
161
337
  const oldOz = orientations[i4 + 2], oldOw = orientations[i4 + 3];
162
- positions[i3 + 0] = _bodyMat[12];
163
- positions[i3 + 1] = _bodyMat[13];
164
- positions[i3 + 2] = _bodyMat[14];
165
- Mat4.toQuatFromArrayInto(_bodyMat, 0, _scratchQuat);
166
- const newOx = _scratchQuat.x, newOy = _scratchQuat.y;
167
- const newOz = _scratchQuat.z, newOw = _scratchQuat.w;
338
+ let tx = to[i4 + 0], ty = to[i4 + 1], tz = to[i4 + 2], tw = to[i4 + 3];
339
+ if (oldOx * tx + oldOy * ty + oldOz * tz + oldOw * tw < 0) {
340
+ tx = -tx;
341
+ ty = -ty;
342
+ tz = -tz;
343
+ tw = -tw;
344
+ }
345
+ let newOx = oldOx + (tx - oldOx) * f;
346
+ let newOy = oldOy + (ty - oldOy) * f;
347
+ let newOz = oldOz + (tz - oldOz) * f;
348
+ let newOw = oldOw + (tw - oldOw) * f;
349
+ const len2 = newOx * newOx + newOy * newOy + newOz * newOz + newOw * newOw;
350
+ if (len2 > 1e-12) {
351
+ const inv = 1 / Math.sqrt(len2);
352
+ newOx *= inv;
353
+ newOy *= inv;
354
+ newOz *= inv;
355
+ newOw *= inv;
356
+ }
357
+ else {
358
+ newOx = tx;
359
+ newOy = ty;
360
+ newOz = tz;
361
+ newOw = tw;
362
+ }
168
363
  orientations[i4 + 0] = newOx;
169
364
  orientations[i4 + 1] = newOy;
170
365
  orientations[i4 + 2] = newOz;
171
366
  orientations[i4 + 3] = newOw;
172
- if (invDt === 0) {
173
- lv[i3 + 0] = 0;
174
- lv[i3 + 1] = 0;
175
- lv[i3 + 2] = 0;
176
- av[i3 + 0] = 0;
177
- av[i3 + 1] = 0;
178
- av[i3 + 2] = 0;
179
- }
180
- else {
181
- lv[i3 + 0] = (_bodyMat[12] - oldPx) * invDt;
182
- lv[i3 + 1] = (_bodyMat[13] - oldPy) * invDt;
183
- lv[i3 + 2] = (_bodyMat[14] - oldPz) * invDt;
184
- // ω ≈ 2 · qDiff.xyz / dt with qDiff = qNew · conj(qOld). Shortest-
185
- // arc sign keeps qDiff and −qDiff (same rotation) from doubling ω.
186
- const cox = -oldOx, coy = -oldOy, coz = -oldOz, cow = oldOw;
187
- const dx = newOw * cox + newOx * cow + newOy * coz - newOz * coy;
188
- const dy = newOw * coy - newOx * coz + newOy * cow + newOz * cox;
189
- const dz = newOw * coz + newOx * coy - newOy * cox + newOz * cow;
190
- const dw = newOw * cow - newOx * cox - newOy * coy - newOz * coz;
191
- const sign = dw < 0 ? -1 : 1;
192
- av[i3 + 0] = 2 * sign * dx * invDt;
193
- av[i3 + 1] = 2 * sign * dy * invDt;
194
- av[i3 + 2] = 2 * sign * dz * invDt;
367
+ }
368
+ }
369
+ // Snap kinematic bodies straight to the frame target with zero velocity.
370
+ // Used on teleports (onlyFlagged) and when the substep budget runs out
371
+ // mid-catchup (all).
372
+ snapKinematicToTargets(onlyFlagged) {
373
+ const N = this.store.count;
374
+ const positions = this.store.positions;
375
+ const orientations = this.store.orientations;
376
+ const lv = this.store.linearVelocities;
377
+ const av = this.store.angularVelocities;
378
+ const types = this.store.type;
379
+ const boneIdx = this.store.boneIndex;
380
+ const tp = this.kinTargetPos;
381
+ const to = this.kinTargetOri;
382
+ const flags = this.teleportFlags;
383
+ for (let i = 0; i < N; i++) {
384
+ const t = types[i];
385
+ if (t !== RigidbodyType.Static && t !== RigidbodyType.Kinematic)
386
+ continue;
387
+ if (boneIdx[i] < 0)
388
+ continue;
389
+ if (onlyFlagged && !flags[i])
390
+ continue;
391
+ const i3 = i * 3;
392
+ const i4 = i * 4;
393
+ positions[i3 + 0] = tp[i3 + 0];
394
+ positions[i3 + 1] = tp[i3 + 1];
395
+ positions[i3 + 2] = tp[i3 + 2];
396
+ orientations[i4 + 0] = to[i4 + 0];
397
+ orientations[i4 + 1] = to[i4 + 1];
398
+ orientations[i4 + 2] = to[i4 + 2];
399
+ orientations[i4 + 3] = to[i4 + 3];
400
+ lv[i3 + 0] = 0;
401
+ lv[i3 + 1] = 0;
402
+ lv[i3 + 2] = 0;
403
+ av[i3 + 0] = 0;
404
+ av[i3 + 1] = 0;
405
+ av[i3 + 2] = 0;
406
+ }
407
+ }
408
+ // Rigidly carry each dynamic body whose kinematic root teleported along
409
+ // with that root's current→target transform delta (velocity zeroed),
410
+ // preserving the cloth pose relative to the character across the jump.
411
+ // Must run before snapKinematicToTargets (it reads the pre-snap pose).
412
+ carryDynamicThroughTeleport() {
413
+ const N = this.store.count;
414
+ const positions = this.store.positions;
415
+ const orientations = this.store.orientations;
416
+ const lv = this.store.linearVelocities;
417
+ const av = this.store.angularVelocities;
418
+ const types = this.store.type;
419
+ const tp = this.kinTargetPos;
420
+ const to = this.kinTargetOri;
421
+ const root = this.kinRoot;
422
+ const flags = this.teleportFlags;
423
+ for (let i = 0; i < N; i++) {
424
+ if (types[i] !== RigidbodyType.Dynamic)
425
+ continue;
426
+ const k = root[i];
427
+ if (k < 0 || !flags[k] || this.store.boneIndex[k] < 0)
428
+ continue;
429
+ const k3 = k * 3;
430
+ const k4 = k * 4;
431
+ // Root delta rotation R = qTarget · conj(qCurrent).
432
+ const cx = -orientations[k4 + 0], cy = -orientations[k4 + 1], cz = -orientations[k4 + 2], cw = orientations[k4 + 3];
433
+ const txq = to[k4 + 0], tyq = to[k4 + 1], tzq = to[k4 + 2], twq = to[k4 + 3];
434
+ const rx = twq * cx + txq * cw + tyq * cz - tzq * cy;
435
+ const ry = twq * cy - txq * cz + tyq * cw + tzq * cx;
436
+ const rz = twq * cz + txq * cy - tyq * cx + tzq * cw;
437
+ const rw = twq * cw - txq * cx - tyq * cy - tzq * cz;
438
+ const i3 = i * 3;
439
+ const i4 = i * 4;
440
+ // Position: rotate the offset from the root by R, re-anchor at target.
441
+ const ox = positions[i3 + 0] - positions[k3 + 0];
442
+ const oy = positions[i3 + 1] - positions[k3 + 1];
443
+ const oz = positions[i3 + 2] - positions[k3 + 2];
444
+ // v' = v + 2·rw·(r × v) + 2·(r × (r × v))
445
+ const c1x = ry * oz - rz * oy;
446
+ const c1y = rz * ox - rx * oz;
447
+ const c1z = rx * oy - ry * ox;
448
+ const c2x = ry * c1z - rz * c1y;
449
+ const c2y = rz * c1x - rx * c1z;
450
+ const c2z = rx * c1y - ry * c1x;
451
+ positions[i3 + 0] = tp[k3 + 0] + ox + 2 * (rw * c1x + c2x);
452
+ positions[i3 + 1] = tp[k3 + 1] + oy + 2 * (rw * c1y + c2y);
453
+ positions[i3 + 2] = tp[k3 + 2] + oz + 2 * (rw * c1z + c2z);
454
+ // Orientation: q' = R · q, renormalized.
455
+ const qx = orientations[i4 + 0], qy = orientations[i4 + 1], qz = orientations[i4 + 2], qw = orientations[i4 + 3];
456
+ let nx = rw * qx + rx * qw + ry * qz - rz * qy;
457
+ let ny = rw * qy - rx * qz + ry * qw + rz * qx;
458
+ let nz = rw * qz + rx * qy - ry * qx + rz * qw;
459
+ let nw = rw * qw - rx * qx - ry * qy - rz * qz;
460
+ const len2 = nx * nx + ny * ny + nz * nz + nw * nw;
461
+ if (len2 > 1e-12) {
462
+ const inv = 1 / Math.sqrt(len2);
463
+ nx *= inv;
464
+ ny *= inv;
465
+ nz *= inv;
466
+ nw *= inv;
467
+ orientations[i4 + 0] = nx;
468
+ orientations[i4 + 1] = ny;
469
+ orientations[i4 + 2] = nz;
470
+ orientations[i4 + 3] = nw;
195
471
  }
472
+ // Momentum doesn't carry across a discontinuity.
473
+ lv[i3 + 0] = 0;
474
+ lv[i3 + 1] = 0;
475
+ lv[i3 + 2] = 0;
476
+ av[i3 + 0] = 0;
477
+ av[i3 + 1] = 0;
478
+ av[i3 + 2] = 0;
479
+ }
480
+ }
481
+ // Overwrite pinned mode-2 bodies' positions with boneWorld × bodyOffset
482
+ // from the animated bone pose, keeping simulated orientation and
483
+ // velocities. prevPositions follows so interpolation doesn't streak.
484
+ alignPinnedBodiesToBones(boneWorldMatrices) {
485
+ const N = this.store.count;
486
+ const pinned = this.alignPinned;
487
+ const boneIdx = this.store.boneIndex;
488
+ const offsets = this.store.bodyOffsetMatrix;
489
+ const positions = this.store.positions;
490
+ const prevPos = this.prevPositions;
491
+ for (let i = 0; i < N; i++) {
492
+ if (!pinned[i])
493
+ continue;
494
+ const b = boneIdx[i];
495
+ if (b < 0 || b >= boneWorldMatrices.length)
496
+ continue;
497
+ Mat4.multiplyArrays(boneWorldMatrices[b].values, 0, offsets, i * 16, _bodyMat, 0);
498
+ const i3 = i * 3;
499
+ positions[i3 + 0] = _bodyMat[12];
500
+ positions[i3 + 1] = _bodyMat[13];
501
+ positions[i3 + 2] = _bodyMat[14];
502
+ prevPos[i3 + 0] = _bodyMat[12];
503
+ prevPos[i3 + 1] = _bodyMat[13];
504
+ prevPos[i3 + 2] = _bodyMat[14];
505
+ }
506
+ }
507
+ // Backstop: if a dynamic body's state went non-finite despite the velocity
508
+ // caps, restore its previous-substep pose with zero velocity instead of
509
+ // letting NaNs spread through constraints and contacts. Runs after every
510
+ // substep so prevState is always a finite pose.
511
+ restoreNonFiniteBodies() {
512
+ const N = this.store.count;
513
+ const positions = this.store.positions;
514
+ const orientations = this.store.orientations;
515
+ const lv = this.store.linearVelocities;
516
+ const av = this.store.angularVelocities;
517
+ const types = this.store.type;
518
+ const prevPos = this.prevPositions;
519
+ const prevOri = this.prevOrientations;
520
+ for (let i = 0; i < N; i++) {
521
+ if (types[i] !== RigidbodyType.Dynamic)
522
+ continue;
523
+ const i3 = i * 3;
524
+ const i4 = i * 4;
525
+ // NaN/Inf propagates through the sum, so one check covers all 13 slots.
526
+ const s = positions[i3 + 0] + positions[i3 + 1] + positions[i3 + 2] +
527
+ orientations[i4 + 0] + orientations[i4 + 1] + orientations[i4 + 2] + orientations[i4 + 3] +
528
+ lv[i3 + 0] + lv[i3 + 1] + lv[i3 + 2] +
529
+ av[i3 + 0] + av[i3 + 1] + av[i3 + 2];
530
+ if (Number.isFinite(s))
531
+ continue;
532
+ positions[i3 + 0] = prevPos[i3 + 0];
533
+ positions[i3 + 1] = prevPos[i3 + 1];
534
+ positions[i3 + 2] = prevPos[i3 + 2];
535
+ orientations[i4 + 0] = prevOri[i4 + 0];
536
+ orientations[i4 + 1] = prevOri[i4 + 1];
537
+ orientations[i4 + 2] = prevOri[i4 + 2];
538
+ orientations[i4 + 3] = prevOri[i4 + 3];
539
+ lv[i3 + 0] = 0;
540
+ lv[i3 + 1] = 0;
541
+ lv[i3 + 2] = 0;
542
+ av[i3 + 0] = 0;
543
+ av[i3 + 1] = 0;
544
+ av[i3 + 2] = 0;
196
545
  }
197
546
  }
198
547
  // Dynamic bodies write their transform back to the bone matrix:
@@ -253,7 +602,23 @@ export class RezePhysics {
253
602
  Mat4.multiplyArrays(_bodyMat, 0, inv, i * 16, _boneMat, 0);
254
603
  // Sanity gate against NaN / extreme values — silently drop the update.
255
604
  if (Number.isFinite(_boneMat[0]) && Math.abs(_boneMat[0]) < 1e6) {
256
- boneWorldMatrices[b].values.set(_boneMat);
605
+ const v = boneWorldMatrices[b].values;
606
+ if (this.alignPinned[i]) {
607
+ // Pinned mode-2: the bone keeps its animated position, physics
608
+ // drives rotation only.
609
+ v[0] = _boneMat[0];
610
+ v[1] = _boneMat[1];
611
+ v[2] = _boneMat[2];
612
+ v[4] = _boneMat[4];
613
+ v[5] = _boneMat[5];
614
+ v[6] = _boneMat[6];
615
+ v[8] = _boneMat[8];
616
+ v[9] = _boneMat[9];
617
+ v[10] = _boneMat[10];
618
+ }
619
+ else {
620
+ v.set(_boneMat);
621
+ }
257
622
  }
258
623
  }
259
624
  }
@@ -1 +1 @@
1
- {"version":3,"file":"solver.d.ts","sourceRoot":"","sources":["../../src/physics/solver.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAC5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAE1D,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,WAAW,CAAA;AAWrD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EACrB,WAAW,EAAE,sBAAsB,EAAE,EACrC,QAAQ,EAAE,WAAW,EACrB,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,MAAM,GACjB,IAAI,CAyBN"}
1
+ {"version":3,"file":"solver.d.ts","sourceRoot":"","sources":["../../src/physics/solver.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAC5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAE1D,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,WAAW,CAAA;AA6BrD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EACrB,WAAW,EAAE,sBAAsB,EAAE,EACrC,QAAQ,EAAE,WAAW,EACrB,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,MAAM,GACjB,IAAI,CAyBN"}