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,7 +1,7 @@
1
1
  import { sceneFsOutWgsl } from "../passes/scene-contract"
2
2
  import { lightsApi } from "../lights"
3
3
  import { SHADOW_CASCADES } from "../../shadow-cascades"
4
- import { pcf9 } from "../scene-light-api"
4
+ import { sunShadowWgsl } from "../scene-light-api"
5
5
  import { WORLD_AMBIENT_WGSL } from "../lights"
6
6
 
7
7
  // Shared WGSL blocks concatenated by every material shader.
@@ -61,6 +61,9 @@ struct LightUniforms {
61
61
  * amount 0 is off. Then the view-projection its map was drawn with. */
62
62
  castShadow: vec4f,
63
63
  castVP: mat4x4f,
64
+ /** The rendering layers each directional light reaches, slot by slot — a
65
+ * draw is lit by the first whose bits it shares (rzMainLight). */
66
+ dirLayers: vec4u,
64
67
  };
65
68
 
66
69
  // Per-material uniforms. Every material binds this layout even if it ignores fields;
@@ -148,13 +151,10 @@ struct LightVP { viewProj: array<mat4x4f, ${SHADOW_CASCADES.length}>, };
148
151
  @group(0) @binding(0) var<uniform> camera: CameraUniforms;
149
152
  @group(0) @binding(1) var<uniform> light: LightUniforms;
150
153
  @group(0) @binding(2) var diffuseSampler: sampler;
151
- @group(0) @binding(3) var shadowMap: texture_depth_2d;
154
+ // The sun's shadow atlas: every cascade in its own tile — see shadow-cascades.ts.
155
+ @group(0) @binding(3) var shadowAtlas: texture_depth_2d;
152
156
  @group(0) @binding(4) var shadowSampler: sampler_comparison;
153
157
  @group(0) @binding(5) var<uniform> lightVP: LightVP;
154
- // The far cascade's map — coarser texels over a much wider box, so the stage
155
- // keeps its shadows when the crisp near volume ends. Binding 7: 6 is the
156
- // positional lights, 9 the BRDF LUT.
157
- @group(0) @binding(7) var shadowMapFar: texture_depth_2d;
158
158
  // The world's own sky, for a surface that REFLECTS it rather than merely being
159
159
  // lit by it. Equirect, in the same layout and the same linear radiance the
160
160
  // composite draws the dome from, and mipped — a rough surface reads a coarser
@@ -188,14 +188,39 @@ fn _rzCastShadow(wp: vec3f) -> f32 {
188
188
  }
189
189
  // binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).
190
190
  @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
191
- // The light this model takes apart from the scene's — see Engine.setModelFill
192
- // and Engine.setModelSun. fill brightens it after its graph (fill ×
193
- // surface colour, graph/slots.ts), zero for a model nobody gave one; sun replaces the scene's sun colour while its w is set,
194
- // which is how a stage keeps the daylight its game lit it by while the cast
195
- // keeps the key the scene set for them. opts.x (Engine.setModelFlatSky) gives
196
- // it the sky's average in place of its shape - see graph/slots.ts.
197
- struct ModelLight { fill: vec4f, sun: vec4f, opts: vec4f }
198
- @group(1) @binding(1) var<uniform> modelLight: ModelLight;
191
+ // What this object's lighting is, apart from the scene's, as the game's
192
+ // pipeline gives each renderer its own:
193
+ // layers.x its rendering-layer bits, which decide the lights that reach it —
194
+ // a stage's daylight and the cast's key are two lights on two
195
+ // layers, as the game lights them;
196
+ // ambient.x 1 while it has an ambient of its own (Engine.setModelAmbient),
197
+ // 0 for the world's;
198
+ // sh that ambient's irradiance, folded as the world's (ibl.ts);
199
+ // fill.rgb its fill (Engine.setModelFill), added after its graph times its
200
+ // surface colour — zero for a model that has none.
201
+ struct ObjectLight { layers: vec4u, ambient: vec4f, sh: array<vec4f, 9>, fill: vec4f }
202
+ @group(1) @binding(1) var<uniform> objectLight: ObjectLight;
203
+
204
+ /** The light arriving at a surface of this object facing n: its own ambient
205
+ * where loaded data gave it one (a stage's probes, a character's), else the
206
+ * world's. The same polynomial rzWorldAmbient evaluates. */
207
+ fn rzObjectAmbient(n: vec3f) -> vec3f {
208
+ if (objectLight.ambient.x < 0.5) { return rzWorldAmbient(n); }
209
+ let s = objectLight.sh;
210
+ let c = s[0].xyz + s[1].xyz * n.y + s[2].xyz * n.z + s[3].xyz * n.x
211
+ + s[4].xyz * (n.x * n.y) + s[5].xyz * (n.y * n.z) + s[6].xyz * (3.0 * n.z * n.z - 1.0)
212
+ + s[7].xyz * (n.x * n.z) + s[8].xyz * (n.x * n.x - n.y * n.y);
213
+ return max(c, vec3f(0.0));
214
+ }
215
+
216
+ /** The directional light slot that lights this draw: the first whose layers
217
+ * it shares, else the sun's. */
218
+ fn rzMainLight() -> u32 {
219
+ for (var k = 0u; k < 4u; k++) {
220
+ if ((light.dirLayers[k] & objectLight.layers.x) != 0u) { return k; }
221
+ }
222
+ return 0u;
223
+ }
199
224
  @group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
