reze-engine 0.66.5 → 0.67.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/camera.d.ts +1 -0
  2. package/dist/camera.d.ts.map +1 -1
  3. package/dist/camera.js +9 -5
  4. package/dist/engine.d.ts +20 -1
  5. package/dist/engine.d.ts.map +1 -1
  6. package/dist/engine.js +91 -28
  7. package/dist/graph/presets/body.js +1 -1
  8. package/dist/graph/presets/cloth_rough.js +1 -1
  9. package/dist/graph/presets/cloth_smooth.js +1 -1
  10. package/dist/graph/presets/face.js +1 -1
  11. package/dist/graph/presets/metal.js +1 -1
  12. package/dist/graph/presets/pool_floor.d.ts +3 -0
  13. package/dist/graph/presets/pool_floor.d.ts.map +1 -0
  14. package/dist/graph/presets/pool_floor.js +42 -0
  15. package/dist/graph/presets/water.d.ts +3 -0
  16. package/dist/graph/presets/water.d.ts.map +1 -0
  17. package/dist/graph/presets/water.js +31 -0
  18. package/dist/graph/registry.d.ts.map +1 -1
  19. package/dist/graph/registry.js +84 -6
  20. package/dist/graph/schema.d.ts +14 -0
  21. package/dist/graph/schema.d.ts.map +1 -1
  22. package/dist/physics/autofit.d.ts +147 -0
  23. package/dist/physics/autofit.d.ts.map +1 -0
  24. package/dist/physics/autofit.js +501 -0
  25. package/dist/shaders/agx-lut.d.ts +11 -0
  26. package/dist/shaders/agx-lut.d.ts.map +1 -0
  27. package/dist/shaders/agx-lut.js +29 -0
  28. package/dist/shaders/dfg_lut.d.ts +3 -0
  29. package/dist/shaders/dfg_lut.d.ts.map +1 -0
  30. package/dist/shaders/dfg_lut.js +129 -0
  31. package/dist/shaders/ltc_mag_lut.d.ts +3 -0
  32. package/dist/shaders/ltc_mag_lut.d.ts.map +1 -0
  33. package/dist/shaders/ltc_mag_lut.js +1033 -0
  34. package/dist/shaders/materials/nodes.d.ts +1 -1
  35. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  36. package/dist/shaders/materials/nodes.js +176 -7
  37. package/dist/shaders/passes/composite.d.ts.map +1 -1
  38. package/dist/shaders/passes/composite.js +4 -2
  39. package/dist/shaders/passes/field-blit.d.ts +26 -0
  40. package/dist/shaders/passes/field-blit.d.ts.map +1 -0
  41. package/dist/shaders/passes/field-blit.js +65 -0
  42. package/dist/shaders/passes/ghosts.d.ts +22 -0
  43. package/dist/shaders/passes/ghosts.d.ts.map +1 -0
  44. package/dist/shaders/passes/ghosts.js +75 -0
  45. package/dist/shaders/passes/ground-noise.d.ts +7 -0
  46. package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
  47. package/dist/shaders/passes/ground-noise.js +88 -0
  48. package/dist/shaders/passes/outline.d.ts +7 -0
  49. package/dist/shaders/passes/outline.d.ts.map +1 -1
  50. package/dist/shaders/passes/outline.js +67 -16
  51. package/dist/shaders/passes/sim.d.ts +34 -0
  52. package/dist/shaders/passes/sim.d.ts.map +1 -0
  53. package/dist/shaders/passes/sim.js +169 -0
  54. package/dist/shaders/score-api.d.ts +10 -0
  55. package/dist/shaders/score-api.d.ts.map +1 -0
  56. package/dist/shaders/score-api.js +114 -0
  57. package/dist/shaders/shadow-api.d.ts +29 -0
  58. package/dist/shaders/shadow-api.d.ts.map +1 -0
  59. package/dist/shaders/shadow-api.js +112 -0
  60. package/package.json +1 -1
  61. package/src/camera.ts +9 -5
  62. package/src/engine.ts +109 -30
  63. package/src/graph/presets/body.ts +1 -1
  64. package/src/graph/presets/cloth_rough.ts +1 -1
  65. package/src/graph/presets/cloth_smooth.ts +1 -1
  66. package/src/graph/presets/face.ts +1 -1
  67. package/src/graph/presets/metal.ts +1 -1
  68. package/src/graph/registry.ts +85 -6
  69. package/src/graph/schema.ts +14 -0
  70. package/src/shaders/materials/nodes.ts +176 -7
  71. package/src/shaders/passes/composite.ts +4 -2
  72. package/src/shaders/passes/outline.ts +67 -16
  73. package/dist/_dbg-loader.mjs +0 -1100
