reze-engine 0.57.6 → 0.58.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/engine.d.ts +31 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +231 -82
- package/dist/shaders/directives.d.ts +18 -1
- package/dist/shaders/directives.d.ts.map +1 -1
- package/dist/shaders/directives.js +22 -3
- package/dist/shaders/lights.d.ts +2 -10
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +4 -24
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +3 -17
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +2 -0
- package/dist/shaders/passes/grid.d.ts.map +1 -1
- package/dist/shaders/passes/grid.js +2 -0
- package/dist/shaders/passes/particles.d.ts +20 -2
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +77 -5
- package/dist/shaders/passes/trails.d.ts +3 -0
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +7 -1
- package/dist/shaders/scene-light-api.d.ts +42 -0
- package/dist/shaders/scene-light-api.d.ts.map +1 -0
- package/dist/shaders/scene-light-api.js +144 -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/engine.ts +272 -96
- package/src/shaders/directives.ts +37 -4
- package/src/shaders/lights.ts +4 -24
- package/src/shaders/materials/common.ts +3 -17
- package/src/shaders/passes/composite.ts +2 -0
- package/src/shaders/passes/grid.ts +2 -0
- package/src/shaders/passes/particles.ts +93 -6
- package/src/shaders/passes/trails.ts +10 -1
- package/src/shaders/scene-light-api.ts +148 -0
package/src/shaders/lights.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { midiApi } from "./midi-api"
|
|
|
38
38
|
import { lyricsApi } from "./lyrics-api"
|
|
39
39
|
import { clockApi, trailSlotsApi, viewportApi } from "./passes/hosted-api"
|
|
40
40
|
import { idApi } from "./id-api"
|
|
41
|
+
import { sceneLightApi, worldAmbientWgsl } from "./scene-light-api"
|
|
41
42
|
|
|
42
43
|
/** Floats before the first record. One is the count; the rest keep the records
|
|
43
44
|
* vec4-aligned, which is what lets a future pass read them as vec4s. */
|
|
@@ -84,29 +85,8 @@ struct RzLight {
|
|
|
84
85
|
}
|
|
85
86
|
`
|
|
86
87
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
* HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
|
|
90
|
-
* ibl.ts for the folding; the shader is a plain polynomial in the normal).
|
|
91
|
-
*
|
|
92
|
-
* One string included by every module that declares LightUniforms with the sh
|
|
93
|
-
* block — the hosted-api lesson: a helper defined in some modules and not
|
|
94
|
-
* others is a compile error waiting for the first file that crosses them.
|
|
95
|
-
*/
|
|
96
|
-
export const WORLD_AMBIENT_WGSL = /* wgsl */ `
|
|
97
|
-
fn rzWorldAmbient(n: vec3f) -> vec3f {
|
|
98
|
-
if (light.sh[0].w < 0.5) { return light.ambientColor.xyz; }
|
|
99
|
-
let x = n.x;
|
|
100
|
-
let y = n.y;
|
|
101
|
-
let z = n.z;
|
|
102
|
-
let c = light.sh[0].xyz
|
|
103
|
-
+ light.sh[1].xyz * y + light.sh[2].xyz * z + light.sh[3].xyz * x
|
|
104
|
-
+ light.sh[4].xyz * (x * y) + light.sh[5].xyz * (y * z)
|
|
105
|
-
+ light.sh[6].xyz * (3.0 * z * z - 1.0) + light.sh[7].xyz * (x * z)
|
|
106
|
-
+ light.sh[8].xyz * (x * x - y * y);
|
|
107
|
-
return max(c, vec3f(0.0));
|
|
108
|
-
}
|
|
109
|
-
`
|
|
88
|
+
/** rzWorldAmbient against the materials' and the ground's `light` uniform. */
|
|
89
|
+
export const WORLD_AMBIENT_WGSL = worldAmbientWgsl("light")
|
|
110
90
|
|
|
111
91
|
/** Does this source define the emit mount? */
|
|
112
92
|
export function hasLightEmit(wgsl: string): boolean {
|
|
@@ -184,7 +164,7 @@ ${viewportApi("viewU[6].w")}
|
|
|
184
164
|
${trailSlotsApi(cast.trailCount)}
|
|
185
165
|
// The id accessors, stubbed: this module cannot read an attachment the
|
|
186
166
|
// scene pass writes. See id-api.ts — the author's whole file compiles here.
|
|
187
|
-
${idApi(false, 0, 0) + castDistanceStub()}
|
|
167
|
+
${idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0)}
|
|
188
168
|
// The dials the author declared, if any. A lamp is exactly the thing someone
|
|
189
169
|
// retunes — its colour and its reach — so an emitter reads params like every
|
|
190
170
|
// other mount rather than being the one place a #param resolves to nothing.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sceneFsOutWgsl } from "../passes/scene-contract"
|
|
2
2
|
import { lightsApi } from "../lights"
|
|
3
3
|
import { SHADOW_CASCADES } from "../../shadow-cascades"
|
|
4
|
+
import { pcf9 } from "../scene-light-api"
|
|
4
5
|
import { WORLD_AMBIENT_WGSL } from "../lights"
|
|
5
6
|
|
|
6
7
|
// Shared WGSL blocks concatenated by every material shader.
|
|
@@ -175,8 +176,8 @@ fn safe_normal(nIn: vec3f) -> vec3f {
|
|
|
175
176
|
`;
|
|
176
177
|
|
|
177
178
|
// ─── Shadow sampler (3×3 PCF, cascade-selected) ─────────────────────
|
|
178
|
-
// Normal-bias 0.08, depth-bias 0.001 NDC
|
|
179
|
-
//
|
|
179
|
+
// Normal-bias 0.08, depth-bias 0.001 NDC; the kernel is pcf9 in scene-light-api.ts,
|
|
180
|
+
// shared with effects. Texel sizes are interpolated
|
|
180
181
|
// from SHADOW_CASCADES so they cannot go stale the way the hardcoded 1/2048
|
|
181
182
|
// once did (PCF taps landed TWO texels apart after the map grew to 4096,
|
|
182
183
|
// quantizing self-shadow edges into crawling gray squares).
|
|
@@ -188,21 +189,6 @@ fn safe_normal(nIn: vec3f) -> vec3f {
|
|
|
188
189
|
// far plane the comparison failed against every stored depth, silently
|
|
189
190
|
// shadowing any stage deeper than the box.
|
|
190
191
|
|
|
191
|
-
const pcf9 = (map: string, ts: string) => /* wgsl */ `
|
|
192
|
-
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
193
|
-
let cmpZ = ndc.z - 0.001;
|
|
194
|
-
let ts = ${ts};
|
|
195
|
-
let s00 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
196
|
-
let s10 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
197
|
-
let s20 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
198
|
-
let s01 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
199
|
-
let s11 = textureSampleCompareLevel(${map}, shadowSampler, suv, cmpZ);
|
|
200
|
-
let s21 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
201
|
-
let s02 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
202
|
-
let s12 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
203
|
-
let s22 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
204
|
-
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
205
|
-
`
|
|
206
192
|
|
|
207
193
|
const SAMPLE_SHADOW_WGSL = /* wgsl */ `
|
|
208
194
|
|
|
@@ -6,6 +6,7 @@ import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewpor
|
|
|
6
6
|
import { EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES } from "../cast-layout"
|
|
7
7
|
import { audioApi } from "../audio-api"
|
|
8
8
|
import { idApi } from "../id-api"
|
|
9
|
+
import { sceneLightApi } from "../scene-light-api"
|
|
9
10
|
import { castDistanceApi, CAST_FIELD_DIV } from "./cast-distance"
|
|
10
11
|
import { lyricsApi, lyricsTextApi } from "../lyrics-api"
|
|
11
12
|
import { midiApi } from "../midi-api"
|
|
@@ -722,6 +723,7 @@ export function buildFieldShader(effect: CompositeEffectSource): string {
|
|
|
722
723
|
viewportApi("viewU[6].w") +
|
|
723
724
|
trailSlotsApi(effect.trailCount) +
|
|
724
725
|
idApi(effect.ids === true, 0, 23) +
|
|
726
|
+
sceneLightApi(false, 0, 0) +
|
|
725
727
|
// Distance to the cast, in SCREEN pixels: the field is half-res, so a field
|
|
726
728
|
// texel is CAST_FIELD_DIV of them and the accessor scales on the way out.
|
|
727
729
|
// An author writes the width they mean and never learns how it is built.
|
|
@@ -8,6 +8,7 @@ import { SCENE_TAP_STUB } from "../scene-tap"
|
|
|
8
8
|
import { type CastLayout } from "./particles"
|
|
9
9
|
import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api"
|
|
10
10
|
import { idApi } from "../id-api"
|
|
11
|
+
import { sceneLightApi } from "../scene-light-api"
|
|
11
12
|
|
|
12
13
|
// A persistent grid an effect can step and read: the one thing an effect could
|
|
13
14
|
// not have before, which is MEMORY.
|
|
@@ -168,6 +169,7 @@ fn rzGrid(uv: vec2f) -> vec4f { return rzGridPrev(uv); }
|
|
|
168
169
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
169
170
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
170
171
|
idApi(false, 0, 0) +
|
|
172
|
+
sceneLightApi(false, 0, 0) +
|
|
171
173
|
// Inlined rather than imported from cast-distance, and NOT for tidiness.
|
|
172
174
|
//
|
|
173
175
|
// This module and composite.ts import each other — grid takes
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { castDistanceStub } from "./cast-distance"
|
|
2
|
+
import { gridReadApi } from "./grid"
|
|
2
3
|
import { RZ_LIGHT_STRUCT_WGSL } from "../lights"
|
|
3
4
|
import { audioApi } from "../audio-api"
|
|
4
5
|
import { lyricsApi } from "../lyrics-api"
|
|
@@ -9,6 +10,7 @@ import { EFFECT_SUBJECT_VEC4S } from "../cast-layout"
|
|
|
9
10
|
import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api"
|
|
10
11
|
import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract"
|
|
11
12
|
import { idApi } from "../id-api"
|
|
13
|
+
import { sceneLightApi } from "../scene-light-api"
|
|
12
14
|
// GPU particles for user effects: a compute step and an instanced quad draw.
|
|
13
15
|
//
|
|
14
16
|
// Its own shader MODULE rather than more source spliced into composite.ts, for
|
|
@@ -75,7 +77,9 @@ fn _rzCastLive() -> i32 {
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
/** How the author's quads combine with the scene. */
|
|
78
|
-
|
|
80
|
+
/** `cutout` shades exactly like `alpha`; the difference is entirely in the
|
|
81
|
+
* pipeline — depth write on, alpha-to-coverage on. See directives.ts. */
|
|
82
|
+
type ParticleBlend = "alpha" | "additive" | "cutout"
|
|
79
83
|
|
|
80
84
|
type ParticleSource = {
|
|
81
85
|
/** The author's WGSL verbatim. */
|
|
@@ -89,8 +93,23 @@ type ParticleSource = {
|
|
|
89
93
|
* rule reads the same dial the shading does, and rain's fall speed is used
|
|
90
94
|
* in the compute half. */
|
|
91
95
|
paramsDecl: string
|
|
96
|
+
/** The effect defines `particleCover(p, uv) -> f32`: the shape alone, cheap.
|
|
97
|
+
* A cutout then draws TWICE — coverage to depth first, then the shading at
|
|
98
|
+
* depthCompare equal — so the lighting runs only for the visible layer. A
|
|
99
|
+
* discard forbids the hardware's own early rejection on a tile-based GPU,
|
|
100
|
+
* where every stacked fragment is otherwise shaded in full; the prepass is
|
|
101
|
+
* the shape of that rejection, done by hand and cheaply. */
|
|
102
|
+
cover: boolean
|
|
103
|
+
/** The side of this effect's grid, or 0 when it declared none. Particles READ
|
|
104
|
+
* the grid; the kernel that steps it lives in the grid pass. A blade of grass
|
|
105
|
+
* bending where a foot pressed is the whole case: the bend has to persist and
|
|
106
|
+
* recover, which is memory, and memory is what the grid is for. */
|
|
107
|
+
gridSize: number
|
|
92
108
|
}
|
|
93
109
|
|
|
110
|
+
/** Where the shading stage's five scene-light bindings start — see scene-light-api.ts. */
|
|
111
|
+
export const PARTICLE_LIGHT_BINDING = 10
|
|
112
|
+
|
|
94
113
|
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
95
114
|
export const PARTICLE_STRIDE = 48
|
|
96
115
|
|
|
@@ -148,12 +167,14 @@ fn rzProject(p: vec3f) -> vec3f {
|
|
|
148
167
|
fn rzCamPos() -> vec3f { return cam.camPos; }
|
|
149
168
|
`
|
|
150
169
|
|
|
151
|
-
/** Does the source define the particle contract? All three are required
|
|
152
|
-
|
|
170
|
+
/** Does the source define the particle contract? All three are required
|
|
171
|
+
* together; `cover` is optional and only means anything to a cutout. */
|
|
172
|
+
export function particleEntryPoints(wgsl: string): { init: boolean; step: boolean; shade: boolean; cover: boolean } {
|
|
153
173
|
return {
|
|
154
174
|
init: /\bfn\s+particleInit\s*\(/.test(wgsl),
|
|
155
175
|
step: /\bfn\s+particleStep\s*\(/.test(wgsl),
|
|
156
176
|
shade: /\bfn\s+particleShade\s*\(/.test(wgsl),
|
|
177
|
+
cover: /\bfn\s+particleCover\s*\(/.test(wgsl),
|
|
157
178
|
}
|
|
158
179
|
}
|
|
159
180
|
|
|
@@ -182,10 +203,15 @@ ${src.paramsDecl}
|
|
|
182
203
|
audioApi(0, 4) +
|
|
183
204
|
midiApi(0, 5) +
|
|
184
205
|
lyricsApi(0, 6) +
|
|
206
|
+
// Always, even with no grid — then it samples a 1x1 of zeroes. Same reason
|
|
207
|
+
// composite compiles it unconditionally, plus one this module owns: an
|
|
208
|
+
// effect's whole file is spliced here, so a `gridStep` written for the grid
|
|
209
|
+
// pass has to RESOLVE here too, and the stubs that let it are in here.
|
|
210
|
+
gridReadApi(0, 8, 9, src.gridSize) +
|
|
185
211
|
PRELUDE +
|
|
186
212
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
187
213
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
188
|
-
idApi(false, 0, 0) + castDistanceStub() +
|
|
214
|
+
idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) +
|
|
189
215
|
"\n// ── user effect ──\n" +
|
|
190
216
|
src.wgsl +
|
|
191
217
|
/* wgsl */ `
|
|
@@ -230,7 +256,13 @@ fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
230
256
|
export function buildParticleRenderShader(src: ParticleSource, cast: CastLayout): string {
|
|
231
257
|
return (
|
|
232
258
|
`override BLOOM: bool = ${src.bloom ? "true" : "false"};
|
|
233
|
-
override ADDITIVE: bool = ${src.blend === "additive" ? "true" : "false"}
|
|
259
|
+
override ADDITIVE: bool = ${src.blend === "additive" ? "true" : "false"};
|
|
260
|
+
override CUTOUT: bool = ${src.blend === "cutout" ? "true" : "false"};
|
|
261
|
+
// A cutout with a prepass: the depth is already written and this pass draws at
|
|
262
|
+
// depthCompare equal, so it neither discards nor masks — the depth test IS the
|
|
263
|
+
// coverage, per sample, and a shader with no discard keeps the hardware's
|
|
264
|
+
// early rejection.
|
|
265
|
+
override PREPASSED: bool = ${src.blend === "cutout" && src.cover ? "true" : "false"};\n` +
|
|
234
266
|
PARTICLE_STRUCT_WGSL +
|
|
235
267
|
CAMERA_STRUCT +
|
|
236
268
|
PARTICLE_UNIFORMS +
|
|
@@ -245,10 +277,18 @@ ${src.paramsDecl}
|
|
|
245
277
|
audioApi(0, 4) +
|
|
246
278
|
midiApi(0, 5) +
|
|
247
279
|
lyricsApi(0, 6) +
|
|
280
|
+
// Always, even with no grid — then it samples a 1x1 of zeroes. Same reason
|
|
281
|
+
// composite compiles it unconditionally, plus one this module owns: an
|
|
282
|
+
// effect's whole file is spliced here, so a `gridStep` written for the grid
|
|
283
|
+
// pass has to RESOLVE here too, and the stubs that let it are in here.
|
|
284
|
+
gridReadApi(0, 8, 9, src.gridSize) +
|
|
248
285
|
PRELUDE +
|
|
249
286
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
250
287
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
251
288
|
idApi(false, 0, 0) + castDistanceStub() +
|
|
289
|
+
// The sun's shadow and the world's ambient, for real: the shadow pass has
|
|
290
|
+
// run by the time this draws, inside the scene pass. See scene-light-api.ts.
|
|
291
|
+
sceneLightApi(true, 0, PARTICLE_LIGHT_BINDING) +
|
|
252
292
|
"\n// ── user effect ──\n" +
|
|
253
293
|
src.wgsl +
|
|
254
294
|
/* wgsl */ `
|
|
@@ -301,6 +341,12 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
|
|
|
301
341
|
|
|
302
342
|
struct FSOut {
|
|
303
343
|
@location(0) color: vec4f,
|
|
344
|
+
// Which of the pixel's MSAA samples this fragment covers. Alpha-to-coverage
|
|
345
|
+
// by hand: the hardware kind reads alpha off the first colour target, and
|
|
346
|
+
// the HDR target is rg11b10, which has no alpha to read. Every particle
|
|
347
|
+
// writes it; a blended one writes all four, which is what not declaring it
|
|
348
|
+
// would have meant.
|
|
349
|
+
@builtin(sample_mask) samples: u32,
|
|
304
350
|
// The scene's aux target: (bloom mask, coverage). Materials write it, so a
|
|
305
351
|
// particle that skipped it would punch a hole in the mask of whatever it drew
|
|
306
352
|
// over.
|
|
@@ -328,8 +374,25 @@ fn fs(in: VSOut) -> FSOut {
|
|
|
328
374
|
c.a *= pu.weight;
|
|
329
375
|
// Weight 0 therefore discards every fragment, so an effect faded out costs
|
|
330
376
|
// nothing past the vertex stage even on the frame the draw is still issued.
|
|
331
|
-
|
|
377
|
+
// Not after a prepass: that pass already decided coverage into depth, and
|
|
378
|
+
// a discard here would cost this pass the early rejection it exists for.
|
|
379
|
+
if (!PREPASSED && c.a <= 0.0) { discard; }
|
|
332
380
|
var out: FSOut;
|
|
381
|
+
out.samples = 0xFu;
|
|
382
|
+
if (PREPASSED) {
|
|
383
|
+
c.a = 1.0;
|
|
384
|
+
} else if (CUTOUT) {
|
|
385
|
+
// Alpha becomes coverage: 0..4 of the pixel's samples, and the fragment is
|
|
386
|
+
// then opaque wherever it lands. The pattern is turned by pixel position
|
|
387
|
+
// so a half-covered edge is not the same two samples on every pixel along
|
|
388
|
+
// it, which is what makes a2c edges look dithered rather than stepped.
|
|
389
|
+
let n = u32(clamp(c.a, 0.0, 1.0) * 4.0 + 0.5);
|
|
390
|
+
if (n == 0u) { discard; }
|
|
391
|
+
let low = (1u << n) - 1u;
|
|
392
|
+
let turn = (u32(in.clip.x) + u32(in.clip.y)) & 3u;
|
|
393
|
+
out.samples = ((low << turn) | (low >> (4u - turn))) & 0xFu;
|
|
394
|
+
c.a = 1.0;
|
|
395
|
+
}
|
|
333
396
|
// PREMULTIPLIED: the scene's colour target blends with srcFactor \"one\", so a
|
|
334
397
|
// straight-alpha fragment would come out over-bright wherever it is
|
|
335
398
|
// translucent — which is most of a soft particle.
|
|
@@ -350,6 +413,30 @@ fn fs(in: VSOut) -> FSOut {
|
|
|
350
413
|
out.mask = vec4f(mg.x, mg.y, 0.0, c.a);
|
|
351
414
|
${sceneIdPadWgsl("out")} return out;
|
|
352
415
|
}
|
|
416
|
+
` +
|
|
417
|
+
(src.blend === "cutout" && src.cover
|
|
418
|
+
? /* wgsl */ `
|
|
419
|
+
/**
|
|
420
|
+
* The prepass: the shape to depth, nothing to colour. Every output is still
|
|
421
|
+
* declared — the pipeline takes the colour targets at writeMask 0 — and the
|
|
422
|
+
* sample mask is exactly what the single-pass cutout would have written, so
|
|
423
|
+
* the shading pass's equal test lands on the same samples.
|
|
424
|
+
*/
|
|
425
|
+
@fragment
|
|
426
|
+
fn fsDepth(in: VSOut) -> FSOut {
|
|
427
|
+
let p = particles[in.id];
|
|
428
|
+
let a = particleCover(p, in.uv) * pu.weight;
|
|
429
|
+
let n = u32(clamp(a, 0.0, 1.0) * 4.0 + 0.5);
|
|
430
|
+
if (n == 0u) { discard; }
|
|
431
|
+
let low = (1u << n) - 1u;
|
|
432
|
+
let turn = (u32(in.clip.x) + u32(in.clip.y)) & 3u;
|
|
433
|
+
var out: FSOut;
|
|
434
|
+
out.samples = ((low << turn) | (low >> (4u - turn))) & 0xFu;
|
|
435
|
+
out.color = vec4f(0.0);
|
|
436
|
+
out.mask = vec4f(0.0);
|
|
437
|
+
${sceneIdPadWgsl("out")} return out;
|
|
438
|
+
}
|
|
353
439
|
`
|
|
440
|
+
: "")
|
|
354
441
|
)
|
|
355
442
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { castDistanceStub } from "./cast-distance"
|
|
2
|
+
import { gridReadApi } from "./grid"
|
|
2
3
|
import { RZ_LIGHT_STRUCT_WGSL } from "../lights"
|
|
3
4
|
import { audioApi } from "../audio-api"
|
|
4
5
|
import { lyricsApi } from "../lyrics-api"
|
|
@@ -9,6 +10,7 @@ import { EFFECT_SUBJECT_VEC4S } from "../cast-layout"
|
|
|
9
10
|
import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api"
|
|
10
11
|
import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract"
|
|
11
12
|
import { idApi } from "../id-api"
|
|
13
|
+
import { sceneLightApi } from "../scene-light-api"
|
|
12
14
|
// Ribbons along a bone's recorded path, drawn as geometry.
|
|
13
15
|
//
|
|
14
16
|
// This is the effect the whole geometry path was built for. As a fullscreen
|
|
@@ -39,6 +41,9 @@ type TrailSource = {
|
|
|
39
41
|
/** Additive, like most glowing ribbons, or straight alpha. */
|
|
40
42
|
blend: "alpha" | "additive"
|
|
41
43
|
bloom: boolean
|
|
44
|
+
/** The side of this effect's grid, or 0 when it declared none. See the note
|
|
45
|
+
* on the same field in particles.ts: a ribbon reads the grid, never steps it. */
|
|
46
|
+
gridSize: number
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
/**
|
|
@@ -291,9 +296,13 @@ fn rzTurnRadius(a: vec3f, b: vec3f, c: vec3f) -> f32 {
|
|
|
291
296
|
audioApi(0, 4) +
|
|
292
297
|
midiApi(0, 5) +
|
|
293
298
|
lyricsApi(0, 6) +
|
|
299
|
+
// Always, with or without a grid — and required either way, because an
|
|
300
|
+
// effect's whole file is spliced here and a `gridStep` written for the grid
|
|
301
|
+
// pass has to resolve in this module too.
|
|
302
|
+
gridReadApi(0, 8, 9, src.gridSize) +
|
|
294
303
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
295
304
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
296
|
-
idApi(false, 0, 0) + castDistanceStub() +
|
|
305
|
+
idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) +
|
|
297
306
|
"\n// ── user effect ──\n" +
|
|
298
307
|
src.wgsl +
|
|
299
308
|
/* wgsl */ `
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { SHADOW_CASCADES } from "../shadow-cascades"
|
|
2
|
+
// The scene's own light, as an effect author sees it: the sun's shadow and the
|
|
3
|
+
// world's ambient.
|
|
4
|
+
//
|
|
5
|
+
// A lawn standing in a character's shadow has to darken where the floor under
|
|
6
|
+
// it does, or the figure floats: the ground catcher draws her shadow, and the
|
|
7
|
+
// blades drawn over it hide it. And the shade has to be the SCENE'S shade — her
|
|
8
|
+
// shaded side takes the world's colour, and a lawn shaded by its own guess
|
|
9
|
+
// reads as a patch of different flowers beside her. Both are already computed
|
|
10
|
+
// for the materials every frame; this hands the same lookups to effects.
|
|
11
|
+
//
|
|
12
|
+
// Bound for real in the particle SHADING stage only. That is where geometry an
|
|
13
|
+
// effect draws inside the scene pass is shaded, and the shadow pass has already
|
|
14
|
+
// run by then. Every other module that compiles the author's file gets stubs:
|
|
15
|
+
// rzShadow answers 1, lit, which is what a point with no occlusion information
|
|
16
|
+
// is — the same answer sampleShadow gives outside every cascade — and
|
|
17
|
+
// rzWorldAmbient answers black, no light.
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The world's light at a surface facing n — the flat colour, or the installed
|
|
21
|
+
* HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
|
|
22
|
+
* ibl.ts for the folding; the shader is a plain polynomial in the normal).
|
|
23
|
+
*
|
|
24
|
+
* Written against whichever name the module gives the light uniform, so the
|
|
25
|
+
* materials, the ground and an effect all evaluate one polynomial.
|
|
26
|
+
*/
|
|
27
|
+
export const worldAmbientWgsl = (u: string) => /* wgsl */ `
|
|
28
|
+
fn rzWorldAmbient(n: vec3f) -> vec3f {
|
|
29
|
+
if (${u}.sh[0].w < 0.5) { return ${u}.ambientColor.xyz; }
|
|
30
|
+
let x = n.x;
|
|
31
|
+
let y = n.y;
|
|
32
|
+
let z = n.z;
|
|
33
|
+
let c = ${u}.sh[0].xyz
|
|
34
|
+
+ ${u}.sh[1].xyz * y + ${u}.sh[2].xyz * z + ${u}.sh[3].xyz * x
|
|
35
|
+
+ ${u}.sh[4].xyz * (x * y) + ${u}.sh[5].xyz * (y * z)
|
|
36
|
+
+ ${u}.sh[6].xyz * (3.0 * z * z - 1.0) + ${u}.sh[7].xyz * (x * z)
|
|
37
|
+
+ ${u}.sh[8].xyz * (x * x - y * y);
|
|
38
|
+
return max(c, vec3f(0.0));
|
|
39
|
+
}
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 3×3 PCF over one cascade's map, for a point already in that cascade's NDC.
|
|
44
|
+
*
|
|
45
|
+
* Shared with the materials, so an effect's shadow and her own shadow soften
|
|
46
|
+
* alike. The sampler is linear-filtered, which makes each tap a 2×2 bilinear
|
|
47
|
+
* compare. Unrolled — Safari's Metal backend doesn't unroll nested shadow
|
|
48
|
+
* loops reliably. The body expects `ndc` in scope and returns.
|
|
49
|
+
*/
|
|
50
|
+
export const pcf9 = (map: string, ts: string, sampler = "shadowSampler") => /* wgsl */ `
|
|
51
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
52
|
+
let cmpZ = ndc.z - 0.001;
|
|
53
|
+
let ts = ${ts};
|
|
54
|
+
let s00 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, -ts), cmpZ);
|
|
55
|
+
let s10 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(0.0, -ts), cmpZ);
|
|
56
|
+
let s20 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, -ts), cmpZ);
|
|
57
|
+
let s01 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, 0.0), cmpZ);
|
|
58
|
+
let s11 = textureSampleCompareLevel(${map}, ${sampler}, suv, cmpZ);
|
|
59
|
+
let s21 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, 0.0), cmpZ);
|
|
60
|
+
let s02 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, ts), cmpZ);
|
|
61
|
+
let s12 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(0.0, ts), cmpZ);
|
|
62
|
+
let s22 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, ts), cmpZ);
|
|
63
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
64
|
+
`
|
|
65
|
+
|
|
66
|
+
/** Bindings the real accessors take, from `binding` upward. */
|
|
67
|
+
export const SCENE_LIGHT_API_BINDINGS = 5
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
|
|
71
|
+
* caster's shadow, soft in between.
|
|
72
|
+
*
|
|
73
|
+
* `rzWorldAmbient(n)`: the world's light arriving at a surface facing `n` —
|
|
74
|
+
* the materials' own ambient, so an effect's shade can match hers.
|
|
75
|
+
*
|
|
76
|
+
* With `on`, declares five bindings from `binding`:
|
|
77
|
+
* +0 the light uniform (ambient, the sun — direction, and in .w the shadow
|
|
78
|
+
* switch — and the HDRI's irradiance)
|
|
79
|
+
* +1 the cascade view-projections, then the caster sphere
|
|
80
|
+
* +2 the near cascade's map +3 the far cascade's map
|
|
81
|
+
* +4 the comparison sampler
|
|
82
|
+
*
|
|
83
|
+
* It takes a POINT and no normal. The materials' sampleShadow also rejects a
|
|
84
|
+
* surface facing away from the sun, which is right for skin and wrong for a
|
|
85
|
+
* blade of grass or a petal: those are thin, lit through, and shaded by their
|
|
86
|
+
* own N·L. The point is nudged toward the sun instead, which lifts a blade's
|
|
87
|
+
* root off a stage floor that casts into the same map.
|
|
88
|
+
*/
|
|
89
|
+
export function sceneLightApi(on: boolean, group: number, binding: number): string {
|
|
90
|
+
if (!on) {
|
|
91
|
+
return /* wgsl */ `
|
|
92
|
+
fn rzShadow(p: vec3f) -> f32 { return 1.0; }
|
|
93
|
+
fn rzWorldAmbient(n: vec3f) -> vec3f { return vec3f(0.0); }
|
|
94
|
+
`
|
|
95
|
+
}
|
|
96
|
+
const n = SHADOW_CASCADES.length
|
|
97
|
+
return /* wgsl */ `
|
|
98
|
+
// The light uniform, as the materials lay it out; lights[0] is the sun.
|
|
99
|
+
struct _RzSceneLight { direction: vec4f, color: vec4f, }
|
|
100
|
+
struct _RzSceneLightU { ambientColor: vec4f, lights: array<_RzSceneLight, 4>, sh: array<vec4f, 9>, }
|
|
101
|
+
// The cascades' matrices, inner to outer, then every caster in one sphere
|
|
102
|
+
// (x, y, z, radius): radius 0 = nothing casts, negative = do not test.
|
|
103
|
+
struct _RzShadowBlock { viewProj: array<mat4x4f, ${n}>, casters: vec4f, }
|
|
104
|
+
@group(${group}) @binding(${binding}) var<uniform> _rzLight: _RzSceneLightU;
|
|
105
|
+
@group(${group}) @binding(${binding + 1}) var<uniform> _rzShadowVP: _RzShadowBlock;
|
|
106
|
+
@group(${group}) @binding(${binding + 2}) var _rzShadowNear: texture_depth_2d;
|
|
107
|
+
@group(${group}) @binding(${binding + 3}) var _rzShadowFar: texture_depth_2d;
|
|
108
|
+
@group(${group}) @binding(${binding + 4}) var _rzShadowCmp: sampler_comparison;
|
|
109
|
+
|
|
110
|
+
fn _rzShadowNearTaps(ndc: vec3f) -> f32 {
|
|
111
|
+
${pcf9("_rzShadowNear", `1.0 / ${SHADOW_CASCADES[0].mapSize}.0`, "_rzShadowCmp")}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn _rzShadowFarTaps(ndc: vec3f) -> f32 {
|
|
115
|
+
${pcf9("_rzShadowFar", `1.0 / ${SHADOW_CASCADES[n - 1].mapSize}.0`, "_rzShadowCmp")}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn rzShadow(p: vec3f) -> f32 {
|
|
119
|
+
// The scene's one shadow switch, on the sun — see sampleShadow.
|
|
120
|
+
let castAmt = _rzLight.lights[0].direction.w;
|
|
121
|
+
if (castAmt <= 0.0) { return 1.0; }
|
|
122
|
+
let toSun = normalize(-_rzLight.lights[0].direction.xyz);
|
|
123
|
+
// Can anything cast onto this point at all? The ground's test, for the same
|
|
124
|
+
// two reasons: most of a lawn is nowhere near her, and a scene with no
|
|
125
|
+
// models never renders the map, whose untouched zeroes compare as shadowed.
|
|
126
|
+
let cs = _rzShadowVP.casters;
|
|
127
|
+
if (cs.w == 0.0) { return 1.0; }
|
|
128
|
+
if (cs.w > 0.0) {
|
|
129
|
+
let toCaster = cs.xyz - p;
|
|
130
|
+
let along = dot(toCaster, toSun);
|
|
131
|
+
let perp = length(toCaster - toSun * along);
|
|
132
|
+
if (along <= -cs.w || perp > cs.w) { return 1.0; }
|
|
133
|
+
}
|
|
134
|
+
let q = p + toSun * 0.08;
|
|
135
|
+
let c0 = _rzShadowVP.viewProj[0] * vec4f(q, 1.0);
|
|
136
|
+
let n0 = c0.xyz / max(c0.w, 1e-6);
|
|
137
|
+
if (all(abs(n0.xy) < vec2f(0.98)) && n0.z > 0.0 && n0.z < 1.0) {
|
|
138
|
+
return mix(1.0, _rzShadowNearTaps(n0), castAmt);
|
|
139
|
+
}
|
|
140
|
+
let c1 = _rzShadowVP.viewProj[${n - 1}] * vec4f(q, 1.0);
|
|
141
|
+
let n1 = c1.xyz / max(c1.w, 1e-6);
|
|
142
|
+
if (all(abs(n1.xy) < vec2f(0.98)) && n1.z > 0.0 && n1.z < 1.0) {
|
|
143
|
+
return mix(1.0, _rzShadowFarTaps(n1), castAmt);
|
|
144
|
+
}
|
|
145
|
+
return 1.0;
|
|
146
|
+
}
|
|
147
|
+
${worldAmbientWgsl("_rzLight")}`
|
|
148
|
+
}
|