200
225
  @group(2) @binding(1) var<uniform> material: MaterialUniforms;
201
226
  // Reserved for future sphere/toon graph nodes; graphs that don't read them get the
@@ -226,58 +251,41 @@ fn safe_normal(nIn: vec3f) -> vec3f {
226
251
  `;
227
252
 
228
253
  // ─── Shadow sampler (3×3 PCF, cascade-selected) ─────────────────────
229
- // Normal-bias 0.08, depth-bias 0.001 NDC; the kernel is pcf9 in scene-light-api.ts,
230
- // shared with effects. Texel sizes are interpolated
231
- // from SHADOW_CASCADES so they cannot go stale the way the hardcoded 1/2048
232
- // once did (PCF taps landed TWO texels apart after the map grew to 4096,
233
- // quantizing self-shadow edges into crawling gray squares).
254
+ // Normal-bias 0.08, depth-bias 0.001 NDC; the lookup is sunShadowWgsl in
255
+ // scene-light-api.ts, shared with the ground and effects.
234
256
  //
235
- // Selection: the crisp near cascade wherever its box covers the point — with a
236
- // margin that keeps the whole PCF kernel inside the map, so selection never
237
- // mixes maps mid-kernel — else the far cascade, else LIT. Lit is a fix, not
238
- // merely a default: the old single-volume path clamped, and past the light's
239
- // far plane the comparison failed against every stored depth, silently
240
- // shadowing any stage deeper than the box.
241
-
257
+ // Selection: the first cascade whose box covers the point — with a margin that
258
+ // keeps the whole PCF kernel inside its tile — else LIT. Lit is a fix, not
259
+ // merely a default: past every cascade there is no occlusion information, and
260
+ // comparing against unrelated depths would silently shadow any stage deeper
261
+ // than the boxes.
242
262
 
243
263
  const SAMPLE_SHADOW_WGSL = /* wgsl */ `
264
+ ${sunShadowWgsl("_rzSun", "shadowAtlas", "shadowSampler", "lightVP.viewProj")}
244
265
 
245
- fn sampleShadowNear(ndc: vec3f) -> f32 {
246
- ${pcf9("shadowMap", `1.0 / ${SHADOW_CASCADES[0].mapSize}.0`)}
247
- }
248
-
249
- fn sampleShadowFar(ndc: vec3f) -> f32 {
250
- ${pcf9("shadowMapFar", `1.0 / ${SHADOW_CASCADES[SHADOW_CASCADES.length - 1].mapSize}.0`)}
266
+ /** The sun's cast shadow alone, without sampleShadow's facing test: what a
267
+ * character's ramp is min'd with (its own N·L carries the terminator). */
268
+ fn rzSunOcclusion(worldPos: vec3f, n: vec3f) -> f32 {
269
+ let castAmt = light.lights[0].direction.w;
270
+ if (castAmt <= 0.0) { return 1.0; }
271
+ let a = _rzSunLocate(worldPos + n * 0.08);
272
+ if (a.w < 0.0) { return 1.0; }
273
+ return mix(1.0, _rzSunTaps(a), castAmt);
251
274
  }
252
275
 
253
276
  fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
254
277
  // THE SCENE'S ONE SHADOW SWITCH, and it lives on the sun because the sun is
255
- // the only thing that casts. Positional lamps are diffuse-only — rzLightsDiffuse
256
- // has no shadow term at all — so turning this off turns off every shadow there
257
- // is, on the ground and on the cast alike. Gated here rather than by not
258
- // rendering the map: the map is also what the ground catcher reads, and a host
259
- // that wants a figure lit with no shadow still wants everything else about the
260
- // frame unchanged.
261
- //
262
- // Lit is the answer when it is off. Returning 0 would be "fully shadowed",
263
- // which is the opposite of what a disabled shadow means.
278
+ // the only thing that casts. Lit is the answer when it is off: returning 0
279
+ // would be "fully shadowed", the opposite of what a disabled shadow means.
264
280
  let castAmt = light.lights[0].direction.w;
265
281
  if (castAmt <= 0.0) { return 1.0; }
266
- if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
267
- let biasedPos = worldPos + n * 0.08;
268
- let c0 = lightVP.viewProj[0] * vec4f(biasedPos, 1.0);
269
- let n0 = c0.xyz / max(c0.w, 1e-6);
270
- if (all(abs(n0.xy) < vec2f(0.98)) && n0.z > 0.0 && n0.z < 1.0) {
271
- return mix(1.0, sampleShadowNear(n0), castAmt);
272
- }
273
- let c1 = lightVP.viewProj[1] * vec4f(biasedPos, 1.0);
274
- let n1 = c1.xyz / max(c1.w, 1e-6);
275
- if (all(abs(n1.xy) < vec2f(0.98)) && n1.z > 0.0 && n1.z < 1.0) {
276
- return mix(1.0, sampleShadowFar(n1), castAmt);
277
- }
278
- // Outside every cascade there is no occlusion information; lit is the only
279
- // honest answer.
280
- return 1.0;
282
+ // A surface turned from the sun is in its own shade — when the sun is what
283
+ // keys it. Under a key of its own layer the atlas is only the occluders'
284
+ // shadow, and the key's N·L decides its terminator.
285
+ if (rzMainLight() == 0u && dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
286
+ let a = _rzSunLocate(worldPos + n * 0.08);
287
+ if (a.w < 0.0) { return 1.0; }
288
+ return mix(1.0, _rzSunTaps(a), castAmt);
281
289
  }
282
290
 
283
291
  `;
@@ -499,4 +507,4 @@ fn rz_dissolve_threshold(restPos: vec3f) -> f32 {
499
507
  // The FSOut struct is NOT in here any more — see commonFsOutWgsl above. Every
500
508
  // consumer of this constant appends it immediately, which is where it was.
501
509
  export const COMMON_MATERIAL_PRELUDE_WGSL =
502
- COMMON_BINDINGS_WGSL + WORLD_AMBIENT_WGSL + lightsApi(0, 6) + SAMPLE_SHADOW_WGSL + COMMON_VS_WGSL
510
+ COMMON_BINDINGS_WGSL + WORLD_AMBIENT_WGSL + lightsApi(0, 6, "objectLight.layers.x") + SAMPLE_SHADOW_WGSL + COMMON_VS_WGSL
@@ -348,6 +348,215 @@ fn rzWorldSpecularLod(dir: vec3f, roughness: f32, unity: f32) -> vec3f {
348
348
  return textureSampleLevel(worldEnvTexture, diffuseSampler, vec2f(u, v), lod).rgb * light.ambientColor.w;
349
349
  }
350
350
 
351
+ // ── Aether Gazer's character shading, step by step ──
352
+ // From the decompiled ForwardBase pass of SimPipeline/Character/Debug. Its
353
+ // spaces are the game's: a view space looking down -Z (Unity's camera space),
354
+ // which the engine's +Z-forward view is turned into by negating z.
355
+
356
+ /** CharacterEffect's key light, in sim_TowardMatrix's frame: the camera's
357
+ * backward direction flattened to the ground (d), up, and up × d. The
358
+ * direction points toward the light. */
359
+ fn ag_key_light(inclination: f32, azimuth: f32) -> vec3f {
360
+ let f = vec3f(camera.view[0].z, camera.view[1].z, camera.view[2].z);
361
+ let flat = vec3f(-f.x, 0.0, -f.z);
362
+ let len = length(flat);
363
+ let d = select(vec3f(0.0, 0.0, 1.0), flat / max(len, 1e-5), len > 1e-5);
364
+ let incl = radians(inclination);
365
+ let az = radians(-azimuth);
366
+ let c = cos(incl);
367
+ let k = vec3f(sin(az) * -c, sin(incl), cos(az) * c);
368
+ return normalize(vec3f(d.z, 0.0, -d.x) * k.x + vec3f(0.0, 1.0, 0.0) * k.y + d * k.z);
369
+ }
370
+
371
+ /** u on the shadow ramp: N·L, darkened by the mask's occlusion
372
+ * (1 - min((1 - ao) * scale, 1)), and min'd with the sun's cast shadow. */
373
+ fn ag_shade_term(n: vec3f, l: vec3f, ao: f32, aoScale: f32, receive: f32, wp: vec3f) -> f32 {
374
+ let ndl = max(dot(n, l), 0.0);
375
+ var t = (1.0 - min((1.0 - ao) * aoScale, 1.0)) * ndl;
376
+ if (receive > 0.5) { t = min(t, rzSunOcclusion(wp, n)); }
377
+ return t;
378
+ }
379
+
380
+ /** A ramp image's uv, kept off the image's edge texels so a repeating sampler
381
+ * does not blend the far end in. The row is Unity's v, which runs up from the
382
+ * image's bottom row; the image is uploaded top row first. */
383
+ fn ag_ramp_uv(value: f32, row: f32) -> vec3f {
384
+ return vec3f(clamp(value, 0.002, 0.998), clamp(1.0 - row, 0.002, 0.998), 0.0);
385
+ }
386
+
387
+ /** The view-space reflection, as the game builds it: the view ray mirrored on
388
+ * the normal, into camera space, halved and pushed half a unit toward the
389
+ * eye, normalised. xy*0.5+0.5 is the sphere image's uv (xyz here, z unused),
390
+ * and z how much it faces the eye (w). */
391
+ fn ag_view_reflection(n: vec3f, v: vec3f) -> vec4f {
392
+ let r = reflect(-v, n);
393
+ let rv = vec3f(
394
+ camera.view[0].x * r.x + camera.view[1].x * r.y + camera.view[2].x * r.z,
395
+ camera.view[0].y * r.x + camera.view[1].y * r.y + camera.view[2].y * r.z,
396
+ -(camera.view[0].z * r.x + camera.view[1].z * r.y + camera.view[2].z * r.z),
397
+ );
398
+ let m = normalize(rv * 0.5 + vec3f(0.0, 0.0, 0.5));
399
+ // A sphere image is addressed as Unity reads it, v up: flipped for the
400
+ // top-row-first upload.
401
+ return vec4f(m.x * 0.5 + 0.5, 0.5 - m.y * 0.5, 0.0, m.z);
402
+ }
403
+
404
+ /** The matcap's lift: max(mask * (matcap * strength - 1) + 1, 1). */
405
+ fn ag_matcap(color: vec3f, matcap: vec3f, strength: f32, mask: f32) -> vec3f {
406
+ return color * max(mask * (matcap * strength - vec3f(1.0)) + vec3f(1.0), vec3f(1.0));
407
+ }
408
+
409
+ /** The rim: smoothstep'd to the silhouette by facing, limited to the two rim
410
+ * directions (CharacterEffect: (cos a·s, sin a·s, z), a = azimuth·π,
411
+ * s = sqrt(1 - z²)), laid on as a colour dodge: min(max(c, 2), c / (1 - 2·rim)). */
412
+ fn ag_rim(color: vec3f, facing: f32, uvw: vec3f, rimColor: vec3f, threshold: f32, fade: f32, range: f32,
413
+ inclination: f32, azimuth1: f32, azimuth2: f32, mask: f32) -> vec3f {
414
+ let m = vec3f(uvw.x * 2.0 - 1.0, 1.0 - uvw.y * 2.0, facing);
415
+ let t = saturate((facing - (threshold + fade)) / (threshold - (threshold + fade)));
416
+ let edge = t * t * (3.0 - 2.0 * t);
417
+ let s = sqrt(max(1.0 - inclination * inclination, 0.0));
418
+ let a1 = azimuth1 * 3.1415927;
419
+ let a2 = azimuth2 * 3.1415927;
420
+ let d1 = vec3f(cos(a1) * s, sin(a1) * s, inclination);
421
+ let d2 = vec3f(cos(a2) * s, sin(a2) * s, inclination);
422
+ let along = saturate(max(dot(m, d1), dot(m, d2)) + range);
423
+ let rim = min(edge, along) * rimColor;
424
+ let f = min(rim * mask * 2.0, vec3f(0.99));
425
+ return min(max(color, vec3f(2.0)), color / (vec3f(1.0) - f));
426
+ }
427
+
428
+ /** The fill: inner to outer by pow(1 - facing², softness + 2), laid over by
429
+ * its amount. */
430
+ fn ag_fill(color: vec3f, facing: f32, inner: vec3f, outer: vec3f, amount: f32, softness: f32) -> vec3f {
431
+ let k = pow(max(1.0 - facing * facing, 0.0), softness + 2.0);
432
+ return mix(color, mix(inner, outer, k), amount);
433
+ }
434
+
435
+ // ── Aether Gazer's PBR character shading (SimPipeline/Character/PBR/Uber) ──
436
+ // The ForwardBase pass of the game's current character shader, decompiled and
437
+ // written back out step by step. Its ramp image carries the whole tone of the
438
+ // look in rows: the diffuse ramp in the row the material picks, the specular
439
+ // ramp at 0.95, the environment's floor at 0.85.
440
+
441
+ /** A ramp image on a group slot, read at a computed coordinate — mip 0 only,
442
+ * as the game reads it, and so legal wherever the shading branches. */
443
+ fn _ag_ramp(slot: u32, uv: vec2f) -> vec4f {
444
+ // Unity's v runs up from the bottom row; the image is uploaded top row first.
445
+ let c = clamp(vec2f(uv.x, 1.0 - uv.y), vec2f(0.002), vec2f(0.998));
446
+ switch slot {
447
+ case 1u: { return textureSampleLevel(groupTexture1, diffuseSampler, c, 0.0); }
448
+ case 2u: { return textureSampleLevel(groupTexture2, diffuseSampler, c, 0.0); }
449
+ case 3u: { return textureSampleLevel(groupTexture3, diffuseSampler, c, 0.0); }
450
+ default: { return textureSampleLevel(groupTexture0, diffuseSampler, c, 0.0); }
451
+ }
452
+ }
453
+
454
+ /**
455
+ * The whole lighting of one Uber material under the character's key light:
456
+ * diffuse = base·(1−metal) · ramp(min(shadow, occlusion·N·L), row)
457
+ * specular = GGX through the ramp's 0.95 row, × F0, plus the rim, × the
458
+ * shade term
459
+ * ambient = the object's SH at n × diffuse
460
+ * reflection = the scene's environment along the reflected view, at a
461
+ * roughness mip, × the mobile environment-BRDF fit
462
+ * (F0·A + ramp(N·L·B, 0.85))
463
+ * Lamps and fog are the engine's, laid on after.
464
+ */
465
+ fn ag_uber(slot: u32, base: vec3f, metallic: f32, roughness: f32, occlusion: f32, n: vec3f, l: vec3f,
466
+ row: f32, receive: f32, rimMask: f32, rimMid: f32, rimWidth: f32, rimTint: vec3f,
467
+ rimIntensity: f32, rimAlbedo: f32, rimInLight: f32, emissive: vec3f, reflection: f32,
468
+ shade: f32, face: f32, wp: vec3f, v: vec3f) -> vec3f {
469
+ let diffuse = base * (1.0 - metallic);
470
+ let f0 = mix(vec3f(0.04), base, metallic);
471
+ let pr = clamp(roughness, 0.0, 1.0);
472
+ let r = max(pr * pr, 0.0078125);
473
+ let a2 = r * r;
474
+ let h = normalize(v + l);
475
+ let ndl = saturate(dot(n, l));
476
+ let ndv = saturate(dot(v, n));
477
+ let ndh = saturate(dot(n, h));
478
+ let vdh = saturate(dot(v, h));
479
+ let ldh = saturate(dot(l, h));
480
+
481
+ // where on the ramp: N·L under occlusion, and the sun's cast shadow
482
+ var shadow = 1.0;
483
+ if (receive > 0.5) { shadow = rzSunOcclusion(wp, n); }
484
+ // A face replaces N·L with its SDF shade; anything else reads N·L.
485
+ let u = min(shadow, occlusion * select(ndl, shade, shade >= 0.0));
486
+ let ramp = _ag_ramp(slot, vec2f(u, row)).rgb;
487
+
488
+ // GGX, as the pass writes it, then through the ramp's specular row
489
+ let dd = ndh * ndh * (a2 - 1.0) + 1.0;
490
+ let D = select(a2 / (dd * dd), 1.0, dd * dd == a2);
491
+ let vis = min(0.5 / (ndl * (ndv * (1.0 - r) + r) + ndv * (ndl * (1.0 - r) + r)), 1.0);
492
+ let vis2 = min(0.5 / (ldh * (vdh * (1.0 - r) + r) + vdh * (ldh * (1.0 - r) + r)), 1.0);
493
+ let x = saturate(D * vis * a2 / vis2);
494
+ let specRamp = _ag_ramp(slot, vec2f(x, 0.95)).rgb;
495
+ let spec = clamp(vis * specRamp * min(1.0 / a2, 2048.0), vec3f(0.0), vec3f(10.0));
496
+
497
+ // the rim: toward the silhouette on the lit side, tinted toward the albedo
498
+ let lit = select(0.0, 1.0, u > 0.001);
499
+ let t = saturate(((ndl * 0.25 + 0.75) * (1.0 - ndv) * lit - (rimMid - rimWidth)) / max(2.0 * rimWidth, 1e-4));
500
+ let edge = t * t * (3.0 - 2.0 * t);
501
+ let tint = min(rimTint * rimIntensity * mix(vec3f(1.0), base, rimAlbedo) * 2.0 * rimMask, vec3f(0.99));
502
+ let rim = tint * edge * (1.0 + rimInLight * (u - 1.0));
503
+
504
+ // A face (FACE_MODE) takes no GGX: its light is the ramp, the rim and its
505
+ // optional SDF highlight (off unless _FaceSpecular).
506
+ let isFace = face > 0.5;
507
+ let direct = (select(spec * f0, vec3f(0.0), isFace) + rim) * u + diffuse * ramp;
508
+
509
+ // the environment, with the game's fit of the split-sum
510
+ let a004 = min((1.0 - pr) * (1.0 - pr), exp2(-9.28 * ndv)) * (1.0 - pr) + (0.0425 - 0.0275 * pr);
511
+ let A = -a004 * 1.04 + (1.04 - 0.572 * pr);
512
+ let ns = saturate(dot(n, mix(reflect(-l, n), h, 0.8)));
513
+ let B = _ag_ramp(slot, vec2f(ns * (a004 * 1.04 + (0.022 * pr - 0.04)), 0.85)).r;
514
+ let env = rzWorldSpecularLod(reflect(-v, n), pr, 1.0) * reflection;
515
+
516
+ // ... and the environment at a flat 0.0157 rather than through the BRDF fit.
517
+ let envTerm = select((f0 * A + B) * env, env * 0.0157, isFace);
518
+ return emissive + rzObjectAmbient(n) * diffuse + envTerm + direct;
519
+ }
520
+
521
+
522
+ /**
523
+ * The face's shade from its SDF image (Uber FACE_MODE, _SDFType 0), in place of
524
+ * N·L: the key light taken into the face's frame and flattened, the field read
525
+ * at the uv and at its mirror, the side the light is on choosing which, both
526
+ * compared against the light's front-to-back angle with _BlendSmoothness, then
527
+ * softened by how level the light is and a half-Lambert. The face frame is the
528
+ * head bone's, turned to the game's convention (x to the face's right, z out of
529
+ * it). An SDF is painted in the face's uvs, so it belongs to models that share
530
+ * the game's face layout; a white slot reads as a face lit evenly until the
531
+ * light goes behind it.
532
+ */
533
+ fn ag_face_sdf(slot: u32, uv: vec2f, l: vec3f, n: vec3f, smoothness: f32, invert: f32) -> f32 {
534
+ let hb = skinMats[u32(max(material.headBoneIndex, 0.0))];
535
+ let fx = -normalize(hb[0].xyz);
536
+ let fy = normalize(hb[1].xyz);
537
+ let fz = -normalize(hb[2].xyz);
538
+ let lf = normalize(vec3f(dot(l, fx), dot(l, fy), dot(l, fz)));
539
+ let fl = lf.xz * inverseSqrt(max(dot(lf.xz, lf.xz), 1.1754944e-38));
540
+ let u0 = mix(uv.x, 1.0 - uv.x, invert);
541
+ let s1 = _ag_ramp(slot, vec2f(u0, 1.0 - uv.y));
542
+ let s2 = _ag_ramp(slot, vec2f(1.0 - u0, 1.0 - uv.y));
543
+ let left = fl.x < 0.0;
544
+ let a = select(s2.x, s1.x, left);
545
+ let b = select(s1.x, s2.x, left);
546
+ let flag = select(0.0, 1.0, s1.w > 0.5);
547
+ let k = max(smoothness, 0.001);
548
+ let th = fl.y * -0.5 + 0.5;
549
+ let w = 1.0 - th;
550
+ var m1 = max(saturate((a - th) / k + 0.5), saturate((b - w - 1.0) / k + 0.5));
551
+ m1 = min(m1, min(saturate((2.0 - a - w) / k + 0.5), saturate((2.0 - b - w) / k + 0.5)));
552
+ var m2 = max(saturate((b - th - 1.0) / k + 0.5), saturate((a - w) / k + 0.5));
553
+ m2 = min(m2, min(saturate((a + w) / k + 0.5), saturate((b + w) / k + 0.5)));
554
+ var r = flag * (m1 - m2) + m2;
555
+ r *= (1.0 - abs(lf.z)) * 0.5 + 0.5;
556
+ r *= saturate(dot(n, l)) * 0.5 + 0.5;
557
+ return r;
558
+ }
559
+
351
560
  fn group_tex0(uv: vec2f) -> vec4f { return textureSample(groupTexture0, diffuseSampler, uv); }
352
561
  fn group_tex1(uv: vec2f) -> vec4f { return textureSample(groupTexture1, diffuseSampler, uv); }
353
562
  fn group_tex2(uv: vec2f) -> vec4f { return textureSample(groupTexture2, diffuseSampler, uv); }
@@ -1056,6 +1265,7 @@ struct _RzLampPair { d: vec3f, s: vec3f };
1056
1265
 
1057
1266
  fn _rzLampOne(i: u32, p: vec3f, n: vec3f, v: vec3f, ndv: f32, roughness: f32, unity: f32) -> _RzLampPair {
1058
1267
  var out = _RzLampPair(vec3f(0.0), vec3f(0.0));
1268
+ if (!_rzLightReaches(i)) { return out; }
1059
1269
  let pr = _rzLightVec(i, 0u);
1060
1270
  let d = pr.xyz - p;
1061
1271
  let dist = length(d);
@@ -1,7 +1,7 @@
1
- // EEVEE 3.6 bloom pyramid: blit (Karis prefilter) → 13-tap downsamples → 9-tap tent upsamples.
2
- // Mirrors source/blender/draw/engines/eevee/shaders/effect_bloom_frag.glsl. Firefly suppression
3
- // lives in the blit (Karis luminance-weighted 4-tap average). A single-pass Gaussian cannot
4
- // reproduce this — hot pixels dominate and produce the sparkle halo.
1
+ // Bloom — Aether Gazer's own chain (AGSimPostFX.Bloom, URP-style): a soft-knee
2
+ // prefilter to half size, a separable Gaussian down to a few pixels, then back
3
+ // up level by level, each blend leaning toward the coarser level by `scatter`.
4
+ // The composite adds the result × tint × intensity before the view transform.
5
5
 
6
6
  const FULLSCREEN_VS = /* wgsl */ `
7
7
  @vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
@@ -11,114 +11,86 @@ const FULLSCREEN_VS = /* wgsl */ `
11
11
  }
12
12
  `
13
13
 
14
- // Full-res HDR → half-res. Karis 4-tap firefly average + EEVEE quadratic knee threshold + clamp.
15
- export const BLOOM_BLIT_SHADER_WGSL = `${FULLSCREEN_VS}
14
+ // Full-res HDR → half-res. The game takes ONE bilinear sample at the half-res
15
+ // pixel's centre, which is the 2×2 box of the block under it; here the four
16
+ // texels are loaded so each can be un-premultiplied (the scene target is
17
+ // premultiplied; its alpha lives in the aux target's .g) and gated by the bloom
18
+ // mask (.r) first.
19
+ export const BLOOM_PREFILTER_SHADER_WGSL = `${FULLSCREEN_VS}
16
20
  @group(0) @binding(0) var hdrTex: texture_2d<f32>;
17
- @group(0) @binding(1) var<uniform> prefilter: vec4<f32>; // threshold, knee, clamp, _unused
21
+ @group(0) @binding(1) var<uniform> params: vec4<f32>; // threshold, knee, clamp, scatter
18
22
  @group(0) @binding(2) var maskTex: texture_2d<f32>;
19
23
 
20
- fn luminance(c: vec3f) -> f32 {
21
- return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));
22
- }
23
- fn fetch(c: vec2<i32>, clampV: f32) -> vec3f {
24
+ fn fetch(c: vec2<i32>) -> vec3f {
24
25
  let d = vec2<i32>(textureDimensions(hdrTex));
25
26
  let cc = clamp(c, vec2<i32>(0), d - vec2<i32>(1));
26
- let s = textureLoad(hdrTex, cc, 0);
27
- // hdrTex is rg11b10ufloat (no alpha channel). Alpha lives in maskTex.g, written
28
- // alongside the bloom mask in .r by the scene pass. Scene uses src-alpha blend
29
- // with clear alpha 0 → hdr rgb is premultiplied; divide by the aux alpha to
30
- // recover straight color before the Karis luminance weighting.
31
27
  let aux = textureLoad(maskTex, cc, 0);
32
- let mask = aux.r;
33
- let rgb = max(s.rgb / max(aux.g, 1e-6), vec3f(0.0));
34
- let masked = rgb * mask;
35
- // Blender clamps each tap BEFORE Karis average (eevee_bloom: color = min(clampIntensity, color)).
36
- return select(masked, min(masked, vec3f(clampV)), clampV > 0.0);
28
+ return max(textureLoad(hdrTex, cc, 0).rgb / max(aux.g, 1e-6), vec3f(0.0)) * aux.r;
37
29
  }
38
30
 
39
31
  @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
40
- let dst = vec2<i32>(p.xy - vec2f(0.5));
41
- let base = dst * 2;
42
- let clampV = prefilter.z;
43
- let a = fetch(base + vec2<i32>(0, 0), clampV);
44
- let b = fetch(base + vec2<i32>(1, 0), clampV);
45
- let c = fetch(base + vec2<i32>(0, 1), clampV);
46
- let d = fetch(base + vec2<i32>(1, 1), clampV);
47
- // Karis partial average: weight each tap by 1/(1+luma) — suppresses fireflies.
48
- let wa = 1.0 / (1.0 + luminance(a));
49
- let wb = 1.0 / (1.0 + luminance(b));
50
- let wc = 1.0 / (1.0 + luminance(c));
51
- let wd = 1.0 / (1.0 + luminance(d));
52
- let avg = (a * wa + b * wb + c * wc + d * wd) / max(wa + wb + wc + wd, 1e-6);
53
- // EEVEE quadratic threshold (brightness = max-channel, then soft-knee curve).
54
- let bright = max(avg.r, max(avg.g, avg.b));
55
- let soft = clamp(bright - prefilter.x + prefilter.y, 0.0, 2.0 * prefilter.y);
56
- let q = (soft * soft) / (4.0 * max(prefilter.y, 1e-4) + 1e-6);
57
- let contrib = max(q, bright - prefilter.x) / max(bright, 1e-4);
58
- return vec4f(max(avg * contrib, vec3f(0.0)), 1.0);
32
+ let base = vec2<i32>(p.xy - vec2f(0.5)) * 2;
33
+ let avg = (fetch(base) + fetch(base + vec2<i32>(1, 0)) + fetch(base + vec2<i32>(0, 1)) + fetch(base + vec2<i32>(1, 1))) * 0.25;
34
+ let c = min(avg, vec3f(params.z));
35
+ // URP's quadratic soft knee on the brightest channel.
36
+ let bright = max(c.r, max(c.g, c.b));
37
+ let knee = params.y;
38
+ let soft = clamp(bright - params.x + knee, 0.0, 2.0 * knee);
39
+ let curve = soft * soft / (4.0 * knee + 1e-4);
40
+ return vec4f(c * (max(curve, bright - params.x) / max(bright, 1e-4)), 1.0);
59
41
  }
