reze-engine 0.61.4 → 0.61.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 (85) hide show
  1. package/dist/engine.d.ts +49 -2
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +232 -25
  4. package/dist/graph/presets/pool_floor.d.ts +3 -0
  5. package/dist/graph/presets/pool_floor.d.ts.map +1 -0
  6. package/dist/graph/presets/pool_floor.js +42 -0
  7. package/dist/graph/presets/water.d.ts +3 -0
  8. package/dist/graph/presets/water.d.ts.map +1 -0
  9. package/dist/graph/presets/water.js +31 -0
  10. package/dist/graph/registry.d.ts.map +1 -1
  11. package/dist/graph/registry.js +11 -0
  12. package/dist/graph/slots.js +2 -2
  13. package/dist/physics/autofit.d.ts +147 -0
  14. package/dist/physics/autofit.d.ts.map +1 -0
  15. package/dist/physics/autofit.js +501 -0
  16. package/dist/shaders/materials/common.js +5 -5
  17. package/dist/shaders/materials/nodes.d.ts +1 -1
  18. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  19. package/dist/shaders/materials/nodes.js +12 -0
  20. package/dist/shaders/passes/field-blit.d.ts +26 -0
  21. package/dist/shaders/passes/field-blit.d.ts.map +1 -0
  22. package/dist/shaders/passes/field-blit.js +65 -0
  23. package/dist/shaders/passes/ground-noise.d.ts +7 -0
  24. package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
  25. package/dist/shaders/passes/ground-noise.js +88 -0
  26. package/dist/shaders/passes/particles.js +6 -6
  27. package/dist/shaders/passes/scene-contract.js +1 -1
  28. package/dist/shaders/passes/sim.d.ts +34 -0
  29. package/dist/shaders/passes/sim.d.ts.map +1 -0
  30. package/dist/shaders/passes/sim.js +169 -0
  31. package/dist/shaders/passes/subsurface.d.ts +34 -0
  32. package/dist/shaders/passes/subsurface.d.ts.map +1 -0
  33. package/dist/shaders/passes/subsurface.js +134 -0
  34. package/dist/shaders/score-api.d.ts +10 -0
  35. package/dist/shaders/score-api.d.ts.map +1 -0
  36. package/dist/shaders/score-api.js +114 -0
  37. package/dist/shaders/shadow-api.d.ts +29 -0
  38. package/dist/shaders/shadow-api.d.ts.map +1 -0
  39. package/dist/shaders/shadow-api.js +112 -0
  40. package/package.json +1 -1
  41. package/src/engine.ts +242 -24
  42. package/src/graph/registry.ts +11 -0
  43. package/src/graph/slots.ts +2 -2
  44. package/src/shaders/materials/common.ts +5 -5
  45. package/src/shaders/materials/nodes.ts +12 -0
  46. package/src/shaders/passes/particles.ts +6 -6
  47. package/src/shaders/passes/scene-contract.ts +1 -1
  48. package/src/shaders/passes/subsurface.ts +136 -0
  49. package/dist/_dbg-loader.mjs +0 -1100
  50. package/dist/physics-debug.d.ts +0 -30
  51. package/dist/physics-debug.d.ts.map +0 -1
  52. package/dist/physics-debug.js +0 -526
  53. package/dist/shaders/materials/body.d.ts +0 -2
  54. package/dist/shaders/materials/body.d.ts.map +0 -1
  55. package/dist/shaders/materials/body.js +0 -95
  56. package/dist/shaders/materials/cloth_rough.d.ts +0 -2
  57. package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
  58. package/dist/shaders/materials/cloth_rough.js +0 -69
  59. package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
  60. package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
  61. package/dist/shaders/materials/cloth_smooth.js +0 -61
  62. package/dist/shaders/materials/default.d.ts +0 -2
  63. package/dist/shaders/materials/default.d.ts.map +0 -1
  64. package/dist/shaders/materials/default.js +0 -43
  65. package/dist/shaders/materials/eye.d.ts +0 -2
  66. package/dist/shaders/materials/eye.d.ts.map +0 -1
  67. package/dist/shaders/materials/eye.js +0 -60
  68. package/dist/shaders/materials/face.d.ts +0 -2
  69. package/dist/shaders/materials/face.d.ts.map +0 -1
  70. package/dist/shaders/materials/face.js +0 -95
  71. package/dist/shaders/materials/hair.d.ts +0 -2
  72. package/dist/shaders/materials/hair.d.ts.map +0 -1
  73. package/dist/shaders/materials/hair.js +0 -90
  74. package/dist/shaders/materials/metal.d.ts +0 -2
  75. package/dist/shaders/materials/metal.d.ts.map +0 -1
  76. package/dist/shaders/materials/metal.js +0 -77
  77. package/dist/shaders/materials/mmd_classic.d.ts +0 -2
  78. package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
  79. package/dist/shaders/materials/mmd_classic.js +0 -66
  80. package/dist/shaders/materials/stockings.d.ts +0 -2
  81. package/dist/shaders/materials/stockings.d.ts.map +0 -1
  82. package/dist/shaders/materials/stockings.js +0 -122
  83. package/dist/shaders/passes/physics-debug.d.ts +0 -2
  84. package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
  85. package/dist/shaders/passes/physics-debug.js +0 -69
