reze-engine 0.54.6 → 0.54.8

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 (36) hide show
  1. package/dist/engine.d.ts +44 -0
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +235 -1
  4. package/dist/graph/presets/body.d.ts.map +1 -1
  5. package/dist/graph/presets/body.js +10 -2
  6. package/dist/graph/presets/cloth_rough.d.ts.map +1 -1
  7. package/dist/graph/presets/cloth_rough.js +11 -3
  8. package/dist/graph/presets/cloth_smooth.d.ts.map +1 -1
  9. package/dist/graph/presets/cloth_smooth.js +11 -3
  10. package/dist/graph/presets/eye.d.ts.map +1 -1
  11. package/dist/graph/presets/eye.js +10 -2
  12. package/dist/graph/presets/face.d.ts.map +1 -1
  13. package/dist/graph/presets/face.js +10 -2
  14. package/dist/graph/presets/hair.d.ts.map +1 -1
  15. package/dist/graph/presets/hair.js +10 -2
  16. package/dist/graph/presets/metal.d.ts.map +1 -1
  17. package/dist/graph/presets/metal.js +9 -1
  18. package/dist/graph/presets/stockings.d.ts.map +1 -1
  19. package/dist/graph/presets/stockings.js +10 -2
  20. package/dist/shaders/passes/cast-distance.d.ts +52 -0
  21. package/dist/shaders/passes/cast-distance.d.ts.map +1 -0
  22. package/dist/shaders/passes/cast-distance.js +269 -0
  23. package/dist/shaders/passes/composite.d.ts.map +1 -1
  24. package/dist/shaders/passes/composite.js +5 -0
  25. package/package.json +1 -1
  26. package/src/engine.ts +258 -1
  27. package/src/graph/presets/body.ts +10 -2
  28. package/src/graph/presets/cloth_rough.ts +11 -3
  29. package/src/graph/presets/cloth_smooth.ts +11 -3
  30. package/src/graph/presets/eye.ts +10 -2
  31. package/src/graph/presets/face.ts +10 -2
  32. package/src/graph/presets/hair.ts +10 -2
  33. package/src/graph/presets/metal.ts +9 -1
  34. package/src/graph/presets/stockings.ts +10 -2
  35. package/src/shaders/passes/cast-distance.ts +280 -0
  36. package/src/shaders/passes/composite.ts +5 -0
package/src/engine.ts CHANGED
@@ -83,6 +83,16 @@ import {
83
83
  buildSimShader,
84
84
  gridEntryPoint,
85
85
  } from "./shaders/passes/grid"
86
+ import {
87
+ buildCastResolveShader,
88
+ buildCastSeedShader,
89
+ buildCastStepShader,
90
+ castDistanceUsed,
91
+ CAST_COVERAGE_FORMAT,
92
+ CAST_DIST_FORMAT,
93
+ CAST_FIELD_DIV,
94
+ CAST_SEED_FORMAT,
95
+ } from "./shaders/passes/cast-distance"
86
96
  import { buildTrailShader, trailEntryPoints, TRAIL_SUBDIVISIONS } from "./shaders/passes/trails"
87
97
  import { PICK_SHADER_WGSL } from "./shaders/passes/pick"
88
98
  import { MIPMAP_BLIT_SHADER_WGSL } from "./shaders/passes/mipmap"
