reze-engine 0.43.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -438
- package/dist/camera.d.ts +3 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +33 -8
- package/dist/engine.d.ts +841 -53
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +3498 -451
- package/dist/graph/registry.d.ts +2 -2
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +1 -1
- package/dist/graph/slots.d.ts +0 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +37 -9
- package/dist/hdr.d.ts +18 -0
- package/dist/hdr.d.ts.map +1 -0
- package/dist/hdr.js +162 -0
- package/dist/ibl.d.ts +19 -0
- package/dist/ibl.d.ts.map +1 -0
- package/dist/ibl.js +113 -0
- package/dist/ik-solver.d.ts +2 -1
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/math.d.ts +20 -1
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +23 -16
- package/dist/midi-loader.d.ts +10 -0
- package/dist/midi-loader.d.ts.map +1 -0
- package/dist/midi-loader.js +247 -0
- package/dist/model.d.ts +2 -13
- package/dist/model.d.ts.map +1 -1
- package/dist/param-track.d.ts +48 -0
- package/dist/param-track.d.ts.map +1 -0
- package/dist/param-track.js +80 -0
- package/dist/physics/types.d.ts.map +1 -1
- package/dist/physics/types.js +3 -0
- package/dist/reflection.d.ts +27 -0
- package/dist/reflection.d.ts.map +1 -0
- package/dist/reflection.js +93 -0
- package/dist/shaders/anchor-table.d.ts +56 -0
- package/dist/shaders/anchor-table.d.ts.map +1 -0
- package/dist/shaders/anchor-table.js +128 -0
- package/dist/shaders/cast-api.d.ts +2 -0
- package/dist/shaders/cast-api.d.ts.map +1 -0
- package/dist/shaders/cast-api.js +121 -0
- package/dist/shaders/cast-layout.d.ts +21 -0
- package/dist/shaders/cast-layout.d.ts.map +1 -0
- package/dist/shaders/cast-layout.js +20 -0
- package/dist/shaders/lights.d.ts +79 -0
- package/dist/shaders/lights.d.ts.map +1 -0
- package/dist/shaders/lights.js +269 -0
- package/dist/shaders/lyrics-api.d.ts +39 -0
- package/dist/shaders/lyrics-api.d.ts.map +1 -0
- package/dist/shaders/lyrics-api.js +187 -0
- package/dist/shaders/materials/common.d.ts +2 -4
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +87 -35
- package/dist/shaders/midi-api.d.ts +10 -0
- package/dist/shaders/midi-api.d.ts.map +1 -0
- package/dist/shaders/midi-api.js +114 -0
- package/dist/shaders/passes/composite.d.ts +28 -22
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +165 -138
- package/dist/shaders/passes/cull.d.ts +2 -0
- package/dist/shaders/passes/cull.d.ts.map +1 -0
- package/dist/shaders/passes/cull.js +138 -0
- 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/grid.d.ts +31 -0
- package/dist/shaders/passes/grid.d.ts.map +1 -0
- package/dist/shaders/passes/grid.js +169 -0
- package/dist/shaders/passes/ground.d.ts +13 -1
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +170 -25
- package/dist/shaders/passes/hosted-api.d.ts +57 -0
- package/dist/shaders/passes/hosted-api.d.ts.map +1 -0
- package/dist/shaders/passes/hosted-api.js +166 -0
- package/dist/shaders/passes/id-debug.d.ts +28 -0
- package/dist/shaders/passes/id-debug.d.ts.map +1 -0
- package/dist/shaders/passes/id-debug.js +74 -0
- package/dist/shaders/passes/particles.d.ts +10 -2
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +37 -109
- package/dist/shaders/passes/scene-contract.d.ts +128 -0
- package/dist/shaders/passes/scene-contract.d.ts.map +1 -0
- package/dist/shaders/passes/scene-contract.js +207 -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/trails.d.ts +14 -2
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +55 -90
- 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/shadow-cascades.d.ts +45 -0
- package/dist/shadow-cascades.d.ts.map +1 -0
- package/dist/shadow-cascades.js +70 -0
- package/dist/vmd-loader.d.ts +3 -2
- package/dist/vmd-loader.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/camera.ts +31 -8
- package/src/engine.ts +4015 -556
- package/src/graph/registry.ts +2 -2
- package/src/graph/slots.ts +37 -9
- package/src/hdr.ts +156 -0
- package/src/ibl.ts +115 -0
- package/src/ik-solver.ts +1 -1
- package/src/index.ts +12 -0
- package/src/math.ts +23 -17
- package/src/midi-loader.ts +246 -0
- package/src/model.ts +3 -3
- package/src/param-track.ts +83 -0
- package/src/physics/types.ts +4 -1
- package/src/reflection.ts +94 -0
- package/src/shaders/anchor-table.ts +147 -0
- package/src/shaders/cast-api.ts +123 -0
- package/src/shaders/cast-layout.ts +20 -0
- package/src/shaders/lights.ts +280 -0
- package/src/shaders/lyrics-api.ts +202 -0
- package/src/shaders/materials/common.ts +89 -35
- package/src/shaders/midi-api.ts +116 -0
- package/src/shaders/passes/composite.ts +182 -139
- package/src/shaders/passes/cull.ts +139 -0
- package/src/shaders/passes/grid.ts +178 -0
- package/src/shaders/passes/ground.ts +172 -25
- package/src/shaders/passes/hosted-api.ts +171 -0
- package/src/shaders/passes/id-debug.ts +75 -0
- package/src/shaders/passes/particles.ts +54 -112
- package/src/shaders/passes/scene-contract.ts +266 -0
- package/src/shaders/passes/trails.ts +77 -93
- package/src/shadow-cascades.ts +97 -0
- package/src/vmd-loader.ts +2 -2
|
@@ -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,31 @@
|
|
|
1
|
+
import { type CastLayout } from "./particles";
|
|
2
|
+
export declare const GRID_MAX = 1024;
|
|
3
|
+
/** rgba16float, ping-ponged. Two of these is the whole memory cost. */
|
|
4
|
+
export declare const SIM_FORMAT: GPUTextureFormat;
|
|
5
|
+
/** `// @grid 256` — the grid's resolution, in texels per side. */
|
|
6
|
+
export declare function parseGridSize(wgsl: string, max: number): number;
|
|
7
|
+
/** Whether this effect drives a grid at all. */
|
|
8
|
+
export declare function gridEntryPoint(wgsl: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* `rzGrid(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 gridReadApi(group: number, tex: number, samp: number, size: number): string;
|
|
17
|
+
/**
|
|
18
|
+
* The step shader: one invocation per texel, once per frame.
|
|
19
|
+
*
|
|
20
|
+
* The previous grid is bound as a SAMPLED texture and the next as a storage
|
|
21
|
+
* texture, which is what makes the two different resources and the whole thing
|
|
22
|
+
* legal — a shader cannot read and write one texture coherently, and the
|
|
23
|
+
* ping-pong is not an optimisation but the only correct way to do this.
|
|
24
|
+
*
|
|
25
|
+
* Sampled rather than only fetched, because advection needs to read BETWEEN
|
|
26
|
+
* texels: a semi-Lagrangian step asks "what was at the place this parcel came
|
|
27
|
+
* from", and that place is almost never a texel centre. Point-sampling it is
|
|
28
|
+
* what turns smoke into a staircase.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildSimShader(wgsl: string, size: number, cast: CastLayout): string;
|
|
31
|
+
//# sourceMappingURL=grid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAiC7C,eAAO,MAAM,QAAQ,OAAO,CAAA;AAC5B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAM/D;AAED,gDAAgD;AAChD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6B1F;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA6DnF"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { audioApi } from "../audio-api";
|
|
2
|
+
import { lyricsApi } from "../lyrics-api";
|
|
3
|
+
import { anchorAliasWgsl } from "../anchor-table";
|
|
4
|
+
import { midiApi } from "../midi-api";
|
|
5
|
+
import { EFFECT_SCENE_API } from "./composite";
|
|
6
|
+
import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api";
|
|
7
|
+
// A persistent grid an effect can step and read: the one thing an effect could
|
|
8
|
+
// not have before, which is MEMORY.
|
|
9
|
+
//
|
|
10
|
+
// Every other mount is a pure function of position and time. A field shader
|
|
11
|
+
// recomputes its noise from scratch every frame; a trail reads history the
|
|
12
|
+
// engine happens to keep. Neither can build on what it produced last frame, and
|
|
13
|
+
// a whole class of effect is nothing but that:
|
|
14
|
+
//
|
|
15
|
+
// Fog that a dancer walks through does not "look displaced near her" — it IS
|
|
16
|
+
// displaced, and stays displaced, and creeps back over seconds. Water does not
|
|
17
|
+
// look rippled where a foot landed; a ripple leaves and propagates and
|
|
18
|
+
// reflects off the far edge long after the foot is gone. Both are the same
|
|
19
|
+
// shape of thing — a grid, stepped by its own previous value.
|
|
20
|
+
//
|
|
21
|
+
// It is also the only way to get the LOOK of a fluid, not merely its motion.
|
|
22
|
+
// Advection compounds: each frame's field is the last one pushed sideways, and a
|
|
23
|
+
// few hundred frames of that stretches and folds a smooth field into filaments
|
|
24
|
+
// and vortex sheets finer than the grid storing them. No noise function
|
|
25
|
+
// produces that, at any octave count, because the structure is not a function of
|
|
26
|
+
// position at all — it is a function of history.
|
|
27
|
+
//
|
|
28
|
+
// The engine owns the ping-pong, the dispatch and the sampling. The author owns
|
|
29
|
+
// the KERNEL, which is the part that differs: advection for smoke, the wave
|
|
30
|
+
// equation for water, a decaying stamp for footprints. That split is the same
|
|
31
|
+
// one the particle mounts make, and for the same reason — the loop is identical
|
|
32
|
+
// in every such effect and the physics never is.
|
|
33
|
+
// A megatexel of rgba16float is 8MB, and two of those is the whole cost. What
|
|
34
|
+
// actually bounds this is the STEP: one invocation per texel per frame, each
|
|
35
|
+
// taking a dozen samples, so doubling the side quadruples the work.
|
|
36
|
+
export const GRID_MAX = 1024;
|
|
37
|
+
/** rgba16float, ping-ponged. Two of these is the whole memory cost. */
|
|
38
|
+
export const SIM_FORMAT = "rgba16float";
|
|
39
|
+
/** `// @grid 256` — the grid's resolution, in texels per side. */
|
|
40
|
+
export function parseGridSize(wgsl, max) {
|
|
41
|
+
const m = /^\s*\/\/\s*@grid\s+(\d+)\s*$/m.exec(wgsl);
|
|
42
|
+
if (!m)
|
|
43
|
+
return 0;
|
|
44
|
+
// Clamped rather than rejected, as the particle count is: an author asking for
|
|
45
|
+
// 4096 gets the most the engine will give and a scene that still runs.
|
|
46
|
+
return Math.max(8, Math.min(max, parseInt(m[1], 10)));
|
|
47
|
+
}
|
|
48
|
+
/** Whether this effect drives a grid at all. */
|
|
49
|
+
export function gridEntryPoint(wgsl) {
|
|
50
|
+
return /\bfn\s+gridStep\s*\(/.test(wgsl);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `rzGrid(uv)`, for a shader that READS the grid rather than steps it.
|
|
54
|
+
*
|
|
55
|
+
* Always compiled in, even for an effect with no grid — it then samples a 1×1
|
|
56
|
+
* of zeroes. An accessor that exists only sometimes is one an author has to
|
|
57
|
+
* guard, and a missing function is a compile error rather than a blank result.
|
|
58
|
+
*/
|
|
59
|
+
export function gridReadApi(group, tex, samp, size) {
|
|
60
|
+
return /* wgsl */ `
|
|
61
|
+
const RZ_GRID_SIZE: f32 = ${size > 0 ? size : 1}.0;
|
|
62
|
+
@group(${group}) @binding(${tex}) var _rzGridTex: texture_2d<f32>;
|
|
63
|
+
@group(${group}) @binding(${samp}) var _rzGridSamp: sampler;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The grid, bilinearly sampled. uv is 0..1 across it, and what that MEANS in
|
|
67
|
+
* the world is the effect's own business — the engine deliberately does not
|
|
68
|
+
* impose a mapping, because a fog laid over a stage and a ripple field around a
|
|
69
|
+
* character want different ones and both are two lines of arithmetic.
|
|
70
|
+
*/
|
|
71
|
+
fn rzGrid(uv: vec2f) -> vec4f {
|
|
72
|
+
return textureSampleLevel(_rzGridTex, _rzGridSamp, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0);
|
|
73
|
+
}
|
|
74
|
+
fn rzGridSize() -> f32 { return RZ_GRID_SIZE; }
|
|
75
|
+
fn rzGridTexel() -> f32 { return 1.0 / RZ_GRID_SIZE; }
|
|
76
|
+
|
|
77
|
+
// ── The step-only half, defined here too and never called here ──
|
|
78
|
+
//
|
|
79
|
+
// One effect file is spliced into EVERY module it has a mount in, so a file
|
|
80
|
+
// with a grid compiles its gridStep inside the field and particle shaders as
|
|
81
|
+
// well — where it is dead code that nothing calls, but still code that has to
|
|
82
|
+
// resolve. Leaving these out is a compile error on a function the author was
|
|
83
|
+
// right to write. rzGridPrev reads the current grid rather than a previous one,
|
|
84
|
+
// which is the honest answer outside the step: there is no previous frame here.
|
|
85
|
+
fn rzGridPrev(uv: vec2f) -> vec4f { return rzGrid(uv); }
|
|
86
|
+
fn rzGridFrame() -> i32 { return 1; }
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
const SIM_UNIFORMS = /* wgsl */ `
|
|
90
|
+
struct SimU {
|
|
91
|
+
time: f32,
|
|
92
|
+
dt: f32,
|
|
93
|
+
size: f32,
|
|
94
|
+
frame: f32,
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
/**
|
|
98
|
+
* The step shader: one invocation per texel, once per frame.
|
|
99
|
+
*
|
|
100
|
+
* The previous grid is bound as a SAMPLED texture and the next as a storage
|
|
101
|
+
* texture, which is what makes the two different resources and the whole thing
|
|
102
|
+
* legal — a shader cannot read and write one texture coherently, and the
|
|
103
|
+
* ping-pong is not an optimisation but the only correct way to do this.
|
|
104
|
+
*
|
|
105
|
+
* Sampled rather than only fetched, because advection needs to read BETWEEN
|
|
106
|
+
* texels: a semi-Lagrangian step asks "what was at the place this parcel came
|
|
107
|
+
* from", and that place is almost never a texel centre. Point-sampling it is
|
|
108
|
+
* what turns smoke into a staircase.
|
|
109
|
+
*/
|
|
110
|
+
export function buildSimShader(wgsl, size, cast) {
|
|
111
|
+
return (SIM_UNIFORMS +
|
|
112
|
+
/* wgsl */ `
|
|
113
|
+
@group(0) @binding(0) var<uniform> su: SimU;
|
|
114
|
+
@group(0) @binding(1) var _rzGridPrevTex: texture_2d<f32>;
|
|
115
|
+
@group(0) @binding(2) var _rzGridSamp: sampler;
|
|
116
|
+
@group(0) @binding(3) var _rzGridOut: texture_storage_2d<${SIM_FORMAT}, write>;
|
|
117
|
+
@group(0) @binding(4) var<storage, read> _rzCast: array<vec4f>;
|
|
118
|
+
// The same view uniform the composite reads, so the scene API below is the real
|
|
119
|
+
// thing here rather than a stub — a kernel can ask where a bone is, and a
|
|
120
|
+
// foreground spliced into this module resolves its camera calls.
|
|
121
|
+
@group(0) @binding(6) var<uniform> viewU: array<vec4<f32>, 15>;
|
|
122
|
+
${clockApi("su.time", "su.dt")}${viewportApi("viewU[6].w")}${trailSlotsApi(cast.trailCount)}
|
|
123
|
+
/** Texels per side. */
|
|
124
|
+
fn rzGridSize() -> f32 { return su.size; }
|
|
125
|
+
/** One texel, in uv — the step a neighbour lookup takes. */
|
|
126
|
+
fn rzGridTexel() -> f32 { return 1.0 / su.size; }
|
|
127
|
+
/**
|
|
128
|
+
* Steps since this effect was installed. Zero on the very first one, which is
|
|
129
|
+
* the only chance to seed a grid: everything after it builds on what is there.
|
|
130
|
+
*/
|
|
131
|
+
fn rzGridFrame() -> i32 { return i32(su.frame); }
|
|
132
|
+
|
|
133
|
+
/** The grid as it was LAST frame, bilinear. The only input a kernel really has. */
|
|
134
|
+
fn rzGridPrev(uv: vec2f) -> vec4f {
|
|
135
|
+
return textureSampleLevel(_rzGridPrevTex, _rzGridSamp, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0);
|
|
136
|
+
}
|
|
137
|
+
` +
|
|
138
|
+
EFFECT_SCENE_API +
|
|
139
|
+
// The alias _rzSlot, which the scene API's accessors route through. Every
|
|
140
|
+
// module embedding EFFECT_SCENE_API must splice this; the sim module was
|
|
141
|
+
// the one that did not, and every gridStep effect failed to install with
|
|
142
|
+
// "unresolved call target" from the day the identity fallback was removed.
|
|
143
|
+
// Found by tools/validate-wgsl.mjs on its first run.
|
|
144
|
+
anchorAliasWgsl(cast.alias) +
|
|
145
|
+
audioApi(0, 5) +
|
|
146
|
+
midiApi(0, 7) +
|
|
147
|
+
lyricsApi(0, 8) +
|
|
148
|
+
// rzGrid itself, so a kernel may read the grid it is writing — through the
|
|
149
|
+
// PREVIOUS frame's texture, which is the only version of it that exists
|
|
150
|
+
// while the current one is still being written.
|
|
151
|
+
/* wgsl */ `
|
|
152
|
+
fn rzGrid(uv: vec2f) -> vec4f { return rzGridPrev(uv); }
|
|
153
|
+
` +
|
|
154
|
+
"\n// ── user effect (setEffect) ──\n" +
|
|
155
|
+
wgsl +
|
|
156
|
+
/* wgsl */ `
|
|
157
|
+
|
|
158
|
+
@compute @workgroup_size(8, 8)
|
|
159
|
+
fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
160
|
+
let n = u32(${size});
|
|
161
|
+
if (gid.x >= n || gid.y >= n) { return; }
|
|
162
|
+
let xy = vec2i(i32(gid.x), i32(gid.y));
|
|
163
|
+
// The texel CENTRE, so a kernel that reads its own uv back through rzGridPrev
|
|
164
|
+
// lands on itself rather than a quarter-texel off.
|
|
165
|
+
let uv = (vec2f(f32(gid.x), f32(gid.y)) + vec2f(0.5)) / su.size;
|
|
166
|
+
textureStore(_rzGridOut, xy, gridStep(uv, textureLoad(_rzGridPrevTex, xy, 0), su.dt));
|
|
167
|
+
}
|
|
168
|
+
`);
|
|
169
|
+
}
|
|
@@ -1,2 +1,14 @@
|
|
|
1
|
-
export declare
|
|
1
|
+
export declare function groundShaderWgsl(): string;
|
|
2
|
+
/**
|
|
3
|
+
* The ground belongs to no model instance, so it takes ids of its own — at the
|
|
4
|
+
* TOP of the u16 range, not at the bottom.
|
|
5
|
+
*
|
|
6
|
+
* Both of the engine's counters are 1-based (model id is instance count + 1,
|
|
7
|
+
* material id counts from one), so 1 is the first model's first material and
|
|
8
|
+
* the floor would have shared it. 0 stays unclaimed in the other direction: it
|
|
9
|
+
* is what the attachment clears to, and so what every pixel nothing drew
|
|
10
|
+
* reports.
|
|
11
|
+
*/
|
|
12
|
+
export declare const GROUND_MATERIAL_ID = 65535;
|
|
13
|
+
export declare const GROUND_OBJECT_ID = 65535;
|
|
2
14
|
//# sourceMappingURL=ground.d.ts.map
|
|
@@ -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":"AAYA,wBAAgB,gBAAgB,IAAI,MAAM,CA2QzC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,QAAS,CAAA;AACxC,eAAO,MAAM,gBAAgB,QAAS,CAAA"}
|
|
@@ -1,22 +1,47 @@
|
|
|
1
|
+
import { sceneFsOutWgsl, sceneIdWriteWgsl } from "./scene-contract";
|
|
2
|
+
import { lightsApi } from "../lights";
|
|
3
|
+
import { SHADOW_CASCADES } from "../../shadow-cascades";
|
|
4
|
+
import { WORLD_AMBIENT_WGSL } from "../lights";
|
|
1
5
|
// Ground shadow-catcher: receives directional shadow, grid lines, frosted noise,
|
|
2
6
|
// radial distance fade. Writes bloom mask = 0 (ground never bloom-bleeds).
|
|
3
|
-
|
|
7
|
+
//
|
|
8
|
+
// A FUNCTION, not a constant, since the id attachment is a device capability:
|
|
9
|
+
// the struct this returns depends on what the probe at init found, and a string
|
|
10
|
+
// baked at import time cannot know that. Called once, when the module is built.
|
|
11
|
+
export function groundShaderWgsl() {
|
|
12
|
+
return /* wgsl */ `
|
|
4
13
|
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
|
|
5
14
|
struct Light { direction: vec4f, color: vec4f, };
|
|
6
|
-
struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
|
|
15
|
+
struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, sh: array<vec4f, 9>, };
|
|
7
16
|
struct GroundShadowMat {
|
|
8
17
|
diffuseColor: vec3f, fadeStart: f32,
|
|
9
18
|
fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, gridSpacing: f32,
|
|
10
19
|
gridLineWidth: f32, gridLineOpacity: f32, noiseStrength: f32, opacity: f32,
|
|
11
|
-
gridLineColor: vec3f,
|
|
20
|
+
gridLineColor: vec3f, mirror: f32,
|
|
21
|
+
mirrorBlur: f32, _mb0: f32, _mb1: f32, _mb2: f32,
|
|
12
22
|
};
|
|
13
|
-
|
|
23
|
+
// One view-projection per shadow cascade, inner to outer — same buffer and
|
|
24
|
+
// same order the materials read.
|
|
25
|
+
struct LightVP { viewProj: array<mat4x4f, ${SHADOW_CASCADES.length}>, };
|
|
14
26
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
15
27
|
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
16
28
|
@group(0) @binding(2) var shadowMap: texture_depth_2d;
|
|
17
29
|
@group(0) @binding(3) var shadowSampler: sampler_comparison;
|
|
18
30
|
@group(0) @binding(4) var<uniform> material: GroundShadowMat;
|
|
19
31
|
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
32
|
+
// The far cascade's map, binding 7 as in the materials' layout.
|
|
33
|
+
@group(0) @binding(7) var shadowMapFar: texture_depth_2d;
|
|
34
|
+
// The floor mirror (step 7D): the reflection target, the camera that rendered
|
|
35
|
+
// it, and an ordinary sampler — the two shadow bindings above are comparison.
|
|
36
|
+
// params = (projA, projB, _, _) — the depth-linearisation pair of the SHARED
|
|
37
|
+
// projection, refreshed per frame beside the matrix.
|
|
38
|
+
struct MirrorVP { viewProj: mat4x4f, params: vec4f, };
|
|
39
|
+
@group(0) @binding(8) var<uniform> mirrorVP: MirrorVP;
|
|
40
|
+
@group(0) @binding(9) var mirrorTex: texture_2d<f32>;
|
|
41
|
+
@group(0) @binding(10) var linearSampler: sampler;
|
|
42
|
+
@group(0) @binding(11) var mirrorDepth: texture_depth_multisampled_2d;
|
|
43
|
+
${WORLD_AMBIENT_WGSL}
|
|
44
|
+
${lightsApi(0, 6)}
|
|
20
45
|
|
|
21
46
|
fn hash2(p: vec2f) -> f32 {
|
|
22
47
|
var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
|
|
@@ -47,8 +72,7 @@ struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @l
|
|
|
47
72
|
var o: VO; o.worldPos = position; o.normal = normal;
|
|
48
73
|
o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;
|
|
49
74
|
}
|
|
50
|
-
|
|
51
|
-
@fragment fn fs(i: VO) -> FSOut {
|
|
75
|
+
${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
|
|
52
76
|
// Derivatives first, unconditionally: WGSL requires fwidth in uniform control
|
|
53
77
|
// flow and everything below branches on world position.
|
|
54
78
|
let gp = i.worldPos.xz / material.gridSpacing;
|
|
@@ -67,15 +91,20 @@ struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
|
|
|
67
91
|
return out;
|
|
68
92
|
}
|
|
69
93
|
|
|
70
|
-
|
|
71
|
-
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
72
|
-
// Outside the light's frustum there IS no shadow information — the clamped
|
|
94
|
+
// Outside a cascade's frustum there IS no shadow information — the clamped
|
|
73
95
|
// border samples compare against unrelated depths and read "shadowed",
|
|
74
96
|
// darkening the whole far plane with a visible band at the frustum edge
|
|
75
97
|
// (masked by the opaque surface normally, glaring in green-screen mode where
|
|
76
|
-
// only the shadow-catcher term renders).
|
|
98
|
+
// only the shadow-catcher term renders). So each cascade fades out near its
|
|
99
|
+
// border — the near one INTO the far one's answer, the far one into lit.
|
|
100
|
+
let l0 = lightVP.viewProj[0] * vec4f(i.worldPos, 1.0);
|
|
101
|
+
let ndc = l0.xyz / max(l0.w, 1e-6);
|
|
77
102
|
let inZ = select(0.0, 1.0, ndc.z > 0.0 && ndc.z < 1.0);
|
|
78
103
|
let frustum = (1.0 - smoothstep(0.88, 0.96, abs(ndc.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc.y))) * inZ;
|
|
104
|
+
let l1 = lightVP.viewProj[1] * vec4f(i.worldPos, 1.0);
|
|
105
|
+
let ndc1 = l1.xyz / max(l1.w, 1e-6);
|
|
106
|
+
let inZ1 = select(0.0, 1.0, ndc1.z > 0.0 && ndc1.z < 1.0);
|
|
107
|
+
let frustum1 = (1.0 - smoothstep(0.88, 0.96, abs(ndc1.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc1.y))) * inZ1;
|
|
79
108
|
|
|
80
109
|
// THE cost of this shader, measured: a full-screen ground ran 25 shadow
|
|
81
110
|
// comparisons per pixel, and each one is hardware-bilinear, so ~100 depth
|
|
@@ -87,10 +116,23 @@ struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
|
|
|
87
116
|
// almost entirely once the comparison sampler's own 2x2 filter is counted.
|
|
88
117
|
// Nine taps instead of twenty-five for the same blur radius.
|
|
89
118
|
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// and
|
|
119
|
+
// The far cascade's taps run ONLY where the near one is fading or absent
|
|
120
|
+
// (frustum < 1), so a pixel in the near core costs exactly what it did with
|
|
121
|
+
// one cascade — and that core is where the camera usually looks.
|
|
93
122
|
var vis = 1.0;
|
|
123
|
+
if (frustum < 1.0 && frustum1 > 0.0) {
|
|
124
|
+
let suv1 = vec2f(ndc1.x * 0.5 + 0.5, 0.5 - ndc1.y * 0.5);
|
|
125
|
+
let suv1_c = clamp(suv1, vec2f(0.02), vec2f(0.98));
|
|
126
|
+
let st1 = ${1 / SHADOW_CASCADES[SHADOW_CASCADES.length - 1].mapSize} * 2.0;
|
|
127
|
+
let compareZ1 = ndc1.z - 0.0035;
|
|
128
|
+
var acc1 = 0.0;
|
|
129
|
+
for (var y = -1; y <= 1; y++) {
|
|
130
|
+
for (var x = -1; x <= 1; x++) {
|
|
131
|
+
acc1 += textureSampleCompareLevel(shadowMapFar, shadowSampler, suv1_c + vec2f(f32(x), f32(y)) * st1, compareZ1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
vis = mix(1.0, acc1 * (1.0 / 9.0), frustum1);
|
|
135
|
+
}
|
|
94
136
|
if (frustum > 0.0) {
|
|
95
137
|
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
96
138
|
let suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));
|
|
@@ -104,7 +146,9 @@ struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
|
|
|
104
146
|
acc += textureSampleCompareLevel(shadowMap, shadowSampler, suv_c + vec2f(f32(x), f32(y)) * st, compareZ);
|
|
105
147
|
}
|
|
106
148
|
}
|
|
107
|
-
|
|
149
|
+
// The base is whatever the far cascade decided, so the near border blends
|
|
150
|
+
// cascade to cascade rather than snapping to lit mid-floor.
|
|
151
|
+
vis = mix(vis, acc * (1.0 / 9.0), frustum);
|
|
108
152
|
}
|
|
109
153
|
|
|
110
154
|
// Frosted/matte micro-texture. Scenes leaving it at zero were still paying
|
|
@@ -122,24 +166,125 @@ struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
|
|
|
122
166
|
smoothstep(halfLine - gridDeriv.x, halfLine + gridDeriv.x, gridFrac.x),
|
|
123
167
|
smoothstep(halfLine - gridDeriv.y, halfLine + gridDeriv.y, gridFrac.y)
|
|
124
168
|
);
|
|
125
|
-
let sun =
|
|
169
|
+
let sun = rzWorldAmbient(n) + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, -light.lights[0].direction.xyz), 0.0);
|
|
170
|
+
// The positional layer reaches the floor too. A stage light that lit the
|
|
171
|
+
// cast and not the ground under her would read as a sticker, which is the
|
|
172
|
+
// same failure the shadow catcher exists to prevent.
|
|
173
|
+
let lamps = rzLightsDiffuse(i.worldPos, n);
|
|
126
174
|
let dark = (1.0 - vis) * material.shadowStrength;
|
|
127
|
-
|
|
175
|
+
// The shadow darkens the SUN only. The dark term comes from the sun's shadow
|
|
176
|
+
// map, so applying it to the lamps too would have a lamp on the far side of
|
|
177
|
+
// the stage dimmed by a shadow cast away from it — the one light in the scene
|
|
178
|
+
// that provably is not blocking it.
|
|
179
|
+
// (No backticks in here: this string is a TS template literal and one ends it
|
|
180
|
+
// mid-shader, which is exactly how this line broke the build a moment ago.)
|
|
181
|
+
var baseColor = material.diffuseColor * (sun * (1.0 - dark * 0.65) + lamps);
|
|
128
182
|
baseColor *= noiseTint;
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
183
|
+
// The floor mirror — its OWN LAYER, not a blend into the surface. The
|
|
184
|
+
// reflection was rendered by the MIRROR camera, so projecting this
|
|
185
|
+
// fragment's world position through that camera yields exactly the texel
|
|
186
|
+
// where its reflection landed — projective mapping, no screen-space guess.
|
|
187
|
+
// There is deliberately NO strength dial: mirror is on or off, and how much
|
|
188
|
+
// of it shows is the SURFACE's own opacity covering it — the same
|
|
189
|
+
// independence the grid won earlier. The branch is on a uniform, so the
|
|
190
|
+
// whole cost vanishes for the scenes that leave it off.
|
|
191
|
+
var reflShadowed = vec3f(0.0);
|
|
192
|
+
if (material.mirror > 0.0) {
|
|
193
|
+
let mc = mirrorVP.viewProj * vec4f(i.worldPos, 1.0);
|
|
194
|
+
let mndc = mc.xyz / max(mc.w, 1e-6);
|
|
195
|
+
let muv = clamp(vec2f(mndc.x * 0.5 + 0.5, 0.5 - mndc.y * 0.5), vec2f(0.0), vec2f(1.0));
|
|
196
|
+
var lod = 0.0;
|
|
197
|
+
if (material.mirrorBlur > 0.0) {
|
|
198
|
+
// DEPTH-PROPORTIONAL softness: the real driver is how far BEHIND the
|
|
199
|
+
// mirror surface the reflected geometry sits — a foot on the floor
|
|
200
|
+
// reflects sharp, a head reflects soft, the empty backdrop softest.
|
|
201
|
+
//
|
|
202
|
+
// Read the mirror pass's own depth at this texel, linearise it with the
|
|
203
|
+
// shared projection's pair (viewZ = projB / (z - projA); the formula
|
|
204
|
+
// inverts both depth conventions, see the composite's linearDepth), and
|
|
205
|
+
// reconstruct the reflected image point along the ray from the MIRRORED
|
|
206
|
+
// eye through this fragment. The plane is y = 0, so mirroring the eye
|
|
207
|
+
// and the camera forward is a sign flip on y, and the image point's
|
|
208
|
+
// depth below the plane IS the reflected object's height above it.
|
|
209
|
+
let dims = vec2f(textureDimensions(mirrorDepth));
|
|
210
|
+
let texel = clamp(vec2i(muv * dims), vec2i(0), vec2i(dims) - vec2i(1));
|
|
211
|
+
let z = textureLoad(mirrorDepth, texel, 0);
|
|
212
|
+
let viewZ = clamp(mirrorVP.params.y / (z - mirrorVP.params.x), 0.05, 100000.0);
|
|
213
|
+
let eyeM = vec3f(camera.viewPos.x, -camera.viewPos.y, camera.viewPos.z);
|
|
214
|
+
let fwd = vec3f(camera.view[0][2], camera.view[1][2], camera.view[2][2]);
|
|
215
|
+
let fwdM = vec3f(fwd.x, -fwd.y, fwd.z);
|
|
216
|
+
let toFrag = i.worldPos - eyeM;
|
|
217
|
+
let dir = toFrag * inverseSqrt(max(dot(toFrag, toFrag), 1e-8));
|
|
218
|
+
let t = viewZ / max(dot(dir, fwdM), 1e-4);
|
|
219
|
+
let height = max(-(eyeM.y + dir.y * t), 0.0);
|
|
220
|
+
// Full softness by BLUR_SPAN units above the floor — about a character's
|
|
221
|
+
// height. The hardware clamps the level, so the nominal 5.0 needs no
|
|
222
|
+
// knowledge of the real chain length, and blur 0 reads exactly level 0.
|
|
223
|
+
let BLUR_SPAN = 14.0;
|
|
224
|
+
lod = material.mirrorBlur * 5.0 * clamp(height / BLUR_SPAN, 0.0, 1.0);
|
|
225
|
+
}
|
|
226
|
+
let refl = textureSampleLevel(mirrorTex, linearSampler, muv, lod).rgb;
|
|
227
|
+
// Still under the received shadow: a polished floor in shade shows a dim
|
|
228
|
+
// reflection, and a mirror that ignored the shadow would glow in it.
|
|
229
|
+
reflShadowed = refl * (1.0 - dark * 0.65);
|
|
230
|
+
}
|
|
231
|
+
// THREE LAYERS, composited premultiplied, bottom to top: the shadow the
|
|
232
|
+
// catcher receives, the ground surface, and the grid.
|
|
233
|
+
//
|
|
234
|
+
// The grid used to be MIXED INTO the surface and then weighted by the
|
|
235
|
+
// surface's own alpha, so turning the ground down to nothing took the grid
|
|
236
|
+
// with it — you could not have a bare grid over a photo, which is most of
|
|
237
|
+
// what a grid is for. It is its own layer now: the ground plane keeps an
|
|
238
|
+
// alpha, and the grid is simply on or off.
|
|
134
239
|
let surfA = edgeFade * material.opacity;
|
|
135
|
-
let
|
|
136
|
-
|
|
137
|
-
|
|
240
|
+
let gridA = gridLine * material.gridLineOpacity * edgeFade;
|
|
241
|
+
// The mirror is opaque across the ground's extent when on — it always shows
|
|
242
|
+
// SOMETHING, because the backdrop rides in the reflection target itself.
|
|
243
|
+
let mirrA = material.mirror * edgeFade;
|
|
244
|
+
// As opacity drops the received shadow becomes a translucent dark layer
|
|
245
|
+
// (Blender's Shadow Catcher), so models still feel grounded on a photo or a
|
|
246
|
+
// 360 backdrop. Colourless by construction: it darkens by covering. The
|
|
247
|
+
// MIRROR subsumes it: the reflection already carries the received shadow,
|
|
248
|
+
// and a catcher stacked on top would darken the same shadow twice.
|
|
249
|
+
let catchA = dark * 0.65 * edgeFade * (1.0 - material.opacity) * (1.0 - material.mirror);
|
|
250
|
+
|
|
251
|
+
// FOUR LAYERS, premultiplied, bottom to top: reflection, received shadow,
|
|
252
|
+
// ground surface, grid. Each with its own coverage, none scaling another —
|
|
253
|
+
// ground opacity REVEALS the mirror rather than the mirror carrying a
|
|
254
|
+
// strength of its own.
|
|
255
|
+
var pm = reflShadowed * mirrA;
|
|
256
|
+
var cov = mirrA;
|
|
257
|
+
// Shadow catcher over the reflection-or-nothing: black, it covers.
|
|
258
|
+
pm = pm * (1.0 - catchA);
|
|
259
|
+
cov = catchA + cov * (1.0 - catchA);
|
|
260
|
+
// Surface over shadow.
|
|
261
|
+
pm = baseColor * surfA + pm * (1.0 - surfA);
|
|
262
|
+
cov = surfA + cov * (1.0 - surfA);
|
|
263
|
+
// Grid over surface. Unshadowed, as it has always been — a painted line reads
|
|
264
|
+
// as paint, and darkening it was never what the mix did either.
|
|
265
|
+
pm = material.gridLineColor * gridA + pm * (1.0 - gridA);
|
|
266
|
+
cov = gridA + cov * (1.0 - gridA);
|
|
267
|
+
|
|
268
|
+
let outA = cov;
|
|
269
|
+
out.color = vec4f(pm, outA);
|
|
138
270
|
// mask.r = 0: ground never contributes to bloom. mask.g = 1.0 with src.a =
|
|
139
271
|
// outA turns the aux blend into alpha-over, so the drawable alpha goes
|
|
140
272
|
// from outA at the center to 0 at the radial edge — letting the page
|
|
141
273
|
// background show through under the premultiplied canvas alphaMode.
|
|
142
274
|
out.mask = vec4f(0.0, 1.0, 0.0, outA);
|
|
143
|
-
return out;
|
|
275
|
+
${sceneIdWriteWgsl("out", `${GROUND_MATERIAL_ID}u`, `${GROUND_OBJECT_ID}u`)} return out;
|
|
144
276
|
}
|
|
145
277
|
`;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The ground belongs to no model instance, so it takes ids of its own — at the
|
|
281
|
+
* TOP of the u16 range, not at the bottom.
|
|
282
|
+
*
|
|
283
|
+
* Both of the engine's counters are 1-based (model id is instance count + 1,
|
|
284
|
+
* material id counts from one), so 1 is the first model's first material and
|
|
285
|
+
* the floor would have shared it. 0 stays unclaimed in the other direction: it
|
|
286
|
+
* is what the attachment clears to, and so what every pixel nothing drew
|
|
287
|
+
* reports.
|
|
288
|
+
*/
|
|
289
|
+
export const GROUND_MATERIAL_ID = 0xffff;
|
|
290
|
+
export const GROUND_OBJECT_ID = 0xffff;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The particle record, laid out by hand — declared in EVERY hosting module.
|
|
3
|
+
*
|
|
4
|
+
* Not only in the particle ones: `fn particleStep(p: Particle)` is part of the
|
|
5
|
+
* author's file, so the trail, field and lightEmit modules compile that
|
|
6
|
+
* signature as dead code and a missing struct is a compile error on a function
|
|
7
|
+
* the author was right to write. The trail module used to carry a hand-copied
|
|
8
|
+
* duplicate for exactly this reason.
|
|
9
|
+
*
|
|
10
|
+
* `age` and `life` sit in the padding that vec3f alignment would waste anyway
|
|
11
|
+
* (a vec3f occupies 12 bytes but aligns the next field to 16), so the struct is
|
|
12
|
+
* 48 bytes rather than the 64 a naive ordering costs. At 4096 particles that is
|
|
13
|
+
* 192KB instead of 256KB, and it is read every frame by both stages.
|
|
14
|
+
*
|
|
15
|
+
* `life <= 0` means "not alive" and is what the pool checks to recycle a slot,
|
|
16
|
+
* so a freshly zeroed buffer is entirely dead and every particle is born on the
|
|
17
|
+
* first step rather than needing a separate seeding pass.
|
|
18
|
+
*
|
|
19
|
+
* RzLight is the same kind of thing and lives in lights.ts, because it is that
|
|
20
|
+
* module's own type; every hosting module splices it for this same reason.
|
|
21
|
+
*/
|
|
22
|
+
export declare const PARTICLE_STRUCT_WGSL = "\nstruct Particle {\n pos: vec3f,\n age: f32,\n vel: vec3f,\n life: f32,\n size: f32,\n rot: f32,\n seed: f32,\n // Aspect along the direction of travel. 1 or less is a square billboard; a\n // raindrop is 10 or 20. Zero-initialised, so an effect that never sets it gets\n // the square it expects.\n stretch: f32,\n}\n";
|
|
23
|
+
/**
|
|
24
|
+
* `rzTime`/`rzDt`, from whatever the module calls its clock.
|
|
25
|
+
*
|
|
26
|
+
* A module with no clock of its own passes a literal. Zero dt is honest there:
|
|
27
|
+
* nothing spliced into it is being stepped, so a kernel that integrates would
|
|
28
|
+
* integrate by nothing rather than by a plausible-looking wrong number.
|
|
29
|
+
*/
|
|
30
|
+
export declare function clockApi(timeExpr: string, dtExpr: string): string;
|
|
31
|
+
/** `rzViewportHeight` — the render target's height in pixels, which is what a
|
|
32
|
+
* point size in world units has to be divided by to become a pixel radius. */
|
|
33
|
+
export declare function viewportApi(heightExpr: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* The one cast number that is PER EFFECT rather than per engine.
|
|
36
|
+
*
|
|
37
|
+
* Author-visible, and published effects loop over it, so its meaning is pinned:
|
|
38
|
+
* how many of THIS effect's anchors asked for a trail. Deliberately not the
|
|
39
|
+
* anchor address space — that is RZ_MAX_ANCHORS in CAST_API, and the two being
|
|
40
|
+
* one number was the old trail bug. Everything else about the cast's shape is a
|
|
41
|
+
* constant and lives there.
|
|
42
|
+
*/
|
|
43
|
+
export declare function trailSlotsApi(trailCount: number): string;
|
|
44
|
+
/**
|
|
45
|
+
* Pure math — no bindings, no uniforms, identical in every module.
|
|
46
|
+
*
|
|
47
|
+
* These were duplicated in the particle and trail modules, byte for byte in the
|
|
48
|
+
* case of rzFalloff and to a `+ vec3f(0.0)` in the case of rzValueNoise. Two
|
|
49
|
+
* copies of a noise function is one bad merge away from an effect that looks
|
|
50
|
+
* different depending on which mount drew it, which is not a bug anyone would
|
|
51
|
+
* think to look for.
|
|
52
|
+
*
|
|
53
|
+
* EVERY hosting module includes this and none defines any of these itself, so
|
|
54
|
+
* "which module am I in" can never change what a helper returns.
|
|
55
|
+
*/
|
|
56
|
+
export declare const EFFECT_MATH_API = "\nfn rzHash11(x: f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\nfn rzHash21(p: vec2f) -> f32 {\n var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n/** Three independent randoms from one seed \u2014 the usual need when spawning. */\nfn rzHash13(x: f32) -> vec3f {\n return vec3f(rzHash11(x), rzHash11(x + 17.13), rzHash11(x + 41.71));\n}\n/**\n * Compact-support falloff: 1 at the centre, exactly 0 at r, smooth between.\n *\n * It reaches exactly zero rather than merely getting small, because a glow that\n * never quite ends has to be culled somewhere, and culling it wherever it looks\n * close enough is what put a visible hard edge on the first halo effect.\n */\nfn rzFalloff(d: f32, r: f32) -> f32 {\n let x = clamp(d / max(r, 1e-6), 0.0, 1.0);\n let f = 1.0 - x;\n return f * f * f;\n}\nfn rzHash31(p: vec3f) -> f32 {\n var p3 = fract(p * 0.1031);\n p3 = p3 + dot(p3, p3.zyx + 31.32);\n return fract((p3.x + p3.y) * p3.z);\n}\nfn rzValueNoise(p: vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = rzHash31(i);\n let n100 = rzHash31(i + vec3f(1.0, 0.0, 0.0));\n let n010 = rzHash31(i + vec3f(0.0, 1.0, 0.0));\n let n110 = rzHash31(i + vec3f(1.0, 1.0, 0.0));\n let n001 = rzHash31(i + vec3f(0.0, 0.0, 1.0));\n let n101 = rzHash31(i + vec3f(1.0, 0.0, 1.0));\n let n011 = rzHash31(i + vec3f(0.0, 1.0, 1.0));\n let n111 = rzHash31(i + vec3f(1.0, 1.0, 1.0));\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n/** Divergence-free flow \u2014 the field a wisp of smoke follows without a solver. */\nfn rzCurlNoise(p: vec3f) -> vec3f {\n let e = 0.1;\n let dx = vec3f(e, 0.0, 0.0);\n let dy = vec3f(0.0, e, 0.0);\n let dz = vec3f(0.0, 0.0, e);\n let x0 = rzValueNoise(p - dx); let x1 = rzValueNoise(p + dx);\n let y0 = rzValueNoise(p - dy); let y1 = rzValueNoise(p + dy);\n let z0 = rzValueNoise(p - dz); let z1 = rzValueNoise(p + dz);\n return normalize(vec3f((y1 - y0) - (z1 - z0), (z1 - z0) - (x1 - x0), (x1 - x0) - (y1 - y0)) + vec3f(1e-6));\n}\n";
|
|
57
|
+
//# sourceMappingURL=hosted-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosted-api.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/hosted-api.ts"],"names":[],"mappings":"AAuBA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,oBAAoB,+UAchC,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAKjE;AAED;+EAC+E;AAC/E,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAItD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,ixEA6D3B,CAAA"}
|