reze-engine 0.11.3 → 0.12.1

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 (64) hide show
  1. package/README.md +59 -39
  2. package/dist/bezier-interpolate.d.ts +15 -0
  3. package/dist/bezier-interpolate.d.ts.map +1 -0
  4. package/dist/bezier-interpolate.js +40 -0
  5. package/dist/engine.d.ts +11 -0
  6. package/dist/engine.d.ts.map +1 -1
  7. package/dist/engine.js +101 -1
  8. package/dist/engine_ts.d.ts +143 -0
  9. package/dist/engine_ts.d.ts.map +1 -0
  10. package/dist/engine_ts.js +1575 -0
  11. package/dist/ik.d.ts +32 -0
  12. package/dist/ik.d.ts.map +1 -0
  13. package/dist/ik.js +337 -0
  14. package/dist/player.d.ts +64 -0
  15. package/dist/player.d.ts.map +1 -0
  16. package/dist/player.js +220 -0
  17. package/dist/pool-scene.d.ts +52 -0
  18. package/dist/pool-scene.d.ts.map +1 -0
  19. package/dist/pool-scene.js +1122 -0
  20. package/dist/pool.d.ts +38 -0
  21. package/dist/pool.d.ts.map +1 -0
  22. package/dist/pool.js +422 -0
  23. package/dist/rzm-converter.d.ts +12 -0
  24. package/dist/rzm-converter.d.ts.map +1 -0
  25. package/dist/rzm-converter.js +40 -0
  26. package/dist/rzm-loader.d.ts +24 -0
  27. package/dist/rzm-loader.d.ts.map +1 -0
  28. package/dist/rzm-loader.js +488 -0
  29. package/dist/rzm-writer.d.ts +27 -0
  30. package/dist/rzm-writer.d.ts.map +1 -0
  31. package/dist/rzm-writer.js +701 -0
  32. package/dist/shaders/body.d.ts +1 -1
  33. package/dist/shaders/body.d.ts.map +1 -1
  34. package/dist/shaders/body.js +28 -7
  35. package/dist/shaders/cloth_rough.d.ts +1 -1
  36. package/dist/shaders/cloth_rough.d.ts.map +1 -1
  37. package/dist/shaders/cloth_rough.js +16 -4
  38. package/dist/shaders/cloth_smooth.d.ts +1 -1
  39. package/dist/shaders/cloth_smooth.d.ts.map +1 -1
  40. package/dist/shaders/cloth_smooth.js +17 -5
  41. package/dist/shaders/default.d.ts +1 -1
  42. package/dist/shaders/default.d.ts.map +1 -1
  43. package/dist/shaders/eye.d.ts +1 -1
  44. package/dist/shaders/eye.d.ts.map +1 -1
  45. package/dist/shaders/face.d.ts +1 -1
  46. package/dist/shaders/face.d.ts.map +1 -1
  47. package/dist/shaders/face.js +57 -21
  48. package/dist/shaders/hair.d.ts +1 -1
  49. package/dist/shaders/hair.d.ts.map +1 -1
  50. package/dist/shaders/hair.js +27 -7
  51. package/dist/shaders/materials/hair.d.ts +1 -1
  52. package/dist/shaders/materials/hair.d.ts.map +1 -1
  53. package/dist/shaders/materials/hair.js +10 -1
  54. package/dist/shaders/metal.d.ts +1 -1
  55. package/dist/shaders/metal.d.ts.map +1 -1
  56. package/dist/shaders/metal.js +17 -4
  57. package/dist/shaders/nodes.d.ts +1 -1
  58. package/dist/shaders/nodes.d.ts.map +1 -1
  59. package/dist/shaders/nodes.js +9 -0
  60. package/dist/shaders/stockings.d.ts +1 -1
  61. package/dist/shaders/stockings.d.ts.map +1 -1
  62. package/package.json +1 -1
  63. package/src/engine.ts +104 -1
  64. package/src/shaders/materials/hair.ts +10 -1
package/src/engine.ts CHANGED
@@ -232,6 +232,7 @@ export class Engine {
232
232
  private metalPipeline!: GPURenderPipeline
233
233
  private bodyPipeline!: GPURenderPipeline
234
234
  private eyePipeline!: GPURenderPipeline
235
+ private hairOverEyesPipeline!: GPURenderPipeline
235
236
  private stockingsPipeline!: GPURenderPipeline
236
237
  private groundShadowPipeline!: GPURenderPipeline
237
238
  private groundShadowBindGroupLayout!: GPUBindGroupLayout
@@ -247,6 +248,9 @@ export class Engine {
247
248
  private hdrResolveTexture!: GPUTexture
248
249
  private static readonly MULTISAMPLE_COUNT = 4
249
250
  private static readonly HDR_FORMAT: GPUTextureFormat = "rgba16float"
251
+ /** Stencil value stamped by eye draws so hair can stencil-test against it and
252
+ * alpha-blend a second pass over eye silhouette pixels (see-through-hair effect). */
253
+ private static readonly STENCIL_EYE_VALUE = 1
250
254
  /** Single-channel mask written alongside HDR color — 1 = model geometry (contributes
251
255
  * to bloom), 0 = ground (never blooms). Sampled by the bloom blit pass to gate the
252
256
  * prefilter so ground brightness can't halo the scene. */
@@ -822,6 +826,9 @@ export class Engine {
822
826
  },
823
827
  })
