reze-engine 0.50.3 → 0.50.5

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 (105) hide show
  1. package/dist/engine.d.ts +247 -2
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +822 -64
  4. package/dist/model.d.ts +49 -1
  5. package/dist/model.d.ts.map +1 -1
  6. package/dist/model.js +160 -4
  7. package/dist/physics/autofit.d.ts +147 -0
  8. package/dist/physics/autofit.d.ts.map +1 -0
  9. package/dist/physics/autofit.js +501 -0
  10. package/dist/physics/physics.d.ts +35 -0
  11. package/dist/physics/physics.d.ts.map +1 -1
  12. package/dist/physics/physics.js +64 -0
  13. package/dist/physics/world.d.ts +4 -0
  14. package/dist/physics/world.d.ts.map +1 -1
  15. package/dist/physics/world.js +6 -0
  16. package/dist/shaders/cast-api.d.ts +1 -1
  17. package/dist/shaders/cast-api.d.ts.map +1 -1
  18. package/dist/shaders/cast-layout.d.ts +44 -1
  19. package/dist/shaders/cast-layout.d.ts.map +1 -1
  20. package/dist/shaders/cast-layout.js +44 -1
  21. package/dist/shaders/materials/common.d.ts.map +1 -1
  22. package/dist/shaders/materials/common.js +7 -1
  23. package/dist/shaders/materials/nodes.d.ts +1 -1
  24. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  25. package/dist/shaders/materials/nodes.js +17 -9
  26. package/dist/shaders/passes/composite.d.ts +1 -1
  27. package/dist/shaders/passes/composite.d.ts.map +1 -1
  28. package/dist/shaders/passes/depth-prepass.d.ts +1 -1
  29. package/dist/shaders/passes/depth-prepass.d.ts.map +1 -1
  30. package/dist/shaders/passes/depth-prepass.js +52 -12
  31. package/dist/shaders/passes/field-blit.d.ts +26 -0
  32. package/dist/shaders/passes/field-blit.d.ts.map +1 -0
  33. package/dist/shaders/passes/field-blit.js +65 -0
  34. package/dist/shaders/passes/ground-noise.d.ts +7 -0
  35. package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
  36. package/dist/shaders/passes/ground-noise.js +88 -0
  37. package/dist/shaders/passes/ground.d.ts +16 -0
  38. package/dist/shaders/passes/ground.d.ts.map +1 -1
  39. package/dist/shaders/passes/ground.js +131 -27
  40. package/dist/shaders/passes/outline.d.ts +1 -1
  41. package/dist/shaders/passes/outline.d.ts.map +1 -1
  42. package/dist/shaders/passes/outline.js +12 -3
  43. package/dist/shaders/passes/particles.d.ts.map +1 -1
  44. package/dist/shaders/passes/particles.js +6 -2
  45. package/dist/shaders/passes/scene-contract.d.ts +38 -6
  46. package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
  47. package/dist/shaders/passes/scene-contract.js +53 -16
  48. package/dist/shaders/passes/sim.d.ts +34 -0
  49. package/dist/shaders/passes/sim.d.ts.map +1 -0
  50. package/dist/shaders/passes/sim.js +169 -0
  51. package/dist/shaders/passes/trails.d.ts.map +1 -1
  52. package/dist/shaders/passes/trails.js +36 -8
  53. package/dist/shaders/score-api.d.ts +10 -0
  54. package/dist/shaders/score-api.d.ts.map +1 -0
  55. package/dist/shaders/score-api.js +114 -0
  56. package/package.json +1 -1
  57. package/src/engine.ts +867 -56
  58. package/src/model.ts +163 -4
  59. package/src/physics/physics.ts +63 -0
  60. package/src/physics/world.ts +7 -0
  61. package/src/shaders/cast-layout.ts +44 -1
  62. package/src/shaders/materials/common.ts +7 -1
  63. package/src/shaders/materials/nodes.ts +17 -9
  64. package/src/shaders/passes/depth-prepass.ts +53 -12
  65. package/src/shaders/passes/ground.ts +133 -27
  66. package/src/shaders/passes/outline.ts +14 -3
  67. package/src/shaders/passes/particles.ts +6 -2
  68. package/src/shaders/passes/scene-contract.ts +55 -16
  69. package/src/shaders/passes/trails.ts +36 -8
  70. package/dist/physics-debug.d.ts +0 -30
  71. package/dist/physics-debug.d.ts.map +0 -1
  72. package/dist/physics-debug.js +0 -526
  73. package/dist/shaders/materials/body.d.ts +0 -2
  74. package/dist/shaders/materials/body.d.ts.map +0 -1
  75. package/dist/shaders/materials/body.js +0 -95
  76. package/dist/shaders/materials/cloth_rough.d.ts +0 -2
  77. package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
  78. package/dist/shaders/materials/cloth_rough.js +0 -69
  79. package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
  80. package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
  81. package/dist/shaders/materials/cloth_smooth.js +0 -61
  82. package/dist/shaders/materials/default.d.ts +0 -2
  83. package/dist/shaders/materials/default.d.ts.map +0 -1
  84. package/dist/shaders/materials/default.js +0 -43
  85. package/dist/shaders/materials/eye.d.ts +0 -2
  86. package/dist/shaders/materials/eye.d.ts.map +0 -1
  87. package/dist/shaders/materials/eye.js +0 -60
  88. package/dist/shaders/materials/face.d.ts +0 -2
  89. package/dist/shaders/materials/face.d.ts.map +0 -1
  90. package/dist/shaders/materials/face.js +0 -95
  91. package/dist/shaders/materials/hair.d.ts +0 -2
  92. package/dist/shaders/materials/hair.d.ts.map +0 -1
  93. package/dist/shaders/materials/hair.js +0 -90
  94. package/dist/shaders/materials/metal.d.ts +0 -2
  95. package/dist/shaders/materials/metal.d.ts.map +0 -1
  96. package/dist/shaders/materials/metal.js +0 -77
  97. package/dist/shaders/materials/mmd_classic.d.ts +0 -2
  98. package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
  99. package/dist/shaders/materials/mmd_classic.js +0 -66
  100. package/dist/shaders/materials/stockings.d.ts +0 -2
  101. package/dist/shaders/materials/stockings.d.ts.map +0 -1
  102. package/dist/shaders/materials/stockings.js +0 -122
  103. package/dist/shaders/passes/physics-debug.d.ts +0 -2
  104. package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
  105. package/dist/shaders/passes/physics-debug.js +0 -69