60
42
  `
61
43
 
62
- // Jimenez/COD 13-tap dual-box — 5 weighted 2×2 averages, rejects nyquist ringing.
63
- export const BLOOM_DOWNSAMPLE_SHADER_WGSL = `${FULLSCREEN_VS}
44
+ // Horizontal 9-tap Gaussian (σ ≈ 2 at the destination's scale), its taps two
45
+ // SOURCE texels apart: the destination is half the source, so this both halves
46
+ // and blurs. Each tap lands on a texel boundary, so bilinear averages pairs.
47
+ export const BLOOM_BLUR_H_SHADER_WGSL = `${FULLSCREEN_VS}
64
48
  @group(0) @binding(0) var srcTex: texture_2d<f32>;
65
49
  @group(0) @binding(1) var srcSamp: sampler;
66
50
 
67
- fn samp(uv: vec2f, off: vec2f) -> vec3f {
68
- return textureSampleLevel(srcTex, srcSamp, uv + off, 0.0).rgb;
51
+ @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
52
+ let srcDims = vec2f(textureDimensions(srcTex));
53
+ let dstDims = max(floor(srcDims * 0.5), vec2f(1.0));
54
+ let uv = p.xy / dstDims;
55
+ let dx = vec2f(2.0 / srcDims.x, 0.0);
56
+ var w = array<f32, 5>(0.22702703, 0.19459459, 0.12162162, 0.05405405, 0.01621622);
57
+ var o = textureSampleLevel(srcTex, srcSamp, uv, 0.0).rgb * w[0];
58
+ for (var i = 1; i < 5; i++) {
59
+ let off = dx * f32(i);
60
+ o += (textureSampleLevel(srcTex, srcSamp, uv - off, 0.0).rgb + textureSampleLevel(srcTex, srcSamp, uv + off, 0.0).rgb) * w[i];
61
+ }
62
+ return vec4f(o, 1.0);
69
63
  }
64
+ `
65
+
66
+ // Vertical pass at the same size: the same 9-tap Gaussian as five bilinear taps.
67
+ export const BLOOM_BLUR_V_SHADER_WGSL = `${FULLSCREEN_VS}
68
+ @group(0) @binding(0) var srcTex: texture_2d<f32>;
69
+ @group(0) @binding(1) var srcSamp: sampler;
70
70
 
71
71
  @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
72
- let srcDims = vec2f(textureDimensions(srcTex));
73
- let t = 1.0 / srcDims;
74
- // fragCoord.xy reports pixel centers (e.g. 0.5,0.5 for first pixel) — divide by dst dims directly.
75
- let dstDims = srcDims * 0.5;
76
- let uv = p.xy / max(dstDims, vec2f(1.0));
77
- let A = samp(uv, t * vec2f(-2.0, -2.0));
78
- let B = samp(uv, t * vec2f( 0.0, -2.0));
79
- let C = samp(uv, t * vec2f( 2.0, -2.0));
80
- let D = samp(uv, t * vec2f(-1.0, -1.0));
81
- let E = samp(uv, t * vec2f( 1.0, -1.0));
82
- let F = samp(uv, t * vec2f(-2.0, 0.0));
83
- let G = samp(uv, t * vec2f( 0.0, 0.0));
84
- let H = samp(uv, t * vec2f( 2.0, 0.0));
85
- let I = samp(uv, t * vec2f(-1.0, 1.0));
86
- let J = samp(uv, t * vec2f( 1.0, 1.0));
87
- let K = samp(uv, t * vec2f(-2.0, 2.0));
88
- let L = samp(uv, t * vec2f( 0.0, 2.0));
89
- let M = samp(uv, t * vec2f( 2.0, 2.0));
90
- var o = (D + E + I + J) * (0.5 / 4.0);
91
- o = o + (A + B + G + F) * (0.125 / 4.0);
92
- o = o + (B + C + H + G) * (0.125 / 4.0);
93
- o = o + (F + G + L + K) * (0.125 / 4.0);
94
- o = o + (G + H + M + L) * (0.125 / 4.0);
72
+ let dims = vec2f(textureDimensions(srcTex));
73
+ let uv = p.xy / dims;
74
+ let near = vec2f(0.0, 1.3846154 / dims.y);
75
+ let far = vec2f(0.0, 3.2307692 / dims.y);
76
+ var o = textureSampleLevel(srcTex, srcSamp, uv, 0.0).rgb * 0.22702703;
77
+ o += (textureSampleLevel(srcTex, srcSamp, uv - near, 0.0).rgb + textureSampleLevel(srcTex, srcSamp, uv + near, 0.0).rgb) * 0.31621623;
78
+ o += (textureSampleLevel(srcTex, srcSamp, uv - far, 0.0).rgb + textureSampleLevel(srcTex, srcSamp, uv + far, 0.0).rgb) * 0.07027027;
95
79
  return vec4f(o, 1.0);
96
80
  }
97
81
  `