package/dist/engine.js CHANGED
@@ -42,6 +42,7 @@ import { boneOverlay, boneMarkerPositions, buildOverlayShapes, jointOverlay, lin
42
42
  import { BLOOM_BLIT_SHADER_WGSL, BLOOM_DOWNSAMPLE_SHADER_WGSL, BLOOM_UPSAMPLE_SHADER_WGSL, } from "./shaders/passes/bloom";
43
43
  import { AGX_LUT_GZ, AGX_LUT_SIZE } from "./shaders/agx-lut";
44
44
  import { buildCompositeShader, EFFECT_SCENE_API, buildFieldShader, EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES, } from "./shaders/passes/composite";
45
+ import { SUBSURFACE_WGSL, SSS_SPIKE } from "./shaders/passes/subsurface";
45
46
  import { buildParticleComputeShader, buildParticleRenderShader, PARTICLE_LIGHT_BINDING, PARTICLE_POINTS_BINDING, particleEntryPoints, PARTICLE_INDIRECT_BINDING, PARTICLE_INDIRECT_BYTES, PARTICLE_INDIRECT_DRAW_OFFSET, PARTICLE_STRIDE, } from "./shaders/passes/particles";
46
47
  import { MAX_EFFECT_POINTS, POINTS_FLOATS } from "./shaders/points-api";
47
48
  import { bonesWithPrefix, pointsData, writeBonePoint } from "./effect-points";
@@ -1139,6 +1140,22 @@ export class Engine {
1139
1140
  /** Scene-clock reading when the current effect was installed. The effect's
1140
1141
  * `time` is measured from here — see where it is written. */
1141
1142
  this.compositeBloomView = null;
1143
+ // Screen-space subsurface scattering (passes/subsurface.ts). The scratch target
1144
+ // is the X pass's output and lives with the canvas size; the bind groups name
1145
+ // it and the HDR resolve, so a resize drops them and the next frame rebuilds.
1146
+ this.sssPipelineX = null;
1147
+ this.sssPipelineY = null;
1148
+ this.sssScratch = null;
1149
+ this.sssBindGroupX = null;
1150
+ this.sssBindGroupY = null;
1151
+ this.sssUniformX = null;
1152
+ this.sssUniformY = null;
1153
+ this.sssUniformData = new Float32Array(8);
1154
+ // Model pipelines' descriptors, and the single-sided twins built from them —
1155
+ // one per pipeline per view (see sidedPipeline).
1156
+ this.pipelineDescs = new WeakMap();
1157
+ this.singleSided = new WeakMap();
1158
+ this.singleSidedMirror = new WeakMap();
1142
1159
  this.bloomBlitUniformData = new Float32Array(4);
1143
1160
  this.bloomUpsampleUniformData = new Float32Array(4);
1144
1161
  this.bloomMipCount = 0;
@@ -4764,6 +4781,129 @@ export class Engine {
4764
4781
  /** The user's field mounts, drawn at half resolution for the composite to
4765
4782
  * upsample. Runs the whole quad — uniform control flow, so effects may use
4766
4783
  * derivatives freely, which the old inline path had to forbid. */
4784
+ /** Whether any installed style group routes through a `subsurface` node —
4785
+ * the scattering pass, and the depth it reads, cost nothing otherwise. */
4786
+ subsurfaceInUse() {
4787
+ for (const inst of this.modelInstances.values()) {
4788
+ for (const g of inst.styleGroups.values()) {
4789
+ if (g.group.materials.length && g.group.graph.nodes.some((n) => n.type === "subsurface"))
4790
+ return true;
4791
+ }
4792
+ }
4793
+ return false;
4794
+ }
4795
+ /**
4796
+ * ray-mmd's skin scattering: a horizontal then a vertical depth-aware blur of
4797
+ * the pixels the scene pass marked as skin (aux .b), the second written back
4798
+ * into the HDR resolve through a blend that keeps ray-mmd's per-channel share
4799
+ * of the unblurred picture. See passes/subsurface.ts.
4800
+ */
4801
+ renderSubsurface(encoder) {
4802
+ if (!this.device || !this.subsurfaceInUse())
4803
+ return;
4804
+ if (!this.hdrResolveTexture || !this.maskResolveView || !this.depthReadView || !this.dofUniformBuffer)
4805
+ return;
4806
+ const w = this.hdrResolveTexture.width;
4807
+ const h = this.hdrResolveTexture.height;
4808
+ if (!this.sssPipelineX || !this.sssPipelineY) {
4809
+ const module = this.device.createShaderModule({ label: "subsurface scattering", code: SUBSURFACE_WGSL });
4810
+ this.sssPipelineX = this.device.createRenderPipeline({
4811
+ label: "subsurface blur x",
4812
+ layout: "auto",
4813
+ vertex: { module, entryPoint: "vs" },
4814
+ fragment: { module, entryPoint: "fsX", targets: [{ format: this.hdrFormat }] },
4815
+ primitive: { topology: "triangle-list" },
4816
+ });
4817
+ // result = blurred·(1 − spike) + original·spike, per channel: the
4818
+ // destination is the original, and the blend constant holds the spike.
4819
+ this.sssPipelineY = this.device.createRenderPipeline({
4820
+ label: "subsurface blur y",
4821
+ layout: "auto",
4822
+ vertex: { module, entryPoint: "vs" },
4823
+ fragment: {
4824
+ module,
4825
+ entryPoint: "fsY",
4826
+ targets: [
4827
+ {
4828
+ format: this.hdrFormat,
4829
+ blend: {
4830
+ color: { srcFactor: "one-minus-constant", dstFactor: "constant", operation: "add" },
4831
+ alpha: { srcFactor: "zero", dstFactor: "one", operation: "add" },
4832
+ },
4833
+ },
4834
+ ],
4835
+ },
4836
+ primitive: { topology: "triangle-list" },
4837
+ });
4838
+ this.sssBindGroupX = null;
4839
+ this.sssBindGroupY = null;
4840
+ }
4841
+ if (!this.sssUniformX || !this.sssUniformY) {
4842
+ const make = (label) => this.device.createBuffer({ label, size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
4843
+ this.sssUniformX = make("subsurface uniforms x");
4844
+ this.sssUniformY = make("subsurface uniforms y");
4845
+ }
4846
+ if (!this.sssScratch || this.sssScratch.width !== w || this.sssScratch.height !== h) {
4847
+ this.sssScratch?.destroy();
4848
+ this.sssScratch = this.device.createTexture({
4849
+ label: "subsurface scratch",
4850
+ size: [w, h],
4851
+ format: this.hdrFormat,
4852
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
4853
+ });
4854
+ this.sssBindGroupX = null;
4855
+ this.sssBindGroupY = null;
4856
+ }
4857
+ if (!this.sssBindGroupX || !this.sssBindGroupY) {
4858
+ const mask = this.maskResolveView;
4859
+ const depth = this.depthReadView;
4860
+ const dof = this.dofUniformBuffer;
4861
+ const group = (pipeline, src, uniforms, label) => this.device.createBindGroup({
4862
+ label,
4863
+ layout: pipeline.getBindGroupLayout(0),
4864
+ entries: [
4865
+ { binding: 0, resource: src.createView() },
4866
+ { binding: 1, resource: mask },
4867
+ { binding: 2, resource: depth },
4868
+ { binding: 3, resource: { buffer: dof } },
4869
+ { binding: 4, resource: { buffer: uniforms } },
4870
+ ],
4871
+ });
4872
+ this.sssBindGroupX = group(this.sssPipelineX, this.hdrResolveTexture, this.sssUniformX, "subsurface x");
4873
+ this.sssBindGroupY = group(this.sssPipelineY, this.sssScratch, this.sssUniformY, "subsurface y");
4874
+ }
4875
+ const tanY = Math.tan(this.camera.fov * 0.5);
4876
+ const u = this.sssUniformData;
4877
+ u[2] = 0;
4878
+ u[3] = 0;
4879
+ u[4] = tanY * this.camera.aspect;
4880
+ u[5] = tanY;
4881
+ u[6] = w;
4882
+ u[7] = h;
4883
+ u[0] = 1;
4884
+ u[1] = 0;
4885
+ this.device.queue.writeBuffer(this.sssUniformX, 0, u);
4886
+ u[0] = 0;
4887
+ u[1] = 1;
4888
+ this.device.queue.writeBuffer(this.sssUniformY, 0, u);
4889
+ const px = encoder.beginRenderPass({
4890
+ label: "subsurface blur x",
4891
+ colorAttachments: [{ view: this.sssScratch.createView(), loadOp: "clear", clearValue: [0, 0, 0, 0], storeOp: "store" }],
4892
+ });
4893
+ px.setPipeline(this.sssPipelineX);
4894
+ px.setBindGroup(0, this.sssBindGroupX);
4895
+ px.draw(3);
4896
+ px.end();
4897
+ const py = encoder.beginRenderPass({
4898
+ label: "subsurface blur y",
4899
+ colorAttachments: [{ view: this.hdrResolveTexture.createView(), loadOp: "load", storeOp: "store" }],
4900
+ });
4901
+ py.setPipeline(this.sssPipelineY);
4902
+ py.setBindGroup(0, this.sssBindGroupY);
4903
+ py.setBlendConstant({ r: SSS_SPIKE[0], g: SSS_SPIKE[1], b: SSS_SPIKE[2], a: 0 });
4904
+ py.draw(3);
4905
+ py.end();
4906
+ }
4767
4907
  renderFieldPass(encoder) {
4768
4908
  // TWO PREDICATES, deliberately, and they are not interchangeable.
4769
4909
  //
@@ -5781,7 +5921,7 @@ export class Engine {
5781
5921
  }
5782
5922
  createRenderPipeline(config) {
5783
5923
  const targets = config.fragmentTargets ?? (config.fragmentTarget ? [config.fragmentTarget] : undefined);
5784
- return this.device.createRenderPipeline({
5924
+ return this.createModelPipeline({
5785
5925
  label: config.label,
5786
5926
  layout: config.layout,
5787
5927
  vertex: {
@@ -6195,7 +6335,7 @@ export class Engine {
6195
6335
  depthCompare: this.depthAhead,
6196
6336
  },
6197
6337
  };
6198
- this.depthPrepassPipeline = this.device.createRenderPipeline({
6338
+ this.depthPrepassPipeline = this.createModelPipeline({
6199
6339
  label: "opaque depth prepass",
6200
6340
  ...prepassDesc,
6201
6341
  fragment: {
@@ -6207,7 +6347,7 @@ export class Engine {
6207
6347
  // The SOLID prime: same module, cutoff forced to exactly 1.0. Only texels
6208
6348
  // whose blend ignores the destination may pre-claim depth in the
6209
6349
  // transparent phase — see the override's note in depth-prepass.ts.
6210
- this.solidPrepassPipeline = this.device.createRenderPipeline({
6350
+ this.solidPrepassPipeline = this.createModelPipeline({
6211
6351
  label: "transparent solid prepass",
6212
6352
  ...prepassDesc,
6213
6353
  fragment: {
@@ -6223,7 +6363,7 @@ export class Engine {
6223
6363
  // hair depth from ever claiming the pixels the see-through-hair pass needs
6224
6364
  // the eye to survive on. (Bundle draws use the PASS's stencil reference;
6225
6365
  // only pipeline/bind/vertex state resets across executeBundles.)
6226
- this.hairPrimePipeline = this.device.createRenderPipeline({
6366
+ this.hairPrimePipeline = this.createModelPipeline({
6227
6367
  label: "hair depth prime",
6228
6368
  ...prepassDesc,
6229
6369
  depthStencil: {
@@ -7035,6 +7175,10 @@ export class Engine {
7035
7175
  usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
7036
7176
  });
7037
7177
  this.maskResolveView = this.maskResolveTexture.createView();
7178
+ this.sssScratch?.destroy();
7179
+ this.sssScratch = null;
7180
+ this.sssBindGroupX = null;
7181
+ this.sssBindGroupY = null;
7038
7182
  // The floor mirror's targets — FULL resolution, the same attachment
7039
7183
  // contract and sample count as the scene pass, which is what lets the
7040
7184
  // mirror bundles reuse every scene pipeline unchanged. Half res was the
@@ -8739,6 +8883,8 @@ export class Engine {
8739
8883
  bindGroup: this.groundShadowBindGroup,
8740
8884
  materialName: "Ground",
8741
8885
  groupId: null,
8886
+ // The ground's own pipeline decides its culling.
8887
+ doubleSided: true,
8742
8888
  // The ground belongs to no model instance, so it is not in the cull list —
8743
8889
  // cullIndex -1 leaves renderGround unconditional. Its box is filled in
8744
8890
  // anyway rather than left a lie for whoever reads this next.
@@ -12449,6 +12595,7 @@ export class Engine {
12449
12595
  groupId: null,
12450
12596
  baseBindGroupEntries,
12451
12597
  castsShadow,
12598
+ doubleSided: inst.isPlane || (mat.edgeFlag & 0x01) !== 0,
12452
12599
  outline,
12453
12600
  bounds,
12454
12601
  cullIndex: -1,
@@ -13746,7 +13893,10 @@ export class Engine {
13746
13893
  // The two real readers are both in the composite and both have their own
13747
13894
  // flag above: linearDepth() feeds the DoF gather and the depth handed to a
13748
13895
  // foreground mount. Nothing else binds depthTex at all.
13749
- const depthRead = dofOn || this.effects.some((e) => e.hasForeground);
13896
+ // The scattering pass is the third reader: it rejects taps across a depth
13897
+ // step and scales its width by distance, so a scene wearing a subsurface
13898
+ // look stores depth for as long as it does.
13899
+ const depthRead = dofOn || this.effects.some((e) => e.hasForeground) || this.subsurfaceInUse();
13750
13900
  this.renderPassDescriptor.depthStencilAttachment.depthStoreOp = depthRead ? "store" : "discard";
13751
13901
  if (depthRead)
13752
13902
  this.writeDepthOfFieldUniforms();
@@ -13946,6 +14096,9 @@ export class Engine {
13946
14096
  // kept ribbons out of bloom.
13947
14097
  this.drawTrails(pass, "camera");
13948
14098
  pass.end();
14099
+ // Skin scattering, on the resolved scene and before anything reads it:
14100
+ // bloom should bleed the scattered skin, not the raw one.
14101
+ this.renderSubsurface(encoder);
13949
14102
  // The field mounts, likewise: after the scene so foregrounds can read its
13950
14103
  // depth, before the composite that samples both layers.
13951
14104
  this.renderFieldPass(encoder);
@@ -14730,11 +14883,53 @@ export class Engine {
14730
14883
  stencilWriteMask: 0xff,
14731
14884
  };
14732
14885
  }
14733
- return this.device.createRenderPipelineAsync({
14886
+ const desc = {
14734
14887
  ...base,
14735
14888
  fragment: { module, constants, targets: blend === "additive" ? this.sceneTargetsAdditive : this.sceneTargets },
14736
14889
  depthStencil,
14737
- });
14890
+ };
14891
+ return this.device.createRenderPipelineAsync(desc).then((pipeline) => {
14892
+ this.pipelineDescs.set(pipeline, desc);
14893
+ return pipeline;
14894
+ });
14895
+ }
14896
+ /** A pipeline whose descriptor is kept, so a single-sided twin can be built
14897
+ * from it on demand (see sidedPipeline). */
14898
+ createModelPipeline(desc) {
14899
+ const pipeline = this.device.createRenderPipeline(desc);
14900
+ this.pipelineDescs.set(pipeline, desc);
14901
+ return pipeline;
14902
+ }
14903
+ /**
14904
+ * The pipeline a draw should use once its double-sided flag is honoured.
14905
+ *
14906
+ * A single-sided material culls its back faces, as MMD does. PMX winds a front
14907
+ * face clockwise, which this engine sees as a BACK face — the eye's front cull
14908
+ * and the outline hull's back cull are the same fact — so the cull is "front"
14909
+ * for the camera and "back" in the mirror, whose reflection flips winding.
14910
+ *
14911
+ * Every pipeline that draws a material's surface goes through here, depth
14912
+ * primes included: a prime that kept a culled face would write depth for a
14913
+ * surface nobody sees and hide what is behind it. A pipeline that already
14914
+ * culls (the eye) is returned as it is.
14915
+ */
14916
+ sidedPipeline(pipeline, draw, mirrored) {
14917
+ if (draw.doubleSided)
14918
+ return pipeline;
14919
+ const desc = this.pipelineDescs.get(pipeline);
14920
+ if (!desc || (desc.primitive?.cullMode ?? "none") !== "none")
14921
+ return pipeline;
14922
+ const cache = mirrored ? this.singleSidedMirror : this.singleSided;
14923
+ let twin = cache.get(pipeline);
14924
+ if (!twin) {
14925
+ twin = this.device.createRenderPipeline({
14926
+ ...desc,
14927
+ label: `${desc.label ?? "pipeline"} (single-sided${mirrored ? ", mirror" : ""})`,
14928
+ primitive: { ...desc.primitive, cullMode: mirrored ? "back" : "front" },
14929
+ });
14930
+ cache.set(pipeline, twin);
14931
+ }
14932
+ return twin;
14738
14933
  }
14739
14934
  // Pipeline for a material draw call: its group's compiled pipeline when grouped, else
14740
14935
  // the neutral base (ungrouped materials render the default graph). Transparent-bucket
@@ -14790,7 +14985,8 @@ export class Engine {
14790
14985
  pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
14791
14986
  bound = true;
14792
14987
  }
14793
- const pipeline = this.pipelineForDrawCall(inst, draw, view.args === "mirror");
14988
+ const mirrored = view.args === "mirror";
14989
+ const pipeline = this.sidedPipeline(this.pipelineForDrawCall(inst, draw, mirrored), draw, mirrored);
14794
14990
  if (pipeline !== currentPipeline) {
14795
14991
  pass.setPipeline(pipeline);
14796
14992
  currentPipeline = pipeline;
@@ -14892,15 +15088,18 @@ export class Engine {
14892
15088
  /** Depth-only prime of the hair's alpha-1 texels, stencil-fenced off the eye
14893
15089
  * silhouette. See the note at its call site and hairPrimePipeline. */
14894
15090
  drawHairDepthPrime(pass, inst, view) {
14895
- let bound = false;
15091
+ let bound = null;
14896
15092
  for (const draw of inst.drawCalls) {
14897
15093
  if (draw.type !== "opaque" || !this.isHairDraw(inst, draw))
14898
15094
  continue;
14899
- if (!bound) {
14900
- pass.setPipeline(this.hairPrimePipeline);
14901
- pass.setBindGroup(0, view.perFrame);
14902
- pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
14903
- bound = true;
15095
+ const pipeline = this.sidedPipeline(this.hairPrimePipeline, draw, view.args === "mirror");
15096
+ if (bound !== pipeline) {
15097
+ pass.setPipeline(pipeline);
15098
+ if (!bound) {
15099
+ pass.setBindGroup(0, view.perFrame);
15100
+ pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
15101
+ }
15102
+ bound = pipeline;
14904
15103
  }
14905
15104
  pass.setBindGroup(2, draw.bindGroup);
14906
15105
  this.issueDraw(pass, draw, view.args);
@@ -14962,6 +15161,7 @@ export class Engine {
14962
15161
  else if (install && install.alphaMode !== "opaque")
14963
15162
  continue;
14964
15163
  }
15164
+ pipeline = this.sidedPipeline(pipeline, draw, view.args === "mirror");
14965
15165
  if (bound !== pipeline) {
14966
15166
  pass.setPipeline(pipeline);
14967
15167
  // One layout for both prepass pipelines, so groups 0 and 1 carry over.
@@ -14995,15 +15195,18 @@ export class Engine {
14995
15195
  drawTransparentSolidPrepass(pass, inst, view) {
14996
15196
  if (inst.isStage)
14997
15197
  return;
14998
- let bound = false;
15198
+ let bound = null;
14999
15199
  for (const draw of inst.drawCalls) {
15000
15200
  if (draw.type !== "transparent")
15001
15201
  continue;
15002
- if (!bound) {
15003
- pass.setPipeline(this.solidPrepassPipeline);
15004
- pass.setBindGroup(0, view.perFrame);
15005
- pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
15006
- bound = true;
15202
+ const pipeline = this.sidedPipeline(this.solidPrepassPipeline, draw, view.args === "mirror");
15203
+ if (bound !== pipeline) {
15204
+ pass.setPipeline(pipeline);
15205
+ if (!bound) {
15206
+ pass.setBindGroup(0, view.perFrame);
15207
+ pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
15208
+ }
15209
+ bound = pipeline;
15007
15210
  }
15008
15211
  pass.setBindGroup(2, draw.bindGroup);
15009
15212
  this.issueDraw(pass, draw, view.args);
@@ -15054,9 +15257,10 @@ export class Engine {
15054
15257
  for (const draw of inst.drawCalls) {
15055
15258
  if (draw.type !== "opaque")
15056
15259
  continue;
15057
- const overEyes = this.overEyesPipelineFor(inst, draw);
15058
- if (!overEyes)
15260
+ const hairOverEyes = this.overEyesPipelineFor(inst, draw);
15261
+ if (!hairOverEyes)
15059
15262
  continue;
15263
+ const overEyes = this.sidedPipeline(hairOverEyes, draw, view.args === "mirror");
15060
15264
  if (!bound) {
15061
15265
  pass.setBindGroup(0, view.perFrame);
15062
15266
  pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
@@ -15626,15 +15830,18 @@ Engine.HDR_FORMAT_OVERRIDE = null;
15626
15830
  /** Stencil value stamped by eye draws so hair can stencil-test against it and
15627
15831
  * alpha-blend a second pass over eye silhouette pixels (see-through-hair effect). */
15628
15832
  Engine.STENCIL_EYE_VALUE = 1;
15629
- /** Aux MRT alongside HDR color. Two channels:
15833
+ /** Aux MRT alongside HDR color. Three channels:
15630
15834
  * .r — bloom mask (1 = model geometry, 0 = ground; sampled by bloom blit to gate prefilter).
15631
15835
  * .g — accumulated alpha (the channel that used to live in hdr.a before the HDR format
15632
15836
  * switched to rg11b10ufloat, which has no alpha). Sampled by composite/bloom to
15633
15837
  * un-premultiply color for tonemap and to produce the canvas-drawable alpha used by
15634
15838
  * the premultiplied alphaMode compositor (so the page background still shows through
15635
15839
  * cleared / edge-faded regions like before).
15636
- * rg8unorm at 4× MSAA is 8 bytes/texel — still fits Apple TBDR tile memory comfortably. */
15637
- Engine.BLOOM_MASK_FORMAT = "rg8unorm";
15840
+ * .b — subsurface strength, set by a graph's `subsurface` node and 0 everywhere
15841
+ * else; the scattering pass reads it to find skin (passes/subsurface.ts).
15842
+ * rgba8unorm at 4× MSAA is 16 bytes/texel; it never leaves tile memory (storeOp
15843
+ * discard), only its single-sample resolve does. */
15844
+ Engine.BLOOM_MASK_FORMAT = "rgba8unorm";
15638
15845
  /**
15639
15846
  * The master switch for the id attachment. OFF — and off having been proven
15640
15847
  * to work, not off because it was never finished.
@@ -0,0 +1,3 @@
1
+ import type { ShaderGraph } from "../schema";
2
+ export declare const POOL_FLOOR_GRAPH: ShaderGraph;
3
+ //# sourceMappingURL=pool_floor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool_floor.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/pool_floor.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C,eAAO,MAAM,gBAAgB,EAAE,WA6B9B,CAAA"}
@@ -0,0 +1,42 @@
1
+ // Pool floor — the tiled bottom seen through water, with caustics on it.
2
+ //
3
+ // The other half of a pool, and the half that does most of the work. A water
4
+ // surface can be tuned for a long time without the shot reading as a pool;
5
+ // putting the focused-light web on the FLOOR is what settles it, because that
6
+ // is the cue a photograph actually carries. It is a floor material, so it is
7
+ // applied to the tile under the water rather than to the water itself.
8
+ //
9
+ // Caustics land between the albedo and the shading deliberately: they are
10
+ // light, so they should still be dimmed by the floor's own shadow rather than
11
+ // glowing through it. That is the difference between water-focused sun and a
12
+ // decal.
13
+ export const POOL_FLOOR_GRAPH = {
14
+ version: 1,
15
+ name: "Pool floor",
16
+ tags: ["stage", "water", "pool"],
17
+ nodes: [
18
+ { id: "tex", type: "texture" },
19
+ { id: "mat", type: "material_diffuse" },
20
+ { id: "base", type: "mix/multiply", inputs: { fac: 1.0 } },
21
+ { id: "caust", type: "caustics" },
22
+ {
23
+ id: "principled",
24
+ type: "principled",
25
+ inputs: { metallic: 0.0, specular_ior_level: 0.4, roughness: 0.45, spec_clamp: 10.0, sheen_weight: 0.0, sheen_tint: 0.0 },
26
+ },
27
+ ],
28
+ links: [
29
+ { from: { node: "tex", socket: "color" }, to: { node: "base", socket: "a" } },
30
+ { from: { node: "mat", socket: "color" }, to: { node: "base", socket: "b" } },
31
+ { from: { node: "base", socket: "color" }, to: { node: "caust", socket: "color" } },
32
+ { from: { node: "caust", socket: "color" }, to: { node: "principled", socket: "base_color" } },
33
+ ],
34
+ output: { node: "principled", socket: "color" },
35
+ params: [
36
+ { id: "tint", label: "Light tint", target: { node: "caust", socket: "tint" }, kind: "color", default: [1.0, 1.0, 1.0] },
37
+ { id: "scale", label: "Caustic scale", target: { node: "caust", socket: "scale" }, kind: "float", min: 0.01, max: 1, default: 0.12 },
38
+ { id: "speed", label: "Caustic speed", target: { node: "caust", socket: "speed" }, kind: "float", min: 0, max: 1, default: 0.15 },
39
+ { id: "strength", label: "Caustic strength", target: { node: "caust", socket: "strength" }, kind: "float", min: 0, max: 3, default: 0.6 },
40
+ { id: "fade", label: "Depth fade", target: { node: "caust", socket: "fade" }, kind: "float", min: 1, max: 200, default: 30 },
41
+ ],
42
+ };
@@ -0,0 +1,3 @@
1
+ import type { ShaderGraph } from "../schema";
2
+ export declare const WATER_GRAPH: ShaderGraph;
3
+ //# sourceMappingURL=water.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"water.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/water.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C,eAAO,MAAM,WAAW,EAAE,WAgBzB,CAAA"}
@@ -0,0 +1,31 @@
1
+ // Water — a planar reflection, rippled, seen through Fresnel.
2
+ //
3
+ // Put a stage's water materials in a style group carrying this graph and the
4
+ // pool starts reflecting. The reflection itself is the engine's mirror pass,
5
+ // which this is the only graph to read; everything else here is the shaping
6
+ // that keeps it from reading as a mirror laid flat in the floor.
7
+ //
8
+ // The exposed params are the ones worth a slider while judging a shot. Scale
9
+ // and speed are the ripple; a real pool's surface is slow and broad, so both
10
+ // defaults sit low. Tint is the water's own colour — a game rip carries that
11
+ // in its material, and the converter hands it straight over.
12
+ //
13
+ // How see-through the water is belongs to the MATERIAL's alpha, not to this
14
+ // graph, which is where every other group's coverage comes from too.
15
+ export const WATER_GRAPH = {
16
+ version: 1,
17
+ name: "Water",
18
+ tags: ["water", "stage"],
19
+ nodes: [{ id: "water", type: "water" }],
20
+ links: [],
21
+ output: { node: "water", socket: "color" },
22
+ params: [
23
+ { id: "tint", label: "Tint", target: { node: "water", socket: "tint" }, kind: "color", default: [0.04, 0.62, 0.66] },
24
+ { id: "scale", label: "Ripple scale", target: { node: "water", socket: "scale" }, kind: "float", min: 0.01, max: 1, default: 0.18 },
25
+ { id: "speed", label: "Ripple speed", target: { node: "water", socket: "speed" }, kind: "float", min: 0, max: 1, default: 0.12 },
26
+ { id: "strength", label: "Ripple depth", target: { node: "water", socket: "strength" }, kind: "float", min: 0, max: 2, default: 0.7 },
27
+ { id: "blur", label: "Reflection blur", target: { node: "water", socket: "blur" }, kind: "float", min: 0, max: 1, default: 0.25 },
28
+ { id: "mirroring", label: "Mirroring", target: { node: "water", socket: "mirroring" }, kind: "float", min: 0, max: 1, default: 0.20 },
29
+ { id: "sparkle", label: "Sparkle", target: { node: "water", socket: "sparkle" }, kind: "float", min: 0, max: 4, default: 1.0 },
30
+ ],
31
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/graph/registry.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAE3C,MAAM,MAAM,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEzD,KAAK,SAAS,GAAG;IACf,IAAI,EAAE,KAAK,CAAA;IACX,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,4FAA4F;IAC5F,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAC9B,2FAA2F;IAC3F,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAA;IAC/C,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACtC,CAAA;AAYD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,GAAG,MAAM,CAoBhE;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO,CAIpE;AAMD,wBAAgB,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,GAAG,OAAO,CAM1D;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKpE;AAyCD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAsuBlD,CAAA"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/graph/registry.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAE3C,MAAM,MAAM,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEzD,KAAK,SAAS,GAAG;IACf,IAAI,EAAE,KAAK,CAAA;IACX,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,4FAA4F;IAC5F,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAC9B,2FAA2F;IAC3F,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAA;IAC/C,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACtC,CAAA;AAYD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,GAAG,MAAM,CAoBhE;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO,CAIpE;AAMD,wBAAgB,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,GAAG,OAAO,CAM1D;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKpE;AAyCD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAivBlD,CAAA"}
@@ -682,6 +682,17 @@ export const NODE_REGISTRY = {
682
682
  outputs: { value: "float" },
683
683
  emit: () => `shader_to_rgb_diffuse(n, l, sun, amb, shadow)`,
684
684
  },
685
+ /**
686
+ * Hands this surface to the screen-space scattering pass: the colour passes
687
+ * through, and the pixel is blurred afterwards the way ray-mmd blurs skin —
688
+ * red furthest, never across a depth step. Strength scales the width; 1 is
689
+ * ray-mmd's skin, up to 4. It has to lie on the path to the output to take effect.
690
+ */
691
+ subsurface: {
692
+ inputs: { color: C([1, 1, 1], true), strength: F(1) },
693
+ outputs: { color: "color" },
694
+ emit: (a) => `rz_subsurface(${a.strength}, ${a.color})`,
695
+ },
685
696
  /**
686
697
  * The same closure as a COLOUR, which is what Blender's Shader to RGB
687
698
  * actually returns.
@@ -205,13 +205,13 @@ function epilogue(renderClass, alphaMode, hasOpacity, blend, takesLight) {
205
205
 
206
206
  var out: FSOut;
207
207
  out.color = vec4f(final_color${LIT}${BURN}, outAlpha);
208
- ${FOG} out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
208
+ ${FOG} out.mask = vec4f(1.0, 1.0, _rzSubsurface, out.color.a);
209
209
  ${ID_WRITE} return out;
210
210
  `;
211
211
  }
212
212
  return `${ALBEDO} var out: FSOut;
213
213
  out.color = vec4f(final_color${LIT}${BURN}, ${alphaBase});
214
- ${FOG} out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
214
+ ${FOG} out.mask = vec4f(1.0, 1.0, _rzSubsurface, out.color.a);
215
215
  ${ID_WRITE} return out;
216
216
  `;
217
217
  }