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.
- package/dist/camera.d.ts +1 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +9 -5
- package/dist/engine.d.ts +20 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +91 -28
- package/dist/graph/presets/body.js +1 -1
- package/dist/graph/presets/cloth_rough.js +1 -1
- package/dist/graph/presets/cloth_smooth.js +1 -1
- package/dist/graph/presets/face.js +1 -1
- package/dist/graph/presets/metal.js +1 -1
- package/dist/graph/presets/pool_floor.d.ts +3 -0
- package/dist/graph/presets/pool_floor.d.ts.map +1 -0
- package/dist/graph/presets/pool_floor.js +42 -0
- package/dist/graph/presets/water.d.ts +3 -0
- package/dist/graph/presets/water.d.ts.map +1 -0
- package/dist/graph/presets/water.js +31 -0
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +84 -6
- package/dist/graph/schema.d.ts +14 -0
- package/dist/graph/schema.d.ts.map +1 -1
- package/dist/physics/autofit.d.ts +147 -0
- package/dist/physics/autofit.d.ts.map +1 -0
- package/dist/physics/autofit.js +501 -0
- package/dist/shaders/agx-lut.d.ts +11 -0
- package/dist/shaders/agx-lut.d.ts.map +1 -0
- package/dist/shaders/agx-lut.js +29 -0
- package/dist/shaders/dfg_lut.d.ts +3 -0
- package/dist/shaders/dfg_lut.d.ts.map +1 -0
- package/dist/shaders/dfg_lut.js +129 -0
- package/dist/shaders/ltc_mag_lut.d.ts +3 -0
- package/dist/shaders/ltc_mag_lut.d.ts.map +1 -0
- package/dist/shaders/ltc_mag_lut.js +1033 -0
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +176 -7
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +4 -2
- package/dist/shaders/passes/field-blit.d.ts +26 -0
- package/dist/shaders/passes/field-blit.d.ts.map +1 -0
- package/dist/shaders/passes/field-blit.js +65 -0
- package/dist/shaders/passes/ghosts.d.ts +22 -0
- package/dist/shaders/passes/ghosts.d.ts.map +1 -0
- package/dist/shaders/passes/ghosts.js +75 -0
- package/dist/shaders/passes/ground-noise.d.ts +7 -0
- package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
- package/dist/shaders/passes/ground-noise.js +88 -0
- package/dist/shaders/passes/outline.d.ts +7 -0
- package/dist/shaders/passes/outline.d.ts.map +1 -1
- package/dist/shaders/passes/outline.js +67 -16
- package/dist/shaders/passes/sim.d.ts +34 -0
- package/dist/shaders/passes/sim.d.ts.map +1 -0
- package/dist/shaders/passes/sim.js +169 -0
- package/dist/shaders/score-api.d.ts +10 -0
- package/dist/shaders/score-api.d.ts.map +1 -0
- package/dist/shaders/score-api.js +114 -0
- package/dist/shaders/shadow-api.d.ts +29 -0
- package/dist/shaders/shadow-api.d.ts.map +1 -0
- package/dist/shaders/shadow-api.js +112 -0
- package/package.json +1 -1
- package/src/camera.ts +9 -5
- package/src/engine.ts +109 -30
- package/src/graph/presets/body.ts +1 -1
- package/src/graph/presets/cloth_rough.ts +1 -1
- package/src/graph/presets/cloth_smooth.ts +1 -1
- package/src/graph/presets/face.ts +1 -1
- package/src/graph/presets/metal.ts +1 -1
- package/src/graph/registry.ts +85 -6
- package/src/graph/schema.ts +14 -0
- package/src/shaders/materials/nodes.ts +176 -7
- package/src/shaders/passes/composite.ts +4 -2
- package/src/shaders/passes/outline.ts +67 -16
- 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>,
|
|
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(
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
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
|
-
|
|
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"}
|