reze-engine 0.16.2 → 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 (74) hide show
  1. package/dist/engine.d.ts.map +1 -1
  2. package/dist/engine.js +5 -5
  3. package/dist/model.d.ts +4 -1
  4. package/dist/model.d.ts.map +1 -1
  5. package/dist/model.js +14 -1
  6. package/dist/physics/body.d.ts +1 -0
  7. package/dist/physics/body.d.ts.map +1 -1
  8. package/dist/physics/body.js +2 -0
  9. package/dist/physics/constraint.d.ts +1 -1
  10. package/dist/physics/constraint.d.ts.map +1 -1
  11. package/dist/physics/constraint.js +6 -1
  12. package/dist/physics/physics.d.ts +3 -0
  13. package/dist/physics/physics.d.ts.map +1 -1
  14. package/dist/physics/physics.js +69 -1
  15. package/dist/physics/solver.js +16 -8
  16. package/dist/physics/types.d.ts +2 -1
  17. package/dist/physics/types.d.ts.map +1 -1
  18. package/dist/physics/types.js +5 -0
  19. package/dist/physics/world.d.ts +3 -0
  20. package/dist/physics/world.d.ts.map +1 -1
  21. package/dist/physics/world.js +19 -3
  22. package/dist/pmx-loader.d.ts +2 -0
  23. package/dist/pmx-loader.d.ts.map +1 -1
  24. package/dist/pmx-loader.js +37 -20
  25. package/dist/shaders/materials/body.d.ts +1 -1
  26. package/dist/shaders/materials/body.d.ts.map +1 -1
  27. package/dist/shaders/materials/body.js +90 -85
  28. package/dist/shaders/materials/cloth_rough.d.ts +1 -1
  29. package/dist/shaders/materials/cloth_rough.d.ts.map +1 -1
  30. package/dist/shaders/materials/cloth_rough.js +1 -1
  31. package/dist/shaders/materials/cloth_smooth.d.ts +1 -1
  32. package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -1
  33. package/dist/shaders/materials/cloth_smooth.js +2 -2
  34. package/dist/shaders/materials/common.d.ts +1 -1
  35. package/dist/shaders/materials/common.d.ts.map +1 -1
  36. package/dist/shaders/materials/common.js +10 -0
  37. package/dist/shaders/materials/default.d.ts +1 -1
  38. package/dist/shaders/materials/default.d.ts.map +1 -1
  39. package/dist/shaders/materials/default.js +37 -32
  40. package/dist/shaders/materials/eye.d.ts +1 -1
  41. package/dist/shaders/materials/eye.d.ts.map +1 -1
  42. package/dist/shaders/materials/eye.js +40 -35
  43. package/dist/shaders/materials/face.d.ts +1 -1
  44. package/dist/shaders/materials/face.d.ts.map +1 -1
  45. package/dist/shaders/materials/face.js +90 -85
  46. package/dist/shaders/materials/hair.d.ts +1 -1
  47. package/dist/shaders/materials/hair.d.ts.map +1 -1
  48. package/dist/shaders/materials/hair.js +85 -75
  49. package/dist/shaders/materials/metal.d.ts +1 -1
  50. package/dist/shaders/materials/metal.d.ts.map +1 -1
  51. package/dist/shaders/materials/metal.js +1 -1
  52. package/dist/shaders/materials/stockings.d.ts +1 -1
  53. package/dist/shaders/materials/stockings.d.ts.map +1 -1
  54. package/dist/shaders/materials/stockings.js +1 -1
  55. package/package.json +1 -1
  56. package/src/engine.ts +36 -21
  57. package/src/model.ts +18 -1
  58. package/src/physics/body.ts +5 -0
  59. package/src/physics/constraint.ts +6 -1
  60. package/src/physics/physics.ts +68 -1
  61. package/src/physics/solver.ts +16 -8
  62. package/src/physics/types.ts +10 -2
  63. package/src/physics/world.ts +20 -3
  64. package/src/pmx-loader.ts +38 -20
  65. package/src/shaders/materials/body.ts +97 -92
  66. package/src/shaders/materials/cloth_rough.ts +1 -1
  67. package/src/shaders/materials/cloth_smooth.ts +2 -2
  68. package/src/shaders/materials/common.ts +10 -0
  69. package/src/shaders/materials/default.ts +45 -40
  70. package/src/shaders/materials/eye.ts +48 -43
  71. package/src/shaders/materials/face.ts +97 -92
  72. package/src/shaders/materials/hair.ts +92 -82
  73. package/src/shaders/materials/metal.ts +1 -1
  74. package/src/shaders/materials/stockings.ts +1 -1
