reze-engine 0.54.16 → 0.55.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 +13 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +53 -2
- package/dist/engine.d.ts +317 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1263 -27
- 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/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +10 -0
- package/dist/overlay.d.ts +200 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +806 -0
- 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/pmx-document.d.ts +218 -0
- package/dist/pmx-document.d.ts.map +1 -0
- package/dist/pmx-document.js +685 -0
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +18 -1
- 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/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/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +63 -17
- package/dist/shaders/passes/overlay.d.ts +3 -0
- package/dist/shaders/passes/overlay.d.ts.map +1 -0
- package/dist/shaders/passes/overlay.js +164 -0
- 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/passes/wireframe.d.ts +2 -0
- package/dist/shaders/passes/wireframe.d.ts.map +1 -0
- package/dist/shaders/passes/wireframe.js +107 -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/package.json +1 -1
- package/src/camera.ts +60 -2
- package/src/engine.ts +1359 -25
- package/src/index.ts +50 -0
- package/src/model.ts +9 -0
- package/src/overlay.ts +1000 -0
- package/src/pmx-document.ts +882 -0
- package/src/shaders/passes/composite.ts +18 -1
- package/src/shaders/passes/ground.ts +65 -17
- package/src/shaders/passes/overlay.ts +166 -0
- package/src/shaders/passes/wireframe.ts +108 -0
|
@@ -109,7 +109,7 @@ override APPLY_GAMMA: bool = true;
|
|
|
109
109
|
// monotone-cubic (Fritsch–Carlson) fit through the same 14 anchors — same values, C1
|
|
110
110
|
// continuity kills the banding — sampled with hardware linear filtering.
|
|
111
111
|
@group(0) @binding(5) var filmicLut: texture_2d<f32>;
|
|
112
|
-
// viewU[0] = (exposure, invGamma,
|
|
112
|
+
// viewU[0] = (exposure, invGamma, grain amount, grain seed); viewU[1] = (tint.rgb, intensity)
|
|
113
113
|
// viewU[2] = (background.rgb, mode) — display-space sRGB, composited UNDER the
|
|
114
114
|
// scene post-tonemap. BASE-layer mode: 0 transparent (DOM shows),
|
|
115
115
|
// 1 solid color, 2 = 360 equirect skybox sampled by view ray. A user
|
|
@@ -495,6 +495,23 @@ const COMPOSITE_BODY = /* wgsl */ `
|
|
|
495
495
|
if (APPLY_GAMMA) {
|
|
496
496
|
disp = pow(disp, vec3f(viewU[0].y));
|
|
497
497
|
}
|
|
498
|
+
// ── Film grain, on the SCENE ONLY ─────────────────────────────────────────
|
|
499
|
+
//
|
|
500
|
+
// Applied here, before the background is composited under, so it rides on what
|
|
501
|
+
// the engine drew and nothing else. That placement is the whole point when the
|
|
502
|
+
// background is footage: the plate came off a real sensor and already carries
|
|
503
|
+
// its own grain, and a second helping over the top would grade the photograph
|
|
504
|
+
// rather than match it. A clean CG figure on a grainy plate is one of the
|
|
505
|
+
// loudest tells there is — the noise gives it away long before the geometry.
|
|
506
|
+
//
|
|
507
|
+
// Multiplicative and weighted toward the mid-tones, which is how film behaves:
|
|
508
|
+
// little grain in the blacks, and the highlights clip it off.
|
|
509
|
+
if (viewU[0].z > 0.0) {
|
|
510
|
+
let gp = fragCoord.xy + vec2f(viewU[0].w, viewU[0].w * 1.7);
|
|
511
|
+
let gn = fract(sin(dot(gp, vec2f(12.9898, 78.233))) * 43758.5453) - 0.5;
|
|
512
|
+
let glum = dot(disp, vec3f(0.2126, 0.7152, 0.0722));
|
|
513
|
+
disp = max(disp * (1.0 + gn * viewU[0].z * 4.0 * glum * (1.0 - glum)), vec3f(0.0));
|
|
514
|
+
}
|
|
498
515
|
// Composite over the background in display space (premultiplied out). The
|
|
499
516
|
// background is TWO layers: a base (transparent / solid color / 360 equirect)
|
|
500
517
|
// and an optional user WGSL effect over-composited onto it.
|
|
@@ -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,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
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ground.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ground.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ground.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/ground.ts"],"names":[],"mappings":"AAmEA,wBAAgB,gBAAgB,IAAI,MAAM,CA2TzC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,QAAS,CAAA;AACxC,eAAO,MAAM,gBAAgB,QAAS,CAAA;AAEtC;;;oEAGoE;AACpE,eAAO,MAAM,iBAAiB,OAAO,CAAA;AAErC;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,+oCAiClC,CAAA"}
|
|
@@ -8,6 +8,59 @@ import { WORLD_AMBIENT_WGSL } from "../lights";
|
|
|
8
8
|
// A FUNCTION, not a constant, since the id attachment is a device capability:
|
|
9
9
|
// the struct this returns depends on what the probe at init found, and a string
|
|
10
10
|
// baked at import time cannot know that. Called once, when the module is built.
|
|
11
|
+
// How far the softest setting spreads the taps, in units of the sharp kernel's
|
|
12
|
+
// own step, and how many taps carry that spread. Sixteen is what keeps a wide
|
|
13
|
+
// disk reading as a penumbra instead of sixteen shadows.
|
|
14
|
+
const SOFT_TAPS = 16;
|
|
15
|
+
const SOFT_MAX_SPREAD = 14;
|
|
16
|
+
/**
|
|
17
|
+
* PCF taps for one cascade, emitted per shadow map.
|
|
18
|
+
*
|
|
19
|
+
* At softness 0 this is the 3x3 box the ground has always used — same offsets,
|
|
20
|
+
* same weights, same result to the bit. Above it the taps spread over a Vogel
|
|
21
|
+
* disk whose radius IS the penumbra: an overcast sky throws no edge, and a
|
|
22
|
+
* razor-edged shadow under one is the loudest thing wrong in a composite.
|
|
23
|
+
*
|
|
24
|
+
* The branch is on a uniform, so a scene at softness 0 genuinely takes the
|
|
25
|
+
* nine-tap side rather than masking the wide one — the same reasoning the
|
|
26
|
+
* shadowStrength branch above it already documents.
|
|
27
|
+
*
|
|
28
|
+
* A string rather than a WGSL function because a texture is the one handle type
|
|
29
|
+
* this shader has never passed as a parameter, and the file is already a
|
|
30
|
+
* template that interpolates its constants.
|
|
31
|
+
*
|
|
32
|
+
* `acc` must already be declared and zeroed; the caller reads it back
|
|
33
|
+
* normalised, so there is no `/ 9.0` left at the call site.
|
|
34
|
+
*/
|
|
35
|
+
function pcfWgsl(map, uv, texel, z, acc, pad) {
|
|
36
|
+
const golden = 2.39996323;
|
|
37
|
+
return [
|
|
38
|
+
`if (material.shadowSoftness <= 0.0) {`,
|
|
39
|
+
` let st = ${texel} * 2.0;`,
|
|
40
|
+
` for (var y = -1; y <= 1; y++) {`,
|
|
41
|
+
` for (var x = -1; x <= 1; x++) {`,
|
|
42
|
+
// ...Level, not the implicit-derivative form: identical on a single-mip
|
|
43
|
+
// shadow map, and legal inside a branch.
|
|
44
|
+
` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, ${uv} + vec2f(f32(x), f32(y)) * st, ${z});`,
|
|
45
|
+
` }`,
|
|
46
|
+
` }`,
|
|
47
|
+
` ${acc} *= ${1 / 9};`,
|
|
48
|
+
`} else {`,
|
|
49
|
+
` let radius = ${texel} * 2.0 * (1.0 + material.shadowSoftness * ${SOFT_MAX_SPREAD}.0);`,
|
|
50
|
+
` for (var s = 0; s < ${SOFT_TAPS}; s++) {`,
|
|
51
|
+
` let fs = f32(s);`,
|
|
52
|
+
// sqrt of the index spaces the ring radii evenly by AREA; the golden angle
|
|
53
|
+
// keeps successive taps from lining up into spokes.
|
|
54
|
+
` let r = sqrt((fs + 0.5) * ${1 / SOFT_TAPS});`,
|
|
55
|
+
` let a = fs * ${golden} + rot;`,
|
|
56
|
+
` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, ${uv} + vec2f(cos(a), sin(a)) * (r * radius), ${z});`,
|
|
57
|
+
` }`,
|
|
58
|
+
` ${acc} *= ${1 / SOFT_TAPS};`,
|
|
59
|
+
`}`,
|
|
60
|
+
]
|
|
61
|
+
.map((l) => pad + l)
|
|
62
|
+
.join("\n");
|
|
63
|
+
}
|
|
11
64
|
export function groundShaderWgsl() {
|
|
12
65
|
return /* wgsl */ `
|
|
13
66
|
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
|
|
@@ -20,7 +73,7 @@ struct GroundShadowMat {
|
|
|
20
73
|
gridLineColor: vec3f, mirror: f32,
|
|
21
74
|
// farCascade: 1 while a stage is loaded, 0 otherwise. See the branch below —
|
|
22
75
|
// with no stage the far map is never drawn into, so its taps are known.
|
|
23
|
-
mirrorBlur: f32, farCascade: f32,
|
|
76
|
+
mirrorBlur: f32, farCascade: f32, shadowSoftness: f32, _mb2: f32,
|
|
24
77
|
// Every shadow caster in one sphere, refreshed per frame. w = radius; 0 means
|
|
25
78
|
// nothing casts, negative means "do not use this" (a rigid caster has no
|
|
26
79
|
// sphere, so a scene with a stage keeps the taps). See rzShadowPossible.
|
|
@@ -150,6 +203,11 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
|
|
|
150
203
|
// The same reasoning the noise tint below already got, applied to the term
|
|
151
204
|
// that costs a hundred times more.
|
|
152
205
|
if (material.shadowStrength > 0.0 && shadowPossible) {
|
|
206
|
+
// Per-pixel rotation for the soft disk, so its rings break up into fine noise
|
|
207
|
+
// rather than banding. Interleaved gradient noise: a function of the pixel
|
|
208
|
+
// alone, so a still camera gives a still shadow — the sharp path ignores it.
|
|
209
|
+
let ign = fract(52.9829189 * fract(dot(i.position.xy, vec2f(0.06711056, 0.00583715))));
|
|
210
|
+
let rot = ign * 6.28318530718;
|
|
153
211
|
// The far cascade's taps, skipped entirely when nothing ever drew into it.
|
|
154
212
|
//
|
|
155
213
|
// This branch is the expensive one on a wide floor: it runs wherever the NEAR
|
|
@@ -163,32 +221,20 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
|
|
|
163
221
|
if (material.farCascade > 0.0 && frustum < 1.0 && frustum1 > 0.0) {
|
|
164
222
|
let suv1 = vec2f(ndc1.x * 0.5 + 0.5, 0.5 - ndc1.y * 0.5);
|
|
165
223
|
let suv1_c = clamp(suv1, vec2f(0.02), vec2f(0.98));
|
|
166
|
-
let st1 = ${1 / SHADOW_CASCADES[SHADOW_CASCADES.length - 1].mapSize} * 2.0;
|
|
167
224
|
let compareZ1 = ndc1.z - 0.0035;
|
|
168
225
|
var acc1 = 0.0;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
acc1 += textureSampleCompareLevel(shadowMapFar, shadowSampler, suv1_c + vec2f(f32(x), f32(y)) * st1, compareZ1);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
vis = mix(1.0, acc1 * (1.0 / 9.0), frustum1);
|
|
226
|
+
${pcfWgsl("shadowMapFar", "suv1_c", `${1 / SHADOW_CASCADES[SHADOW_CASCADES.length - 1].mapSize}`, "compareZ1", "acc1", " ")}
|
|
227
|
+
vis = mix(1.0, acc1, frustum1);
|
|
175
228
|
}
|
|
176
229
|
if (frustum > 0.0) {
|
|
177
230
|
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
178
231
|
let suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));
|
|
179
|
-
let st = material.pcfTexel * 2.0;
|
|
180
232
|
let compareZ = ndc.z - 0.0035;
|
|
181
233
|
var acc = 0.0;
|
|
182
|
-
|
|
183
|
-
for (var x = -1; x <= 1; x++) {
|
|
184
|
-
// ...Level, not the implicit-derivative form: identical on a single-mip
|
|
185
|
-
// shadow map, and legal inside this branch.
|
|
186
|
-
acc += textureSampleCompareLevel(shadowMap, shadowSampler, suv_c + vec2f(f32(x), f32(y)) * st, compareZ);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
234
|
+
${pcfWgsl("shadowMap", "suv_c", "material.pcfTexel", "compareZ", "acc", " ")}
|
|
189
235
|
// The base is whatever the far cascade decided, so the near border blends
|
|
190
236
|
// cascade to cascade rather than snapping to lit mid-floor.
|
|
191
|
-
vis = mix(vis, acc
|
|
237
|
+
vis = mix(vis, acc, frustum);
|
|
192
238
|
}
|
|
193
239
|
}
|
|
194
240
|
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const OVERLAY_SHADER_WGSL = "\nstruct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _pad: f32 };\n// 16 bytes exactly. Scalars rather than a vec3f tail: a vec3f aligns to 16 and\n// would push the struct past the buffer the pipeline is handed.\nstruct OverlayUniforms { viewport: vec2f, dashPeriodPx: f32, _pad0: f32 };\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var<uniform> overlay: OverlayUniforms;\n\nstruct VSOut {\n @builtin(position) pos: vec4f,\n @location(0) @interpolate(flat) color: vec4f,\n}\n\nfn qrot(q: vec4f, v: vec3f) -> vec3f {\n let t = 2.0 * cross(q.xyz, v);\n return v + q.w * t + cross(q.xyz, t);\n}\n\n// Placement mode, carried per VERTEX because it belongs to the shape rather than\n// to the copy.\n//\n// 0 world \u2014 rotate, scale, translate. The shapes that ARE geometry:\n// rigidbody volumes, joint crosses, plain lines.\n// 3 facing \u2014 a flat filled shape in the camera's plane, radius scale.x.\n// The bone marker's solid centre.\n// 5 solid \u2014 placed in world like mode 0, but filled: the rigidbody\n// volumes, which read as volumes rather than as a cage of lines.\n// 4 annulus \u2014 the marker's ring: world radius scale.x, and a stroke that is\n// a PIXEL width like every other line, so the two links leave it\n// as a continuation of the same stroke rather than a step. The\n// inner and outer rims are offset radially in screen space,\n// which is what lets a small circle carry a thick stroke \u2014 the\n// stroked polyline that preceded it tore into a sunburst as soon\n// as the width passed the segment length.\n// 2 link \u2014 the taper from a bone to its child: along the bone, splayed\n// across it in whichever direction the camera can actually see,\n// so the two lines never collapse into one.\nfn placeWorld(p: vec3f, capSign: f32, iRot: vec4f, iPos: vec4f, iScale: vec4f, mode: f32) -> vec3f {\n let right = vec3f(camera.view[0].x, camera.view[1].x, camera.view[2].x);\n let up = vec3f(camera.view[0].y, camera.view[1].y, camera.view[2].y);\n\n if (mode > 2.5) {\n return iPos.xyz + (right * p.x + up * p.y) * iScale.x;\n }\n if (mode > 1.5) {\n let boneDir = normalize(qrot(iRot, vec3f(0.0, 1.0, 0.0)));\n let camFwd = vec3f(camera.view[0].z, camera.view[1].z, camera.view[2].z);\n var splay = cross(boneDir, camFwd);\n let sl = length(splay);\n // Looking straight down the bone there is no visible across, and any\n // perpendicular does \u2014 the taper is a dot on screen either way.\n splay = select(right, splay / max(sl, 1e-6), sl > 1e-4);\n return iPos.xyz + splay * (p.x * iScale.x) + boneDir * (p.y * iScale.y);\n }\n return qrot(iRot, p * iScale.xyz + vec3f(0.0, capSign * iPos.w, 0.0)) + iPos.xyz;\n}\n\n@vertex fn vs(\n @location(0) pos: vec3f,\n @location(1) dir: vec3f,\n @location(2) caps: vec2f,\n @location(3) side: f32,\n @location(4) t: f32,\n @location(5) mode: f32,\n @location(6) iRot: vec4f,\n @location(7) iPos: vec4f,\n @location(8) iScale: vec4f,\n @location(9) iColor: vec4f,\n) -> VSOut {\n let vp = camera.projection * camera.view;\n let half = 0.5 * overlay.viewport;\n\n var out: VSOut;\n out.color = iColor;\n\n // A solid volume is placed in world, like a stroked shape, but filled.\n if (mode > 4.5) {\n out.pos = vp * vec4f(placeWorld(pos, caps.x, iRot, iPos, iScale, 0.0), 1.0);\n return out;\n }\n\n // The marker's ring: a circle of world radius, stroked to a pixel width.\n if (mode > 3.5) {\n let centre = vp * vec4f(iPos.xyz, 1.0);\n let w = max(abs(centre.w), 1e-6);\n let pxPerWorld = camera.projection[1].y * half.y / w;\n // side is -1 on the inner rim and +1 on the outer.\n let radiusPx = iScale.x * pxPerWorld + side * abs(iScale.w) * 0.5;\n let radial = normalize(pos.xy);\n out.pos = vec4f(centre.xy + ((radial * radiusPx) / half) * centre.w, centre.z, centre.w);\n return out;\n }\n\n let c0 = vp * vec4f(placeWorld(pos, caps.x, iRot, iPos, iScale, mode), 1.0);\n\n // A filled vertex is already where it belongs.\n if (mode > 2.5) {\n out.pos = c0;\n return out;\n }\n\n let c1 = vp * vec4f(placeWorld(pos + dir, caps.y, iRot, iPos, iScale, mode), 1.0);\n let w0 = max(abs(c0.w), 1e-6);\n let w1 = max(abs(c1.w), 1e-6);\n let s0 = (c0.xy / w0) * half;\n let s1 = (c1.xy / w1) * half;\n let delta = s1 - s0;\n let segLenPx = max(length(delta), 1e-6);\n let tangent = delta / segLenPx;\n let normalPx = vec2f(-tangent.y, tangent.x);\n let offsetPx = normalPx * side * abs(iScale.w) * 0.5;\n out.pos = vec4f(c0.xy + (offsetPx / half) * c0.w, c0.z, c0.w);\n return out;\n}\n\n@fragment fn fs(in: VSOut) -> @location(0) vec4f {\n return vec4f(in.color.rgb * in.color.a, in.color.a);\n}\n";
|
|
2
|
+
export declare const OVERLAY_COMPOSITE_SHADER_WGSL = "\n@group(0) @binding(0) var overlayTex: texture_2d<f32>;\n\n@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {\n let x = f32((vi & 1u) << 2u) - 1.0;\n let y = f32((vi & 2u) << 1u) - 1.0;\n return vec4f(x, y, 0.0, 1.0);\n}\n\n@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {\n return textureLoad(overlayTex, vec2<i32>(p.xy), 0);\n}\n";
|
|
3
|
+
//# sourceMappingURL=overlay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/overlay.ts"],"names":[],"mappings":"AA8BA,eAAO,MAAM,mBAAmB,81JAuH/B,CAAA;AAID,eAAO,MAAM,6BAA6B,kYAYzC,CAAA"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Editor overlays — bones, rigidbodies and joints — as one instanced pass.
|
|
2
|
+
//
|
|
3
|
+
// Two kinds of geometry, because thick lines and thick rings want different
|
|
4
|
+
// answers.
|
|
5
|
+
//
|
|
6
|
+
// STROKED shapes (mode 0, 2) expand each segment into a screen-space quad, so
|
|
7
|
+
// their width is a pixel count and holds at any zoom. That works while a segment
|
|
8
|
+
// is longer on screen than the stroke is wide; past that, neighbouring quads
|
|
9
|
+
// meet at a corner nothing mitres and a circle drawn this way turns into a
|
|
10
|
+
// sunburst. So the one shape that must be both small and thick — the bone
|
|
11
|
+
// marker — is not stroked at all.
|
|
12
|
+
//
|
|
13
|
+
// FILLED shapes (mode 3) are triangles already: the marker's ring is an annulus
|
|
14
|
+
// and its centre is a disc, both facing the camera. No joins exist to get wrong,
|
|
15
|
+
// and the ring can be as heavy as it likes.
|
|
16
|
+
//
|
|
17
|
+
// The layer is 4x multisampled and composited over the finished frame rather
|
|
18
|
+
// than drawn into it: the swapchain is single-sample and an MSAA attachment
|
|
19
|
+
// cannot load from one. So fragments here are PREMULTIPLIED — rgb already
|
|
20
|
+
// carries alpha — and the two blends compose correctly, once against a
|
|
21
|
+
// transparent layer and once onto the frame.
|
|
22
|
+
//
|
|
23
|
+
// The capsule is why `caps` exists. A PMX capsule is a radius and a cylinder
|
|
24
|
+
// length, so scaling a unit capsule non-uniformly would stretch its hemispheres
|
|
25
|
+
// into ellipsoids. Instead the unit shape carries both caps at the origin and
|
|
26
|
+
// each vertex names which cap it belongs to (-1, 0, +1); the VS pushes it along
|
|
27
|
+
// local Y by that sign times `extent`. A segment can straddle the two — the
|
|
28
|
+
// cylinder's side lines run from +1 to -1 — so `caps` holds the sign at BOTH
|
|
29
|
+
// ends, not one.
|
|
30
|
+
export const OVERLAY_SHADER_WGSL = /* wgsl */ `
|
|
31
|
+
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _pad: f32 };
|
|
32
|
+
// 16 bytes exactly. Scalars rather than a vec3f tail: a vec3f aligns to 16 and
|
|
33
|
+
// would push the struct past the buffer the pipeline is handed.
|
|
34
|
+
struct OverlayUniforms { viewport: vec2f, dashPeriodPx: f32, _pad0: f32 };
|
|
35
|
+
|
|
36
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
37
|
+
@group(0) @binding(1) var<uniform> overlay: OverlayUniforms;
|
|
38
|
+
|
|
39
|
+
struct VSOut {
|
|
40
|
+
@builtin(position) pos: vec4f,
|
|
41
|
+
@location(0) @interpolate(flat) color: vec4f,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn qrot(q: vec4f, v: vec3f) -> vec3f {
|
|
45
|
+
let t = 2.0 * cross(q.xyz, v);
|
|
46
|
+
return v + q.w * t + cross(q.xyz, t);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Placement mode, carried per VERTEX because it belongs to the shape rather than
|
|
50
|
+
// to the copy.
|
|
51
|
+
//
|
|
52
|
+
// 0 world — rotate, scale, translate. The shapes that ARE geometry:
|
|
53
|
+
// rigidbody volumes, joint crosses, plain lines.
|
|
54
|
+
// 3 facing — a flat filled shape in the camera's plane, radius scale.x.
|
|
55
|
+
// The bone marker's solid centre.
|
|
56
|
+
// 5 solid — placed in world like mode 0, but filled: the rigidbody
|
|
57
|
+
// volumes, which read as volumes rather than as a cage of lines.
|
|
58
|
+
// 4 annulus — the marker's ring: world radius scale.x, and a stroke that is
|
|
59
|
+
// a PIXEL width like every other line, so the two links leave it
|
|
60
|
+
// as a continuation of the same stroke rather than a step. The
|
|
61
|
+
// inner and outer rims are offset radially in screen space,
|
|
62
|
+
// which is what lets a small circle carry a thick stroke — the
|
|
63
|
+
// stroked polyline that preceded it tore into a sunburst as soon
|
|
64
|
+
// as the width passed the segment length.
|
|
65
|
+
// 2 link — the taper from a bone to its child: along the bone, splayed
|
|
66
|
+
// across it in whichever direction the camera can actually see,
|
|
67
|
+
// so the two lines never collapse into one.
|
|
68
|
+
fn placeWorld(p: vec3f, capSign: f32, iRot: vec4f, iPos: vec4f, iScale: vec4f, mode: f32) -> vec3f {
|
|
69
|
+
let right = vec3f(camera.view[0].x, camera.view[1].x, camera.view[2].x);
|
|
70
|
+
let up = vec3f(camera.view[0].y, camera.view[1].y, camera.view[2].y);
|
|
71
|
+
|
|
72
|
+
if (mode > 2.5) {
|
|
73
|
+
return iPos.xyz + (right * p.x + up * p.y) * iScale.x;
|
|
74
|
+
}
|
|
75
|
+
if (mode > 1.5) {
|
|
76
|
+
let boneDir = normalize(qrot(iRot, vec3f(0.0, 1.0, 0.0)));
|
|
77
|
+
let camFwd = vec3f(camera.view[0].z, camera.view[1].z, camera.view[2].z);
|
|
78
|
+
var splay = cross(boneDir, camFwd);
|
|
79
|
+
let sl = length(splay);
|
|
80
|
+
// Looking straight down the bone there is no visible across, and any
|
|
81
|
+
// perpendicular does — the taper is a dot on screen either way.
|
|
82
|
+
splay = select(right, splay / max(sl, 1e-6), sl > 1e-4);
|
|
83
|
+
return iPos.xyz + splay * (p.x * iScale.x) + boneDir * (p.y * iScale.y);
|
|
84
|
+
}
|
|
85
|
+
return qrot(iRot, p * iScale.xyz + vec3f(0.0, capSign * iPos.w, 0.0)) + iPos.xyz;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@vertex fn vs(
|
|
89
|
+
@location(0) pos: vec3f,
|
|
90
|
+
@location(1) dir: vec3f,
|
|
91
|
+
@location(2) caps: vec2f,
|
|
92
|
+
@location(3) side: f32,
|
|
93
|
+
@location(4) t: f32,
|
|
94
|
+
@location(5) mode: f32,
|
|
95
|
+
@location(6) iRot: vec4f,
|
|
96
|
+
@location(7) iPos: vec4f,
|
|
97
|
+
@location(8) iScale: vec4f,
|
|
98
|
+
@location(9) iColor: vec4f,
|
|
99
|
+
) -> VSOut {
|
|
100
|
+
let vp = camera.projection * camera.view;
|
|
101
|
+
let half = 0.5 * overlay.viewport;
|
|
102
|
+
|
|
103
|
+
var out: VSOut;
|
|
104
|
+
out.color = iColor;
|
|
105
|
+
|
|
106
|
+
// A solid volume is placed in world, like a stroked shape, but filled.
|
|
107
|
+
if (mode > 4.5) {
|
|
108
|
+
out.pos = vp * vec4f(placeWorld(pos, caps.x, iRot, iPos, iScale, 0.0), 1.0);
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The marker's ring: a circle of world radius, stroked to a pixel width.
|
|
113
|
+
if (mode > 3.5) {
|
|
114
|
+
let centre = vp * vec4f(iPos.xyz, 1.0);
|
|
115
|
+
let w = max(abs(centre.w), 1e-6);
|
|
116
|
+
let pxPerWorld = camera.projection[1].y * half.y / w;
|
|
117
|
+
// side is -1 on the inner rim and +1 on the outer.
|
|
118
|
+
let radiusPx = iScale.x * pxPerWorld + side * abs(iScale.w) * 0.5;
|
|
119
|
+
let radial = normalize(pos.xy);
|
|
120
|
+
out.pos = vec4f(centre.xy + ((radial * radiusPx) / half) * centre.w, centre.z, centre.w);
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let c0 = vp * vec4f(placeWorld(pos, caps.x, iRot, iPos, iScale, mode), 1.0);
|
|
125
|
+
|
|
126
|
+
// A filled vertex is already where it belongs.
|
|
127
|
+
if (mode > 2.5) {
|
|
128
|
+
out.pos = c0;
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let c1 = vp * vec4f(placeWorld(pos + dir, caps.y, iRot, iPos, iScale, mode), 1.0);
|
|
133
|
+
let w0 = max(abs(c0.w), 1e-6);
|
|
134
|
+
let w1 = max(abs(c1.w), 1e-6);
|
|
135
|
+
let s0 = (c0.xy / w0) * half;
|
|
136
|
+
let s1 = (c1.xy / w1) * half;
|
|
137
|
+
let delta = s1 - s0;
|
|
138
|
+
let segLenPx = max(length(delta), 1e-6);
|
|
139
|
+
let tangent = delta / segLenPx;
|
|
140
|
+
let normalPx = vec2f(-tangent.y, tangent.x);
|
|
141
|
+
let offsetPx = normalPx * side * abs(iScale.w) * 0.5;
|
|
142
|
+
out.pos = vec4f(c0.xy + (offsetPx / half) * c0.w, c0.z, c0.w);
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@fragment fn fs(in: VSOut) -> @location(0) vec4f {
|
|
147
|
+
return vec4f(in.color.rgb * in.color.a, in.color.a);
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
150
|
+
// Lays the resolved overlay layer over the finished frame. Its input is
|
|
151
|
+
// premultiplied, so the blend is a straight `src + dst * (1 - src.a)`.
|
|
152
|
+
export const OVERLAY_COMPOSITE_SHADER_WGSL = /* wgsl */ `
|
|
153
|
+
@group(0) @binding(0) var overlayTex: texture_2d<f32>;
|
|
154
|
+
|
|
155
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
156
|
+
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
157
|
+
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
158
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
162
|
+
return textureLoad(overlayTex, vec2<i32>(p.xy), 0);
|
|
163
|
+
}
|
|
164
|
+
`;
|
|
@@ -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"}
|