824
828
 
829
+ // Hair opaque: stencil != EYE_VALUE so fragments on top of eyes are skipped entirely —
830
+ // depth and color stay as the eye wrote them; the follow-up hairOverEyesPipeline then
831
+ // draws those skipped fragments alpha-blended so the eye reads through the hair.
825
832
  this.hairPipeline = this.createRenderPipeline({
826
833
  label: "hair NPR pipeline",
827
834
  layout: mainPipelineLayout,
@@ -833,9 +840,39 @@ export class Engine {
833
840
  format: "depth24plus-stencil8",
834
841
  depthWriteEnabled: true,
835
842
  depthCompare: "less-equal",
843
+ stencilFront: { compare: "not-equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
844
+ stencilBack: { compare: "not-equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
845
+ stencilReadMask: 0xff,
846
+ stencilWriteMask: 0,
836
847
  },
837
848
  })
838
849
 
850
+ // Hair-over-eyes: same shader with IS_OVER_EYES=true so alpha is halved at compile time.
851
+ // Only fragments where eye stencil == EYE_VALUE pass; depth test still culls fragments
852
+ // that are further from camera than the eye, so hair behind the eye never shows through.
853
+ // depthWriteEnabled=false keeps the eye's depth authoritative for everything drawn after.
854
+ this.hairOverEyesPipeline = this.device.createRenderPipeline({
855
+ label: "hair over eyes pipeline",
856
+ layout: mainPipelineLayout,
857
+ vertex: { module: hairShaderModule, buffers: fullVertexBuffers },
858
+ fragment: {
859
+ module: hairShaderModule,
860
+ constants: { IS_OVER_EYES: 1 },
861
+ targets: sceneTargets,
862
+ },
863
+ primitive: { cullMode: "none" },
864
+ depthStencil: {
865
+ format: "depth24plus-stencil8",
866
+ depthWriteEnabled: false,
867
+ depthCompare: "less-equal",
868
+ stencilFront: { compare: "equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
869
+ stencilBack: { compare: "equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
870
+ stencilReadMask: 0xff,
871
+ stencilWriteMask: 0,
872
+ },
873
+ multisample: { count: Engine.MULTISAMPLE_COUNT },
874
+ })
875
+
839
876
  this.clothSmoothPipeline = this.createRenderPipeline({
840
877
  label: "cloth smooth NPR pipeline",
841
878
  layout: mainPipelineLayout,
@@ -892,17 +929,30 @@ export class Engine {
892
929
  },
893
930
  })
894
931
 
932
+ // Eye: stamps stencil = EYE_VALUE on every fragment it writes. Later hair passes read
933
+ // this stamp to split into "draw normally (not over eye)" vs "draw alpha-blended".
934
+ // cullMode="front" + small negative depthBias is the MMD post-alpha-eye trick: only the
935
+ // back half of the eye sphere renders, it passes depth against the face (via bias) when
936
+ // viewed from the front, and it gets culled when viewed from behind — so eye fragments
937
+ // can't leak through the back of the head without needing a per-model skull occluder.
895
938
  this.eyePipeline = this.createRenderPipeline({
896
939
  label: "eye pipeline",
897
940
  layout: mainPipelineLayout,
898
941
  shaderModule: eyeShaderModule,
899
942
  vertexBuffers: fullVertexBuffers,
900
943
  fragmentTargets: sceneTargets,
901
- cullMode: "none",
944
+ cullMode: "front",
902
945
  depthStencil: {
903
946
  format: "depth24plus-stencil8",
904
947
  depthWriteEnabled: true,
905
948
  depthCompare: "less-equal",
949
+ depthBias: -0.00005,
950
+ depthBiasSlopeScale: 0.0,
951
+ depthBiasClamp: 0.0,
952
+ stencilFront: { compare: "always", failOp: "keep", depthFailOp: "keep", passOp: "replace" },
953
+ stencilBack: { compare: "always", failOp: "keep", depthFailOp: "keep", passOp: "replace" },
954
+ stencilReadMask: 0xff,
955
+ stencilWriteMask: 0xff,
906
956
  },
907
957
  })
908
958
 
@@ -1052,6 +1102,13 @@ export class Engine {
1052
1102
  // Don’t write outline into depth buffer — stops z-fighting / black cracks vs body (MMD-style; body depth stays authoritative)
1053
1103
  depthWriteEnabled: false,
1054
1104
  depthCompare: "less-equal",
1105
+ // Skip fragments where the eye stamped stencil=EYE_VALUE. Those pixels are owned by
1106
+ // the see-through-hair blend (hair-over-eyes), so letting the outline's near-black
1107
+ // edge color overwrite them would re-introduce the dark almond we just killed.
1108
+ stencilFront: { compare: "not-equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
1109
+ stencilBack: { compare: "not-equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
1110
+ stencilReadMask: 0xff,
1111
+ stencilWriteMask: 0,
1055
1112
  },
1056
1113
  })
1057
1114
 
@@ -2223,6 +2280,26 @@ export class Engine {
2223
2280
  currentIndexOffset += indexCount
2224
2281
  }
2225
2282
 
2283
+ // Sort so the opaque bucket is emitted in the order the stencil-based
2284
+ // see-through-hair effect requires: {non-hair, non-eye} → {eye} → {hair}.
2285
+ // Eye writes stencil=EYE_VALUE; hair's pipeline stencil-tests "not equal" so
2286
+ // it skips eye pixels; a follow-up hairOverEyes pass (see renderOneModel)
2287
+ // re-fills those skipped pixels alpha-blended. Array.sort is stable in
2288
+ // ES2019+, so within a bucket the PMX material order is preserved.
2289
+ const typeOrder: Record<DrawCallType, number> = {
2290
+ opaque: 0,
2291
+ "opaque-outline": 1,
2292
+ transparent: 2,
2293
+ "transparent-outline": 3,
2294
+ ground: 4,
2295
+ }
2296
+ const presetRank = (p: MaterialPreset): number => (p === "hair" ? 2 : p === "eye" ? 1 : 0)
2297
+ inst.drawCalls.sort((a, b) => {
2298
+ const ta = typeOrder[a.type] - typeOrder[b.type]
2299
+ if (ta !== 0) return ta
2300
+ return presetRank(a.preset) - presetRank(b.preset)
2301
+ })
2302
+
2226
2303
  for (const d of inst.drawCalls) {
2227
2304
  if (d.type === "opaque") inst.shadowDrawCalls.push(d)
2228
2305
  }
@@ -2675,12 +2752,38 @@ export class Engine {
2675
2752
  pass.setVertexBuffer(2, inst.weightsBuffer)
2676
2753
  pass.setIndexBuffer(inst.indexBuffer, "uint32")
2677
2754
 
2755
+ // Single stencil-reference set covers eye (write), hair (read not-equal),
2756
+ // and hairOverEyes (read equal). Non-stencil pipelines ignore the value.
2757
+ pass.setStencilReference(Engine.STENCIL_EYE_VALUE)
2758
+
2678
2759
  this.drawMaterials(pass, inst, "opaque")
2679
2760
  this.drawOutlines(pass, inst, "opaque-outline")
2761
+ this.drawHairOverEyes(pass, inst)
2680
2762
  this.drawMaterials(pass, inst, "transparent")
2681
2763
  this.drawOutlines(pass, inst, "transparent-outline")
2682
2764
  }
2683
2765
 
2766
+ /**
2767
+ * Second hair pass for the see-through-hair effect. Re-draws every hair opaque
2768
+ * draw using `hairOverEyesPipeline` — which stencil-matches `EYE_VALUE` and runs
2769
+ * the hair shader with `IS_OVER_EYES=true` so alpha is halved. depthWriteEnabled
2770
+ * is off, so the eye's depth stays authoritative for anything drawn after.
2771
+ */
2772
+ private drawHairOverEyes(pass: GPURenderPassEncoder, inst: ModelInstance): void {
2773
+ let bound = false
2774
+ for (const draw of inst.drawCalls) {
2775
+ if (draw.type !== "opaque" || draw.preset !== "hair" || !this.shouldRenderDrawCall(inst, draw)) continue
2776
+ if (!bound) {
2777
+ pass.setPipeline(this.hairOverEyesPipeline)
2778
+ pass.setBindGroup(0, this.perFrameBindGroup)
2779
+ pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
2780
+ bound = true
2781
+ }
2782
+ pass.setBindGroup(2, draw.bindGroup)
2783
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
2784
+ }
2785
+ }
2786
+
2684
2787
  private updateCameraUniforms() {
2685
2788
  const viewMatrix = this.camera.getViewMatrix()
2686
2789
  const projectionMatrix = this.camera.getProjectionMatrix()
@@ -7,6 +7,12 @@ export const HAIR_SHADER_WGSL = /* wgsl */ `
7
7
 
8
8
  ${NODES_WGSL}
9
9
 
10
+ // Pipeline-override: the engine compiles two variants — the normal opaque hair pipeline
11
+ // (IS_OVER_EYES=false) and a second pipeline that re-draws hair fragments stencil-matched
12
+ // against the eye stamp with 50% alpha so eyes read through the hair silhouette. Resolved
13
+ // at pipeline-compile time; the dead branch is dropped by the shader compiler.
14
+ override IS_OVER_EYES: bool = false;
15
+
10
16
  struct CameraUniforms {
11
17
  view: mat4x4f,
12
18
  projection: mat4x4f,
@@ -169,8 +175,11 @@ struct FSOut {
169
175
 
170
176
  let final_color = mix(add_shader, principled, 0.2);
171
177
 
178
+ var outAlpha = alpha;
179
+ if (IS_OVER_EYES) { outAlpha = alpha * 0.5; }
180
+
172
181
  var out: FSOut;
173
- out.color = vec4f(final_color, alpha);
182
+ out.color = vec4f(final_color, outAlpha);
174
183
  out.mask = 1.0;
175
184
  return out;
176
185
  }