reze-engine 0.36.0 → 0.37.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.
@@ -2,7 +2,8 @@ import { Vec3 } from "../math"
2
2
  import type { RigidBodyStore } from "./body"
3
3
  import { RigidbodyType } from "./types"
4
4
  import type { SixDofSpringConstraint } from "./constraint"
5
- import { solveConstraints, type SolverCache } from "./solver"
5
+ import { solveConstraints, saveContactImpulses, applySplitImpulsePush, type SolverCache } from "./solver"
6
+ import { ManifoldCache } from "./manifold"
6
7
  import { findContacts, type ContactPool } from "./contact"
7
8
 
8
9
  // World step: predict velocities → collide → solve → position correction →
@@ -38,6 +39,9 @@ export class World {
38
39
  readonly gravity: Vec3
39
40
  solverIterations = 10
40
41
 
42
+ /** Per-pair contact impulse history behind warm starting. */
43
+ private manifolds = new ManifoldCache()
44
+
41
45
  // Per-body damping factors pow(1−damping, dt), cached because damping and
42
46
  // the fixed dt never change — two Math.pow per body per substep otherwise.
43
47
  private dampCacheDt = -1
@@ -166,50 +170,20 @@ export class World {
166
170
 
167
171
  // 3. Solve joint + contact constraints (velocity-only).
168
172
  if (constraints.length > 0 || contacts.count > 0) {
169
- solveConstraints(store, constraints, cache, contacts, dt, this.solverIterations)
170
- }
171
-
172
- // 4. Position correction (split impulse). Direct translation along the
173
- // contact normal — joint constraints in the same SI loop can't undo
174
- // it because it doesn't go through the velocity channel. Inverse-mass
175
- // weighted so a kinematic body stays put and only the dynamic one
176
- // translates.
177
- // Softer and later than a rigid-body engine would use, deliberately. MMD
178
- // cloth is a dense chain of small bodies resting on the character, and
179
- // aggressive penetration recovery feeds those contacts energy that the
180
- // joint springs hand straight back — the tips never settle. Measured on a
181
- // 688-body model: resting peak velocity 1.79 → 1.10, and 12.9 → 6.6 under
182
- // an idle animation, for a slop of two hundredths of a unit (under 2 mm at
183
- // MMD scale) that no eye finds.
184
- const POS_CORRECTION_FACTOR = 0.15
185
- const POS_SLOP = 0.02
186
- for (let ci = 0; ci < contacts.count; ci++) {
187
- const c = contacts.get(ci)
188
- if (c.depth <= POS_SLOP) continue
189
- const imA = invMass[c.bodyA]
190
- const imB = invMass[c.bodyB]
191
- const total = imA + imB
192
- if (total <= 0) continue
193
- const correction = (c.depth - POS_SLOP) * POS_CORRECTION_FACTOR
194
- const dx = correction * c.nx
195
- const dy = correction * c.ny
196
- const dz = correction * c.nz
197
- const ai = c.bodyA * 3
198
- const bi = c.bodyB * 3
199
- if (imA > 0) {
200
- const fA = imA / total
201
- pos[ai + 0] -= dx * fA
202
- pos[ai + 1] -= dy * fA
203
- pos[ai + 2] -= dz * fA
204
- }
205
- if (imB > 0) {
206
- const fB = imB / total
207
- pos[bi + 0] += dx * fB
208
- pos[bi + 1] += dy * fB
209
- pos[bi + 2] += dz * fB
210
- }
173
+ solveConstraints(store, constraints, cache, contacts, dt, this.solverIterations, this.manifolds)
174
+ applySplitImpulsePush(store, dt)
175
+ saveContactImpulses(store, contacts, this.manifolds)
176
+ } else {
177
+ this.manifolds.clear()
211
178
  }
212
179
 
180
+ // 4. Penetration recovery happens in the contact velocity row (Baumgarte,
181
+ // CONTACT_ERP), not here. Bullet 2.75 — the build MMD's physics runs —
182
+ // defaults m_splitImpulse to false and folds positionalError straight
183
+ // into m_rhs, so a separate position-translation pass is a divergence
184
+ // from what PMX rigs are authored against. It also could not be made to
185
+ // behave: every contact translated the body by its own full share, so a
186
+ // panel carrying dozens of rows was shoved out dozens of times.
213
187
  // 5. Integrate. Cap angular velocity at π/2 per step — a high-impulse
214
188
  // contact spike on a low-inertia body would otherwise spin past π
215
189
  // in one step and trash the quaternion integration. Linear velocity
package/src/pmx-loader.ts CHANGED
@@ -9,6 +9,9 @@ import {
9
9
  Morphing,
10
10
  VertexMorphOffset,
11
11
  GroupMorphReference,
12
+ BoneMorphOffset,
13
+ MaterialMorphOffset,
14
+ UvMorphOffset,
12
15
  IKLink,
13
16
  } from "./model"
14
17
  import { Mat4, Vec3 } from "./math"
@@ -533,6 +536,11 @@ export class PmxLoader {
533
536
  vertexOffsets: [],
534
537
  groupReferences: [],
535
538
  }
539
+ // Allocated only for the type that uses them, so `morph.boneOffsets`
540
+ // being undefined means "not that kind of morph" rather than "empty".
541
+ if (morphType === 2) morph.boneOffsets = []
542
+ else if (morphType >= 3 && morphType <= 7) morph.uvOffsets = []
543
+ else if (morphType === 8) morph.materialOffsets = []
536
544
 
537
545
  // Parse vertex morphs (type 1)
538
546
  if (morphType === 1) {
@@ -569,60 +577,92 @@ export class PmxLoader {
569
577
  }
570
578
  }
571
579
  } else {
572
- // Skip other morph types for now
580
+ // Bone, UV and material morphs are kept; flip and impulse (PMX 2.1)
581
+ // are still read past — nothing in the wild uses them, and impulse
582
+ // would need to drive rigidbodies.
573
583
  for (let j = 0; j < offsetCount; j++) {
574
584
  if (this.offset >= this.view.buffer.byteLength) {
575
585
  break
576
586
  }
577
587
  if (morphType === 2) {
578
588
  // Bone morph
579
- this.getNonVertexIndex(this.boneIndexSize) // boneIndex
580
- this.getFloat32() // x
581
- this.getFloat32() // y
582
- this.getFloat32() // z
583
- this.getFloat32() // rotation quaternion x
584
- this.getFloat32() // rotation quaternion y
585
- this.getFloat32() // rotation quaternion z
586
- this.getFloat32() // rotation quaternion w
589
+ const boneIndex = this.getNonVertexIndex(this.boneIndexSize)
590
+ const tx = this.getFloat32()
591
+ const ty = this.getFloat32()
592
+ const tz = this.getFloat32()
593
+ const rx = this.getFloat32()
594
+ const ry = this.getFloat32()
595
+ const rz = this.getFloat32()
596
+ const rw = this.getFloat32()
597
+ if (boneIndex >= 0) {
598
+ const offset: BoneMorphOffset = {
599
+ boneIndex,
600
+ translation: [tx, ty, tz],
601
+ rotation: [rx, ry, rz, rw],
602
+ }
603
+ morph.boneOffsets!.push(offset)
604
+ }
587
605
  } else if (morphType >= 3 && morphType <= 7) {
588
606
  // UV morph + additional UV channels 1-4; offset is always a Float4 per spec
589
- this.getIndex(this.vertexIndexSize) // vertexIndex
590
- this.getFloat32() // x
591
- this.getFloat32() // y
592
- this.getFloat32() // z
593
- this.getFloat32() // w
607
+ const vertexIndex = this.getIndex(this.vertexIndexSize)
608
+ const x = this.getFloat32()
609
+ const y = this.getFloat32()
610
+ const z = this.getFloat32()
611
+ const w = this.getFloat32()
612
+ if (vertexIndex >= 0 && vertexIndex < this.vertexCount) {
613
+ const offset: UvMorphOffset = { vertexIndex, offset: [x, y, z, w] }
614
+ morph.uvOffsets!.push(offset)
615
+ }
594
616
  } else if (morphType === 8) {
595
617
  // Material morph
596
- this.getNonVertexIndex(this.materialIndexSize) // materialIndex
597
- this.getUint8() // offsetType
598
- this.getFloat32() // diffuse r
599
- this.getFloat32() // diffuse g
600
- this.getFloat32() // diffuse b
601
- this.getFloat32() // diffuse a
602
- this.getFloat32() // specular r
603
- this.getFloat32() // specular g
604
- this.getFloat32() // specular b
605
- this.getFloat32() // specular power
606
- this.getFloat32() // ambient r
607
- this.getFloat32() // ambient g
608
- this.getFloat32() // ambient b
609
- this.getFloat32() // edgeColor r
610
- this.getFloat32() // edgeColor g
611
- this.getFloat32() // edgeColor b
612
- this.getFloat32() // edgeColor a
613
- this.getFloat32() // edgeSize
614
- this.getFloat32() // textureCoeff r
615
- this.getFloat32() // textureCoeff g
616
- this.getFloat32() // textureCoeff b
617
- this.getFloat32() // textureCoeff a
618
- this.getFloat32() // sphereCoeff r
619
- this.getFloat32() // sphereCoeff g
620
- this.getFloat32() // sphereCoeff b
621
- this.getFloat32() // sphereCoeff a
622
- this.getFloat32() // toonCoeff r
623
- this.getFloat32() // toonCoeff g
624
- this.getFloat32() // toonCoeff b
625
- this.getFloat32() // toonCoeff a
618
+ const materialIndex = this.getNonVertexIndex(this.materialIndexSize)
619
+ const offsetType = this.getUint8()
620
+ const diffuseR = this.getFloat32()
621
+ const diffuseG = this.getFloat32()
622
+ const diffuseB = this.getFloat32()
623
+ const diffuseA = this.getFloat32()
624
+ const specularR = this.getFloat32()
625
+ const specularG = this.getFloat32()
626
+ const specularB = this.getFloat32()
627
+ const shininess = this.getFloat32()
628
+ const ambientR = this.getFloat32()
629
+ const ambientG = this.getFloat32()
630
+ const ambientB = this.getFloat32()
631
+ const edgeR = this.getFloat32()
632
+ const edgeG = this.getFloat32()
633
+ const edgeB = this.getFloat32()
634
+ const edgeA = this.getFloat32()
635
+ const edgeSize = this.getFloat32()
636
+ const texR = this.getFloat32()
637
+ const texG = this.getFloat32()
638
+ const texB = this.getFloat32()
639
+ const texA = this.getFloat32()
640
+ const sphR = this.getFloat32()
641
+ const sphG = this.getFloat32()
642
+ const sphB = this.getFloat32()
643
+ const sphA = this.getFloat32()
644
+ const toonR = this.getFloat32()
645
+ const toonG = this.getFloat32()
646
+ const toonB = this.getFloat32()
647
+ const toonA = this.getFloat32()
648
+ // -1 is legal and means "every material" — kept as-is so the
649
+ // applier can fan it out without a second convention.
650
+ if (materialIndex >= -1) {
651
+ const offset: MaterialMorphOffset = {
652
+ materialIndex,
653
+ offsetType,
654
+ diffuse: [diffuseR, diffuseG, diffuseB, diffuseA],
655
+ specular: [specularR, specularG, specularB],
656
+ shininess,
657
+ ambient: [ambientR, ambientG, ambientB],
658
+ edgeColor: [edgeR, edgeG, edgeB, edgeA],
659
+ edgeSize,
660
+ textureCoeff: [texR, texG, texB, texA],
661
+ sphereCoeff: [sphR, sphG, sphB, sphA],
662
+ toonCoeff: [toonR, toonG, toonB, toonA],
663
+ }
664
+ morph.materialOffsets!.push(offset)
665
+ }
626
666
  } else if (morphType === 9) {
627
667
  // Flip morph (PMX 2.1): same layout as group morph
628
668
  this.getNonVertexIndex(this.morphIndexSize) // morphIndex