@@ -1337,6 +1347,9 @@ interface EffectInstance {
1337
1347
  * change while an effect is installed, and the frame path should not be
1338
1348
  * running regexes. */
1339
1349
  readsIds: boolean
1350
+ /** Does this source read the distance-to-cast field? Parsed at install for the
1351
+ * same reason readsIds is, and it turns the whole flood on by itself. */
1352
+ readsCastDistance: boolean
1340
1353
  /** Bones this source asked for, in ITS OWN declaration order. The scene table
1341
1354
  * maps these onto shared addresses; this list is what it is rebuilt from. */
1342
1355
  anchors: { bone: string; trail: boolean }[]
@@ -1686,6 +1699,34 @@ export class Engine {
1686
1699
  string,
1687
1700
  { modelName: string; groupId: string; paramId: string; keys: ParamKey[]; last: ParamValue | null }
1688
1701
  >()
1702
+ /**
1703
+ * The distance-to-cast field: seeds ping-ponged by a jump flood, then resolved.
1704
+ *
1705
+ * All null until some installed effect names rzCastDistance. Nothing is
1706
+ * allocated and no pass is encoded for a scene that never asks — the same
1707
+ * bargain the grid and the id buffer strike.
1708
+ */
1709
+ private castSeedTextures: (GPUTexture | null)[] = [null, null]
1710
+ private castSeedViews: (GPUTextureView | null)[] = [null, null]
1711
+ private castCoverageTexture: GPUTexture | null = null
1712
+ private castCoverageView: GPUTextureView | null = null
1713
+ private castDistTexture: GPUTexture | null = null
1714
+ private castDistView: GPUTextureView | null = null
1715
+ /** 1x1 holding half-float 65504, bound whenever the pass is not running: an
1716
+ * effect keyed on distance then finds the cast unreachably far and draws
1717
+ * nothing, rather than the accessor being a name that does not exist. */
1718
+ private castDistFallback: GPUTexture | null = null
1719
+ private castDistFallbackView: GPUTextureView | null = null
1720
+ private castSeedPipeline: GPURenderPipeline | null = null
1721
+ private castStepPipeline: GPURenderPipeline | null = null
1722
+ private castResolvePipeline: GPURenderPipeline | null = null
1723
+ private castSeedBindGroup: GPUBindGroup | null = null
1724
+ private castStepBindGroups: GPUBindGroup[] = []
1725
+ private castResolveBindGroup: GPUBindGroup | null = null
1726
+ private castStepStrideBuffers: GPUBuffer[] = []
1727
+ /** Does anything installed actually read the field? Set from the effect list. */
1728
+ private castDistanceWanted = false
1729
+
1689
1730
  private fieldBgTextures: (GPUTexture | null)[] = [null, null]
1690
1731
  private fieldBgViews: (GPUTextureView | null)[] = [null, null]
1691
1732
  private fieldFgTextures: (GPUTexture | null)[] = [null, null]
@@ -2795,8 +2836,149 @@ export class Engine {
2795
2836
  return this.fieldPairUsed(layer) && view ? view : this.trailFallbackView
2796
2837
  }
2797
2838
 
2839
+ /**
2840
+ * The distance field's textures, and the bind groups that walk the flood.
2841
+ *
2842
+ * Rebuilt with the field targets, because it is sized off the same swap chain
2843
+ * and a resize invalidates every view. Torn down entirely when nothing reads
2844
+ * it: the memory is two coordinate targets and a distance one at half res,
2845
+ * which is real, and a scene with no such effect should not hold it.
2846
+ */
2847
+ private createCastDistanceTargets(): void {
2848
+ for (const t of this.castSeedTextures) t?.destroy()
2849
+ this.castCoverageTexture?.destroy()
2850
+ this.castDistTexture?.destroy()
2851
+ for (const b of this.castStepStrideBuffers) b.destroy()
2852
+ this.castSeedTextures = [null, null]
2853
+ this.castSeedViews = [null, null]
2854
+ this.castCoverageTexture = null
2855
+ this.castCoverageView = null
2856
+ this.castDistTexture = null
2857
+ this.castDistView = null
2858
+ this.castStepStrideBuffers = []
2859
+ this.castStepBindGroups = []
2860
+ this.castSeedBindGroup = null
2861
+ this.castResolveBindGroup = null
2862
+ if (!this.device || !this.castDistanceWanted || this.fieldFullW === 0) return
2863
+ if (!this.castSeedPipeline || !this.castStepPipeline || !this.castResolvePipeline) return
2864
+
2865
+ const w = Math.max(1, Math.ceil(this.fieldFullW / CAST_FIELD_DIV))
2866
+ const h = Math.max(1, Math.ceil(this.fieldFullH / CAST_FIELD_DIV))
2867
+ for (let i = 0; i < 2; i++) {
2868
+ this.castSeedTextures[i] = this.device.createTexture({
2869
+ label: `cast distance seeds ${i}`,
2870
+ size: [w, h],
2871
+ format: CAST_SEED_FORMAT,
2872
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
2873
+ })
2874
+ this.castSeedViews[i] = this.castSeedTextures[i]!.createView()
2875
+ }
2876
+ this.castCoverageTexture = this.device.createTexture({
2877
+ label: "cast coverage",
2878
+ size: [w, h],
2879
+ format: CAST_COVERAGE_FORMAT,
2880
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
2881
+ })
2882
+ this.castCoverageView = this.castCoverageTexture.createView()
2883
+ this.castDistTexture = this.device.createTexture({
2884
+ label: "cast distance",
2885
+ size: [w, h],
2886
+ format: CAST_DIST_FORMAT,
2887
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
2888
+ })
2889
+ this.castDistView = this.castDistTexture.createView()
2890
+
2891
+ // The flood starts at half the longest side and halves to one. That is what
2892
+ // makes it exact everywhere rather than out to some radius: every seed gets
2893
+ // the chance to reach every texel it is nearest to.
2894
+ const strides: number[] = []
2895
+ for (let k = 1 << Math.ceil(Math.log2(Math.max(w, h))); k >= 1; k >>= 1) strides.push(k)
2896
+
2897
+ this.castSeedBindGroup = this.device.createBindGroup({
2898
+ label: "cast distance seed",
2899
+ layout: this.castSeedPipeline.getBindGroupLayout(0),
2900
+ entries: [
2901
+ { binding: 0, resource: this.idView! },
2902
+ { binding: 1, resource: { buffer: this.castBuffer } },
2903
+ ],
2904
+ })
2905
+ strides.forEach((stride, i) => {
2906
+ const buf = this.device!.createBuffer({
2907
+ label: `cast distance stride ${stride}`,
2908
+ size: 16,
2909
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
2910
+ })
2911
+ this.device!.queue.writeBuffer(buf, 0, new Float32Array([stride, 0, 0, 0]))
2912
+ this.castStepStrideBuffers.push(buf)
2913
+ // Pass i reads the texture pass i-1 wrote. The seed lands in 0, so an even
2914
+ // pass reads 0 and writes 1.
2915
+ this.castStepBindGroups.push(
2916
+ this.device!.createBindGroup({
2917
+ label: `cast distance step ${stride}`,
2918
+ layout: this.castStepPipeline!.getBindGroupLayout(0),
2919
+ entries: [
2920
+ { binding: 0, resource: this.castSeedViews[i % 2]! },
2921
+ { binding: 1, resource: { buffer: buf } },
2922
+ ],
2923
+ }),
2924
+ )
2925
+ })
2926
+ this.castResolveBindGroup = this.device.createBindGroup({
2927
+ label: "cast distance resolve",
2928
+ layout: this.castResolvePipeline.getBindGroupLayout(0),
2929
+ entries: [
2930
+ { binding: 0, resource: this.castSeedViews[strides.length % 2]! },
2931
+ { binding: 1, resource: this.castCoverageView! },
2932
+ ],
2933
+ })
2934
+ }
2935
+
2936
+ /**
2937
+ * The flood, encoded once a frame before the field pass reads it.
2938
+ *
2939
+ * Seed, then one pass per halving of the stride, then resolve to a distance.
2940
+ * Nothing here depends on how far any effect intends to look — that is the
2941
+ * whole point of paying for it in passes rather than in per-pixel search.
2942
+ */
2943
+ private encodeCastDistance(encoder: GPUCommandEncoder): void {
2944
+ if (!this.castDistanceWanted || !this.castSeedBindGroup || !this.castResolveBindGroup) return
2945
+ const seed = encoder.beginRenderPass({
2946
+ label: "cast distance (seed)",
2947
+ colorAttachments: [
2948
+ { view: this.castSeedViews[0]!, loadOp: "clear", clearValue: { r: -1, g: -1, b: 0, a: 0 }, storeOp: "store" },
2949
+ { view: this.castCoverageView!, loadOp: "clear", clearValue: { r: 0, g: 0, b: 0, a: 0 }, storeOp: "store" },
2950
+ ],
2951
+ })
2952
+ seed.setPipeline(this.castSeedPipeline!)
2953
+ seed.setBindGroup(0, this.castSeedBindGroup)
2954
+ seed.draw(3)
2955
+ seed.end()
2956
+
2957
+ this.castStepBindGroups.forEach((group, i) => {
2958
+ const pass = encoder.beginRenderPass({
2959
+ label: "cast distance (flood)",
2960
+ colorAttachments: [{ view: this.castSeedViews[(i + 1) % 2]!, loadOp: "clear", clearValue: { r: -1, g: -1, b: 0, a: 0 }, storeOp: "store" }],
2961
+ })
2962
+ pass.setPipeline(this.castStepPipeline!)
2963
+ pass.setBindGroup(0, group)
2964
+ pass.draw(3)
2965
+ pass.end()
2966
+ })
2967
+
2968
+ const resolve = encoder.beginRenderPass({
2969
+ label: "cast distance (resolve)",
2970
+ colorAttachments: [{ view: this.castDistView!, loadOp: "clear", clearValue: { r: 0, g: 0, b: 0, a: 0 }, storeOp: "store" }],
2971
+ })
2972
+ resolve.setPipeline(this.castResolvePipeline!)
2973
+ resolve.setBindGroup(0, this.castResolveBindGroup)
2974
+ resolve.draw(3)
2975
+ resolve.end()
2976
+ }
2977
+
2798
2978
  private createFieldTargets(): void {
2799
2979
  if (!this.device || this.fieldFullW === 0) return
2980
+ // Same swap chain, same invalidation.
2981
+ this.createCastDistanceTargets()
2800
2982
  for (let i = 0; i < Engine.FIELD_SCALES.length; i++) {
2801
2983
  const scale = Engine.FIELD_SCALES[i]
2802
2984
  const w = Math.max(1, Math.ceil(this.fieldFullW / scale))
@@ -2857,6 +3039,7 @@ export class Engine {
2857
3039
  ...(this.idView ? [{ binding: 23, resource: this.idView }] : []),
2858
3040
  { binding: 17, resource: grid },
2859
3041
  { binding: 18, resource: this.simSampler },
3042
+ { binding: 26, resource: this.castDistView ?? this.castDistFallbackView! },
2860
3043
  ],
2861
3044
  })
2862
3045
  // Per effect: the params buffer and the grid are both its own, so two
@@ -3422,6 +3605,7 @@ export class Engine {
3422
3605
  // so matching against it would report every effect as a reader and the
3423
3606
  // attachment would be stored exactly as often as before.
3424
3607
  readsIds: /\brz(?:ObjectAt|MaterialAt)\s*\(/.test(wgsl),
3608
+ readsCastDistance: castDistanceUsed(wgsl),
3425
3609
  anchors,
3426
3610
  // The effect's own clock starts now. Per effect so that one installed
3427
3611
  // later still gets a frame where rzGridFrame() is 0 and can seed.
@@ -3571,6 +3755,17 @@ export class Engine {
3571
3755
  this.releaseTrails()
3572
3756
  this.releaseGrid()
3573
3757
  this.effects = instances
3758
+ // Turn the distance field on or off with the list that asked for it. Only
3759
+ // rebuilds when the answer CHANGES: the targets are half-res render
3760
+ // attachments and reallocating them per install would churn them for every
3761
+ // unrelated effect a scene adds.
3762
+ const wantsCastDistance = instances.some((e) => e.readsCastDistance)
3763
+ if (wantsCastDistance !== this.castDistanceWanted) {
3764
+ this.castDistanceWanted = wantsCastDistance
3765
+ this.createCastDistanceTargets()
3766
+ // The field bind groups name the distance texture, and it has just been
3767
+ // created or destroyed — they are rebuilt below with the new list anyway.
3768
+ }
3574
3769
  // The new list's emitters need their slots before the next frame reads them.
3575
3770
  this.allocateLightSlots()
3576
3771
  // A rig the cap refused is worth saying out loud on the effect that asked
@@ -4207,6 +4402,10 @@ export class Engine {
4207
4402
  // two 16MB writes a frame to produce the transparent black the fallback
4208
4403
  // already is. Most scenes leave the full-res pair empty, since an effect only
4209
4404
  // lands there by declaring #fullres.
4405
+ // The distance field, before anything reads it. Returns immediately when no
4406
+ // installed effect names rzCastDistance, which is the common case.
4407
+ this.encodeCastDistance(encoder)
4408
+
4210
4409
  let stamped = false
4211
4410
  for (let i = 0; i < Engine.FIELD_SCALES.length; i++) {
4212
4411
  const bg = this.fieldBgViews[i]
@@ -5202,12 +5401,68 @@ export class Engine {
5202
5401
  : []),
5203
5402
  { binding: 17, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "2d" } },
5204
5403
  { binding: 18, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
5404
+ // Distance to the cast. Always in the layout — the accessor is always
5405
+ // compiled, and a 1x1 stands in when the flood is not running, so an
5406
+ // author never has to guard the name.
5407
+ { binding: 26, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "2d" } },
5205
5408
  ],
5206
5409
  })
5207
5410
  this.fieldPipelineLayout = this.device.createPipelineLayout({
5208
5411
  bindGroupLayouts: [this.fieldBindGroupLayout],
5209
5412
  })
5210
5413
 
5414
+ // ── The distance-to-cast field's three pipelines ──
5415
+ //
5416
+ // Built once, whether or not anything reads the field: they cost a shader
5417
+ // module each and nothing per frame, and building them lazily would put a
5418
+ // pipeline compile in the frame where an author first types the name.
5419
+ {
5420
+ const seedModule = this.device.createShaderModule({ label: "cast distance seed", code: buildCastSeedShader(Engine.MULTISAMPLE_COUNT) })
5421
+ const stepModule = this.device.createShaderModule({ label: "cast distance step", code: buildCastStepShader() })
5422
+ const resolveModule = this.device.createShaderModule({ label: "cast distance resolve", code: buildCastResolveShader() })
5423
+ this.castSeedPipeline = this.device.createRenderPipeline({
5424
+ label: "cast distance seed",
5425
+ layout: "auto",
5426
+ vertex: { module: seedModule, entryPoint: "vs" },
5427
+ fragment: {
5428
+ module: seedModule,
5429
+ entryPoint: "fs",
5430
+ targets: [{ format: CAST_SEED_FORMAT }, { format: CAST_COVERAGE_FORMAT }],
5431
+ },
5432
+ primitive: { topology: "triangle-list" },
5433
+ })
5434
+ this.castStepPipeline = this.device.createRenderPipeline({
5435
+ label: "cast distance step",
5436
+ layout: "auto",
5437
+ vertex: { module: stepModule, entryPoint: "vs" },
5438
+ fragment: { module: stepModule, entryPoint: "fs", targets: [{ format: CAST_SEED_FORMAT }] },
5439
+ primitive: { topology: "triangle-list" },
5440
+ })
5441
+ this.castResolvePipeline = this.device.createRenderPipeline({
5442
+ label: "cast distance resolve",
5443
+ layout: "auto",
5444
+ vertex: { module: resolveModule, entryPoint: "vs" },
5445
+ fragment: { module: resolveModule, entryPoint: "fs", targets: [{ format: CAST_DIST_FORMAT }] },
5446
+ primitive: { topology: "triangle-list" },
5447
+ })
5448
+ // 65504 is the largest half float. Bound wherever the field is not
5449
+ // running, so rzCastDistance answers "unreachably far" and an effect keyed
5450
+ // on it draws nothing at all.
5451
+ this.castDistFallback = this.device.createTexture({
5452
+ label: "cast distance fallback (1x1, far)",
5453
+ size: [1, 1],
5454
+ format: CAST_DIST_FORMAT,
5455
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
5456
+ })
5457
+ this.device.queue.writeTexture(
5458
+ { texture: this.castDistFallback },
5459
+ new Uint16Array([0x7bff]),
5460
+ { bytesPerRow: 2 },
5461
+ { width: 1, height: 1 },
5462
+ )
5463
+ this.castDistFallbackView = this.castDistFallback.createView()
5464
+ }
5465
+
5211
5466
  this.fallbackMaterialTexture = this.device.createTexture({
5212
5467
  label: "fallback material texture (1x1 white)",
5213
5468
  size: [1, 1],
@@ -10884,7 +11139,9 @@ export class Engine {
10884
11139
  // whose result no one is going to read.
10885
11140
  const idAtt = (this.renderPassDescriptor.colorAttachments as GPURenderPassColorAttachment[])[2]
10886
11141
  if (idAtt) {
10887
- const idsRead = this.idDebug || this.effects.some((e) => e.readsIds)
11142
+ // The flood seeds from the id attachment, so an effect that reads only the
11143
+ // DISTANCE still needs the ids kept — it just never names them itself.
11144
+ const idsRead = this.idDebug || this.effects.some((e) => e.readsIds || e.readsCastDistance)
10888
11145
  idAtt.storeOp = idsRead ? "store" : "discard"
10889
11146
  }
10890
11147
 
@@ -11,6 +11,12 @@ export const BODY_GRAPH: ShaderGraph = {
11
11
  tags: ["body"],
12
12
  nodes: [
13
13
  { id: "tex", type: "texture" },
14
+ // Base colour: the texture times the PMX material's diffuse. MMD authors
15
+ // tint per MATERIAL rather than per texel — an eyebrow sharing the face
16
+ // atlas is painted once and coloured here — so the diffuse belongs in the
17
+ // base of every look.
18
+ { id: "mat_diffuse", type: "material_diffuse" },
19
+ { id: "tex_base", type: "mix/multiply", inputs: { fac: 1.0 } },
14
20
  { id: "geo", type: "geometry" },
15
21
  { id: "str", type: "shader_to_rgb_diffuse" },
16
22
  { id: "toon", type: "ramp_constant", inputs: { pos0: 0.0, pos1: 0.2966 } },
@@ -62,8 +68,10 @@ export const BODY_GRAPH: ShaderGraph = {
62
68
  ],
63
69
  links: [
64
70
  { from: { node: "str", socket: "value" }, to: { node: "toon", socket: "fac" } },
65
- { from: { node: "tex", socket: "color" }, to: { node: "shadow_tint", socket: "color" } },
66
- { from: { node: "tex", socket: "color" }, to: { node: "lit_tint", socket: "color" } },
71
+ { from: { node: "tex", socket: "color" }, to: { node: "tex_base", socket: "a" } },
72
+ { from: { node: "mat_diffuse", socket: "color" }, to: { node: "tex_base", socket: "b" } },
73
+ { from: { node: "tex_base", socket: "color" }, to: { node: "shadow_tint", socket: "color" } },
74
+ { from: { node: "tex_base", socket: "color" }, to: { node: "lit_tint", socket: "color" } },
67
75
  { from: { node: "toon", socket: "fac_out" }, to: { node: "toon_color", socket: "fac" } },
68
76
  { from: { node: "shadow_tint", socket: "color" }, to: { node: "toon_color", socket: "a" } },
69
77
  { from: { node: "lit_tint", socket: "color" }, to: { node: "toon_color", socket: "b" } },
@@ -11,6 +11,12 @@ export const CLOTH_ROUGH_GRAPH: ShaderGraph = {
11
11
  tags: ["cloth_rough"],
12
12
  nodes: [
13
13
  { id: "tex", type: "texture" },
14
+ // Base colour: the texture times the PMX material's diffuse. MMD authors
15
+ // tint per MATERIAL rather than per texel — an eyebrow sharing the face
16
+ // atlas is painted once and coloured here — so the diffuse belongs in the
17
+ // base of every look.
18
+ { id: "mat_diffuse", type: "material_diffuse" },
19
+ { id: "tex_base", type: "mix/multiply", inputs: { fac: 1.0 } },
14
20
  { id: "geo", type: "geometry" },
15
21
  { id: "str", type: "shader_to_rgb_diffuse" },
16
22
  { id: "ramp_008", type: "ramp_constant_aa", inputs: { edge: 0.2966 } },
@@ -37,10 +43,12 @@ export const CLOTH_ROUGH_GRAPH: ShaderGraph = {
37
43
  links: [
38
44
  { from: { node: "str", socket: "value" }, to: { node: "ramp_008", socket: "fac" } },
39
45
  { from: { node: "ramp_008", socket: "fac_out" }, to: { node: "mix04_fac", socket: "a" } },
40
- { from: { node: "tex", socket: "color" }, to: { node: "dark_tex", socket: "color" } },
46
+ { from: { node: "tex", socket: "color" }, to: { node: "tex_base", socket: "a" } },
47
+ { from: { node: "mat_diffuse", socket: "color" }, to: { node: "tex_base", socket: "b" } },
48
+ { from: { node: "tex_base", socket: "color" }, to: { node: "dark_tex", socket: "color" } },
41
49
  { from: { node: "mix04_fac", socket: "value" }, to: { node: "mix_004", socket: "fac" } },
42
50
  { from: { node: "dark_tex", socket: "color" }, to: { node: "mix_004", socket: "a" } },
43
- { from: { node: "tex", socket: "color" }, to: { node: "mix_004", socket: "b" } },
51
+ { from: { node: "tex_base", socket: "color" }, to: { node: "mix_004", socket: "b" } },
44
52
  { from: { node: "geo", socket: "normal" }, to: { node: "sep_n", socket: "vector" } },
45
53
  { from: { node: "sep_n", socket: "y" }, to: { node: "bevel_clamp", socket: "a" } },
46
54
  { from: { node: "bevel_clamp", socket: "value" }, to: { node: "mix_003", socket: "fac" } },
@@ -54,7 +62,7 @@ export const CLOTH_ROUGH_GRAPH: ShaderGraph = {
54
62
  { from: { node: "noise", socket: "value" }, to: { node: "noise_ramp", socket: "fac" } },
55
63
  { from: { node: "noise_ramp", socket: "fac_out" }, to: { node: "bump", socket: "height" } },
56
64
  { from: { node: "geo", socket: "normal" }, to: { node: "bump", socket: "normal" } },
57
- { from: { node: "tex", socket: "color" }, to: { node: "principled_base", socket: "color" } },
65
+ { from: { node: "tex_base", socket: "color" }, to: { node: "principled_base", socket: "color" } },
58
66
  { from: { node: "principled_base", socket: "color" }, to: { node: "principled", socket: "base_color" } },
59
67
  { from: { node: "bump", socket: "vector" }, to: { node: "principled", socket: "normal" } },
60
68
  { from: { node: "npr_emit", socket: "color" }, to: { node: "mix_shader_001", socket: "a" } },
@@ -11,6 +11,12 @@ export const CLOTH_SMOOTH_GRAPH: ShaderGraph = {
11
11
  tags: ["cloth_smooth"],
12
12
  nodes: [
13
13
  { id: "tex", type: "texture" },
14
+ // Base colour: the texture times the PMX material's diffuse. MMD authors
15
+ // tint per MATERIAL rather than per texel — an eyebrow sharing the face
16
+ // atlas is painted once and coloured here — so the diffuse belongs in the
17
+ // base of every look.
18
+ { id: "mat_diffuse", type: "material_diffuse" },
19
+ { id: "tex_base", type: "mix/multiply", inputs: { fac: 1.0 } },
14
20
  { id: "geo", type: "geometry" },
15
21
  { id: "str", type: "shader_to_rgb_diffuse" },
16
22
  { id: "ramp_008", type: "ramp_constant_aa", inputs: { edge: 0.2966 } },
@@ -34,10 +40,12 @@ export const CLOTH_SMOOTH_GRAPH: ShaderGraph = {
34
40
  links: [
35
41
  { from: { node: "str", socket: "value" }, to: { node: "ramp_008", socket: "fac" } },
36
42
  { from: { node: "ramp_008", socket: "fac_out" }, to: { node: "mix04_fac", socket: "a" } },
37
- { from: { node: "tex", socket: "color" }, to: { node: "dark_tex", socket: "color" } },
43
+ { from: { node: "tex", socket: "color" }, to: { node: "tex_base", socket: "a" } },
44
+ { from: { node: "mat_diffuse", socket: "color" }, to: { node: "tex_base", socket: "b" } },
45
+ { from: { node: "tex_base", socket: "color" }, to: { node: "dark_tex", socket: "color" } },
38
46
  { from: { node: "mix04_fac", socket: "value" }, to: { node: "mix_004", socket: "fac" } },
39
47
  { from: { node: "dark_tex", socket: "color" }, to: { node: "mix_004", socket: "a" } },
40
- { from: { node: "tex", socket: "color" }, to: { node: "mix_004", socket: "b" } },
48
+ { from: { node: "tex_base", socket: "color" }, to: { node: "mix_004", socket: "b" } },
41
49
  { from: { node: "geo", socket: "normal" }, to: { node: "sep_n", socket: "vector" } },
42
50
  { from: { node: "sep_n", socket: "y" }, to: { node: "bevel_clamp", socket: "a" } },
43
51
  { from: { node: "bevel_clamp", socket: "value" }, to: { node: "mix_003", socket: "fac" } },
@@ -47,7 +55,7 @@ export const CLOTH_SMOOTH_GRAPH: ShaderGraph = {
47
55
  { from: { node: "mix_003", socket: "color" }, to: { node: "npr_overlay", socket: "a" } },
48
56
  { from: { node: "hue_004", socket: "color" }, to: { node: "npr_overlay", socket: "b" } },
49
57
  { from: { node: "npr_overlay", socket: "color" }, to: { node: "npr_emit", socket: "color" } },
50
- { from: { node: "tex", socket: "color" }, to: { node: "principled_base", socket: "color" } },
58
+ { from: { node: "tex_base", socket: "color" }, to: { node: "principled_base", socket: "color" } },
51
59
  { from: { node: "principled_base", socket: "color" }, to: { node: "principled", socket: "base_color" } },
52
60
  { from: { node: "npr_emit", socket: "color" }, to: { node: "mix_shader_001", socket: "a" } },
53
61
  { from: { node: "principled", socket: "color" }, to: { node: "mix_shader_001", socket: "b" } },
@@ -15,6 +15,12 @@ export const EYE_GRAPH: ShaderGraph = {
15
15
  tags: ["eye"],
16
16
  nodes: [
17
17
  { id: "tex", type: "texture" },
18
+ // Base colour: the texture times the PMX material's diffuse. MMD authors
19
+ // tint per MATERIAL rather than per texel — an eyebrow sharing the face
20
+ // atlas is painted once and coloured here — so the diffuse belongs in the
21
+ // base of every look.
22
+ { id: "mat_diffuse", type: "material_diffuse" },
23
+ { id: "tex_base", type: "mix/multiply", inputs: { fac: 1.0 } },
18
24
  {
19
25
  id: "principled",
20
26
  type: "principled",
@@ -24,8 +30,10 @@ export const EYE_GRAPH: ShaderGraph = {
24
30
  { id: "add", type: "add_shader" },
25
31
  ],
26
32
  links: [
27
- { from: { node: "tex", socket: "color" }, to: { node: "principled", socket: "base_color" } },
28
- { from: { node: "tex", socket: "color" }, to: { node: "emission", socket: "color" } },
33
+ { from: { node: "tex", socket: "color" }, to: { node: "tex_base", socket: "a" } },
34
+ { from: { node: "mat_diffuse", socket: "color" }, to: { node: "tex_base", socket: "b" } },
35
+ { from: { node: "tex_base", socket: "color" }, to: { node: "principled", socket: "base_color" } },
36
+ { from: { node: "tex_base", socket: "color" }, to: { node: "emission", socket: "color" } },
29
37
  { from: { node: "principled", socket: "color" }, to: { node: "add", socket: "a" } },
30
38
  { from: { node: "emission", socket: "color" }, to: { node: "add", socket: "b" } },
31
39
  ],
@@ -14,6 +14,12 @@ export const FACE_GRAPH: ShaderGraph = {
14
14
  tags: ["face"],
15
15
  nodes: [
16
16
  { id: "tex", type: "texture" },
17
+ // Base colour: the texture times the PMX material's diffuse. MMD authors
18
+ // tint per MATERIAL rather than per texel — an eyebrow sharing the face
19
+ // atlas is painted once and coloured here — so the diffuse belongs in the
20
+ // base of every look.
21
+ { id: "mat_diffuse", type: "material_diffuse" },
22
+ { id: "tex_base", type: "mix/multiply", inputs: { fac: 1.0 } },
17
23
  { id: "geo", type: "geometry" },
18
24
 
19
25
  // ── toon shading ──
@@ -82,8 +88,10 @@ export const FACE_GRAPH: ShaderGraph = {
82
88
  links: [
83
89
  // toon
84
90
  { from: { node: "str", socket: "value" }, to: { node: "toon", socket: "fac" } },
85
- { from: { node: "tex", socket: "color" }, to: { node: "shadow_tint", socket: "color" } },
86
- { from: { node: "tex", socket: "color" }, to: { node: "lit_tint", socket: "color" } },
91
+ { from: { node: "tex", socket: "color" }, to: { node: "tex_base", socket: "a" } },
92
+ { from: { node: "mat_diffuse", socket: "color" }, to: { node: "tex_base", socket: "b" } },
93
+ { from: { node: "tex_base", socket: "color" }, to: { node: "shadow_tint", socket: "color" } },
94
+ { from: { node: "tex_base", socket: "color" }, to: { node: "lit_tint", socket: "color" } },
87
95
  { from: { node: "toon", socket: "fac_out" }, to: { node: "toon_color", socket: "fac" } },
88
96
  { from: { node: "shadow_tint", socket: "color" }, to: { node: "toon_color", socket: "a" } },
89
97
  { from: { node: "lit_tint", socket: "color" }, to: { node: "toon_color", socket: "b" } },
@@ -18,6 +18,12 @@ export const HAIR_GRAPH: ShaderGraph = {
18
18
  tags: ["hair"],
19
19
  nodes: [
20
20
  { id: "tex", type: "texture" },
21
+ // Base colour: the texture times the PMX material's diffuse. MMD authors
22
+ // tint per MATERIAL rather than per texel — an eyebrow sharing the face
23
+ // atlas is painted once and coloured here — so the diffuse belongs in the
24
+ // base of every look.
25
+ { id: "mat_diffuse", type: "material_diffuse" },
26
+ { id: "tex_base", type: "mix/multiply", inputs: { fac: 1.0 } },
21
27
  { id: "geo", type: "geometry" },
22
28
  { id: "hs_shadow", type: "hue_sat", inputs: { hue: 0.5, saturation: 1.2, value: 0.5, fac: 1.0 } },
23
29
  { id: "hs_002", type: "hue_sat", inputs: { hue: 0.48, saturation: 1.2, value: 0.7, fac: 1.0 } },
@@ -49,9 +55,11 @@ export const HAIR_GRAPH: ShaderGraph = {
49
55
  { id: "mix_shader_001", type: "mix_shader", inputs: { fac: 0.2 } },
50
56
  ],
51
57
  links: [
52
- { from: { node: "tex", socket: "color" }, to: { node: "hs_shadow", socket: "color" } },
58
+ { from: { node: "tex", socket: "color" }, to: { node: "tex_base", socket: "a" } },
59
+ { from: { node: "mat_diffuse", socket: "color" }, to: { node: "tex_base", socket: "b" } },
60
+ { from: { node: "tex_base", socket: "color" }, to: { node: "hs_shadow", socket: "color" } },
53
61
  { from: { node: "hs_shadow", socket: "color" }, to: { node: "hs_002", socket: "color" } },
54
- { from: { node: "tex", socket: "color" }, to: { node: "hs_001", socket: "color" } },
62
+ { from: { node: "tex_base", socket: "color" }, to: { node: "hs_001", socket: "color" } },
55
63
  { from: { node: "str", socket: "value" }, to: { node: "ramp_008", socket: "fac" } },
56
64
  { from: { node: "ramp_008", socket: "fac_out" }, to: { node: "mix_004", socket: "fac" } },
57
65
  { from: { node: "hs_002", socket: "color" }, to: { node: "mix_004", socket: "a" } },
@@ -12,6 +12,12 @@ export const METAL_GRAPH: ShaderGraph = {
12
12
  tags: ["metal"],
13
13
  nodes: [
14
14
  { id: "tex", type: "texture" },
15
+ // Base colour: the texture times the PMX material's diffuse. MMD authors
16
+ // tint per MATERIAL rather than per texel — an eyebrow sharing the face
17
+ // atlas is painted once and coloured here — so the diffuse belongs in the
18
+ // base of every look.
19
+ { id: "mat_diffuse", type: "material_diffuse" },
20
+ { id: "tex_base", type: "mix/multiply", inputs: { fac: 1.0 } },
15
21
  { id: "geo", type: "geometry" },
16
22
  { id: "tex_tint", type: "hue_sat", inputs: { hue: 0.5, saturation: 1.0, value: 0.800000011920929, fac: 1.0 } },
17
23
  { id: "str", type: "shader_to_rgb_diffuse" },
@@ -35,7 +41,9 @@ export const METAL_GRAPH: ShaderGraph = {
35
41
  { id: "mix_shader_001", type: "mix_shader", inputs: { fac: 0.6967 } },
36
42
  ],
37
43
  links: [
38
- { from: { node: "tex", socket: "color" }, to: { node: "tex_tint", socket: "color" } },
44
+ { from: { node: "tex", socket: "color" }, to: { node: "tex_base", socket: "a" } },
45
+ { from: { node: "mat_diffuse", socket: "color" }, to: { node: "tex_base", socket: "b" } },
46
+ { from: { node: "tex_base", socket: "color" }, to: { node: "tex_tint", socket: "color" } },
39
47
  { from: { node: "str", socket: "value" }, to: { node: "ramp_008", socket: "fac" } },
40
48
  { from: { node: "ramp_008", socket: "fac_out" }, to: { node: "mix04_fac", socket: "a" } },
41
49
  { from: { node: "tex_tint", socket: "color" }, to: { node: "dark_tex", socket: "color" } },
@@ -14,6 +14,12 @@ export const STOCKINGS_GRAPH: ShaderGraph = {
14
14
  tags: ["stockings"],
15
15
  nodes: [
16
16
  { id: "tex", type: "texture" },
17
+ // Base colour: the texture times the PMX material's diffuse. MMD authors
18
+ // tint per MATERIAL rather than per texel — an eyebrow sharing the face
19
+ // atlas is painted once and coloured here — so the diffuse belongs in the
20
+ // base of every look.
21
+ { id: "mat_diffuse", type: "material_diffuse" },
22
+ { id: "tex_base", type: "mix/multiply", inputs: { fac: 1.0 } },
17
23
  { id: "geo", type: "geometry" },
18
24
  { id: "map", type: "mapping", inputs: { loc: [1.0, 1.0, 1.0], rot: [0.0, 1.5708, 1.5708] } },
19
25
  { id: "grad", type: "tex_gradient" },
@@ -47,8 +53,10 @@ export const STOCKINGS_GRAPH: ShaderGraph = {
47
53
  { from: { node: "ramp_face", socket: "fac_out" }, to: { node: "mix_001", socket: "b" } },
48
54
  { from: { node: "mix_001", socket: "color" }, to: { node: "mask", socket: "a" } },
49
55
  { from: { node: "ramp_002", socket: "fac_out" }, to: { node: "mask", socket: "b" } },
50
- { from: { node: "tex", socket: "color" }, to: { node: "emission_hs", socket: "color" } },
51
- { from: { node: "tex", socket: "color" }, to: { node: "principled", socket: "base_color" } },
56
+ { from: { node: "tex", socket: "color" }, to: { node: "tex_base", socket: "a" } },
57
+ { from: { node: "mat_diffuse", socket: "color" }, to: { node: "tex_base", socket: "b" } },
58
+ { from: { node: "tex_base", socket: "color" }, to: { node: "emission_hs", socket: "color" } },
59
+ { from: { node: "tex_base", socket: "color" }, to: { node: "principled", socket: "base_color" } },
52
60
  { from: { node: "emission_hs", socket: "color" }, to: { node: "mix_shader_001", socket: "a" } },
53
61
  { from: { node: "principled", socket: "color" }, to: { node: "mix_shader_001", socket: "b" } },
54
62
  { from: { node: "mask", socket: "color" }, to: { node: "mix_shader_001", socket: "fac" } },