reze-engine 0.63.0 → 0.65.0

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 (106) hide show
  1. package/dist/engine.d.ts +226 -96
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +1187 -732
  4. package/dist/graph/compile.d.ts.map +1 -1
  5. package/dist/graph/compile.js +4 -1
  6. package/dist/graph/registry.d.ts +3 -0
  7. package/dist/graph/registry.d.ts.map +1 -1
  8. package/dist/graph/registry.js +133 -0
  9. package/dist/graph/schema.d.ts +12 -0
  10. package/dist/graph/schema.d.ts.map +1 -1
  11. package/dist/graph/slots.d.ts.map +1 -1
  12. package/dist/graph/slots.js +15 -18
  13. package/dist/index.d.ts +9 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/model.d.ts +10 -1
  17. package/dist/model.d.ts.map +1 -1
  18. package/dist/model.js +15 -1
  19. package/dist/outline-normals.d.ts +12 -0
  20. package/dist/outline-normals.d.ts.map +1 -0
  21. package/dist/outline-normals.js +100 -0
  22. package/dist/pmx-loader.d.ts +3 -0
  23. package/dist/pmx-loader.d.ts.map +1 -1
  24. package/dist/pmx-loader.js +7 -2
  25. package/dist/shaders/lights.d.ts +3 -2
  26. package/dist/shaders/lights.d.ts.map +1 -1
  27. package/dist/shaders/lights.js +16 -3
  28. package/dist/shaders/materials/common.d.ts.map +1 -1
  29. package/dist/shaders/materials/common.js +65 -56
  30. package/dist/shaders/materials/nodes.d.ts +1 -1
  31. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  32. package/dist/shaders/materials/nodes.js +210 -0
  33. package/dist/shaders/passes/bloom.d.ts +4 -3
  34. package/dist/shaders/passes/bloom.d.ts.map +1 -1
  35. package/dist/shaders/passes/bloom.js +58 -87
  36. package/dist/shaders/passes/composite.d.ts.map +1 -1
  37. package/dist/shaders/passes/composite.js +122 -64
  38. package/dist/shaders/passes/cull.d.ts +1 -1
  39. package/dist/shaders/passes/cull.d.ts.map +1 -1
  40. package/dist/shaders/passes/cull.js +7 -3
  41. package/dist/shaders/passes/ground.d.ts.map +1 -1
  42. package/dist/shaders/passes/ground.js +32 -55
  43. package/dist/shaders/passes/mirror.d.ts +18 -12
  44. package/dist/shaders/passes/mirror.d.ts.map +1 -1
  45. package/dist/shaders/passes/mirror.js +59 -13
  46. package/dist/shaders/passes/outline.d.ts.map +1 -1
  47. package/dist/shaders/passes/outline.js +71 -14
  48. package/dist/shaders/passes/particles.d.ts +1 -1
  49. package/dist/shaders/passes/particles.js +1 -1
  50. package/dist/shaders/scene-light-api.d.ts +18 -10
  51. package/dist/shaders/scene-light-api.d.ts.map +1 -1
  52. package/dist/shaders/scene-light-api.js +58 -46
  53. package/dist/shadow-cascades.d.ts +47 -20
  54. package/dist/shadow-cascades.d.ts.map +1 -1
  55. package/dist/shadow-cascades.js +36 -13
  56. package/dist/unity/character.d.ts +59 -0
  57. package/dist/unity/character.d.ts.map +1 -0
  58. package/dist/unity/character.js +120 -0
  59. package/dist/unity/globals.d.ts +93 -0
  60. package/dist/unity/globals.d.ts.map +1 -0
  61. package/dist/unity/globals.js +340 -0
  62. package/dist/unity/host.d.ts +152 -0
  63. package/dist/unity/host.d.ts.map +1 -0
  64. package/dist/unity/host.js +529 -0
  65. package/dist/unity/looks.d.ts +93 -0
  66. package/dist/unity/looks.d.ts.map +1 -0
  67. package/dist/unity/looks.js +312 -0
  68. package/dist/unity/skin.d.ts +9 -0
  69. package/dist/unity/skin.d.ts.map +1 -0
  70. package/dist/unity/skin.js +120 -0
  71. package/dist/unity/stage.d.ts +155 -0
  72. package/dist/unity/stage.d.ts.map +1 -0
  73. package/dist/unity/stage.js +410 -0
  74. package/dist/unity/state.d.ts +22 -0
  75. package/dist/unity/state.d.ts.map +1 -0
  76. package/dist/unity/state.js +170 -0
  77. package/package.json +1 -1
  78. package/src/engine.ts +1535 -964
  79. package/src/graph/compile.ts +3 -1
  80. package/src/graph/registry.ts +142 -0
  81. package/src/graph/schema.ts +9 -0
  82. package/src/graph/slots.ts +15 -18
  83. package/src/index.ts +12 -0
  84. package/src/model.ts +18 -1
  85. package/src/outline-normals.ts +110 -0
  86. package/src/pmx-loader.ts +8 -2
  87. package/src/shaders/lights.ts +16 -3
  88. package/src/shaders/materials/common.ts +65 -57
  89. package/src/shaders/materials/nodes.ts +210 -0
  90. package/src/shaders/passes/bloom.ts +59 -87
  91. package/src/shaders/passes/composite.ts +122 -64
  92. package/src/shaders/passes/cull.ts +7 -3
  93. package/src/shaders/passes/ground.ts +32 -55
  94. package/src/shaders/passes/mirror.ts +60 -13
  95. package/src/shaders/passes/outline.ts +71 -14
  96. package/src/shaders/passes/particles.ts +1 -1
  97. package/src/shaders/scene-light-api.ts +59 -46
  98. package/src/shadow-cascades.ts +61 -32
  99. package/src/unity/character.ts +169 -0
  100. package/src/unity/globals.ts +443 -0
  101. package/src/unity/host.ts +630 -0
  102. package/src/unity/looks.ts +391 -0
  103. package/src/unity/skin.ts +131 -0
  104. package/src/unity/stage.ts +604 -0
  105. package/src/unity/state.ts +172 -0
  106. package/src/shaders/agx-lut.ts +0 -30