@@ -97,7 +97,7 @@ override APPLY_GAMMA: bool = true;
97
97
  @group(0) @binding(0) var hdrTex: texture_2d<f32>;
98
98
  @group(0) @binding(1) var bloomTex: texture_2d<f32>; // bloomUpTexture mip 0 (full pyramid top)
99
99
  @group(0) @binding(2) var bloomSamp: sampler;
100
- @group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 15>;
100
+ @group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 16>;
101
101
  // Aux mask/alpha texture. .r = bloom mask (unused here; bloom blit uses it).
102
102
  // .g = accumulated canvas alpha (what hdr.a carried before the HDR format
103
103
  // became rg11b10ufloat). We unpremultiply HDR by this alpha for tonemap, then
@@ -119,6 +119,8 @@ override APPLY_GAMMA: bool = true;
119
119
  // viewU[9] = (grade slope.rgb, grade flags) — bit 0 the CDL grade(), bit 1 the
120
120
  // scene's own LUT (setStageGrade) — see _rzGradeScene() below.
121
121
  // viewU[10] = (camera world position, _) — refreshed with the basis above.
122
+ // viewU[11..14] = the cast's positions (effects; count in viewU[10].w).
123
+ // viewU[15] = (soft curve contrast, _, _, _).
122
124
  // invGamma = 1/gamma precomputed on CPU — avoids a per-pixel divide.
123
125
  @group(0) @binding(6) var bgEquirect: texture_2d<f32>;
124
126
  // The scene pass's own MSAA depth buffer, bound depth-only. NOT an extra