@@ -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
  }
@@ -182,12 +182,15 @@ function setupConstraint(
182
182
  tgt[i] = target
183
183
  act[i] = denom > 0 ? active : 0
184
184
  con.cacheLinLimitImp[i] = 0
185
- if (con.springEnabled[i] && denom > 0) {
185
+ // A spring on a locked axis is redundant — the bilateral limit row
186
+ // already welds the DOF, and driving it twice overshoots every
187
+ // iteration (PMX rigs routinely put k=100000 springs on locked axes,
188
+ // which turned welded weight-bodies into energy pumps).
189
+ if (con.springEnabled[i] && denom > 0 && lo !== hi) {
186
190
  // Clamp k to the deadbeat limit: an explicit spring with k·dt² > 1
187
191
  // overshoots equilibrium every step and pumps energy — the classic
188
- // pre-Spring2 Bullet 6dof instability this port inherited. (¼ margin
189
- // for Gauss-Seidel coupling in chains.)
190
- const k = Math.min(con.springStiffness[i], 0.25 * invDt * invDt)
192
+ // pre-Spring2 Bullet 6dof instability this port inherited.
193
+ const k = Math.min(con.springStiffness[i], invDt * invDt)
191
194
  const serr = curr - con.equilibriumPoint[i]
192
195
  con.cacheLinSpringTarget[i] = -k * serr * dt
193
196
  con.cacheLinSpringActive[i] = 1
@@ -238,9 +241,11 @@ function setupConstraint(
238
241
  const angAct = con.cacheAngActive
239
242
  for (let i = 0; i < 3; i++) {
240
243
  const idx = i + 3
241
- if (con.springEnabled[idx] && angDenom > 0) {
244
+ // Springs on locked axes are skipped — the limit row welds those, and
245
+ // double-driving a DOF overshoots every iteration (see the linear loop).
246
+ if (con.springEnabled[idx] && angDenom > 0 && con.angularMin[i] !== con.angularMax[i]) {
242
247
  // Same deadbeat clamp as the linear springs.
243
- const k = Math.min(con.springStiffness[idx], 0.25 * invDt * invDt)
248
+ const k = Math.min(con.springStiffness[idx], invDt * invDt)
244
249
  const serr = _angDiffScratch[i] - con.equilibriumPoint[idx]
245
250
  angTgt[i] = k * serr * dt
246
251
  angAct[i] = 1
@@ -388,9 +393,12 @@ function iterateConstraint(
388
393
  }
389
394
 
390
395
  // Spring row: velocity-target drive; the deadbeat k-clamp at setup
391
- // bounds its aggression.
396
+ // bounds its aggression. relVel is refreshed with the limit impulse
397
+ // applied just above (j·denom = j / jac) — driving the spring off the
398
+ // stale value double-corrects the DOF and overshoots every iteration.
392
399
  if (sprAct[i]) {
393
- j += (sprTgt[i] - relVel) * jac[i]
400
+ const relVelNow = j !== 0 ? relVel + j / jac[i] : relVel
401
+ j += (sprTgt[i] - relVelNow) * jac[i]
394
402
  }
395
403
 
396
404
  if (j === 0) continue
@@ -6,10 +6,15 @@ export enum RigidbodyShape {
6
6
  Capsule = 2,
7
7
  }
8
8
 
9
+ // PMX physics mode: 0 = follow bone, 1 = physics, 2 = physics + bone-position
10
+ // alignment. Mode 2 is DYNAMIC — the loader maps it to Dynamic with
11
+ // `aligned: true` (bone gets rotation only, body position re-pins to the
12
+ // animated bone each frame). Mapping the raw byte 1:1 onto this enum froze
13
+ // mode-2 bodies (most modern cloth rigs, and every 胸_回転 breast body).
9
14
  export enum RigidbodyType {
10
- Static = 0,
15
+ Static = 0, // follows bone (anchor)
11
16
  Dynamic = 1,
12
- Kinematic = 2,
17
+ Kinematic = 2, // follows bone (legacy alias; loader no longer emits it)
13
18
  }
14
19
 
15
20
  export interface Rigidbody {
@@ -28,6 +33,9 @@ export interface Rigidbody {
28
33
  restitution: number
29
34
  friction: number
30
35
  type: RigidbodyType
36
+ // PMX mode 2: dynamic body whose bone takes rotation only; the body's
37
+ // position re-pins to the animated bone each frame.
38
+ aligned?: boolean
31
39
  bodyOffsetMatrixInverse: Mat4 // Inverse of body offset matrix, used to sync rigidbody to bone
32
40
  bodyOffsetMatrix?: Mat4 // Cached non-inverse for performance (computed once during initialization)
33
41
  }
@@ -14,6 +14,12 @@ export class World {
14
14
  readonly gravity: Vec3
15
15
  solverIterations = 10
16
16
 
17
+ // Per-body damping factors pow(1−damping, dt), cached because damping and
18
+ // the fixed dt never change — two Math.pow per body per substep otherwise.
19
+ private dampCacheDt = -1
20
+ private linDampFactor: Float32Array | null = null
21
+ private angDampFactor: Float32Array | null = null
22
+
17
23
  constructor(gravity: Vec3) {
18
24
  this.gravity = new Vec3(gravity.x, gravity.y, gravity.z)
19
25
  }
@@ -43,15 +49,26 @@ export class World {
43
49
 
44
50
  // 1. Predict — gravity + damping. The pow form (vs the linear
45
51
  // 1−damping·dt approximation) stays stable at high PMX damping
46
- // values like 0.99.
52
+ // values like 0.99. Factors are cached (damping and dt are constant).
53
+ if (this.dampCacheDt !== dt || !this.linDampFactor || this.linDampFactor.length !== N) {
54
+ this.dampCacheDt = dt
55
+ this.linDampFactor = new Float32Array(N)
56
+ this.angDampFactor = new Float32Array(N)
57
+ for (let i = 0; i < N; i++) {
58
+ this.linDampFactor[i] = Math.pow(Math.max(0, 1 - ldamp[i]), dt)
59
+ this.angDampFactor[i] = Math.pow(Math.max(0, 1 - adamp[i]), dt)
60
+ }
61
+ }
62
+ const linDamp = this.linDampFactor
63
+ const angDamp = this.angDampFactor!
47
64
  for (let i = 0; i < N; i++) {
48
65
  if (types[i] !== RigidbodyType.Dynamic || invMass[i] <= 0) continue
49
66
  const i3 = i * 3
50
67
  lv[i3 + 0] += gx * dt
51
68
  lv[i3 + 1] += gy * dt
52
69
  lv[i3 + 2] += gz * dt
53
- const ld = Math.pow(Math.max(0, 1 - ldamp[i]), dt)
54
- const ad = Math.pow(Math.max(0, 1 - adamp[i]), dt)
70
+ const ld = linDamp[i]
71
+ const ad = angDamp[i]
55
72
  lv[i3 + 0] *= ld; lv[i3 + 1] *= ld; lv[i3 + 2] *= ld
56
73
  av[i3 + 0] *= ad; av[i3 + 1] *= ad; av[i3 + 2] *= ad
57
74
  }
package/src/pmx-loader.ts CHANGED
@@ -37,11 +37,22 @@ export class PmxLoader {
37
37
  private vertexCount: number = 0
38
38
  private rigidbodies: Rigidbody[] = []
39
39
  private joints: Joint[] = []
40
+ private warnings: string[] = []
40
41
 
41
42
  private constructor(buffer: ArrayBuffer) {
42
43
  this.view = new DataView(buffer)
43
44
  }
44
45
 
46
+ // Parse problems are non-fatal (the mesh may still render), but they must
47
+ // be queryable afterwards — a rig whose rigidbody section failed to parse
48
+ // silently loses physics, and console.warn alone drowns in browser log
49
+ // limits. Collected warnings ship on the Model (getLoadWarnings()).
50
+ private warn(message: string, error?: unknown): void {
51
+ const full = error instanceof Error ? `${message}: ${error.message}` : message
52
+ this.warnings.push(full)
53
+ console.warn(message, error ?? "")
54
+ }
55
+
45
56
  static async load(url: string): Promise<Model> {
46
57
  return PmxLoader.loadFromReader(createFetchAssetReader(), url)
47
58
  }
@@ -82,7 +93,7 @@ export class PmxLoader {
82
93
  const version = this.getFloat32()
83
94
  if (version < 2.0 || version > 2.2) {
84
95
  // Continue, but warn for unexpected version
85
- console.warn(`PMX version ${version} may not be fully supported`)
96
+ this.warn(`PMX version ${version} may not be fully supported`)
86
97
  }
87
98
 
88
99
  // PMX: globals count (uint8) followed by that many bytes describing encoding and index sizes
@@ -472,7 +483,7 @@ export class PmxLoader {
472
483
  }
473
484
  this.bones = bones
474
485
  } catch (e) {
475
- console.warn("Error parsing bones:", e)
486
+ this.warn("Error parsing bones:", e)
476
487
  this.bones = []
477
488
  }
478
489
  }
@@ -487,7 +498,7 @@ export class PmxLoader {
487
498
  const count = this.getInt32()
488
499
  if (count < 0 || count > 100000) {
489
500
  // Suspicious count, likely corrupted
490
- console.warn(`Suspicious morph count: ${count}`)
501
+ this.warn(`Suspicious morph count: ${count}`)
491
502
  this.morphs = []
492
503
  return
493
504
  }
@@ -623,11 +634,11 @@ export class PmxLoader {
623
634
  this.morphs.push(morph)
624
635
  } catch (e) {
625
636
  // If we fail to read a morph, skip it
626
- console.warn(`Error reading morph ${i}:`, e)
637
+ this.warn(`Error reading morph ${i}:`, e)
627
638
  }
628
639
  }
629
640
  } catch (e) {
630
- console.warn("Error parsing morphs:", e)
641
+ this.warn("Error parsing morphs:", e)
631
642
  this.morphs = []
632
643
  }
633
644
  }
@@ -669,13 +680,13 @@ export class PmxLoader {
669
680
  }
670
681
  } catch (e) {
671
682
  // If we fail to read a frame, stop skipping
672
- console.warn(`Error reading display frame ${i}:`, e)
683
+ this.warn(`Error reading display frame ${i}:`, e)
673
684
  return false
674
685
  }
675
686
  }
676
687
  return true
677
688
  } catch (e) {
678
- console.warn("Error skipping display frames:", e)
689
+ this.warn("Error skipping display frames:", e)
679
690
  return false
680
691
  }
681
692
  }
@@ -685,7 +696,7 @@ export class PmxLoader {
685
696
  // Note: morphs and display frames are already skipped in parse() before calling this
686
697
  // Check bounds before reading rigidbody count
687
698
  if (this.offset + 4 > this.view.buffer.byteLength) {
688
- console.warn("Not enough bytes for rigidbody count")
699
+ this.warn("Not enough bytes for rigidbody count")
689
700
  this.rigidbodies = []
690
701
  return
691
702
  }
@@ -693,7 +704,7 @@ export class PmxLoader {
693
704
  const count = this.getInt32()
694
705
  if (count < 0 || count > 10000) {
695
706
  // Suspicious count
696
- console.warn(`Suspicious rigidbody count: ${count}`)
707
+ this.warn(`Suspicious rigidbody count: ${count}`)
697
708
  this.rigidbodies = []
698
709
  return
699
710
  }
@@ -704,7 +715,7 @@ export class PmxLoader {
704
715
  try {
705
716
  // Check bounds before reading each rigidbody
706
717
  if (this.offset >= this.view.buffer.byteLength) {
707
- console.warn(`Reached end of buffer while reading rigidbody ${i} of ${count}`)
718
+ this.warn(`Reached end of buffer while reading rigidbody ${i} of ${count}`)
708
719
  break
709
720
  }
710
721
 
@@ -737,7 +748,12 @@ export class PmxLoader {
737
748
  const angularDamping = this.getFloat32()
738
749
  const restitution = this.getFloat32()
739
750
  const friction = this.getFloat32()
740
- const type = this.getUint8() // 0=static, 1=dynamic, 2=kinematic
751
+ // PMX physics mode: 0 = follow bone, 1 = physics,
752
+ // 2 = physics + bone-position alignment. Mode 2 is dynamic — it
753
+ // must NOT be cast onto RigidbodyType (2 = Kinematic there, which
754
+ // freezes the body to its bone and kills physics for most modern
755
+ // cloth rigs and every 胸_回転 breast body).
756
+ const mode = this.getUint8()
741
757
 
742
758
  this.rigidbodies.push({
743
759
  name,
@@ -754,17 +770,18 @@ export class PmxLoader {
754
770
  angularDamping,
755
771
  restitution,
756
772
  friction,
757
- type: type as RigidbodyType,
773
+ type: mode === 0 ? RigidbodyType.Static : RigidbodyType.Dynamic,
774
+ aligned: mode === 2,
758
775
  bodyOffsetMatrixInverse: Mat4.identity(),
759
776
  })
760
777
  } catch (e) {
761
- console.warn(`Error reading rigidbody ${i} of ${count}:`, e)
778
+ this.warn(`Error reading rigidbody ${i} of ${count}:`, e)
762
779
  // Stop parsing if we encounter an error
763
780
  break
764
781
  }
765
782
  }
766
783
  } catch (e) {
767
- console.warn("Error parsing rigidbodies:", e)
784
+ this.warn("Error parsing rigidbodies:", e)
768
785
  this.rigidbodies = []
769
786
  }
770
787
  }
@@ -773,14 +790,14 @@ export class PmxLoader {
773
790
  try {
774
791
  // Check bounds before reading joint count
775
792
  if (this.offset + 4 > this.view.buffer.byteLength) {
776
- console.warn("Not enough bytes for joint count")
793
+ this.warn("Not enough bytes for joint count")
777
794
  this.joints = []
778
795
  return
779
796
  }
780
797
 
781
798
  const count = this.getInt32()
782
799
  if (count < 0 || count > 10000) {
783
- console.warn(`Suspicious joint count: ${count}`)
800
+ this.warn(`Suspicious joint count: ${count}`)
784
801
  this.joints = []
785
802
  return
786
803
  }
@@ -791,7 +808,7 @@ export class PmxLoader {
791
808
  try {
792
809
  // Check bounds before reading each joint
793
810
  if (this.offset >= this.view.buffer.byteLength) {
794
- console.warn(`Reached end of buffer while reading joint ${i} of ${count}`)
811
+ this.warn(`Reached end of buffer while reading joint ${i} of ${count}`)
795
812
  break
796
813
  }
797
814
 
@@ -857,13 +874,13 @@ export class PmxLoader {
857
874
  springRotation: new Vec3(springRotX, springRotY, springRotZ), // Store as Vec3 (stiffness values)
858
875
  })
859
876
  } catch (e) {
860
- console.warn(`Error reading joint ${i} of ${count}:`, e)
877
+ this.warn(`Error reading joint ${i} of ${count}:`, e)
861
878
  // Stop parsing if we encounter an error
862
879
  break
863
880
  }
864
881
  }
865
882
  } catch (e) {
866
- console.warn("Error parsing joints:", e)
883
+ this.warn("Error parsing joints:", e)
867
884
  this.joints = []
868
885
  }
869
886
  }
@@ -1044,7 +1061,8 @@ export class PmxLoader {
1044
1061
  skinning,
1045
1062
  morphing,
1046
1063
  this.rigidbodies,
1047
- this.joints
1064
+ this.joints,
1065
+ this.warnings
1048
1066
  )
1049
1067
  }
1050
1068
 
@@ -1,92 +1,97 @@
1
- // M_Body — 仿深空之眼渲染预设v1.0_by_小绿毛猫 "M_Body". Toon + warm rim + rim1/rim2
2
- // stack mixed 50/50 against a Principled BSDF with noise-bumped normal.
3
-
4
- import { NODES_WGSL } from "./nodes"
5
- import { COMMON_MATERIAL_PRELUDE_WGSL } from "./common"
6
-
7
- export const BODY_SHADER_WGSL = /* wgsl */ `
8
-
9
- ${NODES_WGSL}
10
- ${COMMON_MATERIAL_PRELUDE_WGSL}
11
-
12
- const BODY_ROUGHNESS: f32 = 0.3;
13
- const BODY_SPECULAR: f32 = 0.5;
14
- const BODY_MIX_NPR: f32 = 0.5;
15
- const BODY_SPEC_CLAMP: f32 = 10.0;
16
- const BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;
17
- const BODY_RIM2_POW: f32 = 1.4300000667572021;
18
- const BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);
19
- const BODY_WARM_STR: f32 = 0.30000001192092896;
20
-
21
- // smoothstep-based ramp: t*t*(3-2*t) between two color stops
22
- fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
23
- let t = saturate((f - p0) / max(p1 - p0, 1e-6));
24
- let ss = t * t * (3.0 - 2.0 * t);
25
- return mix(c0, c1, ss);
26
- }
27
-
28
- @fragment fn fs(input: VertexOutput) -> FSOut {
29
- let alpha = material.alpha;
30
- if (alpha < 0.001) { discard; }
31
-
32
- let n = normalize(input.normal);
33
- let v = normalize(camera.viewPos - input.worldPos);
34
- let l = -light.lights[0].direction.xyz;
35
- let sun = light.lights[0].color.xyz * light.lights[0].color.w;
36
- let amb = light.ambientColor.xyz;
37
- let shadow = sampleShadow(input.worldPos, n);
38
-
39
- let tex_color = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
40
-
41
- // ═══ NPR STACK ═══
42
- let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
43
- let toon = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;
44
-
45
- let shadow_tint = hue_sat_id(2.0, 0.3499999940395355, 1.0, tex_color);
46
- let lit_tint = hue_sat_id(1.5, 1.0, 1.0, tex_color);
47
- let toon_color = mix_blend(toon, shadow_tint, lit_tint);
48
- let bc = bright_contrast(toon_color, 0.1, 0.2);
49
-
50
- let emission3 = bc * 4.0;
51
-
52
- let warm_input = clamp(toon + 0.5, 0.0, 1.0);
53
- let warm_color = ramp_cardinal(warm_input, 0.2409,
54
- vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,
55
- vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;
56
- let warm_emission = warm_color * BODY_WARM_STR;
57
-
58
- let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24000005424022675, n, v);
59
- let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;
60
-
61
- let facing_raw = layer_weight_facing(BODY_RIM2_LAYER_BLEND, n, v);
62
- let facing_pow = math_power(facing_raw, BODY_RIM2_POW);
63
- let rim2_fac = ramp_ease(facing_pow, 0.0, vec4f(0,0,0,1), 0.5052, vec4f(1,1,1,1)).r;
64
- let rim2_mixed = mix(emission3, BODY_RIM2_BG, rim2_fac);
65
-
66
- let npr_stack = rim1 + rim2_mixed + warm_emission;
67
-
68
- // ═══ PRINCIPLED BSDF with noise bump ═══
69
- // Mapping loc=rot=0 in the Blender graph folds to a plain scale multiply.
70
- // Height in rest space (restPos) so the bump rides with the skin; geometric basis stays
71
- // worldPos so the perturbed normal matches the world-space n (see face.ts note).
72
- let noise_val = tex_noise_d2(input.restPos * vec3f(1.0, 1.0, 1.5), 1.0);
73
- let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
74
- let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);
75
-
76
- let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6831911206245422, 0.19474034011363983, 0.13732507824897766));
77
- let p_emission = bc * 0.2;
78
-
79
- let principled = eval_principled(
80
- PrincipledIn(principled_base, 0.0, BODY_SPECULAR, BODY_ROUGHNESS, BODY_SPEC_CLAMP, 0.0, 0.0),
81
- bumped_n, l, v, sun, amb, shadow
82
- ) + p_emission;
83
-
84
- let final_color = mix(npr_stack, principled, BODY_MIX_NPR);
85
-
86
- var out: FSOut;
87
- out.color = vec4f(final_color, alpha);
88
- out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
89
- return out;
90
- }
91
-
92
- `
1
+ // M_Body — 仿深空之眼渲染预设v1.0_by_小绿毛猫 "M_Body". Toon + warm rim + rim1/rim2
2
+ // stack mixed 50/50 against a Principled BSDF with noise-bumped normal.
3
+
4
+ import { NODES_WGSL } from "./nodes"
5
+ import { COMMON_MATERIAL_PRELUDE_WGSL } from "./common"
6
+
7
+ export const BODY_SHADER_WGSL = /* wgsl */ `
8
+
9
+ ${NODES_WGSL}
10
+ ${COMMON_MATERIAL_PRELUDE_WGSL}
11
+
12
+ const BODY_ROUGHNESS: f32 = 0.3;
13
+ const BODY_SPECULAR: f32 = 0.5;
14
+ const BODY_MIX_NPR: f32 = 0.5;
15
+ const BODY_SPEC_CLAMP: f32 = 10.0;
16
+ const BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;
17
+ const BODY_RIM2_POW: f32 = 1.4300000667572021;
18
+ const BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);
19
+ const BODY_WARM_STR: f32 = 0.30000001192092896;
20
+
21
+ // smoothstep-based ramp: t*t*(3-2*t) between two color stops
22
+ fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
23
+ let t = saturate((f - p0) / max(p1 - p0, 1e-6));
24
+ let ss = t * t * (3.0 - 2.0 * t);
25
+ return mix(c0, c1, ss);
26
+ }
27
+
28
+ @fragment fn fs(input: VertexOutput) -> FSOut {
29
+ let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
30
+ // MMD alpha semantics: material alpha × texture alpha. Hair/lace/accessory
31
+ // textures cut their shapes in the alpha channel — ignoring it renders each
32
+ // card's full quad with the texture's padding color (white shimmer on dark
33
+ // hair for models whose textures pad with white).
34
+ let alpha = material.alpha * tex_s.a;
35
+ if (alpha < 0.001) { discard; }
36
+
37
+ let n = safe_normal(input.normal);
38
+ let v = normalize(camera.viewPos - input.worldPos);
39
+ let l = -light.lights[0].direction.xyz;
40
+ let sun = light.lights[0].color.xyz * light.lights[0].color.w;
41
+ let amb = light.ambientColor.xyz;
42
+ let shadow = sampleShadow(input.worldPos, n);
43
+
44
+ let tex_color = tex_s.rgb;
45
+
46
+ // ═══ NPR STACK ═══
47
+ let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
48
+ let toon = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;
49
+
50
+ let shadow_tint = hue_sat_id(2.0, 0.3499999940395355, 1.0, tex_color);
51
+ let lit_tint = hue_sat_id(1.5, 1.0, 1.0, tex_color);
52
+ let toon_color = mix_blend(toon, shadow_tint, lit_tint);
53
+ let bc = bright_contrast(toon_color, 0.1, 0.2);
54
+
55
+ let emission3 = bc * 4.0;
56
+
57
+ let warm_input = clamp(toon + 0.5, 0.0, 1.0);
58
+ let warm_color = ramp_cardinal(warm_input, 0.2409,
59
+ vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,
60
+ vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;
61
+ let warm_emission = warm_color * BODY_WARM_STR;
62
+
63
+ let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24000005424022675, n, v);
64
+ let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;
65
+
66
+ let facing_raw = layer_weight_facing(BODY_RIM2_LAYER_BLEND, n, v);
67
+ let facing_pow = math_power(facing_raw, BODY_RIM2_POW);
68
+ let rim2_fac = ramp_ease(facing_pow, 0.0, vec4f(0,0,0,1), 0.5052, vec4f(1,1,1,1)).r;
69
+ let rim2_mixed = mix(emission3, BODY_RIM2_BG, rim2_fac);
70
+
71
+ let npr_stack = rim1 + rim2_mixed + warm_emission;
72
+
73
+ // ═══ PRINCIPLED BSDF with noise bump ═══
74
+ // Mapping loc=rot=0 in the Blender graph folds to a plain scale multiply.
75
+ // Height in rest space (restPos) so the bump rides with the skin; geometric basis stays
76
+ // worldPos so the perturbed normal matches the world-space n (see face.ts note).
77
+ let noise_val = tex_noise_d2(input.restPos * vec3f(1.0, 1.0, 1.5), 1.0);
78
+ let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
79
+ let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);
80
+
81
+ let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6831911206245422, 0.19474034011363983, 0.13732507824897766));
82
+ let p_emission = bc * 0.2;
83
+
84
+ let principled = eval_principled(
85
+ PrincipledIn(principled_base, 0.0, BODY_SPECULAR, BODY_ROUGHNESS, BODY_SPEC_CLAMP, 0.0, 0.0),
86
+ bumped_n, l, v, sun, amb, shadow
87
+ ) + p_emission;
88
+
89
+ let final_color = mix(npr_stack, principled, BODY_MIX_NPR);
90
+
91
+ var out: FSOut;
92
+ out.color = vec4f(final_color, alpha);
93
+ out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
94
+ return out;
95
+ }
96
+
97
+ `
@@ -20,7 +20,7 @@ const CLOTH_R_BUMP_STR: f32 = 1.0;
20
20
  const CLOTH_R_SPEC_CLAMP: f32 = 10.0;
21
21
 
22
22
  @fragment fn fs(input: VertexOutput) -> FSOut {
23
- let n = normalize(input.normal);
23
+ let n = safe_normal(input.normal);
24
24
  let v = normalize(camera.viewPos - input.worldPos);
25
25
  let l = -light.lights[0].direction.xyz;
26
26
  let sun = light.lights[0].color.xyz * light.lights[0].color.w;
@@ -17,7 +17,7 @@ const NPR_EMIT_STR: f32 = 18.200000762939453;
17
17
  const NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;
18
18
 
19
19
  @fragment fn fs(input: VertexOutput) -> FSOut {
20
- let n = normalize(input.normal);
20
+ let n = safe_normal(input.normal);
21
21
  let v = normalize(camera.viewPos - input.worldPos);
22
22
  let l = -light.lights[0].direction.xyz;
23
23
  let sun = light.lights[0].color.xyz * light.lights[0].color.w;
@@ -47,7 +47,7 @@ const NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;
47
47
  // ═══ PRINCIPLED BSDF ═══
48
48
  let principled_base = hue_sat_id(1.0, 0.800000011920929, 1.0, tex_rgb);
49
49
  let principled = eval_principled(
50
- PrincipledIn(principled_base, 0.0, CLOTH_SPECULAR, CLOTH_ROUGHNESS, 1e30, 0.0, 0.0),
50
+ PrincipledIn(principled_base, 0.0, CLOTH_SPECULAR, CLOTH_ROUGHNESS, 10.0, 0.0, 0.0),
51
51
  n, l, v, sun, amb, shadow
52
52
  );
53
53