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
@@ -32,6 +32,34 @@ export class RezePhysics {
32
32
  private prevPositions: Float32Array
33
33
  private prevOrientations: Float32Array
34
34
 
35
+ // Per-frame kinematic body targets (boneWorld × bodyOffset). Kinematic
36
+ // bodies advance toward these incrementally per substep instead of jumping
37
+ // to the final pose before substep 1. Velocities come from the target
38
+ // trajectory (frame-to-frame target delta over render dt) — deriving them
39
+ // from the per-substep advancement instead ripples with the accumulator
40
+ // phase at render rates above 60 Hz and visibly excites cloth chains.
41
+ private kinTargetPos: Float32Array
42
+ private kinTargetOri: Float32Array
43
+ private kinTargetVel: Float32Array
44
+ private kinTargetAngVel: Float32Array
45
+
46
+ // For each dynamic body, the kinematic body its constraint chain roots at
47
+ // (-1 if unreachable). On a teleport, dynamic bodies are carried rigidly by
48
+ // their root's transform delta so cloth keeps its pose relative to the
49
+ // character instead of being dragged across the jump.
50
+ private kinRoot: Int32Array
51
+ // Kinematic bodies whose target jumped discontinuously this frame.
52
+ private teleportFlags: Uint8Array
53
+ // PMX mode-2 bodies jointed *directly* to a bone-follow body (breasts,
54
+ // chain roots). Only these get MMD's bone-position alignment: their bone's
55
+ // animated matrix is trustworthy, whereas deeper chain bones' animated
56
+ // matrices don't include the physics deflection of their parents, so
57
+ // pinning those would freeze the chain.
58
+ private alignPinned: Uint8Array
59
+ // Debug counter: frames on which a teleport (scrub/jump discontinuity)
60
+ // was detected and settled.
61
+ teleportCount = 0
62
+
35
63
  constructor(rigidbodies: Rigidbody[], joints: Joint[] = []) {
36
64
  this.rigidbodies = rigidbodies
37
65
  this.joints = joints
@@ -41,6 +69,52 @@ export class RezePhysics {
41
69
  this.contacts = new ContactPool()
42
70
  this.prevPositions = new Float32Array(this.store.count * 3)
43
71
  this.prevOrientations = new Float32Array(this.store.count * 4)
72
+ this.kinTargetPos = new Float32Array(this.store.count * 3)
73
+ this.kinTargetOri = new Float32Array(this.store.count * 4)
74
+ this.kinTargetVel = new Float32Array(this.store.count * 3)
75
+ this.kinTargetAngVel = new Float32Array(this.store.count * 3)
76
+ this.kinRoot = this.buildKinematicRoots()
77
+ this.teleportFlags = new Uint8Array(this.store.count)
78
+
79
+ this.alignPinned = new Uint8Array(this.store.count)
80
+ const types = this.store.type
81
+ for (const c of this.constraints) {
82
+ const tA = types[c.bodyA]
83
+ const tB = types[c.bodyB]
84
+ const aFollows = tA === RigidbodyType.Static || tA === RigidbodyType.Kinematic
85
+ const bFollows = tB === RigidbodyType.Static || tB === RigidbodyType.Kinematic
86
+ if (aFollows && this.store.aligned[c.bodyB]) this.alignPinned[c.bodyB] = 1
87
+ if (bFollows && this.store.aligned[c.bodyA]) this.alignPinned[c.bodyA] = 1
88
+ }
89
+ }
90
+
91
+ // BFS over the joint graph from every kinematic body, assigning each
92
+ // reachable dynamic body the kinematic body it (transitively) hangs off.
93
+ private buildKinematicRoots(): Int32Array {
94
+ const N = this.store.count
95
+ const root = new Int32Array(N).fill(-1)
96
+ const types = this.store.type
97
+ const adj: number[][] = Array.from({ length: N }, () => [])
98
+ for (const c of this.constraints) {
99
+ adj[c.bodyA].push(c.bodyB)
100
+ adj[c.bodyB].push(c.bodyA)
101
+ }
102
+ const queue: number[] = []
103
+ for (let i = 0; i < N; i++) {
104
+ if (types[i] === RigidbodyType.Static || types[i] === RigidbodyType.Kinematic) {
105
+ root[i] = i
106
+ queue.push(i)
107
+ }
108
+ }
109
+ for (let h = 0; h < queue.length; h++) {
110
+ const i = queue[h]
111
+ for (const j of adj[i]) {
112
+ if (root[j] !== -1) continue
113
+ root[j] = root[i]
114
+ queue.push(j)
115
+ }
116
+ }
117
+ return root
44
118
  }
45
119
 
46
120
  // Snapshot the current body pose as the interpolation "previous" state.
@@ -86,6 +160,13 @@ export class RezePhysics {
86
160
  if (this.firstFrame) return
87
161
  this.snapBodiesToBones(boneWorldMatrices)
88
162
  this.savePrevState() // prev == curr after a snap, so no interpolation jump
163
+ // Reseed kinematic targets from the snapped pose — otherwise the next
164
+ // step derives the target-trajectory velocity against the pre-reset
165
+ // targets and feeds one frame of enormous anchor velocity to the solver.
166
+ this.kinTargetPos.set(this.store.positions)
167
+ this.kinTargetOri.set(this.store.orientations)
168
+ this.kinTargetVel.fill(0)
169
+ this.kinTargetAngVel.fill(0)
89
170
  this.timeAccum = 0
90
171
  }
91
172
 
@@ -96,26 +177,57 @@ export class RezePhysics {
96
177
  // that skip frame 0 don't pop bodies on first step.
97
178
  this.snapBodiesToBones(boneWorldMatrices)
98
179
  this.savePrevState()
180
+ // Seed kinematic targets so the first frame's target-delta velocity
181
+ // is zero instead of a jump from the origin.
182
+ this.kinTargetPos.set(this.store.positions)
183
+ this.kinTargetOri.set(this.store.orientations)
99
184
  this.firstFrame = false
100
185
  }
101
186
 
102
- // Sync once per render frame; kinematic targets don't change between
103
- // substeps. Render dt is used to derive kinematic velocities from the
104
- // bone-pose delta so joints feel the kinematic motion.
105
- this.syncFromBones(boneWorldMatrices, dt)
187
+ // Compute this frame's kinematic targets from the current bone pose. A
188
+ // target jump beyond what continuous motion can produce (timeline scrub)
189
+ // is a teleport, handled per kinematic root: rigidly carry that root's
190
+ // dynamic chains across the jump with zeroed momentum, snap the root,
191
+ // and keep simulating — dragging cloth through a discontinuity at the
192
+ // raw derived velocity is what used to explode the solver. Chains under
193
+ // unaffected roots keep their momentum untouched.
194
+ if (this.computeKinematicTargets(boneWorldMatrices, dt)) {
195
+ this.teleportCount++
196
+ this.carryDynamicThroughTeleport()
197
+ this.snapKinematicToTargets(true)
198
+ this.savePrevState() // prev == curr so interpolation doesn't streak
199
+ }
106
200
 
107
201
  // Fixed-timestep substeps. The maxSubSteps cap prevents runaway after
108
202
  // a long stall (tab backgrounded, etc.). Snapshot the pose before each step so
109
203
  // after the loop prevState is one substep behind the live (current) state.
204
+ // Kinematic bodies split the remaining gap evenly across this frame's
205
+ // substeps (f = 1/substeps-left) and land exactly on the frame's bone
206
+ // pose by the last one: at 60 Hz render that reproduces the classic
207
+ // sync-once-per-frame behavior exactly (no fractional lag trembling
208
+ // against the rendered mesh), while at lower rates the per-substep
209
+ // constraint error stays bounded to one fixed step of bone motion.
110
210
  this.timeAccum += dt
111
- let sub = 0
112
- while (this.timeAccum >= this.fixedTimeStep && sub < this.maxSubSteps) {
211
+ let nSub = Math.floor(this.timeAccum / this.fixedTimeStep)
212
+ if (nSub > this.maxSubSteps) nSub = this.maxSubSteps
213
+ for (let k = 0; k < nSub; k++) {
113
214
  this.savePrevState()
215
+ this.advanceKinematicToTargets(1 / (nSub - k))
114
216
  this.world.step(this.store, this.constraints, this.contacts, this.fixedTimeStep)
217
+ this.restoreNonFiniteBodies()
115
218
  this.timeAccum -= this.fixedTimeStep
116
- sub++
117
219
  }
118
- if (sub === this.maxSubSteps) this.timeAccum = 0
220
+ if (this.timeAccum >= this.fixedTimeStep) {
221
+ // Substep budget exhausted mid-catchup: drop the remaining time and
222
+ // snap kinematic bodies the rest of the way (zero velocity) so they
223
+ // don't start next frame lagging behind their bones.
224
+ this.timeAccum = 0
225
+ this.snapKinematicToTargets(false)
226
+ }
227
+
228
+ // MMD mode-2 bone alignment for pinned (depth-1) bodies: position
229
+ // re-pins to the animated bone, rotation stays simulated.
230
+ this.alignPinnedBodiesToBones(boneWorldMatrices)
119
231
 
120
232
  // Fraction into the next (not-yet-taken) step; always in [0, 1).
121
233
  const alpha = this.fixedTimeStep > 0 ? this.timeAccum / this.fixedTimeStep : 0
@@ -158,22 +270,34 @@ export class RezePhysics {
158
270
  }
159
271
  }
160
272
 
161
- // Pull Static / Kinematic bodies to their bones and derive velocities
162
- // from the bone-pose delta joints attached to fast limbs need to see
163
- // the kinematic motion, not just the position jump, or dependent cloth
164
- // bodies lag behind quick movement.
165
- private syncFromBones(boneWorldMatrices: Mat4[], dt: number): void {
273
+ // Fill kinTargetPos/kinTargetOri = boneWorld × bodyOffset for every bone-
274
+ // bound Static/Kinematic body. Returns true if any target is discontinuous
275
+ // with the current body pose farther than continuous motion can carry it
276
+ // in one render frame, or rotated more than 90°.
277
+ private computeKinematicTargets(boneWorldMatrices: Mat4[], dt: number): boolean {
166
278
  const N = this.store.count
167
279
  const offsets = this.store.bodyOffsetMatrix
168
280
  const positions = this.store.positions
169
281
  const orientations = this.store.orientations
170
- const lv = this.store.linearVelocities
171
- const av = this.store.angularVelocities
172
282
  const types = this.store.type
173
283
  const boneIdx = this.store.boneIndex
284
+ const tp = this.kinTargetPos
285
+ const to = this.kinTargetOri
286
+
287
+ // 250 units/s scaled by frame time, floored at 4 units: far above the
288
+ // fastest limb motion (a false positive freezes that chain's momentum
289
+ // for a frame, which reads as stutter), far below any real scrub jump.
290
+ const maxJump = Math.max(4, 250 * dt)
291
+ const maxJumpSq = maxJump * maxJump
292
+ const flags = this.teleportFlags
293
+ let teleport = false
294
+
295
+ const tv = this.kinTargetVel
296
+ const tav = this.kinTargetAngVel
174
297
  const invDt = dt > 0 ? 1 / dt : 0
175
298
 
176
299
  for (let i = 0; i < N; i++) {
300
+ flags[i] = 0
177
301
  const t = types[i]
178
302
  if (t !== RigidbodyType.Static && t !== RigidbodyType.Kinematic) continue
179
303
  const b = boneIdx[i]
@@ -183,57 +307,291 @@ export class RezePhysics {
183
307
 
184
308
  const i3 = i * 3
185
309
  const i4 = i * 4
310
+ // Previous frame's target — the reference for the trajectory velocity.
311
+ const oldTx = tp[i3 + 0], oldTy = tp[i3 + 1], oldTz = tp[i3 + 2]
312
+ const oldOx = to[i4 + 0], oldOy = to[i4 + 1], oldOz = to[i4 + 2], oldOw = to[i4 + 3]
313
+
314
+ tp[i3 + 0] = _bodyMat[12]
315
+ tp[i3 + 1] = _bodyMat[13]
316
+ tp[i3 + 2] = _bodyMat[14]
317
+ Mat4.toQuatFromArrayInto(_bodyMat, 0, _scratchQuat)
318
+ const nOx = _scratchQuat.x, nOy = _scratchQuat.y, nOz = _scratchQuat.z, nOw = _scratchQuat.w
319
+ to[i4 + 0] = nOx
320
+ to[i4 + 1] = nOy
321
+ to[i4 + 2] = nOz
322
+ to[i4 + 3] = nOw
323
+
324
+ const dx = tp[i3 + 0] - positions[i3 + 0]
325
+ const dy = tp[i3 + 1] - positions[i3 + 1]
326
+ const dz = tp[i3 + 2] - positions[i3 + 2]
327
+ // |q·q'| = cos(θ/2); below cos(45°) the body turned more than 90°.
328
+ const dot =
329
+ to[i4 + 0] * orientations[i4 + 0] +
330
+ to[i4 + 1] * orientations[i4 + 1] +
331
+ to[i4 + 2] * orientations[i4 + 2] +
332
+ to[i4 + 3] * orientations[i4 + 3]
333
+ if (dx * dx + dy * dy + dz * dz > maxJumpSq || Math.abs(dot) < 0.7071) {
334
+ flags[i] = 1
335
+ teleport = true
336
+ tv[i3 + 0] = 0; tv[i3 + 1] = 0; tv[i3 + 2] = 0
337
+ tav[i3 + 0] = 0; tav[i3 + 1] = 0; tav[i3 + 2] = 0
338
+ continue
339
+ }
340
+
341
+ tv[i3 + 0] = (tp[i3 + 0] - oldTx) * invDt
342
+ tv[i3 + 1] = (tp[i3 + 1] - oldTy) * invDt
343
+ tv[i3 + 2] = (tp[i3 + 2] - oldTz) * invDt
344
+ // ω ≈ 2 · qDiff.xyz / dt with qDiff = qNew · conj(qOld). Shortest-arc
345
+ // sign keeps qDiff and −qDiff (same rotation) from doubling ω.
346
+ const cox = -oldOx, coy = -oldOy, coz = -oldOz, cow = oldOw
347
+ const qdx = nOw * cox + nOx * cow + nOy * coz - nOz * coy
348
+ const qdy = nOw * coy - nOx * coz + nOy * cow + nOz * cox
349
+ const qdz = nOw * coz + nOx * coy - nOy * cox + nOz * cow
350
+ const qdw = nOw * cow - nOx * cox - nOy * coy - nOz * coz
351
+ const sign = qdw < 0 ? -1 : 1
352
+ tav[i3 + 0] = 2 * sign * qdx * invDt
353
+ tav[i3 + 1] = 2 * sign * qdy * invDt
354
+ tav[i3 + 2] = 2 * sign * qdz * invDt
355
+ }
356
+ return teleport
357
+ }
358
+
359
+ // Move kinematic bodies fraction f of the way to the frame target with the
360
+ // target-trajectory velocity, so joints see continuous anchor motion (and a
361
+ // smooth velocity signal) instead of a frame-sized jump on substep 1.
362
+ private advanceKinematicToTargets(f: number): void {
363
+ const N = this.store.count
364
+ const positions = this.store.positions
365
+ const orientations = this.store.orientations
366
+ const lv = this.store.linearVelocities
367
+ const av = this.store.angularVelocities
368
+ const types = this.store.type
369
+ const boneIdx = this.store.boneIndex
370
+ const tp = this.kinTargetPos
371
+ const to = this.kinTargetOri
372
+ const tv = this.kinTargetVel
373
+ const tav = this.kinTargetAngVel
374
+
375
+ for (let i = 0; i < N; i++) {
376
+ const t = types[i]
377
+ if (t !== RigidbodyType.Static && t !== RigidbodyType.Kinematic) continue
378
+ const b = boneIdx[i]
379
+ if (b < 0) continue
186
380
 
187
- // Save previous transform for the velocity diff. invDt = 0 (first
188
- // frame / reset) skips the diff and zeros velocities.
189
- const oldPx = positions[i3 + 0],
190
- oldPy = positions[i3 + 1],
191
- oldPz = positions[i3 + 2]
381
+ const i3 = i * 3
382
+ const i4 = i * 4
383
+ positions[i3 + 0] += (tp[i3 + 0] - positions[i3 + 0]) * f
384
+ positions[i3 + 1] += (tp[i3 + 1] - positions[i3 + 1]) * f
385
+ positions[i3 + 2] += (tp[i3 + 2] - positions[i3 + 2]) * f
386
+ lv[i3 + 0] = tv[i3 + 0]
387
+ lv[i3 + 1] = tv[i3 + 1]
388
+ lv[i3 + 2] = tv[i3 + 2]
389
+ av[i3 + 0] = tav[i3 + 0]
390
+ av[i3 + 1] = tav[i3 + 1]
391
+ av[i3 + 2] = tav[i3 + 2]
392
+
393
+ // Shortest-arc nlerp toward the target orientation.
192
394
  const oldOx = orientations[i4 + 0],
193
395
  oldOy = orientations[i4 + 1]
194
396
  const oldOz = orientations[i4 + 2],
195
397
  oldOw = orientations[i4 + 3]
196
-
197
- positions[i3 + 0] = _bodyMat[12]
198
- positions[i3 + 1] = _bodyMat[13]
199
- positions[i3 + 2] = _bodyMat[14]
200
- Mat4.toQuatFromArrayInto(_bodyMat, 0, _scratchQuat)
201
- const newOx = _scratchQuat.x,
202
- newOy = _scratchQuat.y
203
- const newOz = _scratchQuat.z,
204
- newOw = _scratchQuat.w
398
+ let tx = to[i4 + 0],
399
+ ty = to[i4 + 1],
400
+ tz = to[i4 + 2],
401
+ tw = to[i4 + 3]
402
+ if (oldOx * tx + oldOy * ty + oldOz * tz + oldOw * tw < 0) {
403
+ tx = -tx; ty = -ty; tz = -tz; tw = -tw
404
+ }
405
+ let newOx = oldOx + (tx - oldOx) * f
406
+ let newOy = oldOy + (ty - oldOy) * f
407
+ let newOz = oldOz + (tz - oldOz) * f
408
+ let newOw = oldOw + (tw - oldOw) * f
409
+ const len2 = newOx * newOx + newOy * newOy + newOz * newOz + newOw * newOw
410
+ if (len2 > 1e-12) {
411
+ const inv = 1 / Math.sqrt(len2)
412
+ newOx *= inv; newOy *= inv; newOz *= inv; newOw *= inv
413
+ } else {
414
+ newOx = tx; newOy = ty; newOz = tz; newOw = tw
415
+ }
205
416
  orientations[i4 + 0] = newOx
206
417
  orientations[i4 + 1] = newOy
207
418
  orientations[i4 + 2] = newOz
208
419
  orientations[i4 + 3] = newOw
420
+ }
421
+ }
209
422
 
210
- if (invDt === 0) {
211
- lv[i3 + 0] = 0
212
- lv[i3 + 1] = 0
213
- lv[i3 + 2] = 0
214
- av[i3 + 0] = 0
215
- av[i3 + 1] = 0
216
- av[i3 + 2] = 0
217
- } else {
218
- lv[i3 + 0] = (_bodyMat[12] - oldPx) * invDt
219
- lv[i3 + 1] = (_bodyMat[13] - oldPy) * invDt
220
- lv[i3 + 2] = (_bodyMat[14] - oldPz) * invDt
221
-
222
- // ω ≈ 2 · qDiff.xyz / dt with qDiff = qNew · conj(qOld). Shortest-
223
- // arc sign keeps qDiff and −qDiff (same rotation) from doubling ω.
224
- const cox = -oldOx,
225
- coy = -oldOy,
226
- coz = -oldOz,
227
- cow = oldOw
228
- const dx = newOw * cox + newOx * cow + newOy * coz - newOz * coy
229
- const dy = newOw * coy - newOx * coz + newOy * cow + newOz * cox
230
- const dz = newOw * coz + newOx * coy - newOy * cox + newOz * cow
231
- const dw = newOw * cow - newOx * cox - newOy * coy - newOz * coz
232
- const sign = dw < 0 ? -1 : 1
233
- av[i3 + 0] = 2 * sign * dx * invDt
234
- av[i3 + 1] = 2 * sign * dy * invDt
235
- av[i3 + 2] = 2 * sign * dz * invDt
423
+ // Snap kinematic bodies straight to the frame target with zero velocity.
424
+ // Used on teleports (onlyFlagged) and when the substep budget runs out
425
+ // mid-catchup (all).
426
+ private snapKinematicToTargets(onlyFlagged: boolean): void {
427
+ const N = this.store.count
428
+ const positions = this.store.positions
429
+ const orientations = this.store.orientations
430
+ const lv = this.store.linearVelocities
431
+ const av = this.store.angularVelocities
432
+ const types = this.store.type
433
+ const boneIdx = this.store.boneIndex
434
+ const tp = this.kinTargetPos
435
+ const to = this.kinTargetOri
436
+ const flags = this.teleportFlags
437
+
438
+ for (let i = 0; i < N; i++) {
439
+ const t = types[i]
440
+ if (t !== RigidbodyType.Static && t !== RigidbodyType.Kinematic) continue
441
+ if (boneIdx[i] < 0) continue
442
+ if (onlyFlagged && !flags[i]) continue
443
+ const i3 = i * 3
444
+ const i4 = i * 4
445
+ positions[i3 + 0] = tp[i3 + 0]
446
+ positions[i3 + 1] = tp[i3 + 1]
447
+ positions[i3 + 2] = tp[i3 + 2]
448
+ orientations[i4 + 0] = to[i4 + 0]
449
+ orientations[i4 + 1] = to[i4 + 1]
450
+ orientations[i4 + 2] = to[i4 + 2]
451
+ orientations[i4 + 3] = to[i4 + 3]
452
+ lv[i3 + 0] = 0; lv[i3 + 1] = 0; lv[i3 + 2] = 0
453
+ av[i3 + 0] = 0; av[i3 + 1] = 0; av[i3 + 2] = 0
454
+ }
455
+ }
456
+
457
+ // Rigidly carry each dynamic body whose kinematic root teleported along
458
+ // with that root's current→target transform delta (velocity zeroed),
459
+ // preserving the cloth pose relative to the character across the jump.
460
+ // Must run before snapKinematicToTargets (it reads the pre-snap pose).
461
+ private carryDynamicThroughTeleport(): void {
462
+ const N = this.store.count
463
+ const positions = this.store.positions
464
+ const orientations = this.store.orientations
465
+ const lv = this.store.linearVelocities
466
+ const av = this.store.angularVelocities
467
+ const types = this.store.type
468
+ const tp = this.kinTargetPos
469
+ const to = this.kinTargetOri
470
+ const root = this.kinRoot
471
+ const flags = this.teleportFlags
472
+
473
+ for (let i = 0; i < N; i++) {
474
+ if (types[i] !== RigidbodyType.Dynamic) continue
475
+ const k = root[i]
476
+ if (k < 0 || !flags[k] || this.store.boneIndex[k] < 0) continue
477
+ const k3 = k * 3
478
+ const k4 = k * 4
479
+ // Root delta rotation R = qTarget · conj(qCurrent).
480
+ const cx = -orientations[k4 + 0],
481
+ cy = -orientations[k4 + 1],
482
+ cz = -orientations[k4 + 2],
483
+ cw = orientations[k4 + 3]
484
+ const txq = to[k4 + 0],
485
+ tyq = to[k4 + 1],
486
+ tzq = to[k4 + 2],
487
+ twq = to[k4 + 3]
488
+ const rx = twq * cx + txq * cw + tyq * cz - tzq * cy
489
+ const ry = twq * cy - txq * cz + tyq * cw + tzq * cx
490
+ const rz = twq * cz + txq * cy - tyq * cx + tzq * cw
491
+ const rw = twq * cw - txq * cx - tyq * cy - tzq * cz
492
+
493
+ const i3 = i * 3
494
+ const i4 = i * 4
495
+ // Position: rotate the offset from the root by R, re-anchor at target.
496
+ const ox = positions[i3 + 0] - positions[k3 + 0]
497
+ const oy = positions[i3 + 1] - positions[k3 + 1]
498
+ const oz = positions[i3 + 2] - positions[k3 + 2]
499
+ // v' = v + 2·rw·(r × v) + 2·(r × (r × v))
500
+ const c1x = ry * oz - rz * oy
501
+ const c1y = rz * ox - rx * oz
502
+ const c1z = rx * oy - ry * ox
503
+ const c2x = ry * c1z - rz * c1y
504
+ const c2y = rz * c1x - rx * c1z
505
+ const c2z = rx * c1y - ry * c1x
506
+ positions[i3 + 0] = tp[k3 + 0] + ox + 2 * (rw * c1x + c2x)
507
+ positions[i3 + 1] = tp[k3 + 1] + oy + 2 * (rw * c1y + c2y)
508
+ positions[i3 + 2] = tp[k3 + 2] + oz + 2 * (rw * c1z + c2z)
509
+
510
+ // Orientation: q' = R · q, renormalized.
511
+ const qx = orientations[i4 + 0],
512
+ qy = orientations[i4 + 1],
513
+ qz = orientations[i4 + 2],
514
+ qw = orientations[i4 + 3]
515
+ let nx = rw * qx + rx * qw + ry * qz - rz * qy
516
+ let ny = rw * qy - rx * qz + ry * qw + rz * qx
517
+ let nz = rw * qz + rx * qy - ry * qx + rz * qw
518
+ let nw = rw * qw - rx * qx - ry * qy - rz * qz
519
+ const len2 = nx * nx + ny * ny + nz * nz + nw * nw
520
+ if (len2 > 1e-12) {
521
+ const inv = 1 / Math.sqrt(len2)
522
+ nx *= inv; ny *= inv; nz *= inv; nw *= inv
523
+ orientations[i4 + 0] = nx
524
+ orientations[i4 + 1] = ny
525
+ orientations[i4 + 2] = nz
526
+ orientations[i4 + 3] = nw
236
527
  }
528
+
529
+ // Momentum doesn't carry across a discontinuity.
530
+ lv[i3 + 0] = 0; lv[i3 + 1] = 0; lv[i3 + 2] = 0
531
+ av[i3 + 0] = 0; av[i3 + 1] = 0; av[i3 + 2] = 0
532
+ }
533
+ }
534
+
535
+ // Overwrite pinned mode-2 bodies' positions with boneWorld × bodyOffset
536
+ // from the animated bone pose, keeping simulated orientation and
537
+ // velocities. prevPositions follows so interpolation doesn't streak.
538
+ private alignPinnedBodiesToBones(boneWorldMatrices: Mat4[]): void {
539
+ const N = this.store.count
540
+ const pinned = this.alignPinned
541
+ const boneIdx = this.store.boneIndex
542
+ const offsets = this.store.bodyOffsetMatrix
543
+ const positions = this.store.positions
544
+ const prevPos = this.prevPositions
545
+
546
+ for (let i = 0; i < N; i++) {
547
+ if (!pinned[i]) continue
548
+ const b = boneIdx[i]
549
+ if (b < 0 || b >= boneWorldMatrices.length) continue
550
+ Mat4.multiplyArrays(boneWorldMatrices[b].values, 0, offsets, i * 16, _bodyMat, 0)
551
+ const i3 = i * 3
552
+ positions[i3 + 0] = _bodyMat[12]
553
+ positions[i3 + 1] = _bodyMat[13]
554
+ positions[i3 + 2] = _bodyMat[14]
555
+ prevPos[i3 + 0] = _bodyMat[12]
556
+ prevPos[i3 + 1] = _bodyMat[13]
557
+ prevPos[i3 + 2] = _bodyMat[14]
558
+ }
559
+ }
560
+
561
+ // Backstop: if a dynamic body's state went non-finite despite the velocity
562
+ // caps, restore its previous-substep pose with zero velocity instead of
563
+ // letting NaNs spread through constraints and contacts. Runs after every
564
+ // substep so prevState is always a finite pose.
565
+ private restoreNonFiniteBodies(): void {
566
+ const N = this.store.count
567
+ const positions = this.store.positions
568
+ const orientations = this.store.orientations
569
+ const lv = this.store.linearVelocities
570
+ const av = this.store.angularVelocities
571
+ const types = this.store.type
572
+ const prevPos = this.prevPositions
573
+ const prevOri = this.prevOrientations
574
+
575
+ for (let i = 0; i < N; i++) {
576
+ if (types[i] !== RigidbodyType.Dynamic) continue
577
+ const i3 = i * 3
578
+ const i4 = i * 4
579
+ // NaN/Inf propagates through the sum, so one check covers all 13 slots.
580
+ const s =
581
+ positions[i3 + 0] + positions[i3 + 1] + positions[i3 + 2] +
582
+ orientations[i4 + 0] + orientations[i4 + 1] + orientations[i4 + 2] + orientations[i4 + 3] +
583
+ lv[i3 + 0] + lv[i3 + 1] + lv[i3 + 2] +
584
+ av[i3 + 0] + av[i3 + 1] + av[i3 + 2]
585
+ if (Number.isFinite(s)) continue
586
+ positions[i3 + 0] = prevPos[i3 + 0]
587
+ positions[i3 + 1] = prevPos[i3 + 1]
588
+ positions[i3 + 2] = prevPos[i3 + 2]
589
+ orientations[i4 + 0] = prevOri[i4 + 0]
590
+ orientations[i4 + 1] = prevOri[i4 + 1]
591
+ orientations[i4 + 2] = prevOri[i4 + 2]
592
+ orientations[i4 + 3] = prevOri[i4 + 3]
593
+ lv[i3 + 0] = 0; lv[i3 + 1] = 0; lv[i3 + 2] = 0
594
+ av[i3 + 0] = 0; av[i3 + 1] = 0; av[i3 + 2] = 0
237
595
  }
238
596
  }
239
597
 
@@ -289,7 +647,16 @@ export class RezePhysics {
289
647
 
290
648
  // Sanity gate against NaN / extreme values — silently drop the update.
291
649
  if (Number.isFinite(_boneMat[0]) && Math.abs(_boneMat[0]) < 1e6) {
292
- boneWorldMatrices[b].values.set(_boneMat)
650
+ const v = boneWorldMatrices[b].values
651
+ if (this.alignPinned[i]) {
652
+ // Pinned mode-2: the bone keeps its animated position, physics
653
+ // drives rotation only.
654
+ v[0] = _boneMat[0]; v[1] = _boneMat[1]; v[2] = _boneMat[2]
655
+ v[4] = _boneMat[4]; v[5] = _boneMat[5]; v[6] = _boneMat[6]
656
+ v[8] = _boneMat[8]; v[9] = _boneMat[9]; v[10] = _boneMat[10]
657
+ } else {
658
+ v.set(_boneMat)
659
+ }
293
660
  }
294
661
  }
295
662
  }