package/src/model.ts CHANGED
@@ -30,6 +30,10 @@ const _animSlerp = new Quat(0, 0, 0, 1)
30
30
  const _animInterpT = new Vec3(0, 0, 0)
31
31
  const _convOut = new Vec3(0, 0, 0)
32
32
  const _convMat = new Float32Array(16)
33
+ // Scratch for the post-physics append recovery — see applyPhysicsAppend.
34
+ const _appendBasisX = new Vec3(0, 0, 0)
35
+ const _appendBasisY = new Vec3(0, 0, 0)
36
+ const _appendBasisZ = new Vec3(0, 0, 0)
33
37
  // Blend-path scratch: per-entry sample target and the crossfade's two fixed entries.
34
38
  const _blendQ = new Quat(0, 0, 0, 1)
35
39
  const _blendT = new Vec3(0, 0, 0)
@@ -388,6 +392,15 @@ export class Model {
388
392
  // visited-array + closure. Order depends only on parentIndex (static), so this
389
393
  // reproduces the old recursion's finishing order exactly. See buildDeformOrder.
390
394
  private deformOrder!: Int32Array
395
+ /** 1 where the simulation overwrites the bone's world matrix. See setPhysicsDrivenBones. */
396
+ private physicsDriven: Uint8Array | null = null
397
+ /** Bones to recompute after a step, in deform order — null when no rig needs it. */
398
+ private physicsAppendOrder: Int32Array | null = null
399
+ /** Simulated bones something inherits from; their local rotation is recovered. */
400
+ private physicsAppendSources: Int32Array | null = null
401
+ /** Recovered post-step local rotations, kept OUT of localRotations on purpose. */
402
+ private appendRotOverride: Quat[] | null = null
403
+ private appendRotOverrideSet: Uint8Array | null = null
391
404
 
392
405
  // Bind-pose absolute (world) position per bone, xyz packed. Static (bindTranslation
393
406
  // accumulated down the hierarchy). Precomputed once so convertVMDTranslationToLocal
@@ -2612,7 +2625,141 @@ export class Model {
2612
2625
  }
2613
2626
  }
2614
2627
 
