reze-engine 0.15.1 → 0.16.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 (65) hide show
  1. package/dist/camera.d.ts +2 -0
  2. package/dist/camera.d.ts.map +1 -1
  3. package/dist/camera.js +8 -3
  4. package/dist/engine.d.ts +19 -4
  5. package/dist/engine.d.ts.map +1 -1
  6. package/dist/engine.js +294 -27
  7. package/dist/ik-solver.d.ts.map +1 -1
  8. package/dist/ik-solver.js +13 -7
  9. package/dist/math.d.ts +2 -0
  10. package/dist/math.d.ts.map +1 -1
  11. package/dist/math.js +65 -46
  12. package/dist/model.d.ts +28 -1
  13. package/dist/model.d.ts.map +1 -1
  14. package/dist/model.js +259 -75
  15. package/dist/physics/physics.d.ts +3 -0
  16. package/dist/physics/physics.d.ts.map +1 -1
  17. package/dist/physics/physics.js +55 -4
  18. package/dist/pmx-loader.d.ts.map +1 -1
  19. package/dist/pmx-loader.js +2 -30
  20. package/dist/shaders/materials/body.d.ts +1 -1
  21. package/dist/shaders/materials/body.d.ts.map +1 -1
  22. package/dist/shaders/materials/body.js +3 -1
  23. package/dist/shaders/materials/cloth_rough.d.ts +1 -1
  24. package/dist/shaders/materials/cloth_rough.d.ts.map +1 -1
  25. package/dist/shaders/materials/cloth_rough.js +3 -1
  26. package/dist/shaders/materials/cloth_smooth.d.ts +1 -1
  27. package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -1
  28. package/dist/shaders/materials/common.d.ts +2 -2
  29. package/dist/shaders/materials/common.d.ts.map +1 -1
  30. package/dist/shaders/materials/common.js +8 -0
  31. package/dist/shaders/materials/default.d.ts +1 -1
  32. package/dist/shaders/materials/default.d.ts.map +1 -1
  33. package/dist/shaders/materials/eye.d.ts +1 -1
  34. package/dist/shaders/materials/eye.d.ts.map +1 -1
  35. package/dist/shaders/materials/face.d.ts +1 -1
  36. package/dist/shaders/materials/face.d.ts.map +1 -1
  37. package/dist/shaders/materials/face.js +4 -1
  38. package/dist/shaders/materials/hair.d.ts +1 -1
  39. package/dist/shaders/materials/hair.d.ts.map +1 -1
  40. package/dist/shaders/materials/metal.d.ts +1 -1
  41. package/dist/shaders/materials/metal.d.ts.map +1 -1
  42. package/dist/shaders/materials/stockings.d.ts +1 -1
  43. package/dist/shaders/materials/stockings.d.ts.map +1 -1
  44. package/dist/shaders/materials/stockings.js +13 -9
  45. package/dist/shaders/passes/composite.d.ts +1 -1
  46. package/dist/shaders/passes/composite.d.ts.map +1 -1
  47. package/dist/shaders/passes/composite.js +18 -12
  48. package/dist/shaders/passes/morph.d.ts +2 -0
  49. package/dist/shaders/passes/morph.d.ts.map +1 -0
  50. package/dist/shaders/passes/morph.js +49 -0
  51. package/package.json +1 -1
  52. package/src/camera.ts +9 -3
  53. package/src/engine.ts +336 -29
  54. package/src/ik-solver.ts +14 -7
  55. package/src/math.ts +41 -51
  56. package/src/model.ts +288 -91
  57. package/src/physics/physics.ts +56 -13
  58. package/src/pmx-loader.ts +2 -35
  59. package/src/shaders/materials/body.ts +3 -1
  60. package/src/shaders/materials/cloth_rough.ts +3 -1
  61. package/src/shaders/materials/common.ts +8 -0
  62. package/src/shaders/materials/face.ts +4 -1
  63. package/src/shaders/materials/stockings.ts +13 -9
  64. package/src/shaders/passes/composite.ts +18 -12
  65. package/src/shaders/passes/morph.ts +50 -0
package/dist/model.js CHANGED
@@ -6,6 +6,13 @@ import { VMDLoader } from "./vmd-loader";
6
6
  import { VMDWriter } from "./vmd-writer";
7
7
  import { AnimationState, interpolateControlPoints, rawInterpolationToBoneInterpolation, } from "./animation";
8
8
  const VERTEX_STRIDE = 8;