98
82
 
99
- // 9-tap tent, progressively added to matching downsample mip. Blender radius = sample scale.
83
+ // One level up: this level's blur leaned toward the coarser one by scatter.
100
84
  export const BLOOM_UPSAMPLE_SHADER_WGSL = `${FULLSCREEN_VS}
101
- @group(0) @binding(0) var srcTex: texture_2d<f32>; // coarser accumulator
102
- @group(0) @binding(1) var baseTex: texture_2d<f32>; // matching downsample mip
85
+ @group(0) @binding(0) var highTex: texture_2d<f32>; // down[i]
86
+ @group(0) @binding(1) var lowTex: texture_2d<f32>; // coarser: down[i+1] at the top, up[i+1] after
103
87
  @group(0) @binding(2) var srcSamp: sampler;
104
- @group(0) @binding(3) var<uniform> upU: vec4<f32>; // sampleScale, _, _, _
88
+ @group(0) @binding(3) var<uniform> params: vec4<f32>; // threshold, knee, clamp, scatter
105
89
 
106
90
  @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
107
- let srcDims = vec2f(textureDimensions(srcTex));
108
- let baseDims = vec2f(textureDimensions(baseTex));
109
- let uv = p.xy / max(baseDims, vec2f(1.0));
110
- let t = upU.x / srcDims;
111
- var o = textureSampleLevel(srcTex, srcSamp, uv + t * vec2f(-1.0, -1.0), 0.0).rgb * 1.0;
112
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 0.0, -1.0), 0.0).rgb * 2.0;
113
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 1.0, -1.0), 0.0).rgb * 1.0;
114
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f(-1.0, 0.0), 0.0).rgb * 2.0;
115
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 0.0, 0.0), 0.0).rgb * 4.0;
116
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 1.0, 0.0), 0.0).rgb * 2.0;
117
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f(-1.0, 1.0), 0.0).rgb * 1.0;
118
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 0.0, 1.0), 0.0).rgb * 2.0;
119
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 1.0, 1.0), 0.0).rgb * 1.0;
120
- o = o * (1.0 / 16.0);
121
- let base = textureSampleLevel(baseTex, srcSamp, uv, 0.0).rgb;
122
- return vec4f(o + base, 1.0);
91
+ let uv = p.xy / vec2f(textureDimensions(highTex));
92
+ let high = textureSampleLevel(highTex, srcSamp, uv, 0.0).rgb;
93
+ let low = textureSampleLevel(lowTex, srcSamp, uv, 0.0).rgb;
94
+ return vec4f(mix(high, low, params.w), 1.0);
123
95
  }
124
96
  `