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
package/src/model.ts CHANGED
@@ -43,6 +43,9 @@ export interface Material {
43
43
  sphereTextureIndex: number
44
44
  sphereMode: number
45
45
  toonTextureIndex: number
46
+ // True when toonTextureIndex refers to the shared toon set (toon01–10)
47
+ // instead of the model's texture table.
48
+ sharedToon: boolean
46
49
  edgeFlag: number
47
50
  edgeColor: [number, number, number, number]
48
51
  edgeSize: number
@@ -235,6 +238,9 @@ export class Model {
235
238
  private rigidbodies: Rigidbody[] = []
236
239
  private joints: Joint[] = []
237
240
 
241
+ // Non-fatal problems collected while parsing the PMX (see PmxLoader.warn).
242
+ private loadWarnings: string[] = []
243
+
238
244
  // Runtime skeleton pose state (updated each frame)
239
245
  private runtimeSkeleton!: SkeletonRuntime
240
246
 
@@ -293,7 +299,8 @@ export class Model {
293
299
  skinning: Skinning,
294
300
  morphing: Morphing,
295
301
  rigidbodies: Rigidbody[] = [],
296
- joints: Joint[] = []
302
+ joints: Joint[] = [],
303
+ loadWarnings: string[] = []
297
304
  ) {
298
305
  // Store base vertex data (original positions before morphing)
299
306
  this.baseVertexData = new Float32Array(vertexData)
@@ -307,6 +314,7 @@ export class Model {
307
314
  this.morphing = morphing
308
315
  this.rigidbodies = rigidbodies
309
316
  this.joints = joints
317
+ this.loadWarnings = loadWarnings
310
318
 
311
319
  if (this.skeleton.bones.length == 0) {
312
320
  throw new Error("Model has no bones")
@@ -631,6 +639,18 @@ export class Model {
631
639
  return this.skinning
632
640
  }
633
641
 
642
+ // True when the PMX carried a usable rigidbody section. False means the
643
+ // model renders but has no physics — surface this in the UI instead of
644
+ // letting it read as "physics silently broken".
645
+ hasPhysicsData(): boolean {
646
+ return this.rigidbodies.length > 0
647
+ }
648
+
649
+ // Non-fatal PMX parse problems (truncated sections, suspicious counts…).
650
+ getLoadWarnings(): readonly string[] {
651
+ return this.loadWarnings
652
+ }
653
+
634
654
  getRigidbodies(): Rigidbody[] {
635
655
  return this.rigidbodies
636
656
  }
@@ -12,13 +12,20 @@ export class RigidBodyStore {
12
12
  readonly angularVelocities: Float32Array // 3*N
13
13
 
14
14
  readonly invMass: Float32Array // N (0 for static / kinematic)
15
- // Scalar isotropic inverse inertia. The full 3×3 tensor would be more
16
- // accurate but PMX shapes are roughly compact, and a single I⁻¹ is far
17
- // better than reusing invMass (which under-rotates by 100–1000×).
18
- readonly invInertia: Float32Array
15
+ // Full anisotropic inertia. Local diagonal (body frame, Bullet's shape
16
+ // formulas) plus the per-substep world tensor I⁻¹ = R·diag·Rᵀ (9 floats
17
+ // row-major, symmetric). The old scalar approximation deposited constraint
18
+ // impulses into rotational modes at the wrong rates on elongated capsules
19
+ // (5:1 skirt/hair bodies are ~25× anisotropic), leaving cloth with several
20
+ // times the kinetic energy real Bullet retains — visible as perpetual boil.
21
+ readonly invInertiaLocal: Float32Array // 3*N
22
+ readonly invInertiaWorld: Float32Array // 9*N
19
23
  readonly linearDamping: Float32Array
20
24
  readonly angularDamping: Float32Array
21
25
  readonly type: Uint8Array
26
+ // PMX mode-2 bodies: dynamic, but the bone takes rotation only and the
27
+ // body position re-pins to the animated bone each frame.
28
+ readonly aligned: Uint8Array
22
29
  readonly boneIndex: Int32Array
23
30
  readonly friction: Float32Array
24
31
  readonly restitution: Float32Array
@@ -55,10 +62,12 @@ export class RigidBodyStore {
55
62
  this.linearVelocities = new Float32Array(N * 3)
56
63
  this.angularVelocities = new Float32Array(N * 3)
57
64
  this.invMass = new Float32Array(N)
58
- this.invInertia = new Float32Array(N)
65
+ this.invInertiaLocal = new Float32Array(N * 3)
66
+ this.invInertiaWorld = new Float32Array(N * 9)
59
67
  this.linearDamping = new Float32Array(N)
60
68
  this.angularDamping = new Float32Array(N)
61
69
  this.type = new Uint8Array(N)
70
+ this.aligned = new Uint8Array(N)
62
71
  this.boneIndex = new Int32Array(N)
63
72
  this.bodyOffsetMatrix = new Float32Array(N * 16)
64
73
  this.bodyOffsetInverse = new Float32Array(N * 16)
@@ -88,10 +97,11 @@ export class RigidBodyStore {
88
97
 
89
98
  const dynamic = rb.type === RigidbodyType.Dynamic && rb.mass > 0
90
99
  this.invMass[i] = dynamic ? 1 / rb.mass : 0
91
- this.invInertia[i] = dynamic ? computeInvInertia(rb) : 0
100
+ if (dynamic) computeLocalInvInertia(rb, this.invInertiaLocal, i * 3)
92
101
  this.linearDamping[i] = rb.linearDamping
93
102
  this.angularDamping[i] = rb.angularDamping
94
103
  this.type[i] = rb.type
104
+ this.aligned[i] = rb.aligned ? 1 : 0
95
105
  this.boneIndex[i] = rb.boneIndex
96
106
  this.friction[i] = rb.friction
97
107
  this.restitution[i] = rb.restitution
@@ -104,6 +114,47 @@ export class RigidBodyStore {
104
114
  }
105
115
  }
106
116
 
117
+ // Refresh I⁻¹_world = R·diag(invInertiaLocal)·Rᵀ for every dynamic body.
118
+ // Called once per substep before constraint setup (orientations are
119
+ // constant during a solve).
120
+ updateInvInertiaWorld(): void {
121
+ const N = this.count
122
+ const ori = this.orientations
123
+ const local = this.invInertiaLocal
124
+ const W = this.invInertiaWorld
125
+ const invMass = this.invMass
126
+
127
+ for (let i = 0; i < N; i++) {
128
+ if (invMass[i] <= 0) continue
129
+ const i3 = i * 3
130
+ const i4 = i * 4
131
+ const i9 = i * 9
132
+ const qx = ori[i4 + 0], qy = ori[i4 + 1], qz = ori[i4 + 2], qw = ori[i4 + 3]
133
+ const x2 = qx + qx, y2 = qy + qy, z2 = qz + qz
134
+ const xx = qx * x2, yy = qy * y2, zz = qz * z2
135
+ const xy = qx * y2, xz = qx * z2, yz = qy * z2
136
+ const wx = qw * x2, wy = qw * y2, wz = qw * z2
137
+ // R columns (column-major rotation matrix)
138
+ const r00 = 1 - (yy + zz), r01 = xy - wz, r02 = xz + wy
139
+ const r10 = xy + wz, r11 = 1 - (xx + zz), r12 = yz - wx
140
+ const r20 = xz - wy, r21 = yz + wx, r22 = 1 - (xx + yy)
141
+ const d0 = local[i3 + 0], d1 = local[i3 + 1], d2 = local[i3 + 2]
142
+ // W = R·diag·Rᵀ (symmetric)
143
+ const a0 = r00 * d0, a1 = r01 * d1, a2 = r02 * d2
144
+ const b0 = r10 * d0, b1 = r11 * d1, b2 = r12 * d2
145
+ const c0 = r20 * d0, c1 = r21 * d1, c2 = r22 * d2
146
+ const w00 = a0 * r00 + a1 * r01 + a2 * r02
147
+ const w01 = a0 * r10 + a1 * r11 + a2 * r12
148
+ const w02 = a0 * r20 + a1 * r21 + a2 * r22
149
+ const w11 = b0 * r10 + b1 * r11 + b2 * r12
150
+ const w12 = b0 * r20 + b1 * r21 + b2 * r22
151
+ const w22 = c0 * r20 + c1 * r21 + c2 * r22
152
+ W[i9 + 0] = w00; W[i9 + 1] = w01; W[i9 + 2] = w02
153
+ W[i9 + 3] = w01; W[i9 + 4] = w11; W[i9 + 5] = w12
154
+ W[i9 + 6] = w02; W[i9 + 7] = w12; W[i9 + 8] = w22
155
+ }
156
+ }
157
+
107
158
  // World-space AABBs for every body. Inflated by margin so contacts stay
108
159
  // paired across small velocity jitter without recomputing per iteration.
109
160
  updateAabbs(margin = 0.5): void {
@@ -282,36 +333,48 @@ const _scratchA = new Float32Array(16)
282
333
  const _scratchB = new Float32Array(16)
283
334
  const _scratchC = new Float32Array(16)
284
335
 
285
- // Scalar isotropic inverse inertia. Returns 0 for static bodies (mass = 0).
286
- // Sphere: I = (2/5)·m·r²
287
- // Box: I = (1/3)·m·max(a,b,c)²
288
- // Capsule: I = (1/12)·(3r² + h²) (cylinder transverse axis)
289
- function computeInvInertia(rb: Rigidbody): number {
336
+ // Diagonal local inverse inertia, matching Bullet's calculateLocalInertia
337
+ // per shape (so behavior tracks Ammo-based MMD engines):
338
+ // Sphere: I = (2/5)·m·r² on every axis
339
+ // Box: Ix = m/12·(ly²+lz²), with l = full extents (2·half)
340
+ // Capsule: Bullet's bounding-box approximation of the Y-axis capsule
341
+ // (lx = lz = 2r, ly = h + 2r)
342
+ function computeLocalInvInertia(rb: Rigidbody, out: Float32Array, o: number): void {
290
343
  const m = rb.mass
291
- if (m <= 0) return 0
292
- let I: number
344
+ if (m <= 0) return
345
+ let Ix: number, Iy: number, Iz: number
293
346
  switch (rb.shape) {
294
347
  case RigidbodyShape.Sphere: {
295
- const r = rb.size.x
296
- I = 0.4 * m * r * r
348
+ const I = 0.4 * m * rb.size.x * rb.size.x
349
+ Ix = I; Iy = I; Iz = I
297
350
  break
298
351
  }
299
352
  case RigidbodyShape.Box: {
300
- // Largest half-extent so the long axis isn't under-rotated.
301
- const a = Math.max(rb.size.x, rb.size.y, rb.size.z)
302
- I = (1 / 3) * m * a * a
353
+ const lx2 = 4 * rb.size.x * rb.size.x
354
+ const ly2 = 4 * rb.size.y * rb.size.y
355
+ const lz2 = 4 * rb.size.z * rb.size.z
356
+ Ix = (m / 12) * (ly2 + lz2)
357
+ Iy = (m / 12) * (lx2 + lz2)
358
+ Iz = (m / 12) * (lx2 + ly2)
303
359
  break
304
360
  }
305
361
  case RigidbodyShape.Capsule: {
306
- const r = rb.size.x
307
- const h = rb.size.y
308
- I = (1 / 12) * m * (3 * r * r + h * h)
362
+ const lx = 2 * rb.size.x
363
+ const ly = rb.size.y + 2 * rb.size.x
364
+ const lx2 = lx * lx
365
+ const ly2 = ly * ly
366
+ Ix = (m / 12) * (ly2 + lx2)
367
+ Iy = (m / 12) * (lx2 + lx2)
368
+ Iz = (m / 12) * (lx2 + ly2)
309
369
  break
310
370
  }
311
- default:
312
- I = m
371
+ default: {
372
+ Ix = m; Iy = m; Iz = m
373
+ }
313
374
  }
314
- return I > 0 ? 1 / I : 0
375
+ out[o + 0] = Ix > 0 ? 1 / Ix : 0
376
+ out[o + 1] = Iy > 0 ? 1 / Iy : 0
377
+ out[o + 2] = Iz > 0 ? 1 / Iz : 0
315
378
  }
316
379
 
317
380
  function identity16(out: Float32Array, offset: number): void {
@@ -1,5 +1,6 @@
1
1
  import { Mat4 } from "../math"
2
2
  import type { Joint, Rigidbody } from "./types"
3
+ import { RigidbodyType } from "./types"
3
4
 
4
5
  // 6DOF spring constraint trimmed to what MMD uses. Connects bodyA and bodyB
5
6
  // via local-space anchor frames; at simulate time the world frames are
@@ -26,6 +27,11 @@ export interface SixDofSpringConstraint {
26
27
  springEnabled: Uint8Array // length 6
27
28
  springStiffness: Float32Array // length 6 (k)
28
29
  equilibriumPoint: Float32Array// length 6, baked at setup time
30
+ // True for joints that close a cycle in the joint graph (e.g. the
31
+ // horizontal ring welds of cross-linked skirt lattices). Loop edges get
32
+ // reduced limit-correction rates in the solver — a cycle over-determines
33
+ // positions and full-rate corrections chatter (see LOOP_ERP_SCALE).
34
+ isLoop: boolean
29
35
 
30
36
  // Per-substep cache. Filled by solver's setup pass once before SI iters,
31
37
  // read by the velocity-only iter loop. None of these depend on lv/av — only
@@ -34,8 +40,8 @@ export interface SixDofSpringConstraint {
34
40
  cacheLeverA: Float32Array // 3: rA = anchor − posA (world-space)
35
41
  cacheLeverB: Float32Array // 3
36
42
  cacheLinAxes: Float32Array // 9: 3 linear axes × xyz, world-space
37
- cacheLinCrossA: Float32Array // 9: (rA × ax) per axis
38
- cacheLinCrossB: Float32Array // 9
43
+ cacheLinCrossA: Float32Array // 9: I⁻¹A·(rA × ax) per axis
44
+ cacheLinCrossB: Float32Array // 9: I⁻¹B·(rB × ax) per axis
39
45
  cacheLinJacInv: Float32Array // 3: 1/(im+im+cA²·ii+cB²·ii) per axis
40
46
  // Limit rows are unilateral (Bullet-style): the per-substep accumulated
41
47
  // impulse is clamped to the corrective sign, so a limit can push a body
@@ -45,24 +51,54 @@ export interface SixDofSpringConstraint {
45
51
  cacheLinActive: Uint8Array // 3
46
52
  cacheLinLimitImp: Float32Array // 3: accumulated limit impulse (per substep)
47
53
  // Spring rows are velocity-target drives; setup clamps k to the deadbeat
48
- // stability bound so they cannot pump energy.
54
+ // stability bound so they cannot pump energy. maxImp bounds the per-substep
55
+ // accumulated impulse by the real spring force (k·|err|·dt) for rows that
56
+ // must stay force-limited (loop-edge welds); Infinity for authored springs.
49
57
  cacheLinSpringTarget: Float32Array // 3
58
+ cacheLinSpringMaxImp: Float32Array // 3
59
+ cacheLinSpringImp: Float32Array // 3
50
60
  cacheLinSpringActive: Uint8Array // 3
51
61
  cacheAngAxes: Float32Array // 9 (spring rows)
52
62
  cacheAngTargetVel: Float32Array // 3 (spring rows)
53
63
  cacheAngActive: Uint8Array // 3 (spring rows)
54
- cacheAngJacInv: number // 1 same for all angular rows
64
+ // Per-axis angular Jacobians and tensor-multiplied axes: with full inertia
65
+ // tensors the effective mass differs per axis, and impulse application
66
+ // needs I⁻¹·axis per body.
67
+ cacheAngJacInv: Float32Array // 3: 1/(axᵀ(I⁻¹A+I⁻¹B)ax) per axis
68
+ // Angular spring force clamp (|k·err|·dt) + per-substep accumulator —
69
+ // unclamped spring velocity-drives pump energy slowly (the restoring
70
+ // magnitude on derived euler axes is orientation-dependent by up to ~25%,
71
+ // and a mis-scaled oscillator restoring force injects per cycle).
72
+ cacheAngSpringMaxImp: Float32Array // 3
73
+ cacheAngSpringImp: Float32Array // 3
74
+ cacheAngWA: Float32Array // 9: I⁻¹A·axis per axis
75
+ cacheAngWB: Float32Array // 9
55
76
  // Single geodesic limit row: shortest rotation from the current relative
56
77
  // orientation to the euler-clamped target. Per-axis euler limit rows are
57
78
  // geometrically inconsistent for large violations (asin singularity) and
58
79
  // pump energy instead of converging. Unilateral like the linear limits.
59
80
  cacheAngLimAxis: Float32Array // 3, world-space unit axis
81
+ cacheAngLimWA: Float32Array // 3: I⁻¹A·axis
82
+ cacheAngLimWB: Float32Array // 3
83
+ cacheAngLimJacInv: number
60
84
  cacheAngLimTarget: number // target relative angular velocity along axis
61
85
  cacheAngLimActive: number
62
86
  cacheAngLimImp: number // accumulated impulse (per substep)
87
+ // Per-axis angular limit rows for the small-violation regime. act:
88
+ // 1 = bilateral (locked axis), 2 = unilateral stop (ranged axis) — a
89
+ // bilateral row on a ranged axis brakes natural recovery and pumps
90
+ // energy into swinging cloth.
91
+ cacheAngPATarget: Float32Array // 3
92
+ cacheAngPAActive: Uint8Array // 3
93
+ cacheAngPAImp: Float32Array // 3
63
94
  }
64
95
 
65
- export const STOP_ERP = 0.475
96
+ // Stop-limit ERP. PMX rigs are tuned against MMD's stiff limit response;
97
+ // lowering this makes cloth resting against its limits sink visibly deeper
98
+ // (equilibrium penetration scales with 1/ERP). Rest chatter at this
99
+ // stiffness was fixed at the source (spring double-drive, unilateral
100
+ // stops) — don't lower ERP to paper over jitter.
101
+ export const STOP_ERP = 0.45
66
102
 
67
103
  // Build per-joint constraints from PMX data:
68
104
  // frameA = (bodyA_worldBind)^-1 · jointWorldBind
@@ -136,6 +172,7 @@ export function buildConstraints(
136
172
  springEnabled,
137
173
  springStiffness,
138
174
  equilibriumPoint: new Float32Array(6),
175
+ isLoop: false,
139
176
  cacheSkip: false,
140
177
  cacheLeverA: new Float32Array(3),
141
178
  cacheLeverB: new Float32Array(3),
@@ -147,18 +184,55 @@ export function buildConstraints(
147
184
  cacheLinActive: new Uint8Array(3),
148
185
  cacheLinLimitImp: new Float32Array(3),
149
186
  cacheLinSpringTarget: new Float32Array(3),
187
+ cacheLinSpringMaxImp: new Float32Array(3),
188
+ cacheLinSpringImp: new Float32Array(3),
150
189
  cacheLinSpringActive: new Uint8Array(3),
151
190
  cacheAngAxes: new Float32Array(9),
152
191
  cacheAngTargetVel: new Float32Array(3),
153
192
  cacheAngActive: new Uint8Array(3),
154
- cacheAngJacInv: 0,
193
+ cacheAngJacInv: new Float32Array(3),
194
+ cacheAngSpringMaxImp: new Float32Array(3),
195
+ cacheAngSpringImp: new Float32Array(3),
196
+ cacheAngWA: new Float32Array(9),
197
+ cacheAngWB: new Float32Array(9),
155
198
  cacheAngLimAxis: new Float32Array(3),
199
+ cacheAngLimWA: new Float32Array(3),
200
+ cacheAngLimWB: new Float32Array(3),
201
+ cacheAngLimJacInv: 0,
156
202
  cacheAngLimTarget: 0,
157
203
  cacheAngLimActive: 0,
158
204
  cacheAngLimImp: 0,
205
+ cacheAngPATarget: new Float32Array(3),
206
+ cacheAngPAActive: new Uint8Array(3),
207
+ cacheAngPAImp: new Float32Array(3),
159
208
  })
160
209
  }
161
210
 
211
+ // Mark loop-closing joints via union-find over the joint graph. All
212
+ // bone-follow bodies count as one "world" component (they're each anchored
213
+ // to the skeleton), so a joint bridging two separately anchored subtrees
214
+ // also closes a kinematic cycle. One edge per cycle gets softened, which
215
+ // releases the over-determination regardless of which edge it is.
216
+ const parent = new Int32Array(rigidbodies.length + 1)
217
+ for (let i = 0; i < parent.length; i++) parent[i] = i
218
+ const find = (x: number): number => {
219
+ while (parent[x] !== x) {
220
+ parent[x] = parent[parent[x]]
221
+ x = parent[x]
222
+ }
223
+ return x
224
+ }
225
+ const world = rigidbodies.length
226
+ for (let i = 0; i < rigidbodies.length; i++) {
227
+ if (rigidbodies[i].type !== RigidbodyType.Dynamic) parent[find(i)] = find(world)
228
+ }
229
+ for (const con of out) {
230
+ const ra = find(con.bodyA)
231
+ const rb = find(con.bodyB)
232
+ if (ra === rb) con.isLoop = true
233
+ else parent[ra] = rb
234
+ }
235
+
162
236
  return out
163
237
  }
164
238
 
@@ -50,6 +50,15 @@ export class RezePhysics {
50
50
  private kinRoot: Int32Array
51
51
  // Kinematic bodies whose target jumped discontinuously this frame.
52
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
53
62
 
54
63
  constructor(rigidbodies: Rigidbody[], joints: Joint[] = []) {
55
64
  this.rigidbodies = rigidbodies
@@ -66,6 +75,17 @@ export class RezePhysics {
66
75
  this.kinTargetAngVel = new Float32Array(this.store.count * 3)
67
76
  this.kinRoot = this.buildKinematicRoots()
68
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
+ }
69
89
  }
70
90
 
71
91
  // BFS over the joint graph from every kinematic body, assigning each
@@ -140,6 +160,13 @@ export class RezePhysics {
140
160
  if (this.firstFrame) return
141
161
  this.snapBodiesToBones(boneWorldMatrices)
142
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)
143
170
  this.timeAccum = 0
144
171
  }
145
172
 
@@ -165,6 +192,7 @@ export class RezePhysics {
165
192
  // raw derived velocity is what used to explode the solver. Chains under
166
193
  // unaffected roots keep their momentum untouched.
167
194
  if (this.computeKinematicTargets(boneWorldMatrices, dt)) {
195
+ this.teleportCount++
168
196
  this.carryDynamicThroughTeleport()
169
197
  this.snapKinematicToTargets(true)
170
198
  this.savePrevState() // prev == curr so interpolation doesn't streak
@@ -197,6 +225,10 @@ export class RezePhysics {
197
225
  this.snapKinematicToTargets(false)
198
226
  }
199
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)
231
+
200
232
  // Fraction into the next (not-yet-taken) step; always in [0, 1).
201
233
  const alpha = this.fixedTimeStep > 0 ? this.timeAccum / this.fixedTimeStep : 0
202
234
  this.applyDynamicsToBones(boneWorldMatrices, alpha)
@@ -500,6 +532,32 @@ export class RezePhysics {
500
532
  }
501
533
  }
502
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
+
503
561
  // Backstop: if a dynamic body's state went non-finite despite the velocity
504
562
  // caps, restore its previous-substep pose with zero velocity instead of
505
563
  // letting NaNs spread through constraints and contacts. Runs after every
@@ -589,7 +647,16 @@ export class RezePhysics {
589
647
 
590
648
  // Sanity gate against NaN / extreme values — silently drop the update.
591
649
  if (Number.isFinite(_boneMat[0]) && Math.abs(_boneMat[0]) < 1e6) {
592
- 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
+ }
593
660
  }
594
661
  }
595
662
  }