9
+ // Animation-sampling scratch (applyPoseFromClip → convertVMDTranslationToLocal). These
10
+ // run sequentially and are not reentrant with the world-matrix scratch in math.ts, so
11
+ // plain module singletons are safe and let the per-bone sample path allocate nothing.
12
+ const _animSlerp = new Quat(0, 0, 0, 1);
13
+ const _animInterpT = new Vec3(0, 0, 0);
14
+ const _convOut = new Vec3(0, 0, 0);
15
+ const _convMat = new Float32Array(16);
9
16
  export class Model {
10
17
  get name() {
11
18
  return this._name;
@@ -37,6 +44,15 @@ export class Model {
37
44
  }
38
45
  constructor(vertexData, indexData, textures, materials, skeleton, skinning, morphing, rigidbodies = [], joints = []) {
39
46
  this._name = "";
47
+ this.morphPrevMinVert = -1; // vertices touched by last applyMorphs (reset to base this pass)
48
+ this.morphPrevMaxVert = -1;
49
+ this.morphPendingMinVert = -1; // accumulated range awaiting a GPU upload
50
+ this.morphPendingMaxVert = -1;
51
+ this.morphUploadFull = true; // first upload after load pushes the whole buffer once
52
+ // GPU morph path: when enabled (engine set up the compute buffers), applyMorphs only
53
+ // resolves effective weights and flags them dirty — the compute pass does the vertex work.
54
+ this.gpuMorphEnabled = false;
55
+ this.morphWeightsDirty = false;
40
56
  this.textures = [];
41
57
  this.materials = [];
42
58
  // Physics data from PMX
@@ -141,6 +157,58 @@ export class Model {
141
157
  }
142
158
  this.runtimeSkeleton.ikChainInfo = ikChainInfo;
143
159
  this.runtimeSkeleton.ikSolvers = ikSolvers;
160
+ this.buildDeformOrder();
161
+ }
162
+ // Precompute the bone order that computeWorldMatrices iterates every frame: every
163
+ // bone appears after its parent. This is the exact finishing order the previous
164
+ // recursive computeWorld() produced (walk up the not-yet-emitted ancestor chain,
165
+ // then emit it top-down; ties broken by ascending index) — so behavior is identical,
166
+ // minus the per-frame closure + visited-array allocation and the recursion overhead.
167
+ buildDeformOrder() {
168
+ const bones = this.skeleton.bones;
169
+ const n = bones.length;
170
+ const order = new Int32Array(n);
171
+ const done = new Uint8Array(n);
172
+ const stack = [];
173
+ let k = 0;
174
+ for (let i = 0; i < n; i++) {
175
+ if (done[i])
176
+ continue;
177
+ stack.length = 0;
178
+ let cur = i;
179
+ // Collect the chain of not-yet-emitted ancestors up to the root (or a done one).
180
+ while (cur >= 0 && cur < n && !done[cur]) {
181
+ stack.push(cur);
182
+ cur = bones[cur].parentIndex;
183
+ }
184
+ // Emit from the topmost ancestor down so parents precede children.
185
+ for (let s = stack.length - 1; s >= 0; s--) {
186
+ const b = stack[s];
187
+ done[b] = 1;
188
+ order[k++] = b;
189
+ }
190
+ }
191
+ this.deformOrder = order;
192
+ // Accumulate bind-pose world positions in the same parent-before-child order and
193
+ // with the same add order as the old recursive computeBindPoseWorldPosition, so the
194
+ // downstream arithmetic stays bit-identical.
195
+ const bindWorld = new Float32Array(n * 3);
196
+ for (let idx = 0; idx < n; idx++) {
197
+ const i = order[idx];
198
+ const bt = bones[i].bindTranslation;
199
+ const p = bones[i].parentIndex;
200
+ if (p >= 0 && p < n) {
201
+ bindWorld[i * 3 + 0] = bindWorld[p * 3 + 0] + bt[0];
202
+ bindWorld[i * 3 + 1] = bindWorld[p * 3 + 1] + bt[1];
203
+ bindWorld[i * 3 + 2] = bindWorld[p * 3 + 2] + bt[2];
204
+ }
205
+ else {
206
+ bindWorld[i * 3 + 0] = bt[0];
207
+ bindWorld[i * 3 + 1] = bt[1];
208
+ bindWorld[i * 3 + 2] = bt[2];
209
+ }
210
+ }
211
+ this.bindWorldPos = bindWorld;
144
212
  }
145
213
  initializeTweenBuffers() {
146
214
  const boneCount = this.skeleton.bones.length;
@@ -411,51 +479,50 @@ export class Model {
411
479
  }
412
480
  }
413
481
  // VMD translation (world delta from bind pose) → bone local space; optional rotation for animation vs IK
482
+ // Returns a REUSED scratch Vec3 (_convOut) — callers must copy immediately (they do:
483
+ // .set() / destructure). Zero allocation; result is bit-identical to the previous
484
+ // recursive implementation (verified numerically).
414
485
  convertVMDTranslationToLocal(boneIdx, vmdRelativeTranslation, rotation) {
415
- const skeleton = this.skeleton;
416
- const bones = skeleton.bones;
417
- const localRot = this.runtimeSkeleton.localRotations;
418
- // Compute bind pose world positions for all bones
419
- const computeBindPoseWorldPosition = (idx) => {
420
- const bone = bones[idx];
421
- const bindPos = new Vec3(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2]);
422
- if (bone.parentIndex >= 0 && bone.parentIndex < bones.length) {
423
- const parentWorldPos = computeBindPoseWorldPosition(bone.parentIndex);
424
- return parentWorldPos.add(bindPos);
425
- }
426
- else {
427
- return bindPos;
428
- }
429
- };
430
- const bone = bones[boneIdx];
431
- // VMD translation is relative to bind pose world position
432
- // targetWorldPos = bindPoseWorldPos + vmdRelativeTranslation
433
- const bindPoseWorldPos = computeBindPoseWorldPosition(boneIdx);
434
- const targetWorldPos = bindPoseWorldPos.add(vmdRelativeTranslation);
435
- // Convert target world position to local translation
436
- // We need parent's bind pose world position to transform to parent space
437
- let parentBindPoseWorldPos;
438
- if (bone.parentIndex >= 0) {
439
- parentBindPoseWorldPos = computeBindPoseWorldPosition(bone.parentIndex);
486
+ const bone = this.skeleton.bones[boneIdx];
487
+ const bindWorld = this.bindWorldPos;
488
+ const bt = bone.bindTranslation;
489
+ const p = bone.parentIndex;
490
+ // afterBindTranslation = (bindWorld[bone] + vmd − bindWorld[parent]) bindTranslation.
491
+ // (Algebraically this reduces to vmd, since bindWorld[bone] = bindWorld[parent] +
492
+ // bindTranslation, but the explicit form keeps the result bit-identical to before.)
493
+ const bi3 = boneIdx * 3;
494
+ const targetX = bindWorld[bi3 + 0] + vmdRelativeTranslation.x;
495
+ const targetY = bindWorld[bi3 + 1] + vmdRelativeTranslation.y;
496
+ const targetZ = bindWorld[bi3 + 2] + vmdRelativeTranslation.z;
497
+ const px = p >= 0 ? bindWorld[p * 3 + 0] : 0;
498
+ const py = p >= 0 ? bindWorld[p * 3 + 1] : 0;
499
+ const pz = p >= 0 ? bindWorld[p * 3 + 2] : 0;
500
+ const abx = targetX - px - bt[0];
501
+ const aby = targetY - py - bt[1];
502
+ const abz = targetZ - pz - bt[2];
503
+ // Inverse rotation = conjugate + normalize (matches localRotation.clone().conjugate()
504
+ // .normalize()), applied via a rotation matrix (matches Mat4.fromQuat). Uses animation
505
+ // rotation when provided so IK-modified localRot doesn't perturb the conversion.
506
+ const q = rotation ?? this.runtimeSkeleton.localRotations[boneIdx];
507
+ const qlen = Math.sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
508
+ let ix, iy, iz, iw;
509
+ if (qlen === 0) {
510
+ ix = 0;
511
+ iy = 0;
512
+ iz = 0;
513
+ iw = 1;
440
514
  }
441
515
  else {
442
- parentBindPoseWorldPos = Vec3.zeros();
516
+ const inv = 1 / qlen;
517
+ ix = -q.x * inv;
518
+ iy = -q.y * inv;
519
+ iz = -q.z * inv;
520
+ iw = q.w * inv;
443
521
  }
444
- // Transform target world position to parent's local space
445
- // In bind pose, parent's world matrix is just a translation
446
- const parentSpacePos = targetWorldPos.subtract(parentBindPoseWorldPos);
447
- // Subtract bindTranslation to get position after bind translation
448
- const afterBindTranslation = parentSpacePos.subtract(new Vec3(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2]));
449
- // Apply inverse rotation to get local translation
450
- // Use provided rotation (animation rotation) or fall back to current localRotation
451
- // Using animation rotation prevents conflicts when IK modifies the rotation
452
- const localRotation = rotation ?? localRot[boneIdx];
453
- // Clone to avoid mutating, then conjugate and normalize
454
- const invRotation = localRotation.clone().conjugate().normalize();
455
- const rotationMat = Mat4.fromQuat(invRotation.x, invRotation.y, invRotation.z, invRotation.w);
456
- const rm = rotationMat.values;
457
- const localTranslation = new Vec3(rm[0] * afterBindTranslation.x + rm[4] * afterBindTranslation.y + rm[8] * afterBindTranslation.z, rm[1] * afterBindTranslation.x + rm[5] * afterBindTranslation.y + rm[9] * afterBindTranslation.z, rm[2] * afterBindTranslation.x + rm[6] * afterBindTranslation.y + rm[10] * afterBindTranslation.z);
458
- return localTranslation;
522
+ Mat4.fromQuatInto(ix, iy, iz, iw, _convMat, 0);
523
+ const rm = _convMat;
524
+ _convOut.setXYZ(rm[0] * abx + rm[4] * aby + rm[8] * abz, rm[1] * abx + rm[5] * aby + rm[9] * abz, rm[2] * abx + rm[6] * aby + rm[10] * abz);
525
+ return _convOut;
459
526
  }
460
527
  getWorldMatrices() {
461
528
  return this.runtimeSkeleton.worldMatrices;
@@ -552,13 +619,15 @@ export class Model {
552
619
  this.applyMorphs();
553
620
  }
554
621
  applyMorphs() {
555
- // Reset vertex data to base positions
556
- this.vertexData.set(this.baseVertexData);
557
622
  const vertexCount = this.vertexCount;
558
623
  const morphCount = this.morphing.morphs.length;
559
624
  const weights = this.runtimeMorph.weights;
560
- // First pass: Compute effective weights for all morphs (handling group morphs)
561
- const effectiveWeights = new Float32Array(morphCount);
625
+ // Effective weights (group-morph resolution + clamp). Both paths need these: the GPU
626
+ // path uploads them for the compute pass; the CPU path applies them below. Reused buffer.
627
+ if (!this.morphEffectiveWeights || this.morphEffectiveWeights.length !== morphCount) {
628
+ this.morphEffectiveWeights = new Float32Array(morphCount);
629
+ }
630
+ const effectiveWeights = this.morphEffectiveWeights;
562
631
  effectiveWeights.set(weights); // Start with direct weights
563
632
  // Apply group morphs: group morph weight * ratio affects referenced morphs
564
633
  for (let morphIdx = 0; morphIdx < morphCount; morphIdx++) {
@@ -568,18 +637,30 @@ export class Model {
568
637
  if (groupWeight > 0.0001) {
569
638
  for (const ref of morph.groupReferences) {
570
639
  if (ref.morphIndex >= 0 && ref.morphIndex < morphCount) {
571
- // Add group morph's contribution to the referenced morph
572
640
  effectiveWeights[ref.morphIndex] += groupWeight * ref.ratio;
573
641
  }
574
642
  }
575
643
  }
576
644
  }
577
645
  }
578
- // Clamp effective weights to [0, 1]
579
646
  for (let i = 0; i < morphCount; i++) {
580
647
  effectiveWeights[i] = Math.max(0, Math.min(1, effectiveWeights[i]));
581
648
  }
582
- // Second pass: Apply vertex morphs with their effective weights
649
+ // GPU path: the compute pass applies the vertex offsets from these weights.
650
+ if (this.gpuMorphEnabled) {
651
+ this.morphWeightsDirty = true;
652
+ return;
653
+ }
654
+ // ── CPU path ── Reset only the vertices morphed by the previous pass back to base
655
+ // (targeted reset; vertexData never diverges from base outside that range).
656
+ if (this.morphPrevMaxVert >= 0) {
657
+ const s = this.morphPrevMinVert * VERTEX_STRIDE;
658
+ const e = (this.morphPrevMaxVert + 1) * VERTEX_STRIDE;
659
+ this.vertexData.set(this.baseVertexData.subarray(s, e), s);
660
+ }
661
+ // Apply vertex morphs, tracking the touched vertex-index range for partial upload.
662
+ let curMinVert = -1;
663
+ let curMaxVert = -1;
583
664
  for (let morphIdx = 0; morphIdx < morphCount; morphIdx++) {
584
665
  const effectiveWeight = effectiveWeights[morphIdx];
585
666
  if (effectiveWeight === 0 || effectiveWeight < 0.0001)
@@ -587,26 +668,136 @@ export class Model {
587
668
  const morph = this.morphing.morphs[morphIdx];
588
669
  if (morph.type !== 1)
589
670
  continue; // Only process vertex morphs
590
- // For vertex morphs, iterate through vertices that have offsets
591
671
  for (const vertexOffset of morph.vertexOffsets) {
592
672
  const vIdx = vertexOffset.vertexIndex;
593
673
  if (vIdx < 0 || vIdx >= vertexCount)
594
674
  continue;
595
- // Get morph offset for this vertex
596
675
  const offsetX = vertexOffset.positionOffset[0];
597
676
  const offsetY = vertexOffset.positionOffset[1];
598
677
  const offsetZ = vertexOffset.positionOffset[2];
599
- // Skip if offset is zero
600
678
  if (Math.abs(offsetX) < 0.0001 && Math.abs(offsetY) < 0.0001 && Math.abs(offsetZ) < 0.0001) {
601
679
  continue;
602
680
  }
603
- // Apply weighted offset to vertex position (positions are at stride 0, 8, 16, ...)
604
681
  const vertexIdx = vIdx * VERTEX_STRIDE;
605
682
  this.vertexData[vertexIdx] += offsetX * effectiveWeight;
606
683
  this.vertexData[vertexIdx + 1] += offsetY * effectiveWeight;
607
684
  this.vertexData[vertexIdx + 2] += offsetZ * effectiveWeight;
685
+ if (curMinVert < 0 || vIdx < curMinVert)
686
+ curMinVert = vIdx;
687
+ if (vIdx > curMaxVert)
688
+ curMaxVert = vIdx;
689
+ }
690
+ }
691
+ let dirtyMin = curMinVert;
692
+ let dirtyMax = curMaxVert;
693
+ if (this.morphPrevMaxVert >= 0) {
694
+ if (dirtyMin < 0 || this.morphPrevMinVert < dirtyMin)
695
+ dirtyMin = this.morphPrevMinVert;
696
+ if (this.morphPrevMaxVert > dirtyMax)
697
+ dirtyMax = this.morphPrevMaxVert;
698
+ }
699
+ if (dirtyMin >= 0 && dirtyMax >= 0) {
700
+ this.morphPendingMinVert =
701
+ this.morphPendingMinVert < 0 ? dirtyMin : Math.min(this.morphPendingMinVert, dirtyMin);
702
+ this.morphPendingMaxVert =
703
+ this.morphPendingMaxVert < 0 ? dirtyMax : Math.max(this.morphPendingMaxVert, dirtyMax);
704
+ }
705
+ this.morphPrevMinVert = curMinVert;
706
+ this.morphPrevMaxVert = curMaxVert;
707
+ }
708
+ // ── GPU morph path support ──
709
+ // Called by the engine once it has created the compute buffers for this model; switches
710
+ // applyMorphs to the weights-only branch.
711
+ enableGpuMorphs() {
712
+ this.gpuMorphEnabled = true;
713
+ }
714
+ // True (once) when morph weights changed since the last check — the engine then uploads
715
+ // getEffectiveMorphWeights() and dispatches the compute pass.
716
+ consumeMorphWeightsDirty() {
717
+ const d = this.morphWeightsDirty;
718
+ this.morphWeightsDirty = false;
719
+ return d;
720
+ }
721
+ // Effective (group-resolved, clamped) morph weights for GPU upload. Ensures they're
722
+ // computed at least once even before the first weight change.
723
+ getEffectiveMorphWeights() {
724
+ if (!this.morphEffectiveWeights)
725
+ this.applyMorphs();
726
+ return this.morphEffectiveWeights ?? new Float32Array(0);
727
+ }
728
+ // Build the CSR inversion of vertex-morph offsets for the GPU compute pass. Returns null
729
+ // when the model has no vertex-morph offsets (no GPU path needed). Entries for each vertex
730
+ // are emitted in ascending morph-index order, matching the CPU accumulation order.
731
+ buildMorphComputeData() {
732
+ const V = this.vertexCount;
733
+ const M = this.morphing.morphs.length;
734
+ const morphs = this.morphing.morphs;
735
+ const EPS = 0.0001;
736
+ const isLive = (o) => o.vertexIndex >= 0 &&
737
+ o.vertexIndex < V &&
738
+ (Math.abs(o.positionOffset[0]) >= EPS ||
739
+ Math.abs(o.positionOffset[1]) >= EPS ||
740
+ Math.abs(o.positionOffset[2]) >= EPS);
741
+ const counts = new Uint32Array(V);
742
+ for (let m = 0; m < M; m++) {
743
+ const morph = morphs[m];
744
+ if (morph.type !== 1)
745
+ continue;
746
+ for (const o of morph.vertexOffsets)
747
+ if (isLive(o))
748
+ counts[o.vertexIndex]++;
749
+ }
750
+ const rowStart = new Uint32Array(V + 1);
751
+ let acc = 0;
752
+ for (let v = 0; v < V; v++) {
753
+ rowStart[v] = acc;
754
+ acc += counts[v];
755
+ }
756
+ rowStart[V] = acc;
757
+ const E = acc;
758
+ if (E === 0)
759
+ return null;
760
+ const colMorph = new Uint32Array(E);
761
+ const colOffset = new Float32Array(E * 3);
762
+ const fill = new Uint32Array(V);
763
+ for (let m = 0; m < M; m++) {
764
+ const morph = morphs[m];
765
+ if (morph.type !== 1)
766
+ continue;
767
+ for (const o of morph.vertexOffsets) {
768
+ if (!isLive(o))
769
+ continue;
770
+ const v = o.vertexIndex;
771
+ const p = rowStart[v] + fill[v]++;
772
+ colMorph[p] = m;
773
+ colOffset[p * 3] = o.positionOffset[0];
774
+ colOffset[p * 3 + 1] = o.positionOffset[1];
775
+ colOffset[p * 3 + 2] = o.positionOffset[2];
608
776
  }
609
777
  }
778
+ const basePositions = new Float32Array(V * 3);
779
+ for (let v = 0; v < V; v++) {
780
+ const vi = v * VERTEX_STRIDE;
781
+ basePositions[v * 3] = this.baseVertexData[vi];
782
+ basePositions[v * 3 + 1] = this.baseVertexData[vi + 1];
783
+ basePositions[v * 3 + 2] = this.baseVertexData[vi + 2];
784
+ }
785
+ return { basePositions, rowStart, colMorph, colOffset, morphCount: M, vertexCount: V, entryCount: E };
786
+ }
787
+ // Consume the pending morph vertex-upload range for the engine. Returns null when a
788
+ // full upload is needed (first time after load, or nothing tracked), else the inclusive
789
+ // [minVert, maxVert] slice that changed. Resets pending state.
790
+ consumeVertexUploadRange() {
791
+ if (this.morphUploadFull || this.morphPendingMaxVert < 0) {
792
+ this.morphUploadFull = false;
793
+ this.morphPendingMinVert = -1;
794
+ this.morphPendingMaxVert = -1;
795
+ return null;
796
+ }
797
+ const range = { minVert: this.morphPendingMinVert, maxVert: this.morphPendingMaxVert };
798
+ this.morphPendingMinVert = -1;
799
+ this.morphPendingMaxVert = -1;
800
+ return range;
610
801
  }
611
802
  buildClipFromVmdKeyFrames(vmdKeyFrames) {
612
803
  const boneTracksByBone = {};
@@ -835,11 +1026,11 @@ export class Model {
835
1026
  const gradient = frameDelta > 0 ? (clampedFrame - frameA.frame) / frameDelta : 0;
836
1027
  const interp = frameB.interpolation;
837
1028
  const rotT = interpolateControlPoints(interp.rotation, gradient);
838
- const rotation = Quat.slerp(frameA.rotation, frameB.rotation, rotT);
1029
+ const rotation = Quat.slerpInto(frameA.rotation, frameB.rotation, rotT, _animSlerp);
839
1030
  const txWeight = interpolateControlPoints(interp.translationX, gradient);
840
1031
  const tyWeight = interpolateControlPoints(interp.translationY, gradient);
841
1032
  const tzWeight = interpolateControlPoints(interp.translationZ, gradient);
842
- const interpolatedVMDTranslation = new Vec3(frameA.translation.x + (frameB.translation.x - frameA.translation.x) * txWeight, frameA.translation.y + (frameB.translation.y - frameA.translation.y) * tyWeight, frameA.translation.z + (frameB.translation.z - frameA.translation.z) * tzWeight);
1033
+ const interpolatedVMDTranslation = _animInterpT.setXYZ(frameA.translation.x + (frameB.translation.x - frameA.translation.x) * txWeight, frameA.translation.y + (frameB.translation.y - frameA.translation.y) * tyWeight, frameA.translation.z + (frameB.translation.z - frameA.translation.z) * tzWeight);
843
1034
  const localTranslation = this.convertVMDTranslationToLocal(boneIdx, interpolatedVMDTranslation, rotation);
844
1035
  localRot.set(rotation);
845
1036
  localTrans.set(localTranslation);
@@ -906,10 +1097,14 @@ export class Model {
906
1097
  if (!ikChainInfo)
907
1098
  return;
908
1099
  // Solve each IK solver sequentially, ensuring consistent state between solvers
1100
+ let firstSolver = true;
909
1101
  for (const solver of ikSolvers) {
910
- // Recompute ALL world matrices before each solver starts
911
- // This ensures each solver sees the effects of previous solvers on localRotations
912
- this.computeWorldMatrices();
1102
+ // Each solver must see the effects of previous solvers on localRotations, so
1103
+ // recompute world matrices between solvers. The first solver is skipped: the
1104
+ // caller (update) already computed them and nothing has changed localRotations yet.
1105
+ if (!firstSolver)
1106
+ this.computeWorldMatrices();
1107
+ firstSolver = false;
913
1108
  // Clear computed set for this solver's pass
914
1109
  this.ikComputedSet.clear();
915
1110
  // Solve this IK chain
@@ -1033,20 +1228,13 @@ export class Model {
1033
1228
  const boneCount = bones.length;
1034
1229
  if (boneCount === 0)
1035
1230
  return;
1036
- // Local computed array (avoids instance field overhead)
1037
- const computed = new Array(boneCount).fill(false);
1038
- const computeWorld = (i) => {
1039
- if (computed[i])
1040
- return;
1231
+ // Flat traversal in precomputed order: every bone's parent is already done, so no
1232
+ // per-bone visited check, no recursion, and no per-call allocation. Same per-bone
1233
+ // math as before. Scratch slots are safe to reuse since there's no reentrancy now.
1234
+ const order = this.deformOrder;
1235
+ for (let k = 0; k < boneCount; k++) {
1236
+ const i = order[k];
1041
1237
  const b = bones[i];
1042
- if (b.parentIndex >= boneCount) {
1043
- console.warn(`[RZM] bone ${i} parent out of range: ${b.parentIndex}`);
1044
- }
1045
- // Ensure parent is computed FIRST, before we touch any scratch buffers.
1046
- // Recursion may itself use scratchMat4Values[0] / scratchQuat; doing it up
1047
- // front keeps the current frame's scratch slots untouched when we use them below.
1048
- if (b.parentIndex >= 0 && !computed[b.parentIndex])
1049
- computeWorld(b.parentIndex);
1050
1238
  const boneRot = localRot[i];
1051
1239
  let fx = boneRot.x, fy = boneRot.y, fz = boneRot.z, fw = boneRot.w;
1052
1240
  let addLocalTx = 0, addLocalTy = 0, addLocalTz = 0;
@@ -1103,10 +1291,6 @@ export class Model {
1103
1291
  else {
1104
1292
  worldMat.values.set(localMVals);
1105
1293
  }
1106
- computed[i] = true;
1107
- };
1108
- // Process all bones (recursion handles dependencies automatically)
1109
- for (let i = 0; i < boneCount; i++)
1110
- computeWorld(i);
1294
+ }
1111
1295
  }
1112
1296
  }
@@ -12,7 +12,10 @@ export declare class RezePhysics {
12
12
  private timeAccum;
13
13
  private readonly fixedTimeStep;
14
14
  private readonly maxSubSteps;
15
+ private prevPositions;
16
+ private prevOrientations;
15
17
  constructor(rigidbodies: Rigidbody[], joints?: Joint[]);
18
+ private savePrevState;
16
19
  setGravity(gravity: Vec3): void;
17
20
  getGravity(): Vec3;
18
21
  getRigidbodies(): Rigidbody[];
@@ -1 +1 @@
1
- {"version":3,"file":"physics.d.ts","sourceRoot":"","sources":["../../src/physics/physics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAYvC,qBAAa,WAAW;IACtB,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,KAAK,CAAO;IACpB,OAAO,CAAC,WAAW,CAA0B;IAC7C,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,UAAU,CAAO;IACzB,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAI;gBAEpB,WAAW,EAAE,SAAS,EAAE,EAAE,MAAM,GAAE,KAAK,EAAO;IAS1D,UAAU,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI;IAG/B,UAAU,IAAI,IAAI;IAGlB,cAAc,IAAI,SAAS,EAAE;IAG7B,SAAS,IAAI,KAAK,EAAE;IAGpB,QAAQ,IAAI,cAAc;IAI1B,sBAAsB,IAAI,KAAK,CAAC;QAAE,QAAQ,EAAE,IAAI,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAA;KAAE,CAAC;IAiBnE,KAAK,CAAC,iBAAiB,EAAE,IAAI,EAAE,GAAG,IAAI;IAKtC,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,IAAI,EAAE,EAAE,uBAAuB,EAAE,YAAY,GAAG,IAAI;IA6BxF,OAAO,CAAC,iBAAiB;IAuCzB,OAAO,CAAC,aAAa;IA6ErB,OAAO,CAAC,oBAAoB;CAiC7B"}
1
+ {"version":3,"file":"physics.d.ts","sourceRoot":"","sources":["../../src/physics/physics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAYvC,qBAAa,WAAW;IACtB,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,KAAK,CAAO;IACpB,OAAO,CAAC,WAAW,CAA0B;IAC7C,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,UAAU,CAAO;IACzB,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAI;IAMhC,OAAO,CAAC,aAAa,CAAc;IACnC,OAAO,CAAC,gBAAgB,CAAc;gBAE1B,WAAW,EAAE,SAAS,EAAE,EAAE,MAAM,GAAE,KAAK,EAAO;IAY1D,OAAO,CAAC,aAAa;IAKrB,UAAU,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI;IAG/B,UAAU,IAAI,IAAI;IAGlB,cAAc,IAAI,SAAS,EAAE;IAG7B,SAAS,IAAI,KAAK,EAAE;IAGpB,QAAQ,IAAI,cAAc;IAI1B,sBAAsB,IAAI,KAAK,CAAC;QAAE,QAAQ,EAAE,IAAI,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAA;KAAE,CAAC;IAiBnE,KAAK,CAAC,iBAAiB,EAAE,IAAI,EAAE,GAAG,IAAI;IAOtC,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,IAAI,EAAE,EAAE,uBAAuB,EAAE,YAAY,GAAG,IAAI;IAkCxF,OAAO,CAAC,iBAAiB;IAuCzB,OAAO,CAAC,aAAa;IA+ErB,OAAO,CAAC,oBAAoB;CAoD7B"}
@@ -22,6 +22,13 @@ export class RezePhysics {
22
22
  this.world = new World(new Vec3(0, -98, 0));
23
23
  this.constraints = buildConstraints(rigidbodies, joints);
24
24
  this.contacts = new ContactPool();
25
+ this.prevPositions = new Float32Array(this.store.count * 3);
26
+ this.prevOrientations = new Float32Array(this.store.count * 4);
27
+ }
28
+ // Snapshot the current body pose as the interpolation "previous" state.
29
+ savePrevState() {
30
+ this.prevPositions.set(this.store.positions);
31
+ this.prevOrientations.set(this.store.orientations);
25
32
  }
26
33
  setGravity(gravity) {
27
34
  this.world.setGravity(gravity);
@@ -58,6 +65,8 @@ export class RezePhysics {
58
65
  if (this.firstFrame)
59
66
  return;
60
67
  this.snapBodiesToBones(boneWorldMatrices);
68
+ this.savePrevState(); // prev == curr after a snap, so no interpolation jump
69
+ this.timeAccum = 0;
61
70
  }
62
71
  step(dt, boneWorldMatrices, boneInverseBindMatrices) {
63
72
  if (this.firstFrame) {
@@ -65,6 +74,7 @@ export class RezePhysics {
65
74
  // Start at current bone pose, not the PMX bind pose, so animations
66
75
  // that skip frame 0 don't pop bodies on first step.
67
76
  this.snapBodiesToBones(boneWorldMatrices);
77
+ this.savePrevState();
68
78
  this.firstFrame = false;
69
79
  }
70
80
  // Sync once per render frame; kinematic targets don't change between
@@ -72,17 +82,21 @@ export class RezePhysics {
72
82
  // bone-pose delta so joints feel the kinematic motion.
73
83
  this.syncFromBones(boneWorldMatrices, dt);
74
84
  // Fixed-timestep substeps. The maxSubSteps cap prevents runaway after
75
- // a long stall (tab backgrounded, etc.).
85
+ // a long stall (tab backgrounded, etc.). Snapshot the pose before each step so
86
+ // after the loop prevState is one substep behind the live (current) state.
76
87
  this.timeAccum += dt;
77
88
  let sub = 0;
78
89
  while (this.timeAccum >= this.fixedTimeStep && sub < this.maxSubSteps) {
90
+ this.savePrevState();
79
91
  this.world.step(this.store, this.constraints, this.contacts, this.fixedTimeStep);
80
92
  this.timeAccum -= this.fixedTimeStep;
81
93
  sub++;
82
94
  }
83
95
  if (sub === this.maxSubSteps)
84
96
  this.timeAccum = 0;
85
- this.applyDynamicsToBones(boneWorldMatrices);
97
+ // Fraction into the next (not-yet-taken) step; always in [0, 1).
98
+ const alpha = this.fixedTimeStep > 0 ? this.timeAccum / this.fixedTimeStep : 0;
99
+ this.applyDynamicsToBones(boneWorldMatrices, alpha);
86
100
  }
87
101
  // Snap all bone-bound bodies to boneWorld × bodyOffset, zero velocities.
88
102
  snapBodiesToBones(boneWorldMatrices) {
@@ -183,13 +197,18 @@ export class RezePhysics {
183
197
  }
184
198
  // Dynamic bodies write their transform back to the bone matrix:
185
199
  // boneWorld = bodyWorld × bodyOffsetInverse.
186
- applyDynamicsToBones(boneWorldMatrices) {
200
+ // The body pose is the render-interpolated pose between the previous and current
201
+ // substep states (alpha = fraction into the next step), which removes fixed-step judder.
202
+ applyDynamicsToBones(boneWorldMatrices, alpha) {
187
203
  const N = this.store.count;
188
204
  const inv = this.store.bodyOffsetInverse;
189
205
  const positions = this.store.positions;
190
206
  const orientations = this.store.orientations;
207
+ const prevPos = this.prevPositions;
208
+ const prevOri = this.prevOrientations;
191
209
  const types = this.store.type;
192
210
  const boneIdx = this.store.boneIndex;
211
+ const oneMinus = 1 - alpha;
193
212
  for (let i = 0; i < N; i++) {
194
213
  if (types[i] !== RigidbodyType.Dynamic)
195
214
  continue;
@@ -198,7 +217,39 @@ export class RezePhysics {
198
217
  continue;
199
218
  const i3 = i * 3;
200
219
  const i4 = i * 4;
201
- Mat4.fromPositionRotationInto(positions[i3 + 0], positions[i3 + 1], positions[i3 + 2], orientations[i4 + 0], orientations[i4 + 1], orientations[i4 + 2], orientations[i4 + 3], _bodyMat);
220
+ // Position: straight lerp.
221
+ const px = prevPos[i3 + 0] * oneMinus + positions[i3 + 0] * alpha;
222
+ const py = prevPos[i3 + 1] * oneMinus + positions[i3 + 1] * alpha;
223
+ const pz = prevPos[i3 + 2] * oneMinus + positions[i3 + 2] * alpha;
224
+ // Orientation: shortest-arc nlerp (bodies rotate little per fixed step, so nlerp
225
+ // tracks slerp closely and avoids the trig).
226
+ const ax = prevOri[i4 + 0], ay = prevOri[i4 + 1], az = prevOri[i4 + 2], aw = prevOri[i4 + 3];
227
+ let bx = orientations[i4 + 0], by = orientations[i4 + 1], bz = orientations[i4 + 2], bw = orientations[i4 + 3];
228
+ if (ax * bx + ay * by + az * bz + aw * bw < 0) {
229
+ bx = -bx;
230
+ by = -by;
231
+ bz = -bz;
232
+ bw = -bw;
233
+ }
234
+ let qx = ax * oneMinus + bx * alpha;
235
+ let qy = ay * oneMinus + by * alpha;
236
+ let qz = az * oneMinus + bz * alpha;
237
+ let qw = aw * oneMinus + bw * alpha;
238
+ const ql = Math.sqrt(qx * qx + qy * qy + qz * qz + qw * qw);
239
+ if (ql > 0) {
240
+ const invL = 1 / ql;
241
+ qx *= invL;
242
+ qy *= invL;
243
+ qz *= invL;
244
+ qw *= invL;
245
+ }
246
+ else {
247
+ qx = 0;
248
+ qy = 0;
249
+ qz = 0;
250
+ qw = 1;
251
+ }
252
+ Mat4.fromPositionRotationInto(px, py, pz, qx, qy, qz, qw, _bodyMat);
202
253
  Mat4.multiplyArrays(_bodyMat, 0, inv, i * 16, _boneMat, 0);
203
254
  // Sanity gate against NaN / extreme values — silently drop the update.
204
255
  if (Number.isFinite(_boneMat[0]) && Math.abs(_boneMat[0]) < 1e6) {
@@ -1 +1 @@
1
- {"version":3,"file":"pmx-loader.d.ts","sourceRoot":"","sources":["../src/pmx-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EAWN,MAAM,SAAS,CAAA;AAGhB,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEzE,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,eAAe,CAAI;IAC3B,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,aAAa,CAAI;IACzB,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,kBAAkB,CAAI;IAC9B,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,mBAAmB,CAA4B;IACvD,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,WAAW,CAAY;IAC/B,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,MAAM,CAAc;IAE5B,OAAO;WAIM,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAI9C,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK;WAIpC,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAKxF,OAAO,CAAC,KAAK;IAiBb,OAAO,CAAC,WAAW;IA+CnB,OAAO,CAAC,aAAa;IA+FrB,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,cAAc;IAoEtB,OAAO,CAAC,UAAU;IAsKlB,OAAO,CAAC,WAAW;IA+InB,OAAO,CAAC,iBAAiB;IAgDzB,OAAO,CAAC,gBAAgB;IAyFxB,OAAO,CAAC,WAAW;IAmGnB,OAAO,CAAC,kBAAkB;IAmC1B,OAAO,CAAC,OAAO;IAkLf,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,OAAO;IAmBf,OAAO,CAAC,QAAQ;CAIjB"}
1
+ {"version":3,"file":"pmx-loader.d.ts","sourceRoot":"","sources":["../src/pmx-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EAWN,MAAM,SAAS,CAAA;AAGhB,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEzE,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,eAAe,CAAI;IAC3B,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,aAAa,CAAI;IACzB,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,kBAAkB,CAAI;IAC9B,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,mBAAmB,CAA4B;IACvD,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,WAAW,CAAY;IAC/B,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,MAAM,CAAc;IAE5B,OAAO;WAIM,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAI9C,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK;WAIpC,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAKxF,OAAO,CAAC,KAAK;IAiBb,OAAO,CAAC,WAAW;IA+CnB,OAAO,CAAC,aAAa;IA+FrB,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,cAAc;IAoEtB,OAAO,CAAC,UAAU;IAsKlB,OAAO,CAAC,WAAW;IA+InB,OAAO,CAAC,iBAAiB;IAgDzB,OAAO,CAAC,gBAAgB;IAyFxB,OAAO,CAAC,WAAW;IAmGnB,OAAO,CAAC,kBAAkB;IAmC1B,OAAO,CAAC,OAAO;IAiJf,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,OAAO;IAmBf,OAAO,CAAC,QAAQ;CAIjB"}
@@ -916,38 +916,10 @@ export class PmxLoader {
916
916
  }
917
917
  skinning = { joints, weights };
918
918
  }
919
- // Create morphing data structure
920
- const morphCount = this.morphs.length;
921
- // Dense buffer: morphCount * vertexCount * 3 floats (one vec3 per morph per vertex)
922
- const offsetsBuffer = new Float32Array(morphCount * this.vertexCount * 3);
923
- // Initialize all offsets to zero
924
- offsetsBuffer.fill(0);
925
- // Fill in actual offsets for vertex morphs
926
- for (let morphIdx = 0; morphIdx < morphCount; morphIdx++) {
927
- const morph = this.morphs[morphIdx];
928
- if (morph.type === 1) {
929
- // Vertex morph
930
- // Store offsets in dense buffer: [morph0_v0, morph0_v1, ..., morph1_v0, ...]
931
- // Each vec3 is 3 consecutive floats
932
- for (const offset of morph.vertexOffsets) {
933
- // Calculate index in the dense buffer
934
- // Layout: morphIdx * (vertexCount * 3) + vertexIndex * 3
935
- // This gives us the starting float index for this morph's vertex offset
936
- const bufferIdx = morphIdx * this.vertexCount * 3 + offset.vertexIndex * 3;
937
- if (bufferIdx >= 0 &&
938
- bufferIdx + 2 < offsetsBuffer.length &&
939
- offset.vertexIndex >= 0 &&
940
- offset.vertexIndex < this.vertexCount) {
941
- offsetsBuffer[bufferIdx] = offset.positionOffset[0];
942
- offsetsBuffer[bufferIdx + 1] = offset.positionOffset[1];
943
- offsetsBuffer[bufferIdx + 2] = offset.positionOffset[2];
944
- }
945
- }
946
- }
947
- }
919
+ // Morphs are applied from the sparse per-morph vertexOffsets lists (see
920
+ // Model.applyMorphs); no dense morphCount*vertexCount buffer is needed.
948
921
  const morphing = {
949
922
  morphs: this.morphs,
950
- offsetsBuffer,
951
923
  };
952
924
  return new Model(vertexData, indexData, this.textures, this.materials, skeleton, skinning, morphing, this.rigidbodies, this.joints);
953
925
  }