reze-engine 0.57.5 → 0.58.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/dist/engine.d.ts +23 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +187 -77
- 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/materials/common.js +4 -4
- package/dist/shaders/passes/particles.d.ts +18 -2
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +70 -4
- 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 +5 -0
- package/package.json +1 -1
- package/src/engine.ts +226 -91
- package/src/shaders/directives.ts +37 -4
- package/src/shaders/materials/common.ts +4 -4
- package/src/shaders/passes/particles.ts +85 -5
- package/src/shaders/passes/trails.ts +8 -0
|
@@ -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"
|
|
@@ -75,7 +76,9 @@ fn _rzCastLive() -> i32 {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
/** How the author's quads combine with the scene. */
|
|
78
|
-
|
|
79
|
+
/** `cutout` shades exactly like `alpha`; the difference is entirely in the
|
|
80
|
+
* pipeline — depth write on, alpha-to-coverage on. See directives.ts. */
|
|
81
|
+
type ParticleBlend = "alpha" | "additive" | "cutout"
|
|
79
82
|
|
|
80
83
|
type ParticleSource = {
|
|
81
84
|
/** The author's WGSL verbatim. */
|
|
@@ -89,6 +92,18 @@ type ParticleSource = {
|
|
|
89
92
|
* rule reads the same dial the shading does, and rain's fall speed is used
|
|
90
93
|
* in the compute half. */
|
|
91
94
|
paramsDecl: string
|
|
95
|
+
/** The effect defines `particleCover(p, uv) -> f32`: the shape alone, cheap.
|
|
96
|
+
* A cutout then draws TWICE — coverage to depth first, then the shading at
|
|
97
|
+
* depthCompare equal — so the lighting runs only for the visible layer. A
|
|
98
|
+
* discard forbids the hardware's own early rejection on a tile-based GPU,
|
|
99
|
+
* where every stacked fragment is otherwise shaded in full; the prepass is
|
|
100
|
+
* the shape of that rejection, done by hand and cheaply. */
|
|
101
|
+
cover: boolean
|
|
102
|
+
/** The side of this effect's grid, or 0 when it declared none. Particles READ
|
|
103
|
+
* the grid; the kernel that steps it lives in the grid pass. A blade of grass
|
|
104
|
+
* bending where a foot pressed is the whole case: the bend has to persist and
|
|
105
|
+
* recover, which is memory, and memory is what the grid is for. */
|
|
106
|
+
gridSize: number
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
@@ -148,12 +163,14 @@ fn rzProject(p: vec3f) -> vec3f {
|
|
|
148
163
|
fn rzCamPos() -> vec3f { return cam.camPos; }
|
|
149
164
|
`
|
|
150
165
|
|
|
151
|
-
/** Does the source define the particle contract? All three are required
|
|
152
|
-
|
|
166
|
+
/** Does the source define the particle contract? All three are required
|
|
167
|
+
* together; `cover` is optional and only means anything to a cutout. */
|
|
168
|
+
export function particleEntryPoints(wgsl: string): { init: boolean; step: boolean; shade: boolean; cover: boolean } {
|
|
153
169
|
return {
|
|
154
170
|
init: /\bfn\s+particleInit\s*\(/.test(wgsl),
|
|
155
171
|
step: /\bfn\s+particleStep\s*\(/.test(wgsl),
|
|
156
172
|
shade: /\bfn\s+particleShade\s*\(/.test(wgsl),
|
|
173
|
+
cover: /\bfn\s+particleCover\s*\(/.test(wgsl),
|
|
157
174
|
}
|
|
158
175
|
}
|
|
159
176
|
|
|
@@ -182,6 +199,11 @@ ${src.paramsDecl}
|
|
|
182
199
|
audioApi(0, 4) +
|
|
183
200
|
midiApi(0, 5) +
|
|
184
201
|
lyricsApi(0, 6) +
|
|
202
|
+
// Always, even with no grid — then it samples a 1x1 of zeroes. Same reason
|
|
203
|
+
// composite compiles it unconditionally, plus one this module owns: an
|
|
204
|
+
// effect's whole file is spliced here, so a `gridStep` written for the grid
|
|
205
|
+
// pass has to RESOLVE here too, and the stubs that let it are in here.
|
|
206
|
+
gridReadApi(0, 8, 9, src.gridSize) +
|
|
185
207
|
PRELUDE +
|
|
186
208
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
187
209
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
@@ -230,7 +252,13 @@ fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
230
252
|
export function buildParticleRenderShader(src: ParticleSource, cast: CastLayout): string {
|
|
231
253
|
return (
|
|
232
254
|
`override BLOOM: bool = ${src.bloom ? "true" : "false"};
|
|
233
|
-
override ADDITIVE: bool = ${src.blend === "additive" ? "true" : "false"}
|
|
255
|
+
override ADDITIVE: bool = ${src.blend === "additive" ? "true" : "false"};
|
|
256
|
+
override CUTOUT: bool = ${src.blend === "cutout" ? "true" : "false"};
|
|
257
|
+
// A cutout with a prepass: the depth is already written and this pass draws at
|
|
258
|
+
// depthCompare equal, so it neither discards nor masks — the depth test IS the
|
|
259
|
+
// coverage, per sample, and a shader with no discard keeps the hardware's
|
|
260
|
+
// early rejection.
|
|
261
|
+
override PREPASSED: bool = ${src.blend === "cutout" && src.cover ? "true" : "false"};\n` +
|
|
234
262
|
PARTICLE_STRUCT_WGSL +
|
|
235
263
|
CAMERA_STRUCT +
|
|
236
264
|
PARTICLE_UNIFORMS +
|
|
@@ -245,6 +273,11 @@ ${src.paramsDecl}
|
|
|
245
273
|
audioApi(0, 4) +
|
|
246
274
|
midiApi(0, 5) +
|
|
247
275
|
lyricsApi(0, 6) +
|
|
276
|
+
// Always, even with no grid — then it samples a 1x1 of zeroes. Same reason
|
|
277
|
+
// composite compiles it unconditionally, plus one this module owns: an
|
|
278
|
+
// effect's whole file is spliced here, so a `gridStep` written for the grid
|
|
279
|
+
// pass has to RESOLVE here too, and the stubs that let it are in here.
|
|
280
|
+
gridReadApi(0, 8, 9, src.gridSize) +
|
|
248
281
|
PRELUDE +
|
|
249
282
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
250
283
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
@@ -301,6 +334,12 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
|
|
|
301
334
|
|
|
302
335
|
struct FSOut {
|
|
303
336
|
@location(0) color: vec4f,
|
|
337
|
+
// Which of the pixel's MSAA samples this fragment covers. Alpha-to-coverage
|
|
338
|
+
// by hand: the hardware kind reads alpha off the first colour target, and
|
|
339
|
+
// the HDR target is rg11b10, which has no alpha to read. Every particle
|
|
340
|
+
// writes it; a blended one writes all four, which is what not declaring it
|
|
341
|
+
// would have meant.
|
|
342
|
+
@builtin(sample_mask) samples: u32,
|
|
304
343
|
// The scene's aux target: (bloom mask, coverage). Materials write it, so a
|
|
305
344
|
// particle that skipped it would punch a hole in the mask of whatever it drew
|
|
306
345
|
// over.
|
|
@@ -328,8 +367,25 @@ fn fs(in: VSOut) -> FSOut {
|
|
|
328
367
|
c.a *= pu.weight;
|
|
329
368
|
// Weight 0 therefore discards every fragment, so an effect faded out costs
|
|
330
369
|
// nothing past the vertex stage even on the frame the draw is still issued.
|
|
331
|
-
|
|
370
|
+
// Not after a prepass: that pass already decided coverage into depth, and
|
|
371
|
+
// a discard here would cost this pass the early rejection it exists for.
|
|
372
|
+
if (!PREPASSED && c.a <= 0.0) { discard; }
|
|
332
373
|
var out: FSOut;
|
|
374
|
+
out.samples = 0xFu;
|
|
375
|
+
if (PREPASSED) {
|
|
376
|
+
c.a = 1.0;
|
|
377
|
+
} else if (CUTOUT) {
|
|
378
|
+
// Alpha becomes coverage: 0..4 of the pixel's samples, and the fragment is
|
|
379
|
+
// then opaque wherever it lands. The pattern is turned by pixel position
|
|
380
|
+
// so a half-covered edge is not the same two samples on every pixel along
|
|
381
|
+
// it, which is what makes a2c edges look dithered rather than stepped.
|
|
382
|
+
let n = u32(clamp(c.a, 0.0, 1.0) * 4.0 + 0.5);
|
|
383
|
+
if (n == 0u) { discard; }
|
|
384
|
+
let low = (1u << n) - 1u;
|
|
385
|
+
let turn = (u32(in.clip.x) + u32(in.clip.y)) & 3u;
|
|
386
|
+
out.samples = ((low << turn) | (low >> (4u - turn))) & 0xFu;
|
|
387
|
+
c.a = 1.0;
|
|
388
|
+
}
|
|
333
389
|
// PREMULTIPLIED: the scene's colour target blends with srcFactor \"one\", so a
|
|
334
390
|
// straight-alpha fragment would come out over-bright wherever it is
|
|
335
391
|
// translucent — which is most of a soft particle.
|
|
@@ -350,6 +406,30 @@ fn fs(in: VSOut) -> FSOut {
|
|
|
350
406
|
out.mask = vec4f(mg.x, mg.y, 0.0, c.a);
|
|
351
407
|
${sceneIdPadWgsl("out")} return out;
|
|
352
408
|
}
|
|
409
|
+
` +
|
|
410
|
+
(src.blend === "cutout" && src.cover
|
|
411
|
+
? /* wgsl */ `
|
|
412
|
+
/**
|
|
413
|
+
* The prepass: the shape to depth, nothing to colour. Every output is still
|
|
414
|
+
* declared — the pipeline takes the colour targets at writeMask 0 — and the
|
|
415
|
+
* sample mask is exactly what the single-pass cutout would have written, so
|
|
416
|
+
* the shading pass's equal test lands on the same samples.
|
|
417
|
+
*/
|
|
418
|
+
@fragment
|
|
419
|
+
fn fsDepth(in: VSOut) -> FSOut {
|
|
420
|
+
let p = particles[in.id];
|
|
421
|
+
let a = particleCover(p, in.uv) * pu.weight;
|
|
422
|
+
let n = u32(clamp(a, 0.0, 1.0) * 4.0 + 0.5);
|
|
423
|
+
if (n == 0u) { discard; }
|
|
424
|
+
let low = (1u << n) - 1u;
|
|
425
|
+
let turn = (u32(in.clip.x) + u32(in.clip.y)) & 3u;
|
|
426
|
+
var out: FSOut;
|
|
427
|
+
out.samples = ((low << turn) | (low >> (4u - turn))) & 0xFu;
|
|
428
|
+
out.color = vec4f(0.0);
|
|
429
|
+
out.mask = vec4f(0.0);
|
|
430
|
+
${sceneIdPadWgsl("out")} return out;
|
|
431
|
+
}
|
|
353
432
|
`
|
|
433
|
+
: "")
|
|
354
434
|
)
|
|
355
435
|
}
|
|
@@ -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"
|
|
@@ -39,6 +40,9 @@ type TrailSource = {
|
|
|
39
40
|
/** Additive, like most glowing ribbons, or straight alpha. */
|
|
40
41
|
blend: "alpha" | "additive"
|
|
41
42
|
bloom: boolean
|
|
43
|
+
/** The side of this effect's grid, or 0 when it declared none. See the note
|
|
44
|
+
* on the same field in particles.ts: a ribbon reads the grid, never steps it. */
|
|
45
|
+
gridSize: number
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
/**
|
|
@@ -291,6 +295,10 @@ fn rzTurnRadius(a: vec3f, b: vec3f, c: vec3f) -> f32 {
|
|
|
291
295
|
audioApi(0, 4) +
|
|
292
296
|
midiApi(0, 5) +
|
|
293
297
|
lyricsApi(0, 6) +
|
|
298
|
+
// Always, with or without a grid — and required either way, because an
|
|
299
|
+
// effect's whole file is spliced here and a `gridStep` written for the grid
|
|
300
|
+
// pass has to resolve in this module too.
|
|
301
|
+
gridReadApi(0, 8, 9, src.gridSize) +
|
|
294
302
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
295
303
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
296
304
|
idApi(false, 0, 0) + castDistanceStub() +
|