@@ -1,6 +1,7 @@
1
1
  import { sceneFsOutWgsl, sceneIdWriteWgsl } from "./scene-contract";
2
2
  import { lightsApi } from "../lights";
3
- import { SHADOW_CASCADES } from "../../shadow-cascades";
3
+ import { SHADOW_ATLAS_SIZE, SHADOW_CASCADES } from "../../shadow-cascades";
4
+ import { SHADOW_MARGIN } from "../scene-light-api";
4
5
  import { WORLD_AMBIENT_WGSL } from "../lights";
5
6
  // Ground shadow-catcher: receives directional shadow, grid lines, frosted noise,
6
7
  // radial distance fade. Writes bloom mask = 0 (ground never bloom-bleeds).
@@ -33,6 +34,9 @@ const SOFT_MAX_SPREAD = 14;
33
34
  * normalised, so there is no `/ 9.0` left at the call site.
34
35
  */
35
36
  function pcfWgsl(map, uv, texel, z, acc, pad, soft) {
37
+ // Every tap stays inside the cascade's own tile (tileLo..tileHi, in scope at
38
+ // the call): the soft disk reaches further than the selection margin, and a
39
+ // tap in the neighbouring tile compares against another cascade's depths.
36
40
  const golden = 2.39996323;
37
41
  // ONE BRANCH IS COMPILED, NOT BOTH.
38
42
  //
@@ -55,7 +59,7 @@ function pcfWgsl(map, uv, texel, z, acc, pad, soft) {
55
59
  ` for (var x = -1; x <= 1; x++) {`,
56
60
  // ...Level, not the implicit-derivative form: identical on a single-mip
57
61
  // shadow map, and legal inside a branch.
58
- ` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, ${uv} + vec2f(f32(x), f32(y)) * st, ${z});`,
62
+ ` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, clamp(${uv} + vec2f(f32(x), f32(y)) * st, tileLo, tileHi), ${z});`,
59
63
  ` }`,
60
64
  `}`,
61
65
  `${acc} *= ${1 / 9};`,
@@ -68,7 +72,7 @@ function pcfWgsl(map, uv, texel, z, acc, pad, soft) {
68
72
  // keeps successive taps from lining up into spokes.
69
73
  ` let r = sqrt((fs + 0.5) * ${1 / SOFT_TAPS});`,
70
74
  ` let a = fs * ${golden} + rot;`,
71
- ` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, ${uv} + vec2f(cos(a), sin(a)) * (r * radius), ${z});`,
75
+ ` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, clamp(${uv} + vec2f(cos(a), sin(a)) * (r * radius), tileLo, tileHi), ${z});`,
72
76
  `}`,
73
77
  `${acc} *= ${1 / SOFT_TAPS};`,
74
78
  ];
@@ -85,12 +89,10 @@ struct Light { direction: vec4f, color: vec4f, };
85
89
  struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, sh: array<vec4f, 9>, };
86
90
  struct GroundShadowMat {
87
91
  diffuseColor: vec3f, fadeStart: f32,
88
- fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, gridSpacing: f32,
92
+ fadeEnd: f32, shadowStrength: f32, _pad6: f32, gridSpacing: f32,
89
93
  gridLineWidth: f32, gridLineOpacity: f32, noiseStrength: f32, opacity: f32,
90
94
  gridLineColor: vec3f, mirror: f32,
91
- // farCascade: 1 while a stage is loaded, 0 otherwise. See the branch below —
92
- // with no stage the far map is never drawn into, so its taps are known.
93
- mirrorBlur: f32, farCascade: f32, shadowSoftness: f32, groundY: f32,
95
+ mirrorBlur: f32, _pad17: f32, shadowSoftness: f32, groundY: f32,
94
96
  // Every shadow caster in one sphere, refreshed per frame. w = radius; 0 means
95
97
  // nothing casts, negative means "do not use this" (a rigid caster has no
96
98
  // sphere, so a scene with a stage keeps the taps). See rzShadowPossible.
@@ -101,12 +103,11 @@ struct GroundShadowMat {
101
103
  struct LightVP { viewProj: array<mat4x4f, ${SHADOW_CASCADES.length}>, };
102
104
  @group(0) @binding(0) var<uniform> camera: CameraUniforms;
103
105
  @group(0) @binding(1) var<uniform> light: LightUniforms;
104
- @group(0) @binding(2) var shadowMap: texture_depth_2d;
106
+ // The sun's shadow atlas — see shadow-cascades.ts.
107
+ @group(0) @binding(2) var shadowAtlas: texture_depth_2d;
105
108
  @group(0) @binding(3) var shadowSampler: sampler_comparison;
106
109
  @group(0) @binding(4) var<uniform> material: GroundShadowMat;
107
110
  @group(0) @binding(5) var<uniform> lightVP: LightVP;
108
- // The far cascade's map, binding 7 as in the materials' layout.
109
- @group(0) @binding(7) var shadowMapFar: texture_depth_2d;
110
111
  // The floor mirror (step 7D): the reflection target, the camera that rendered
111
112
  // it, and an ordinary sampler — the two shadow bindings above are comparison.
112
113
  // params = (projA, projB, _, _) — the depth-linearisation pair of the SHARED
@@ -140,7 +141,7 @@ struct MirrorVP { viewProj: mat4x4f, params: vec4f, };
140
141
  struct MirrorClip { plane: vec4f, on: f32, _p0: f32, _p1: f32, _p2: f32, };
141
142
  @group(0) @binding(14) var<uniform> clip: MirrorClip;
142
143
  ${WORLD_AMBIENT_WGSL}
143
- ${lightsApi(0, 6)}
144
+ ${lightsApi(0, 6, "1u")}
144
145
 
145
146
  struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };
146
147
  @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {
@@ -174,21 +175,6 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
174
175
  return out;
175
176
  }
176
177
 
177
- // Outside a cascade's frustum there IS no shadow information — the clamped
178
- // border samples compare against unrelated depths and read "shadowed",
179
- // darkening the whole far plane with a visible band at the frustum edge
180
- // (masked by the opaque surface normally, glaring in green-screen mode where
181
- // only the shadow-catcher term renders). So each cascade fades out near its
182
- // border — the near one INTO the far one's answer, the far one into lit.
183
- let l0 = lightVP.viewProj[0] * vec4f(i.worldPos, 1.0);
184
- let ndc = l0.xyz / max(l0.w, 1e-6);
185
- let inZ = select(0.0, 1.0, ndc.z > 0.0 && ndc.z < 1.0);
186
- let frustum = (1.0 - smoothstep(0.88, 0.96, abs(ndc.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc.y))) * inZ;
187
- let l1 = lightVP.viewProj[1] * vec4f(i.worldPos, 1.0);
188
- let ndc1 = l1.xyz / max(l1.w, 1e-6);
189
- let inZ1 = select(0.0, 1.0, ndc1.z > 0.0 && ndc1.z < 1.0);
190
- let frustum1 = (1.0 - smoothstep(0.88, 0.96, abs(ndc1.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc1.y))) * inZ1;
191
-
192
178
  // THE cost of this shader, measured: a full-screen ground ran 25 shadow
193
179
  // comparisons per pixel, and each one is hardware-bilinear, so ~100 depth
194
180
  // samples per pixel. Gutting the rest of the shader changed nothing; cutting
@@ -197,11 +183,9 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
197
183
  // 3x3 at two-texel spacing covers the same +/-2 texel penumbra the 5x5 at
198
184
  // one-texel spacing did — that kernel was oversampled, its taps overlapping
199
185
  // almost entirely once the comparison sampler's own 2x2 filter is counted.
200
- // Nine taps instead of twenty-five for the same blur radius.
186
+ // Nine taps instead of twenty-five for the same blur radius, and one
187
+ // cascade's worth per pixel: the first that holds it.
201
188
  //
202
- // The far cascade's taps run ONLY where the near one is fading or absent
203
- // (frustum < 1), so a pixel in the near core costs exactly what it did with
204
- // one cascade — and that core is where the camera usually looks.
205
189
  // Can anything cast a shadow ONTO this point at all?
206
190
  //
207
191
  // The ground is vastly larger than what stands on it, and the answer for most
@@ -255,33 +239,26 @@ ${soft
255
239
  let ign = fract(52.9829189 * fract(dot(i.position.xy, vec2f(0.06711056, 0.00583715))));
256
240
  let rot = ign * 6.28318530718;`
257
241
  : ""}
258
- // The far cascade's taps, skipped entirely when nothing ever drew into it.
259
- //
260
- // This branch is the expensive one on a wide floor: it runs wherever the NEAR
261
- // cascade does not reach, and the near cascade is a 64-unit box around the
262
- // camera target, so a floor receding to the horizon takes it almost
263
- // everywhere. Nine comparison taps, per pixel, on the largest draw in the
264
- // frame — against a map that is cleared unless a stage is in the scene.
265
- //
266
- // Cleared depth compares as "no occluder", so the skipped path leaves vis at
267
- // 1.0, which is exactly what the taps returned. Free, not cheaper.
268
- if (material.farCascade > 0.0 && frustum < 1.0 && frustum1 > 0.0) {
269
- let suv1 = vec2f(ndc1.x * 0.5 + 0.5, 0.5 - ndc1.y * 0.5);
270
- let suv1_c = clamp(suv1, vec2f(0.02), vec2f(0.98));
271
- let compareZ1 = ndc1.z - 0.0035;
272
- var acc1 = 0.0;
273
- ${pcfWgsl("shadowMapFar", "suv1_c", `${1 / SHADOW_CASCADES[SHADOW_CASCADES.length - 1].mapSize}`, "compareZ1", "acc1", " ", soft)}
274
- vis = mix(1.0, acc1, frustum1);
275
- }
276
- if (frustum > 0.0) {
277
- let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
278
- let suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));
242
+ // The first cascade whose box holds the point, as the materials pick it.
243
+ // Outside a cascade's box there IS no shadow information — the clamped
244
+ // border samples compare against unrelated depths and read "shadowed",
245
+ // darkening the far floor with a band at the edge — so the outermost one
246
+ // fades into lit near its border. Inner cascades hand over to the next.
247
+ let n = ${SHADOW_CASCADES.length}u;
248
+ for (var ci = 0u; ci < n; ci++) {
249
+ let lc = lightVP.viewProj[ci] * vec4f(i.worldPos, 1.0);
250
+ let ndc = lc.xyz / max(lc.w, 1e-6);
251
+ if (!(all(abs(ndc.xy) < vec2f(${SHADOW_MARGIN})) && ndc.z > 0.0 && ndc.z < 1.0)) { continue; }
252
+ let frustum = select(1.0, (1.0 - smoothstep(0.88, 0.96, abs(ndc.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc.y))), ci == n - 1u);
253
+ let tile = vec2f(f32(ci % 2u), f32(ci / 2u)) * ${SHADOW_CASCADES[0].mapSize / SHADOW_ATLAS_SIZE};
254
+ let tileLo = tile + vec2f(${0.5 / SHADOW_ATLAS_SIZE});
255
+ let tileHi = tile + vec2f(${(SHADOW_CASCADES[0].mapSize - 0.5) / SHADOW_ATLAS_SIZE});
256
+ let suv = tile + vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5) * ${SHADOW_CASCADES[0].mapSize / SHADOW_ATLAS_SIZE};
279
257
  let compareZ = ndc.z - 0.0035;
280
258
  var acc = 0.0;
281
- ${pcfWgsl("shadowMap", "suv_c", "material.pcfTexel", "compareZ", "acc", " ", soft)}
282
- // The base is whatever the far cascade decided, so the near border blends
283
- // cascade to cascade rather than snapping to lit mid-floor.
284
- vis = mix(vis, acc, frustum);
259
+ ${pcfWgsl("shadowAtlas", "suv", `${1 / SHADOW_ATLAS_SIZE}`, "compareZ", "acc", " ", soft)}
260
+ vis = mix(1.0, acc, frustum);
261
+ break;
285
262
  }
286
263
  }
287
264
 
@@ -1,6 +1,24 @@
1
1
  /** Bytes in MirrorMat below: mat4x4f + three 16-byte rows. */
2
2
  export declare const MIRROR_MAT_BYTES: number;
3
3
  export declare function mirrorShaderWgsl(): string;
4
+ /**
5
+ * Downsample for the reflection's COLOUR chain, one pass per mip level: the
6
+ * Jimenez/COD 13-tap dual box — five weighted 2x2 averages, which rejects the
7
+ * nyquist ringing a plain box would leave in a blurred reflection.
8
+ */
9
+ export declare const MIRROR_DOWNSAMPLE_WGSL = "\n@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {\n let x = f32((vi & 1u) << 2u) - 1.0;\n let y = f32((vi & 2u) << 1u) - 1.0;\n return vec4f(x, y, 0.0, 1.0);\n}\n\n@group(0) @binding(0) var srcTex: texture_2d<f32>;\n@group(0) @binding(1) var srcSamp: sampler;\n\nfn samp(uv: vec2f, off: vec2f) -> vec3f {\n return textureSampleLevel(srcTex, srcSamp, uv + off, 0.0).rgb;\n}\n\n@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {\n let srcDims = vec2f(textureDimensions(srcTex));\n let t = 1.0 / srcDims;\n // fragCoord.xy reports pixel centers (e.g. 0.5,0.5 for first pixel) \u2014 divide by dst dims directly.\n let dstDims = srcDims * 0.5;\n let uv = p.xy / max(dstDims, vec2f(1.0));\n let A = samp(uv, t * vec2f(-2.0, -2.0));\n let B = samp(uv, t * vec2f( 0.0, -2.0));\n let C = samp(uv, t * vec2f( 2.0, -2.0));\n let D = samp(uv, t * vec2f(-1.0, -1.0));\n let E = samp(uv, t * vec2f( 1.0, -1.0));\n let F = samp(uv, t * vec2f(-2.0, 0.0));\n let G = samp(uv, t * vec2f( 0.0, 0.0));\n let H = samp(uv, t * vec2f( 2.0, 0.0));\n let I = samp(uv, t * vec2f(-1.0, 1.0));\n let J = samp(uv, t * vec2f( 1.0, 1.0));\n let K = samp(uv, t * vec2f(-2.0, 2.0));\n let L = samp(uv, t * vec2f( 0.0, 2.0));\n let M = samp(uv, t * vec2f( 2.0, 2.0));\n var o = (D + E + I + J) * (0.5 / 4.0);\n o = o + (A + B + G + F) * (0.125 / 4.0);\n o = o + (B + C + H + G) * (0.125 / 4.0);\n o = o + (F + G + L + K) * (0.125 / 4.0);\n o = o + (G + H + M + L) * (0.125 / 4.0);\n return vec4f(o, 1.0);\n}\n";
10
+ /**
11
+ * Downsample for the reflection's COVERAGE chain.
12
+ *
13
+ * Colour and coverage are a premultiplied PAIR, and a blur has to move them
14
+ * together. Blurring colour alone drags it toward zero wherever it spreads into
15
+ * the empty part of the reflection, while a coverage still sampled sharp says
16
+ * "fully covered" — so the reflection simply got darker as the dial came up.
17
+ *
18
+ * One bilinear tap at the destination texel centre averages the source's 2x2
19
+ * exactly, which is the box filter this wants and the cheapest way to write it.
20
+ */
21
+ export declare const MIRROR_MASK_DOWNSAMPLE_WGSL = "\n@group(0) @binding(0) var src: texture_2d<f32>;\n@group(0) @binding(1) var samp: sampler;\n\nstruct VSOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f, };\n\n@vertex fn vs(@builtin(vertex_index) i: u32) -> VSOut {\n var out: VSOut;\n let x = f32(i32(i / 2u) * 4 - 1);\n let y = f32(i32(i % 2u) * 4 - 1);\n out.pos = vec4f(x, y, 0.0, 1.0);\n out.uv = vec2f(x * 0.5 + 0.5, 0.5 - y * 0.5);\n return out;\n}\n\n@fragment fn fs(in: VSOut) -> @location(0) vec4f {\n return textureSampleLevel(src, samp, in.uv, 0.0);\n}\n";
4
22
  /**
5
23
  * The mirror in the SHADOW pass — the frame throwing shade on the floor.
6
24
  *
@@ -16,17 +34,5 @@ export declare function mirrorShaderWgsl(): string;
16
34
  * offset: the cascade matrices sit 64 bytes apart in one array, and a dynamic
17
35
  * uniform offset has to be 256-aligned.
18
36
  */
19
- /**
20
- * Downsample for the reflection's COVERAGE chain.
21
- *
22
- * Colour and coverage are a premultiplied PAIR, and a blur has to move them
23
- * together. Blurring colour alone drags it toward zero wherever it spreads into
24
- * the empty part of the reflection, while a coverage still sampled sharp says
25
- * "fully covered" — so the reflection simply got darker as the dial came up.
26
- *
27
- * One bilinear tap at the destination texel centre averages the source's 2x2
28
- * exactly, which is the box filter this wants and the cheapest way to write it.
29
- */
30
- export declare const MIRROR_MASK_DOWNSAMPLE_WGSL = "\n@group(0) @binding(0) var src: texture_2d<f32>;\n@group(0) @binding(1) var samp: sampler;\n\nstruct VSOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f, };\n\n@vertex fn vs(@builtin(vertex_index) i: u32) -> VSOut {\n var out: VSOut;\n let x = f32(i32(i / 2u) * 4 - 1);\n let y = f32(i32(i % 2u) * 4 - 1);\n out.pos = vec4f(x, y, 0.0, 1.0);\n out.uv = vec2f(x * 0.5 + 0.5, 0.5 - y * 0.5);\n return out;\n}\n\n@fragment fn fs(in: VSOut) -> @location(0) vec4f {\n return textureSampleLevel(src, samp, in.uv, 0.0);\n}\n";
31
37
  export declare function mirrorShadowWgsl(cascades: number): string;
32
38
  //# sourceMappingURL=mirror.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mirror.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/mirror.ts"],"names":[],"mappings":"AAqBA,8DAA8D;AAC9D,eAAO,MAAM,gBAAgB,QAAc,CAAA;AAE3C,wBAAgB,gBAAgB,IAAI,MAAM,CAiMzC;AAED;;;;;;;;;;;;;;GAcG;AACH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,2hBAkBvC,CAAA;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAuBzD"}
1
+ {"version":3,"file":"mirror.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/mirror.ts"],"names":[],"mappings":"AAqBA,8DAA8D;AAC9D,eAAO,MAAM,gBAAgB,QAAc,CAAA;AAE3C,wBAAgB,gBAAgB,IAAI,MAAM,CAiMzC;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,2hDAwClC,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,2hBAkBvC,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAuBzD"}
@@ -214,20 +214,51 @@ ${sceneIdPadWgsl("out")}
214
214
  `;
215
215
  }
216
216
  /**
217
- * The mirror in the SHADOW pass — the frame throwing shade on the floor.
218
- *
219
- * Its own shader because the mirror has no vertex buffer and no skeleton: the
220
- * quad is six generated vertices and the model matrix is the whole of it, while
221
- * the ordinary shadow shader is built for skinned geometry with an alpha test.
222
- *
223
- * Depth only, no fragment stage. The pane is opaque across its whole rectangle
224
- * — glass included, because this glass is not see-through — so there is nothing
225
- * to alpha-test and nothing to colour.
226
- *
227
- * The cascade index arrives in its own tiny uniform rather than as a dynamic
228
- * offset: the cascade matrices sit 64 bytes apart in one array, and a dynamic
229
- * uniform offset has to be 256-aligned.
217
+ * Downsample for the reflection's COLOUR chain, one pass per mip level: the
218
+ * Jimenez/COD 13-tap dual box — five weighted 2x2 averages, which rejects the
219
+ * nyquist ringing a plain box would leave in a blurred reflection.
230
220
  */
221
+ export const MIRROR_DOWNSAMPLE_WGSL = /* wgsl */ `
222
+ @vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
223
+ let x = f32((vi & 1u) << 2u) - 1.0;
224
+ let y = f32((vi & 2u) << 1u) - 1.0;
225
+ return vec4f(x, y, 0.0, 1.0);
226
+ }
227
+
228
+ @group(0) @binding(0) var srcTex: texture_2d<f32>;
229
+ @group(0) @binding(1) var srcSamp: sampler;
230
+
231
+ fn samp(uv: vec2f, off: vec2f) -> vec3f {
232
+ return textureSampleLevel(srcTex, srcSamp, uv + off, 0.0).rgb;
233
+ }
234
+
235
+ @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
236
+ let srcDims = vec2f(textureDimensions(srcTex));
237
+ let t = 1.0 / srcDims;
238
+ // fragCoord.xy reports pixel centers (e.g. 0.5,0.5 for first pixel) — divide by dst dims directly.
239
+ let dstDims = srcDims * 0.5;
240
+ let uv = p.xy / max(dstDims, vec2f(1.0));
241
+ let A = samp(uv, t * vec2f(-2.0, -2.0));
242
+ let B = samp(uv, t * vec2f( 0.0, -2.0));
243
+ let C = samp(uv, t * vec2f( 2.0, -2.0));
244
+ let D = samp(uv, t * vec2f(-1.0, -1.0));
245
+ let E = samp(uv, t * vec2f( 1.0, -1.0));
246
+ let F = samp(uv, t * vec2f(-2.0, 0.0));
247
+ let G = samp(uv, t * vec2f( 0.0, 0.0));
248
+ let H = samp(uv, t * vec2f( 2.0, 0.0));
249
+ let I = samp(uv, t * vec2f(-1.0, 1.0));
250
+ let J = samp(uv, t * vec2f( 1.0, 1.0));
251
+ let K = samp(uv, t * vec2f(-2.0, 2.0));
252
+ let L = samp(uv, t * vec2f( 0.0, 2.0));
253
+ let M = samp(uv, t * vec2f( 2.0, 2.0));
254
+ var o = (D + E + I + J) * (0.5 / 4.0);
255
+ o = o + (A + B + G + F) * (0.125 / 4.0);
256
+ o = o + (B + C + H + G) * (0.125 / 4.0);
257
+ o = o + (F + G + L + K) * (0.125 / 4.0);
258
+ o = o + (G + H + M + L) * (0.125 / 4.0);
259
+ return vec4f(o, 1.0);
260
+ }
261
+ `;
231
262
  /**
232
263
  * Downsample for the reflection's COVERAGE chain.
233
264
  *
@@ -258,6 +289,21 @@ struct VSOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f, };
258
289
  return textureSampleLevel(src, samp, in.uv, 0.0);
259
290
  }
260
291
  `;
292
+ /**
293
+ * The mirror in the SHADOW pass — the frame throwing shade on the floor.
294
+ *
295
+ * Its own shader because the mirror has no vertex buffer and no skeleton: the
296
+ * quad is six generated vertices and the model matrix is the whole of it, while
297
+ * the ordinary shadow shader is built for skinned geometry with an alpha test.
298
+ *
299
+ * Depth only, no fragment stage. The pane is opaque across its whole rectangle
300
+ * — glass included, because this glass is not see-through — so there is nothing
301
+ * to alpha-test and nothing to colour.
302
+ *
303
+ * The cascade index arrives in its own tiny uniform rather than as a dynamic
304
+ * offset: the cascade matrices sit 64 bytes apart in one array, and a dynamic
305
+ * uniform offset has to be 256-aligned.
306
+ */
261
307
  export function mirrorShadowWgsl(cascades) {
262
308
  return /* wgsl */ `
263
309
  struct LightVP { viewProj: array<mat4x4f, ${cascades}>, };
@@ -1 +1 @@
1
- {"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/outline.ts"],"names":[],"mappings":"AAqBA;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAE5C,wBAAgB,iBAAiB,IAAI,MAAM,CAoI1C"}
1
+ {"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/outline.ts"],"names":[],"mappings":"AAqBA;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAE5C,wBAAgB,iBAAiB,IAAI,MAAM,CA6L1C"}
@@ -69,14 +69,39 @@ struct VertexOutput {
69
69
  /** The triangle's own threshold, flat — see the material VertexOutput. The
70
70
  * hull traces the body's faces, so it has to lose the same ones. */
71
71
  @location(2) @interpolate(flat) faceT: f32,
72
+ /** How much of a pixel-wide line this one really is (1 = all of it): a line
73
+ * held at the minimum width fades by what it was short. */
74
+ @location(3) coverage: f32,
72
75
  };
73
76
 
77
+ /**
78
+ * Half the frame height (engine units, at the subject) below which a line keeps
79
+ * its constant screen width; wider than that it thins as a world-size line.
80
+ * 12.5 = a frame 25 units tall — a full figure (an MMD model is ~20) with
81
+ * room around her.
82
+ *
83
+ * Not the game's own break (w = 1 game unit = 8 engine units). The game's
84
+ * near width is ~3× MMD's 2·edgeSize px; its world-size line comes down to
85
+ * MMD's width only at a full-figure framing, so that is where the break goes:
86
+ * close-ups and full figures keep today's width, and past that the game's rule
87
+ * holds — NDC offset ∝ 1/w, scaled by the zoom as its projection._m11 scales
88
+ * it. At the game's 8 the demo's own camera (33 units) drew a quarter of
89
+ * today's width, which put every line on the faded minimum.
90
+ */
91
+ const RZ_OUTLINE_FULL_FIGURE = 12.5;
92
+ /** The thinnest a line is drawn, in pixels at 1080p (and never under this many
93
+ * device pixels). A line due to be thinner is drawn this wide and faded by
94
+ * the shortfall, so far lines thin out without aliasing into dashes. */
95
+ const RZ_OUTLINE_MIN_PX = 1.5;
96
+
74
97
  @vertex fn vs(
75
98
  @location(0) position: vec3f,
76
99
  @location(1) normal: vec3f,
77
100
  @location(2) uv: vec2f,
78
101
  @location(3) joints0: vec4<u32>,
79
- @location(4) weights0: vec4<f32>
102
+ @location(4) weights0: vec4<f32>,
103
+ /** xyz: the smoothed rest normal (outline-normals.ts), w: PMX edge scale. */
104
+ @location(5) outlineNormal: vec4f
80
105
  ) -> VertexOutput {
81
106
  var output: VertexOutput;
82
107
  let pos4 = vec4f(position, 1.0);
@@ -93,30 +118,58 @@ struct VertexOutput {
93
118
  let m = skinMats[j];
94
119
  skinnedPos += (m * pos4) * w;
95
120
  let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
96
- skinnedNrm += (r3 * normal) * w;
121
+ skinnedNrm += (r3 * outlineNormal.xyz) * w;
97
122
  }
98
123
  let worldPos = skinnedPos.xyz;
99
124
  let worldNormal = normalize(skinnedNrm);
100
125
 
101
126
  let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
102
127
 
103
- // babylon-mmd: screenNormal = normalize((view * worldNormal).xy)
128
+ // The push direction is the view-space normal's XY, NOT renormalized — the
129
+ // game's: full length at the silhouette, where the normal lies across the
130
+ // view, and shrinking on surfaces turned toward the camera, so creases
131
+ // facing her draw finer than the rim. Skinned exactly as the colour pass
132
+ // skins the normal, from the smoothed rest normal, so every vertex sharing a
133
+ // position pushes the same way and the hull stays closed over hard edges
134
+ // and UV seams.
104
135
  let viewNormal = (camera.view * vec4f(worldNormal, 0.0)).xyz;
105
- let snLen = length(viewNormal.xy);
106
- let screenNormal = select(vec2f(0.0, 0.0), viewNormal.xy / snLen, snLen > 1e-5);
107
136
 
108
137
  // Reference-height normalization (babylon-mmd ships this variant commented
109
- // out as \`renderHeight = 1080\`): thickness is a constant FRACTION of the
110
- // frame — 2·edgeSize px at 1080p — so retina DPR and 4K export don't thin
111
- // the rims to sub-pixel. Width follows the projection aspect.
138
+ // out as \`renderHeight = 1080\`): the near width is a constant FRACTION of
139
+ // the frame — 2·edgeSize px at 1080p — so retina DPR and 4K export don't
140
+ // thin the rims to sub-pixel. Width follows the projection aspect.
112
141
  // projection[1][1]/projection[0][0] = width/height for a symmetric frustum.
113
142
  let aspect = camera.projection[1][1] / camera.projection[0][0];
114
- let viewport = vec2f(1080.0 * aspect, 1080.0);
143
+ let refViewport = vec2f(1080.0 * aspect, 1080.0);
144
+ let deviceViewport = vec2f(camera.viewportHeight * aspect, camera.viewportHeight);
145
+
146
+ // The game's width (Character/Debug.shader, pass "Outline"): the clip offset
147
+ // is the near offset × min(w, break depth). Up close the ×w cancels the
148
+ // perspective divide — constant screen width; past the break it stops
149
+ // growing, so the NDC offset falls as 1/w like a line of fixed world width.
150
+ // The break is the depth at which the frame is a full figure tall, so a
151
+ // zoomed lens moves it out (see RZ_OUTLINE_FULL_FIGURE). Under an
152
+ // orthographic camera w is 1 and the same min() makes the line world-size
153
+ // once the frame is taller than a figure. The PMX per-vertex edge scale
154
+ // multiplies it all, as vertex colour alpha does in the game.
155
+ let w = max(clipPos.w, 1e-4);
156
+ let breakDepth = RZ_OUTLINE_FULL_FIGURE * camera.projection[1][1];
157
+ let nearNdc = viewNormal.xy * (material.edgeSize * outlineNormal.w * 4.0 / refViewport);
158
+ var ndc = nearNdc * (min(w, breakDepth) / w);
115
159
 
116
- // NDC offset = edgeSize · 4/viewport, ×w so the perspective divide cancels:
117
- // constant screen thickness at any distance (babylon-mmd parity).
118
- let offset = screenNormal * (material.edgeSize * 4.0 / viewport) * clipPos.w;
119
- output.position = vec4f(clipPos.xy + offset, clipPos.z, clipPos.w);
160
+ // Antialiased minimum (the game's _OutlineAntialias): held at the minimum
161
+ // width and faded by the shortfall, never thinner. The game fades by
162
+ // sqrt(coverage.x · coverage.y), per axis; for one width that is the ratio
163
+ // itself.
164
+ let px = length(ndc * deviceViewport * 0.5);
165
+ let minPx = RZ_OUTLINE_MIN_PX * max(1.0, camera.viewportHeight / 1080.0);
166
+ var coverage = 1.0;
167
+ if (px < minPx) {
168
+ coverage = px / minPx;
169
+ ndc = select(vec2f(0.0), ndc * (minPx / px), px > 1e-6);
170
+ }
171
+ output.coverage = coverage;
172
+ output.position = vec4f(clipPos.xy + ndc * w, clipPos.z, clipPos.w);
120
173
  output.uv = uv;
121
174
  output.restPos = position;
122
175
  output.faceT = rz_dissolve_threshold(position);
@@ -151,7 +204,11 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
151
204
  discard;
152
205
  }
153
206
  var out: FSOut;
154
- out.color = vec4f(material.edgeColor.rgb, material.edgeColor.a * texA);
207
+ // Blended, not alpha-to-coverage: this pass already blends (the texture
208
+ // alpha above), it is drawn right after the surface it traces so what lies
209
+ // under a faded line is already there, and 4× MSAA would quantize a fade to
210
+ // four steps.
211
+ out.color = vec4f(material.edgeColor.rgb, material.edgeColor.a * texA * input.coverage);
155
212
  out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
156
213
  ${sceneIdPadWgsl("out")} return out;
157
214
  }
@@ -63,7 +63,7 @@ type ParticleSource = {
63
63
  * N > 0 — see texture-api.ts. Omitted = 0. */
64
64
  textures?: number;
65
65
  };
66
- /** Where the shading stage's five scene-light bindings start — see scene-light-api.ts. */
66
+ /** Where the shading stage's four scene-light bindings start — see scene-light-api.ts. */
67
67
  export declare const PARTICLE_LIGHT_BINDING = 10;
68
68
  /** The effect's named points (`#points`), in both stages — see points-api.ts. */
69
69
  export declare const PARTICLE_POINTS_BINDING = 15;
@@ -43,7 +43,7 @@ fn _rzCastLive() -> i32 {
43
43
  trailSlotsApi(cast.trailCount) +
44
44
  anchorAliasWgsl(cast.alias));
45
45
  }
46
- /** Where the shading stage's five scene-light bindings start — see scene-light-api.ts. */
46
+ /** Where the shading stage's four scene-light bindings start — see scene-light-api.ts. */
47
47
  export const PARTICLE_LIGHT_BINDING = 10;
48
48
  /** The effect's named points (`#points`), in both stages — see points-api.ts. */
49
49
  export const PARTICLE_POINTS_BINDING = 15;
@@ -8,16 +8,25 @@
8
8
  */
9
9
  export declare const worldAmbientWgsl: (u: string) => string;
10
10
  /**
11
- * 3×3 PCF over one cascade's map, for a point already in that cascade's NDC.
11
+ * The sun's shadow atlas, as every reader looks it up: `<p>Locate(q)` finds
12
+ * world point q in the first cascade whose box holds it and answers
13
+ * (atlas uv, depth, cascade) — cascade -1 outside them all — and `<p>Taps(a)`
14
+ * is the 3×3 PCF at that place.
12
15
  *
13
- * Shared with the materials, so an effect's shadow and her own shadow soften
14
- * alike. The sampler is linear-filtered, which makes each tap a 2×2 bilinear
15
- * compare. Unrolled — Safari's Metal backend doesn't unroll nested shadow
16
- * loops reliably. The body expects `ndc` in scope and returns.
16
+ * ONE implementation for the materials, the ground and effects, so a blade of
17
+ * grass and her own skin go dark at the same edge. `vp` names the cascades'
18
+ * view-projection array in the module that includes it.
19
+ *
20
+ * The box test keeps a margin (SHADOW_MARGIN of the tile's half-width) so the
21
+ * whole filter stays inside the tile: a tap that crossed into the neighbouring
22
+ * tile would compare against another cascade's depths. The sampler is linear,
23
+ * so each tap is itself a 2×2 compare. Unrolled — Safari's Metal backend
24
+ * doesn't unroll nested shadow loops reliably.
17
25
  */
18
- export declare const pcf9: (map: string, ts: string, sampler?: string) => string;
26
+ export declare const SHADOW_MARGIN = 0.98;
27
+ export declare const sunShadowWgsl: (p: string, map: string, sampler: string, vp: string) => string;
19
28
  /** Bindings the real accessors take, from `binding` upward. */
20
- export declare const SCENE_LIGHT_API_BINDINGS = 5;
29
+ export declare const SCENE_LIGHT_API_BINDINGS = 4;
21
30
  /**
22
31
  * `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
23
32
  * caster's shadow, soft in between.
@@ -25,12 +34,11 @@ export declare const SCENE_LIGHT_API_BINDINGS = 5;
25
34
  * `rzWorldAmbient(n)`: the world's light arriving at a surface facing `n` —
26
35
  * the materials' own ambient, so an effect's shade can match hers.
27
36
  *
28
- * With `on`, declares five bindings from `binding`:
37
+ * With `on`, declares four bindings from `binding`:
29
38
  * +0 the light uniform (ambient, the sun — direction, and in .w the shadow
30
39
  * switch — and the HDRI's irradiance)
31
40
  * +1 the cascade view-projections, then the caster sphere
32
- * +2 the near cascade's map +3 the far cascade's map
33
- * +4 the comparison sampler
41
+ * +2 the sun's shadow atlas +3 the comparison sampler
34
42
  *
35
43
  * It takes a POINT and no normal. The materials' sampleShadow also rejects a
36
44
  * surface facing away from the sun, which is right for skin and wrong for a
@@ -1 +1 @@
1
- {"version":3,"file":"scene-light-api.d.ts","sourceRoot":"","sources":["../../src/shaders/scene-light-api.ts"],"names":[],"mappings":"AAkBA;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,GAAG,MAAM,WAwBzC,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,GAAI,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,gBAAyB,WActE,CAAA;AAED,+DAA+D;AAC/D,eAAO,MAAM,wBAAwB,IAAI,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA2DjF"}
1
+ {"version":3,"file":"scene-light-api.d.ts","sourceRoot":"","sources":["../../src/shaders/scene-light-api.ts"],"names":[],"mappings":"AAkBA;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,GAAG,MAAM,WAwBzC,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,OAAO,CAAA;AAEjC,eAAO,MAAM,aAAa,GAAI,GAAG,MAAM,EAAE,KAAK,MAAM,EAAE,SAAS,MAAM,EAAE,IAAI,MAAM,WAkChF,CAAA;AAED,+DAA+D;AAC/D,eAAO,MAAM,wBAAwB,IAAI,CAAA;AAEzC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA2CjF"}