2615
- computeWorldMatrices(): void {
2628
+ /**
2629
+ * Which bones the physics simulation overwrites, and what that costs the
2630
+ * append (付与) pass — the 胸 problem.
2631
+ *
2632
+ * PMX lets a bone inherit a fraction of another bone's rotation (付与親 /
2633
+ * append parent). The pipeline computes that inheritance inside
2634
+ * computeWorldMatrices, which runs BEFORE physics — and it reads the
2635
+ * parent's LOCAL rotation, which physics never writes: the simulation
2636
+ * publishes world matrices only. So when a rig hangs a bone off a simulated
2637
+ * one, the inheritance saw the animated pose and nothing else, and the
2638
+ * dependent bone sat still no matter how much the parent swung. Rigs that
2639
+ * drive a chest this way — a simulated bone with the visible bones
2640
+ * appending from it — produced no motion at all.
2641
+ *
2642
+ * The engine calls this once, after building the simulation, and it
2643
+ * precomputes the whole answer: WHICH bones need revisiting after a step,
2644
+ * in deform order. Everything downstream is a walk over that list.
2645
+ */
2646
+ setPhysicsDrivenBones(boneIndices: number[]): void {
2647
+ const bones = this.skeleton.bones
2648
+ const n = bones.length
2649
+ this.physicsDriven = new Uint8Array(n)
2650
+ for (const b of boneIndices) if (b >= 0 && b < n) this.physicsDriven[b] = 1
2651
+
2652
+ // Bones to recompute after a step: anything that INHERITS from a simulated
2653
+ // bone, everything under it, and anything inheriting from those in turn.
2654
+ // Simulated bones themselves are deliberately excluded — their world matrix
2655
+ // IS the simulation's output, and recomputing it from a local pose the
2656
+ // simulation never wrote would throw the step away.
2657
+ const affected = new Uint8Array(n)
2658
+ let changed = true
2659
+ while (changed) {
2660
+ changed = false
2661
+ for (let k = 0; k < n; k++) {
2662
+ const i = this.deformOrder[k]
2663
+ if (affected[i] || this.physicsDriven[i]) continue
2664
+ const b = bones[i]
2665
+ const ap = b.appendParentIndex
2666
+ const inheritsAffected =
2667
+ (b.appendRotate || b.appendMove) && ap !== undefined && ap >= 0 && ap < n && (this.physicsDriven[ap] || affected[ap])
2668
+ const parentAffected = b.parentIndex >= 0 && affected[b.parentIndex]
2669
+ if (inheritsAffected || parentAffected) {
2670
+ affected[i] = 1
2671
+ changed = true
2672
+ }
2673
+ }
2674
+ }
2675
+
2676
+ const order: number[] = []
2677
+ for (let k = 0; k < n; k++) {
2678
+ const i = this.deformOrder[k]
2679
+ if (affected[i]) order.push(i)
2680
+ }
2681
+ this.physicsAppendOrder = order.length > 0 ? Int32Array.from(order) : null
2682
+ // The simulated bones something actually inherits from — the only ones
2683
+ // whose post-step local rotation has to be recovered below.
2684
+ const sources = new Set<number>()
2685
+ for (const i of order) {
2686
+ const ap = bones[i].appendParentIndex
2687
+ if (ap !== undefined && ap >= 0 && ap < n && this.physicsDriven[ap]) sources.add(ap)
2688
+ }
2689
+ this.physicsAppendSources = sources.size > 0 ? Int32Array.from(sources) : null
2690
+ if (this.appendRotOverride === null && this.physicsAppendOrder) {
2691
+ this.appendRotOverride = Array.from({ length: n }, () => Quat.identity())
2692
+ this.appendRotOverrideSet = new Uint8Array(n)
2693
+ }
2694
+ }
2695
+
2696
+ /** The simulated bones that visible bones INHERIT from — the chest rig, in
2697
+ * one list. Empty unless setPhysicsDrivenBones found such a relationship. */
2698
+ getAppendSourceBones(): number[] {
2699
+ return this.physicsAppendSources ? Array.from(this.physicsAppendSources) : []
2700
+ }
2701
+
2702
+ /**
2703
+ * Re-run the append pass against the simulation's result. Call after a step.
2704
+ *
2705
+ * Two halves. First the simulated bones an append parent list names get their
2706
+ * post-step LOCAL rotation recovered — physics published only world matrices,
2707
+ * and the append math speaks local. The recovery is the ordinary change of
2708
+ * basis: local = parentWorld⁻¹ · world, read back as a quaternion off the
2709
+ * relative basis, which is exact for the rigid transforms these are.
2710
+ *
2711
+ * Then the dependent bones recompute, in deform order, reading that recovered
2712
+ * rotation instead of the animated one. The recovered value lives in its OWN
2713
+ * array rather than in localRotations, deliberately: localRotations is what
2714
+ * next frame's pose blends against and what the simulation reads to build
2715
+ * kinematic targets, and writing a simulation result back into it would make
2716
+ * the animation chase its own tail.
2717
+ */
2718
+ applyPhysicsAppend(): void {
2719
+ const order = this.physicsAppendOrder
2720
+ const sources = this.physicsAppendSources
2721
+ if (!order || !sources || !this.appendRotOverride || !this.appendRotOverrideSet) return
2722
+ const bones = this.skeleton.bones
2723
+ const worldMats = this.runtimeSkeleton.worldMatrices
2724
+
2725
+ this.appendRotOverrideSet.fill(0)
2726
+ for (let s = 0; s < sources.length; s++) {
2727
+ const i = sources[s]
2728
+ const w = worldMats[i].values
2729
+ const p = bones[i].parentIndex
2730
+ // Columns of the bone's own basis, expressed in its parent's frame. With
2731
+ // no parent the world basis already IS the local one.
2732
+ let bx0 = w[0], bx1 = w[1], bx2 = w[2]
2733
+ let by0 = w[4], by1 = w[5], by2 = w[6]
2734
+ let bz0 = w[8], bz1 = w[9], bz2 = w[10]
2735
+ if (p >= 0) {
2736
+ const pm = worldMats[p].values
2737
+ // parentᵀ · child, the rotation half of parentWorld⁻¹ · world: the
2738
+ // parent basis is orthonormal, so its inverse is its transpose.
2739
+ const r0 = bx0, r1 = bx1, r2 = bx2
2740
+ bx0 = pm[0] * r0 + pm[1] * r1 + pm[2] * r2
2741
+ bx1 = pm[4] * r0 + pm[5] * r1 + pm[6] * r2
2742
+ bx2 = pm[8] * r0 + pm[9] * r1 + pm[10] * r2
2743
+ const g0 = by0, g1 = by1, g2 = by2
2744
+ by0 = pm[0] * g0 + pm[1] * g1 + pm[2] * g2
2745
+ by1 = pm[4] * g0 + pm[5] * g1 + pm[6] * g2
2746
+ by2 = pm[8] * g0 + pm[9] * g1 + pm[10] * g2
2747
+ const b0 = bz0, b1 = bz1, b2 = bz2
2748
+ bz0 = pm[0] * b0 + pm[1] * b1 + pm[2] * b2
2749
+ bz1 = pm[4] * b0 + pm[5] * b1 + pm[6] * b2
2750
+ bz2 = pm[8] * b0 + pm[9] * b1 + pm[10] * b2
2751
+ }
2752
+ _appendBasisX.setXYZ(bx0, bx1, bx2)
2753
+ _appendBasisY.setXYZ(by0, by1, by2)
2754
+ _appendBasisZ.setXYZ(bz0, bz1, bz2)
2755
+ Quat.fromBasisInto(_appendBasisX, _appendBasisY, _appendBasisZ, this.appendRotOverride[i])
2756
+ this.appendRotOverrideSet[i] = 1
2757
+ }
2758
+
2759
+ this.computeWorldMatrices(order)
2760
+ }
2761
+
2762
+ computeWorldMatrices(subset?: Int32Array): void {
2616
2763
  const bones = this.skeleton.bones
2617
2764
  const localRot = this.runtimeSkeleton.localRotations
2618
2765
  const localTrans = this.runtimeSkeleton.localTranslations
@@ -2624,8 +2771,15 @@ export class Model {
2624
2771
  // Flat traversal in precomputed order: every bone's parent is already done, so no
2625
2772
  // per-bone visited check, no recursion, and no per-call allocation. Same per-bone
2626
2773
  // math as before. Scratch slots are safe to reuse since there's no reentrancy now.
2627
- const order = this.deformOrder
2628
- for (let k = 0; k < boneCount; k++) {
2774
+ //
2775
+ // A SUBSET is the post-physics pass (applyPhysicsAppend): the same walk over
2776
+ // the bones that inherit from a simulated one, in the same relative order,
2777
+ // leaving every other bone — the simulated ones above all — untouched.
2778
+ const order = subset ?? this.deformOrder
2779
+ const count = subset ? subset.length : boneCount
2780
+ const override = this.appendRotOverride
2781
+ const overrideSet = this.appendRotOverrideSet
2782
+ for (let k = 0; k < count; k++) {
2629
2783
  const i = order[k]
2630
2784
  const b = bones[i]
2631
2785
 
@@ -2643,7 +2797,12 @@ export class Model {
2643
2797
 
2644
2798
  if (hasRatio) {
2645
2799
  if (b.appendRotate) {
2646
- const appendRot = localRot[appendParentIdx]
2800
+ // The simulated parent's RECOVERED rotation when there is one — the
2801
+ // whole point of the post-physics pass. localRotations still holds
2802
+ // the animated pose for that bone, which is exactly what must not
2803
+ // be inherited here.
2804
+ const appendRot =
2805
+ override && overrideSet && overrideSet[appendParentIdx] ? override[appendParentIdx] : localRot[appendParentIdx]
2647
2806
  let ax = appendRot.x, ay = appendRot.y, az = appendRot.z
2648
2807
  const aw = appendRot.w
2649
2808
  const absRatio = ratio < 0 ? -ratio : ratio
@@ -706,6 +706,69 @@ export class RezePhysics {
706
706
  // boneWorld = bodyWorld × bodyOffsetInverse.
707
707
  // The body pose is the render-interpolated pose between the previous and current
708
708
  // substep states (alpha = fraction into the next step), which removes fixed-step judder.
709
+ /**
710
+ * Let the bodies carrying these bones swing longer, by damping them less.
711
+ *
712
+ * DAMPING, and not solver iterations, and the difference is the whole point.
713
+ * Under-converging a joint does make it swing further — it also stops it ever
714
+ * reaching equilibrium, so the body hangs visibly low at rest. Sag and swing
715
+ * come as a pair there and no amount of tuning separates them. Damping does
716
+ * separate them: for m·x″ + c·x′ + k·x = mg the rest position is mg/k, which
717
+ * c does not appear in. Less damping is a longer, larger oscillation about
718
+ * exactly the same resting height.
719
+ *
720
+ * Scoped to the bones asked for, because it is a look and not a correction —
721
+ * rigs whose visible bones inherit from a simulated one (付与親) are authored
722
+ * against an MMD that lets them move more than a faithfully damped
723
+ * simulation does. Hair and skirt keep their authored damping.
724
+ *
725
+ * `scale` multiplies the AUTHORED damping: 1 restores it, 0.5 halves it,
726
+ * 0 leaves the body undamped and ringing. Idempotent — the authored values
727
+ * are snapshotted on first use, so repeated calls set rather than compound.
728
+ */
729
+ setJiggleDamping(boneIndices: number[], scale: number): void {
730
+ const wanted = new Set(boneIndices.filter((b) => b >= 0))
731
+ if (wanted.size === 0) return
732
+ if (!this.authoredLinDamp || !this.authoredAngDamp) {
733
+ this.authoredLinDamp = Float32Array.from(this.store.linearDamping)
734
+ this.authoredAngDamp = Float32Array.from(this.store.angularDamping)
735
+ }
736
+ const s = Math.max(0, Math.min(1, scale))
737
+ const boneOf = this.store.boneIndex
738
+ for (let i = 0; i < this.store.count; i++) {
739
+ const b = boneOf[i]
740
+ if (b < 0 || !wanted.has(b)) continue
741
+ this.store.linearDamping[i] = this.authoredLinDamp[i] * s
742
+ this.store.angularDamping[i] = this.authoredAngDamp[i] * s
743
+ }
744
+ // The factors are cached against dt, which has not changed.
745
+ this.world.invalidateDampingCache()
746
+ }
747
+
748
+ /** Authored damping, kept so setJiggleDamping sets rather than compounds. */
749
+ private authoredLinDamp: Float32Array | null = null
750
+ private authoredAngDamp: Float32Array | null = null
751
+
752
+ /**
753
+ * Bones whose world matrix this simulation OVERWRITES each step.
754
+ *
755
+ * The same test applyDynamicsToBones runs — a Dynamic body bound to a real
756
+ * bone — exposed because the pose pipeline has to know. PMX lets a bone
757
+ * inherit rotation from an 付与親 (append parent), and when that parent is
758
+ * simulated the inheritance has to consume the SIMULATED result, not the
759
+ * animated pose the frame started with. Nothing else can answer which bones
760
+ * those are: the mapping lives in this store.
761
+ */
762
+ getPhysicsDrivenBones(): number[] {
763
+ const out: number[] = []
764
+ for (let i = 0; i < this.store.count; i++) {
765
+ if (this.store.type[i] !== RigidbodyType.Dynamic) continue
766
+ const b = this.store.boneIndex[i]
767
+ if (b >= 0) out.push(b)
768
+ }
769
+ return out
770
+ }
771
+
709
772
  private applyDynamicsToBones(boneWorldMatrices: Mat4[], alpha: number): void {
710
773
  const N = this.store.count
711
774
  const inv = this.store.bodyOffsetInverse
@@ -45,6 +45,13 @@ export class World {
45
45
  // Per-body damping factors pow(1−damping, dt), cached because damping and
46
46
  // the fixed dt never change — two Math.pow per body per substep otherwise.
47
47
  private dampCacheDt = -1
48
+
49
+ /** Drop the cached damping factors. The cache is keyed on dt alone, because
50
+ * authored damping never changed — until a rig asked for softer jiggle (see
51
+ * RezePhysics.setJiggleDamping), which rewrites the store's values. */
52
+ invalidateDampingCache(): void {
53
+ this.dampCacheDt = -1
54
+ }
48
55
  private linDampFactor: Float32Array | null = null
49
56
  private angDampFactor: Float32Array | null = null
50
57
 
@@ -14,7 +14,50 @@
14
14
  * the whole engine failing to start on an import order nobody chose.
15
15
  */
16
16
  export const EFFECT_SUBJECTS = 4
17
- export const EFFECT_ANCHORS = 8
17
+ /**
18
+ * Bone anchors, for the WHOLE SCENE rather than per effect — this is an address
19
+ * space that every installed effect draws slots from, and an effect asking for
20
+ * one the table has already given away is told so and has it dropped.
21
+ *
22
+ * 8 was reachable, and quietly: two effects each wanting two hands, two feet and
23
+ * a head is ten, so the second one silently lost its ribbons to a diagnostic
24
+ * nobody was reading. 16 is double the headroom for 131KB of storage buffer,
25
+ * where 8 cost 66KB.
26
+ *
27
+ * The cost really is only that. The per-frame upload is bounded by the last
28
+ * TRAILED slot, not by this cap (see the writeBuffer in updateCastBuffer), so a
29
+ * scene using three anchors uploads three anchors' worth whatever this says.
30
+ * The CPU-side path rings are keyed by (model, slot) and allocated on use. And
31
+ * effects never index this directly — they loop to rzTrailCount / rzSubjectCount
32
+ * and read through rzAnchor/rzTrail, both of which bounds-check against it.
33
+ *
34
+ * It stays a compile-time constant because the accessors in cast-api.ts are
35
+ * interpolated into every effect module as WGSL literals; making it dynamic
36
+ * means resizing the buffer and recompiling every installed effect whenever the
37
+ * scene's anchor count grows, which is a different feature from raising a number
38
+ * that was never load-bearing.
39
+ */
40
+ export const EFFECT_ANCHORS = 16
41
+ /**
42
+ * Path samples kept per trailed anchor — 128 at the 60Hz sampling rate is a
43
+ * ~2.1 second ribbon.
44
+ *
45
+ * Briefly 256, and reverted with the reason, because the reason is the useful
46
+ * part: a ribbon's cost is not its geometry, it is its FRAGMENTS. It is a wide
47
+ * translucent strip blended additively into the HDR target at the pass's sample
48
+ * count, and it overlaps itself — so its cost tracks the screen AREA it covers,
49
+ * and a twice-as-long ribbon covers roughly twice as much.
50
+ *
51
+ * That lands very differently on the two backends. Overdraw and blend bandwidth
52
+ * are what a tile-based GPU pays for most and what an immediate-mode desktop GPU
53
+ * absorbs, which is why ribbons were reported as costing far more on Safari than
54
+ * on Chrome for the same scene. Doubling this doubles the one thing already
55
+ * known to be the bottleneck there.
56
+ *
57
+ * Raising it is still SAFE — effects loop to rzTrailCount and nothing breaks —
58
+ * it is simply not cheap, and the cost shows up on the slower of the two
59
+ * browsers rather than the one it would be measured on.
60
+ */
18
61
  export const EFFECT_TRAIL_SAMPLES = 128
19
62
  /** vec4 slot where the trails begin — after the subjects and the anchors. */
20
63
  export const EFFECT_TRAIL_BASE = EFFECT_SUBJECTS * 3 + EFFECT_ANCHORS * EFFECT_SUBJECTS * 3
@@ -73,7 +73,13 @@ struct MaterialUniforms {
73
73
  };
74
74
 
75
75
  struct VertexOutput {
76
- @builtin(position) position: vec4f,
76
+ // @invariant: the opaque depth prepass rasterises this same skinned position
77
+ // through a DIFFERENT shader module, and the colour pass then depth-tests
78
+ // less-equal against what it wrote. Without invariance a backend is free to
79
+ // optimise the two position computations differently, and a one-ulp
80
+ // disagreement is a pixel of missing character. Invariance pins both to the
81
+ // same result; it costs only that freedom.
82
+ @builtin(position) @invariant position: vec4f,
77
83
  @location(0) normal: vec3f,
78
84
  @location(1) uv: vec2f,
79
85
  @location(2) worldPos: vec3f,
@@ -216,18 +216,26 @@ fn curve5(t0: f32, y0: f32, y1: f32, y2: f32, y3: f32, y4: f32) -> f32 {
216
216
  let t = clamp(t0, 0.0, 1.0) * 4.0;
217
217
  let i = min(floor(t), 3.0);
218
218
  let f = t - i;
219
- // A var, not a let: WGSL only allows a dynamic index on a REFERENCE, and a
220
- // let-bound array is a value. Indexing it with a runtime k is a compile error —
221
- // and because this file is concatenated into every material shader, that error
222
- // took every graph in the library down with it, not just curves.
223
- var ys = array<f32, 5>(y0, y1, y2, y3, y4);
219
+ // NO local array, deliberately. This used to build array<f32, 5> and index it
220
+ // with the runtime k — and a dynamic index on function-local memory is the one
221
+ // construct this codebase has already caught Metal lowering to a
222
+ // per-invocation copy plus a switch (the filmic LUT note in composite.ts).
223
+ // rgb_curve calls this three times per node per pixel, so on a graph-heavy
224
+ // close-up that lowering was paid in the hottest loop the frame has. Four
225
+ // segments select cleanly: the values below are the SAME subtractions the
226
+ // array indexing produced, chosen by k instead of loaded through it —
227
+ // bit-identical results, no local memory, nothing for the backend to spill.
224
228
  let k = i32(i);
225
- let pa = ys[k];
226
- let pb = ys[k + 1];
229
+ let s0 = y1 - y0;
230
+ let s1 = y2 - y1;
231
+ let s2 = y3 - y2;
232
+ let s3 = y4 - y3;
233
+ let pa = select(select(y0, y1, k == 1), select(y2, y3, k == 3), k >= 2);
234
+ let pb = select(select(y1, y2, k == 1), select(y3, y4, k == 3), k >= 2);
227
235
  // Secants either side of each knot, clamped at the ends.
228
- let dPrev = select(ys[max(k, 1)] - ys[max(k, 1) - 1], pb - pa, k == 0);
229
236
  let dHere = pb - pa;
230
- let dNext = select(ys[min(k + 2, 4)] - ys[min(k + 1, 3)], pb - pa, k == 3);
237
+ let dPrev = select(s0, select(s1, s2, k == 3), k >= 2);
238
+ let dNext = select(select(s1, s2, k == 1), s3, k >= 2);
231
239
  let m0 = curve_slope(dPrev, dHere);
232
240
  let m1 = curve_slope(dHere, dNext);
233
241
  let f2 = f * f;
@@ -1,17 +1,36 @@
1
- // Depth-only prepass for the TRANSPARENT bucket. Transparent color draws keep
2
- // depth write OFF so self-overlapping sheer cloth blends both layers instead of
3
- // a triangle-order patchwork — but that leaves no depth record, so anything
4
- // drawn later (the outline hulls) shows straight through the fabric as black
5
- // shapes. This pass re-draws the transparent geometry depth-only AFTER its
6
- // color pass: solid-enough texels (alpha ≥ 0.5) write depth, so outlines get
7
- // occluded behind fabric exactly like they are behind opaque cloth, while
8
- // truly sheer texels (a veil) stay non-occluding.
1
+ // The depth-only prime, one module behind four pipelines.
2
+ //
3
+ // The oldest fps complaint the engine had — zoom close and the frame drops, in
4
+ // every material generation — was per-fragment shading TIMES layering: an MMD
5
+ // model at close-up is cloth over body over face under hair, author-order drawn,
6
+ // every buried layer fully shaded and then covered. This module lets depth go
7
+ // down FIRST so the colour passes shade each pixel once:
8
+ //
9
+ // · opaque prepass (CUTOFF 0.5) — plain auto-class opaque draws, before the
10
+ // opaque colour walk. See drawOpaqueDepthPrepass for who is in and why.
11
+ // · hair prime (CUTOFF 1.0, stencil not-equal) — between the non-hair and
12
+ // hair colour walks, fenced off the eye silhouette the see-through-hair
13
+ // stencil pass needs. See drawHairDepthPrime.
14
+ // · transparent solid prime (CUTOFF 1.0) — a translucent material's alpha-1
15
+ // texels, where over-blending is plain replacement and the buried work
16
+ // provably never shows. See drawTransparentSolidPrepass.
17
+ // · transparent depth prepass (CUTOFF 0.5, AFTER colour) — the original
18
+ // occupant, dormant: re-records sheer fabric's depth so outline hulls are
19
+ // occluded behind it. Kept for a future OIT path.
9
20
  //
10
21
  // Reuses mainPipelineLayout: camera g0b0, diffuseSampler g0b2, skinMats g1b0,
11
22
  // diffuse texture g2b0, material uniforms g2b1 — the same bind groups the
12
23
  // color draws already set, so drawing it costs no extra binding work.
24
+ //
25
+ // A FUNCTION rather than the constant it was, for the reason commonFsOutWgsl is
26
+ // one: the fragment outputs below depend on whether the device carries the id
27
+ // attachment, and that answer does not exist at import time. The constant could
28
+ // not have taken the outputs at all, which is most of why it did not have them.
13
29
 
14
- export const TRANSPARENT_DEPTH_PREPASS_WGSL = /* wgsl */ `
30
+ import { sceneFsOutWgsl, sceneIdPadWgsl } from "./scene-contract"
31
+
32
+ export function transparentDepthPrepassWgsl(): string {
33
+ return /* wgsl */ `
15
34
  struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
16
35
  struct MaterialUniforms {
17
36
  diffuseColor: vec3f,
@@ -25,7 +44,9 @@ struct MaterialUniforms {
25
44
  @group(2) @binding(1) var<uniform> material: MaterialUniforms;
26
45
 
27
46
  struct VSOut {
28
- @builtin(position) position: vec4f,
47
+ // @invariant, to the same end as the material VertexOutput: the colour pass
48
+ // must land on exactly the depths this wrote.
49
+ @builtin(position) @invariant position: vec4f,
29
50
  @location(0) uv: vec2f,
30
51
  };
31
52
 
@@ -50,8 +71,28 @@ struct VSOut {
50
71
  return o;
51
72
  }
52
73
 
53
- @fragment fn fs(in: VSOut) {
74
+ // Every attachment the pass carries, declared and then not written: the
75
+ // pipeline takes all of them at writeMask 0 (see sceneTargets), so what this
76
+ // returns is discarded by the hardware and only the depth write survives — which
77
+ // is the entire purpose of the pass. Declaring them anyway is what keeps the
78
+ // pipeline valid on a browser that requires an output per target rather than
79
+ // per WRITTEN target. Costs one dead struct store on a fragment that already
80
+ // runs, because the alpha test below needs it to.
81
+ ${sceneFsOutWgsl({ name: "PrepassOut", aux: "mask" })}
82
+ // The cutout threshold, per pipeline. 0.5 is the OPAQUE prime's "solid enough"
83
+ // — safe there because opaque colour replaces rather than blends. The
84
+ // TRANSPARENT prime overrides it to 1.0: a translucent fragment's blend reads
85
+ // what is behind it, so only a texel at EXACTLY alpha 1 — where over-blending
86
+ // collapses to plain replacement and the destination stops mattering — may
87
+ // claim depth ahead of its buried layers without changing the pixel.
88
+ override CUTOFF: f32 = 0.5;
89
+ @fragment fn fs(in: VSOut) -> PrepassOut {
54
90
  let a = material.alpha * textureSample(diffuseTexture, diffuseSampler, in.uv).a;
55
- if (a < 0.5) { discard; }
91
+ if (a < CUTOFF) { discard; }
92
+ var out: PrepassOut;
93
+ out.color = vec4f(0.0);
94
+ out.mask = vec4f(0.0);
95
+ ${sceneIdPadWgsl("out")} return out;
56
96
  }
57
97
  `
98
+ }