@@ -217,7 +219,7 @@ fn srgb_encode(x: f32) -> f32 {
217
219
  * The game's ACES branch is off in every scene measured.
218
220
  */
219
221
  fn softTransform(c: vec3f) -> vec3f {
220
- let t = pow(max(vec3f(1.0) - exp(-2.5 * max(c, vec3f(0.0))), vec3f(0.0)), vec3f(1.4));
222
+ let t = pow(max(vec3f(1.0) - exp(-2.5 * max(c, vec3f(0.0))), vec3f(0.0)), vec3f(viewU[15].x));
221
223
  return vec3f(srgb_encode(t.r), srgb_encode(t.g), srgb_encode(t.b));
222
224
  }
223
225
 
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The field layer, drawn INTO the scene.
3
+ *
4
+ * Field effects render into their own targets at their own resolution — that
5
+ * part was always right. What was wrong was where the result landed: the
6
+ * composite sampled it AFTER tone mapping, which is why a field effect could
7
+ * never bloom, could never light anything, and tone mapped differently from the
8
+ * particle and ribbon mounts drawing the same kind of glow.
9
+ *
10
+ * Two draws inside the scene pass — background before any geometry, foreground
11
+ * after all of it — so the bloom pyramid sees field effects and one tone map
12
+ * covers the whole frame.
13
+ *
14
+ * NO DEPTH, neither written nor tested. A foreground is handed the scene's
15
+ * depth as a parameter and does its own occlusion; testing it here would
16
+ * occlude it a second time, and a background has nothing to test against.
17
+ *
18
+ * COVERAGE IS CLAMPED HERE. Additive field effects SUM their coverage into the
19
+ * layer (see FIELD_LAYER_BLEND_ADDITIVE), which is what stops the composite's
20
+ * unpremultiply from dividing colour by nothing — but N additive effects can
21
+ * sum past 1, and the layer is rgba16float so nothing stopped it on the way in.
22
+ * A coverage above 1 would darken what the layer is drawn over by more than
23
+ * fully covering it.
24
+ */
25
+ export declare function fieldBlitShaderWgsl(): string;
26
+ //# sourceMappingURL=field-blit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-blit.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/field-blit.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAuC5C"}
@@ -0,0 +1,65 @@
1
+ import { sceneFsOutWgsl } from "./scene-contract";
2
+ /**
3
+ * The field layer, drawn INTO the scene.
4
+ *
5
+ * Field effects render into their own targets at their own resolution — that
6
+ * part was always right. What was wrong was where the result landed: the
7
+ * composite sampled it AFTER tone mapping, which is why a field effect could
8
+ * never bloom, could never light anything, and tone mapped differently from the
9
+ * particle and ribbon mounts drawing the same kind of glow.
10
+ *
11
+ * Two draws inside the scene pass — background before any geometry, foreground
12
+ * after all of it — so the bloom pyramid sees field effects and one tone map
13
+ * covers the whole frame.
14
+ *
15
+ * NO DEPTH, neither written nor tested. A foreground is handed the scene's
16
+ * depth as a parameter and does its own occlusion; testing it here would
17
+ * occlude it a second time, and a background has nothing to test against.
18
+ *
19
+ * COVERAGE IS CLAMPED HERE. Additive field effects SUM their coverage into the
20
+ * layer (see FIELD_LAYER_BLEND_ADDITIVE), which is what stops the composite's
21
+ * unpremultiply from dividing colour by nothing — but N additive effects can
22
+ * sum past 1, and the layer is rgba16float so nothing stopped it on the way in.
23
+ * A coverage above 1 would darken what the layer is drawn over by more than
24
+ * fully covering it.
25
+ */
26
+ export function fieldBlitShaderWgsl() {
27
+ return /* wgsl */ `
28
+ @group(0) @binding(0) var _rzLayer: texture_2d<f32>;
29
+ @group(0) @binding(1) var _rzLayerSamp: sampler;
30
+ // (bloom, _, fullW, fullH). The size is the SCENE target's, not the layer's:
31
+ // the layer is often half resolution, and normalised uv is what makes that
32
+ // invisible.
33
+ @group(0) @binding(2) var<uniform> _rzBlitU: vec4f;
34
+
35
+ @vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
36
+ let x = f32((vi & 1u) << 2u) - 1.0;
37
+ let y = f32((vi & 2u) << 1u) - 1.0;
38
+ return vec4f(x, y, 0.0, 1.0);
39
+ }
40
+
41
+ ${sceneFsOutWgsl()}
42
+ @fragment fn fs(@builtin(position) fragCoord: vec4f) -> FSOut {
43
+ var out: FSOut;
44
+ // Bilinear, because the layer is usually half resolution and the whole point
45
+ // of that choice is that low-frequency effects upsample invisibly.
46
+ let uv = fragCoord.xy / max(_rzBlitU.zw, vec2f(1.0));
47
+ let c = textureSampleLevel(_rzLayer, _rzLayerSamp, uv, 0.0);
48
+ let cov = clamp(c.a, 0.0, 1.0);
49
+ // PREMULTIPLIED, and blitted with a premultiplied-over blend — the field
50
+ // targets have been premultiplied since N effects had to compose into them
51
+ // associatively. Handing this to the material blend would scale rgb by alpha
52
+ // a second time and every layer would come out dark.
53
+ out.color = vec4f(max(c.rgb, vec3f(0.0)), cov);
54
+ // The aux target: bloom mask, then coverage. THIS is what finally makes
55
+ // // @bloom mean something on a field effect — the flag has been parsed and
56
+ // ignored for every mount but particles and ribbons, which is the kind of
57
+ // author-surface lie the install-time guards exist to kill.
58
+ //
59
+ // The mask is written UNWEIGHTED and the blend weights it by src.a — the
60
+ // convention the materials already follow (they write vec4f(1, 1, 0, alpha)).
61
+ out.mask = vec4f(_rzBlitU.x, 1.0, 0.0, cov);
62
+ return out;
63
+ }
64
+ `;
65
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Afterimages — the cast drawn again at the poses it held a moment ago.
3
+ *
4
+ * The VERTEX stage is the material one, unchanged: a ghost is the same mesh
5
+ * skinned by different matrices, and the engine binds a slot of its pose
6
+ * history where the live skinning matrices would be. Only the fragment is new.
7
+ *
8
+ * What a ghost looks like is Elden Ring's phantom: a body you can nearly see
9
+ * through, drawn by its EDGES. Three of them, all in the tint — the rim where
10
+ * the surface turns away (the silhouette), and inside it the lines a drawing
11
+ * would have: where the texture's brightness changes sharply from one pixel to
12
+ * the next (painted seams, trim, straps) and where the surface itself folds
13
+ * (the normal turning fast across a pixel). The body under them is the
14
+ * surface's own colour mixed toward the tint by a dial, at its own opacity.
15
+ *
16
+ * Built at first use rather than at import: the output struct depends on
17
+ * whether the device carries the id attachment, which is probed at init.
18
+ */
19
+ /** One rank's uniforms, at a 256-byte stride for the dynamic offset. */
20
+ export declare const GHOST_UNIFORM_STRIDE = 256;
21
+ export declare function ghostShaderWgsl(): string;
22
+ //# sourceMappingURL=ghosts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ghosts.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ghosts.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AAEH,wEAAwE;AACxE,eAAO,MAAM,oBAAoB,MAAM,CAAA;AAEvC,wBAAgB,eAAe,IAAI,MAAM,CAqDxC"}
@@ -0,0 +1,75 @@
1
+ import { NODES_WGSL } from "../materials/nodes";
2
+ import { COMMON_MATERIAL_PRELUDE_WGSL, DISSOLVE_WGSL, commonFsOutWgsl } from "../materials/common";
3
+ import { sceneIdPadWgsl } from "./scene-contract";
4
+ /**
5
+ * Afterimages — the cast drawn again at the poses it held a moment ago.
6
+ *
7
+ * The VERTEX stage is the material one, unchanged: a ghost is the same mesh
8
+ * skinned by different matrices, and the engine binds a slot of its pose
9
+ * history where the live skinning matrices would be. Only the fragment is new.
10
+ *
11
+ * What a ghost looks like is Elden Ring's phantom: a body you can nearly see
12
+ * through, drawn by its EDGES. Three of them, all in the tint — the rim where
13
+ * the surface turns away (the silhouette), and inside it the lines a drawing
14
+ * would have: where the texture's brightness changes sharply from one pixel to
15
+ * the next (painted seams, trim, straps) and where the surface itself folds
16
+ * (the normal turning fast across a pixel). The body under them is the
17
+ * surface's own colour mixed toward the tint by a dial, at its own opacity.
18
+ *
19
+ * Built at first use rather than at import: the output struct depends on
20
+ * whether the device carries the id attachment, which is probed at init.
21
+ */
22
+ /** One rank's uniforms, at a 256-byte stride for the dynamic offset. */
23
+ export const GHOST_UNIFORM_STRIDE = 256;
24
+ export function ghostShaderWgsl() {
25
+ return (NODES_WGSL +
26
+ COMMON_MATERIAL_PRELUDE_WGSL +
27
+ commonFsOutWgsl() +
28
+ DISSOLVE_WGSL +
29
+ /* wgsl */ `
30
+ struct GhostU {
31
+ tint: vec3f,
32
+ tintMix: f32,
33
+ alpha: f32,
34
+ glow: f32,
35
+ lines: f32,
36
+ body: f32,
37
+ }
38
+ @group(3) @binding(0) var<uniform> ghost: GhostU;
39
+
40
+ @fragment fn fsGhost(input: VertexOutput) -> FSOut {
41
+ let tex = textureSample(diffuseTexture, diffuseSampler, input.uv);
42
+ let base = tex.rgb * material.diffuseColor;
43
+ let n = safe_normal(input.normal);
44
+ // Derivatives BEFORE the discard below: they need every pixel of the quad.
45
+ let lum = dot(base, vec3f(0.2126, 0.7152, 0.0722));
46
+ let detail = smoothstep(0.02, 0.12, fwidth(lum));
47
+ let crease = smoothstep(0.08, 0.35, length(fwidth(n)));
48
+ let cover = material.alpha * tex.a;
49
+ // Cutouts stay cut: lace and hair cards would otherwise ghost as solid cards.
50
+ if (cover < 0.1) { discard; }
51
+ let v = normalize(camera.viewPos - input.worldPos);
52
+ let turn = 1.0 - abs(dot(n, v));
53
+ let rim = turn * turn * turn;
54
+ let edge = rim * ghost.glow + max(detail, crease) * ghost.lines;
55
+ // The dial arrives as the picked #rrggbb scaled to 0..1 — sRGB. Decoded, so
56
+ // the colour mixed with the linear surface and added as light is the colour
57
+ // that was picked.
58
+ let tint = pow(ghost.tint, vec3f(2.2));
59
+ let rgb = mix(base, tint, ghost.tintMix) * ghost.body + tint * edge;
60
+ let a = clamp(ghost.alpha * cover * (ghost.body + edge), 0.0, 1.0);
61
+ var out: FSOut;
62
+ out.color = vec4f(rgb, a);
63
+ out.mask = vec4f(1.0, 1.0, 0.0, a);
64
+ ${sceneIdPadWgsl("out")} return out;
65
+ }
66
+
67
+ // The cast mark writes stencil only; this satisfies the pass's attachments.
68
+ @fragment fn fsGhostMark(input: VertexOutput) -> FSOut {
69
+ var out: FSOut;
70
+ out.color = vec4f(0.0);
71
+ out.mask = vec4f(0.0);
72
+ ${sceneIdPadWgsl("out")} return out;
73
+ }
74
+ `);
75
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The tile as 8-bit luminance, plus its mip chain (box-filtered, which is exact
3
+ * for a repeating texture). Mips are the point as much as the speed: procedural
4
+ * noise has no mip chain, so at grazing angles it shimmered.
5
+ */
6
+ export declare function buildGroundNoiseMips(): Uint8Array[];
7
+ //# sourceMappingURL=ground-noise.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ground-noise.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ground-noise.ts"],"names":[],"mappings":"AAkDA;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAsCnD"}
@@ -0,0 +1,88 @@
1
+ // Bakes the ground's frosted micro-texture into a seamless, mipmapped tile.
2
+ //
3
+ // The shader used to evaluate four octaves of value noise per pixel — sixteen
4
+ // hash rounds — and the ground covers the entire screen as soon as the camera
5
+ // tilts toward the horizon. This produces the same character of pattern once, at
6
+ // load, so the fragment shader pays one filtered fetch instead.
7
+ //
8
+ // SEAMLESS BY CONSTRUCTION. The original hash was not periodic, so a finite
9
+ // crop of it could never tile. Here every octave's lattice wraps at a whole
10
+ // number of cells across the tile, which makes the result repeat exactly at the
11
+ // edges. The pattern is therefore not pixel-identical to the analytic version —
12
+ // it is the same kind of noise, not the same noise. At the default strength of
13
+ // 0.05 it modulates brightness by ±2.5%, where the repeat is not findable by eye.
14
+ import { GROUND_NOISE_RES, GROUND_NOISE_TILE_WORLD } from "./ground";
15
+ /** Lattice cells across the tile at octave 0. The analytic version sampled fbm
16
+ * at worldXZ × 3, so a tile spanning 8 world units covers 24 base cells — the
17
+ * same spatial frequency the surface had before. */
18
+ const BASE_CELLS = GROUND_NOISE_TILE_WORLD * 3;
19
+ const OCTAVES = 4;
20
+ /** Deterministic lattice hash. Integer coordinates are wrapped by the caller,
21
+ * which is what makes the tile seamless. */
22
+ function hash(ix, iy) {
23
+ let h = Math.imul(ix, 374761393) + Math.imul(iy, 668265263);
24
+ h = Math.imul(h ^ (h >>> 13), 1274126177);
25
+ return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
26
+ }
27
+ /** Value noise on a lattice that repeats every `period` cells. */
28
+ function periodicValueNoise(x, y, period) {
29
+ const ix = Math.floor(x);
30
+ const iy = Math.floor(y);
31
+ const fx = x - ix;
32
+ const fy = y - iy;
33
+ // Smoothstep weights — same curve the WGSL used.
34
+ const ux = fx * fx * (3 - 2 * fx);
35
+ const uy = fy * fy * (3 - 2 * fy);
36
+ const x0 = ((ix % period) + period) % period;
37
+ const y0 = ((iy % period) + period) % period;
38
+ const x1 = (x0 + 1) % period;
39
+ const y1 = (y0 + 1) % period;
40
+ const a = hash(x0, y0);
41
+ const b = hash(x1, y0);
42
+ const c = hash(x0, y1);
43
+ const d = hash(x1, y1);
44
+ return (a + (b - a) * ux) * (1 - uy) + (c + (d - c) * ux) * uy;
45
+ }
46
+ /**
47
+ * The tile as 8-bit luminance, plus its mip chain (box-filtered, which is exact
48
+ * for a repeating texture). Mips are the point as much as the speed: procedural
49
+ * noise has no mip chain, so at grazing angles it shimmered.
50
+ */
51
+ export function buildGroundNoiseMips() {
52
+ const levels = [];
53
+ const res = GROUND_NOISE_RES;
54
+ const base = new Uint8Array(res * res);
55
+ for (let py = 0; py < res; py++) {
56
+ for (let px = 0; px < res; px++) {
57
+ // Texel centre, in units of octave-0 lattice cells.
58
+ const x = ((px + 0.5) / res) * BASE_CELLS;
59
+ const y = ((py + 0.5) / res) * BASE_CELLS;
60
+ let v = 0;
61
+ let amp = 0.5;
62
+ let freq = 1;
63
+ for (let o = 0; o < OCTAVES; o++) {
64
+ v += amp * periodicValueNoise(x * freq, y * freq, BASE_CELLS * freq);
65
+ freq *= 2;
66
+ amp *= 0.5;
67
+ }
68
+ base[py * res + px] = Math.max(0, Math.min(255, Math.round(v * 255)));
69
+ }
70
+ }
71
+ levels.push(base);
72
+ let src = base;
73
+ let size = res;
74
+ while (size > 1) {
75
+ const half = size >> 1;
76
+ const dst = new Uint8Array(half * half);
77
+ for (let y = 0; y < half; y++) {
78
+ for (let x = 0; x < half; x++) {
79
+ const s = y * 2 * size + x * 2;
80
+ dst[y * half + x] = (src[s] + src[s + 1] + src[s + size] + src[s + size + 1] + 2) >> 2;
81
+ }
82
+ }
83
+ levels.push(dst);
84
+ src = dst;
85
+ size = half;
86
+ }
87
+ return levels;
88
+ }
@@ -12,5 +12,12 @@ export declare const RZ_OUTLINE_WIDTH_OFFSET = 24;
12
12
  /** Where the scene-wide colour override sits, in bytes: rgb and a weight, 0
13
13
  * for the material's own edge colour (Engine.setOutlineColor). */
14
14
  export declare const RZ_OUTLINE_COLOR_OFFSET = 32;
15
+ /** Where a style graph's outline (ShaderGraph.outline) sits, in bytes: the
16
+ * textured flag at 28, then its shadow colour at 48. Unset, a hull draws its
17
+ * PMX edge colour flat, as MMD does. */
18
+ export declare const RZ_OUTLINE_TEXTURED_OFFSET = 28;
19
+ export declare const RZ_OUTLINE_SHADOW_OFFSET = 48;
20
+ /** The hull's own block, in floats. */
21
+ export declare const RZ_OUTLINE_UNIFORM_FLOATS = 16;
15
22
  export declare function outlineShaderWgsl(): string;
16
23
  //# sourceMappingURL=outline.d.ts.map
@@ -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;AAC5C;yCACyC;AACzC,eAAO,MAAM,uBAAuB,KAAK,CAAA;AACzC;mEACmE;AACnE,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC,wBAAgB,iBAAiB,IAAI,MAAM,CAoM1C"}
1
+ {"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/outline.ts"],"names":[],"mappings":"AAqBA;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAC5C;yCACyC;AACzC,eAAO,MAAM,uBAAuB,KAAK,CAAA;AACzC;mEACmE;AACnE,eAAO,MAAM,uBAAuB,KAAK,CAAA;AACzC;;yCAEyC;AACzC,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAC5C,eAAO,MAAM,wBAAwB,KAAK,CAAA;AAC1C,uCAAuC;AACvC,eAAO,MAAM,yBAAyB,KAAK,CAAA;AAE3C,wBAAgB,iBAAiB,IAAI,MAAM,CAgP1C"}
@@ -30,6 +30,13 @@ export const RZ_OUTLINE_WIDTH_OFFSET = 24;
30
30
  /** Where the scene-wide colour override sits, in bytes: rgb and a weight, 0
31
31
  * for the material's own edge colour (Engine.setOutlineColor). */
32
32
  export const RZ_OUTLINE_COLOR_OFFSET = 32;
33
+ /** Where a style graph's outline (ShaderGraph.outline) sits, in bytes: the
34
+ * textured flag at 28, then its shadow colour at 48. Unset, a hull draws its
35
+ * PMX edge colour flat, as MMD does. */
36
+ export const RZ_OUTLINE_TEXTURED_OFFSET = 28;
37
+ export const RZ_OUTLINE_SHADOW_OFFSET = 48;
38
+ /** The hull's own block, in floats. */
39
+ export const RZ_OUTLINE_UNIFORM_FLOATS = 16;
33
40
  export function outlineShaderWgsl() {
34
41
  return /* wgsl */ `
35
42
  ${DISSOLVE_WGSL}
@@ -59,17 +66,30 @@ struct MaterialUniforms {
59
66
  * scales the author's edgeSize rather than replacing it, so a model's thin
60
67
  * and thick lines keep their proportion. */
61
68
  widthScale: f32,
62
- _padding3: f32,
69
+ /** 1: a style graph's outline, the game's (Uber pass "Outline"): edgeSize is
70
+ * a world width, held between 1.2 and 3 px of a 1920-wide frame, and the
71
+ * colour is the picture × lerp(shadowColor, edgeColor, the key's
72
+ * half-Lambert). 0: the PMX edge, flat. */
73
+ textured: f32,
63
74
  /** The scene's colour in place of edgeColor.rgb, by .a (0 or 1). The
64
75
  * material's own alpha still holds — a line its author made sheer stays so. */
65
76
  colorOverride: vec4f,
77
+ shadowColor: vec4f,
66
78
  };
67
79
 
80
+ struct OutlineLight { direction: vec4f, color: vec4f, };
81
+ /** The head of the scene's light block: the key is lights[0]. */
82
+ struct OutlineLights { ambientColor: vec4f, lights: array<OutlineLight, 4>, };
83
+
68
84
  @group(0) @binding(0) var<uniform> camera: CameraUniforms;
69
85
  @group(0) @binding(1) var edgeSampler: sampler;
86
+ @group(0) @binding(2) var<uniform> light: OutlineLights;
70
87
  @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
71
88
  @group(2) @binding(0) var<uniform> material: MaterialUniforms;
72
89
  @group(2) @binding(1) var diffuseTexture: texture_2d<f32>;
90
+ /** A look's outline tint (ShaderGraph.outline.tint): one of its images, white
91
+ * when it names none. */
92
+ @group(2) @binding(2) var tintTexture: texture_2d<f32>;
73
93
 
74
94
  struct VertexOutput {
75
95
  @builtin(position) position: vec4f,
@@ -84,6 +104,7 @@ struct VertexOutput {
84
104
  /** How much of a pixel-wide line this one really is (1 = all of it): a line
85
105
  * held at the minimum width fades by what it was short. */
86
106
  @location(3) coverage: f32,
107
+ @location(4) normal: vec3f,
87
108
  };
88
109
 
89
110
  /**
@@ -106,6 +127,11 @@ const RZ_OUTLINE_FULL_FIGURE = 12.5;
106
127
  * the shortfall, so far lines thin out without aliasing into dashes. */
107
128
  const RZ_OUTLINE_MIN_PX = 1.5;
108
129
 
130
+ fn safe_normal2(v: vec2f) -> vec2f {
131
+ let l = length(v);
132
+ return select(vec2f(0.0), v / l, l > 1e-6);
133
+ }
134
+
109
135
  @vertex fn vs(
110
136
  @location(0) position: vec3f,
111
137
  @location(1) normal: vec3f,
@@ -165,21 +191,40 @@ const RZ_OUTLINE_MIN_PX = 1.5;
165
191
  // once the frame is taller than a figure. The PMX per-vertex edge scale
166
192
  // multiplies it all, as vertex colour alpha does in the game.
167
193
  let w = max(clipPos.w, 1e-4);
168
- let breakDepth = RZ_OUTLINE_FULL_FIGURE * camera.projection[1][1];
169
- let nearNdc = viewNormal.xy * (material.edgeSize * material.widthScale * outlineNormal.w * 4.0 / refViewport);
170
- var ndc = nearNdc * (min(w, breakDepth) / w);
171
-
172
- // Antialiased minimum (the game's _OutlineAntialias): held at the minimum
173
- // width and faded by the shortfall, never thinner. The game fades by
174
- // sqrt(coverage.x · coverage.y), per axis; for one width that is the ratio
175
- // itself.
176
- let px = length(ndc * deviceViewport * 0.5);
177
- let minPx = RZ_OUTLINE_MIN_PX * max(1.0, camera.viewportHeight / 1080.0);
194
+ var ndc: vec2f;
178
195
  var coverage = 1.0;
179
- if (px < minPx) {
180
- coverage = px / minPx;
181
- ndc = select(vec2f(0.0), ndc * (minPx / px), px > 1e-6);
196
+ if (material.textured > 0.5) {
197
+ // The game's: a view-space offset of edgeSize world units along the view
198
+ // normal, projected, then each axis held between 1.2 px and 1.2 px ×
199
+ // (1 + _OutlineMaxOffsetMultiplier, 1.5) of a 1920-wide frame; a line the
200
+ // minimum had to push fades by how far it was pushed.
201
+ let off = viewNormal.xy * material.edgeSize * material.widthScale * outlineNormal.w;
202
+ let d1 = vec2f(off.x * camera.projection[0][0], off.y * camera.projection[1][1]) / w;
203
+ // the minimum along the push direction (the game's dir · 2/screen · 1.2)
204
+ let pxNdc = 2.0 / refViewport * 1.2 * safe_normal2(viewNormal.xy);
205
+ let mx = abs(pxNdc) * 2.5;
206
+ let o = min(mx, max(abs(d1), abs(pxNdc)));
207
+ // fadeOut = min(length(saturate(off − |d1|))·75, 1), on clip offsets (× w)
208
+ // in game units: w here is MMD units, 8 to the game's metre
209
+ coverage = 1.0 - min(length(saturate((o - abs(d1)) * w / 8.0)) * 75.0, 1.0);
210
+ ndc = o * select(vec2f(1.0), sign(d1), abs(d1) > vec2f(0.0));
211
+ } else {
212
+ let breakDepth = RZ_OUTLINE_FULL_FIGURE * camera.projection[1][1];
213
+ let nearNdc = viewNormal.xy * (material.edgeSize * material.widthScale * outlineNormal.w * 4.0 / refViewport);
214
+ ndc = nearNdc * (min(w, breakDepth) / w);
215
+
216
+ // Antialiased minimum (the game's _OutlineAntialias): held at the minimum
217
+ // width and faded by the shortfall, never thinner. The game fades by
218
+ // sqrt(coverage.x · coverage.y), per axis; for one width that is the ratio
219
+ // itself.
220
+ let px = length(ndc * deviceViewport * 0.5);
221
+ let minPx = RZ_OUTLINE_MIN_PX * max(1.0, camera.viewportHeight / 1080.0);
222
+ if (px < minPx) {
223
+ coverage = px / minPx;
224
+ ndc = select(vec2f(0.0), ndc * (minPx / px), px > 1e-6);
225
+ }
182
226
  }
227
+ output.normal = worldNormal;
183
228
  output.coverage = coverage;
184
229
  output.position = vec4f(clipPos.xy + ndc * w, clipPos.z, clipPos.w);
185
230
  output.uv = uv;
@@ -211,7 +256,8 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
211
256
  // bind-pose position — three passes, one rule, or they disagree about which
212
257
  // pieces are still there.
213
258
  if (material.dissolve < 0.9995 && input.faceT > material.dissolve) { discard; }
214
- let texA = textureSample(diffuseTexture, edgeSampler, input.uv).a;
259
+ let tex = textureSample(diffuseTexture, edgeSampler, input.uv);
260
+ let texA = tex.a;
215
261
  if (texA < 0.05) {
216
262
  discard;
217
263
  }
@@ -220,7 +266,12 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
220
266
  // alpha above), it is drawn right after the surface it traces so what lies
221
267
  // under a faded line is already there, and 4× MSAA would quantize a fade to
222
268
  // four steps.
223
- let rgb = mix(material.edgeColor.rgb, material.colorOverride.rgb, material.colorOverride.a);
269
+ var rgb = mix(material.edgeColor.rgb, material.colorOverride.rgb, material.colorOverride.a);
270
+ if (material.textured > 0.5) {
271
+ let lit = dot(-light.lights[0].direction.xyz, normalize(input.normal)) * 0.5 + 0.5;
272
+ let tint = textureSample(tintTexture, edgeSampler, input.uv).rgb;
273
+ rgb = tex.rgb * tint * mix(material.shadowColor.rgb, rgb, lit);
274
+ }
224
275
  out.color = vec4f(rgb, material.edgeColor.a * texA * input.coverage);
225
276
  out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
226
277
  ${sceneIdPadWgsl("out")} return out;
@@ -0,0 +1,34 @@
1
+ import { type CastLayout } from "./particles";
2
+ export declare const SIM_MAX = 1024;
3
+ /** rgba16float, ping-ponged. Two of these is the whole memory cost. */
4
+ export declare const SIM_FORMAT: GPUTextureFormat;
5
+ /** `// @sim 256` — the grid's resolution, in texels per side. */
6
+ export declare function parseSimSize(wgsl: string, max: number): number;
7
+ /** Whether this effect drives a grid at all. */
8
+ export declare function simEntryPoint(wgsl: string): boolean;
9
+ /**
10
+ * `rzSim(uv)`, for a shader that READS the grid rather than steps it.
11
+ *
12
+ * Always compiled in, even for an effect with no grid — it then samples a 1×1
13
+ * of zeroes. An accessor that exists only sometimes is one an author has to
14
+ * guard, and a missing function is a compile error rather than a blank result.
15
+ */
16
+ export declare function simReadApi(group: number, tex: number, samp: number, size: number): string;
17
+ /** `rzTime`/`rzDt`, for a module that has no clock of its own — the field pass.
18
+ * Same reason as above: a spliced simStep refers to them. */
19
+ export declare function simClockApi(timeExpr: string): string;
20
+ /**
21
+ * The step shader: one invocation per texel, once per frame.
22
+ *
23
+ * The previous grid is bound as a SAMPLED texture and the next as a storage
24
+ * texture, which is what makes the two different resources and the whole thing
25
+ * legal — a shader cannot read and write one texture coherently, and the
26
+ * ping-pong is not an optimisation but the only correct way to do this.
27
+ *
28
+ * Sampled rather than only fetched, because advection needs to read BETWEEN
29
+ * texels: a semi-Lagrangian step asks "what was at the place this parcel came
30
+ * from", and that place is almost never a texel centre. Point-sampling it is
31
+ * what turns smoke into a staircase.
32
+ */
33
+ export declare function buildSimShader(wgsl: string, size: number, _cast: CastLayout): string;
34
+ //# sourceMappingURL=sim.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sim.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/sim.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAgC7C,eAAO,MAAM,OAAO,OAAO,CAAA;AAC3B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9D;AAED,gDAAgD;AAChD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6BzF;AAED;8DAC8D;AAC9D,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKpD;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAwDpF"}