reze-engine 0.42.3 → 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 -410
- 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 +924 -29
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +4043 -278
- 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/model.js +29 -0
- 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/audio-api.d.ts +3 -0
- package/dist/shaders/audio-api.d.ts.map +1 -0
- package/dist/shaders/audio-api.js +81 -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 +31 -15
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +225 -135
- 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 +66 -0
- package/dist/shaders/passes/particles.d.ts.map +1 -0
- package/dist/shaders/passes/particles.js +279 -0
- 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 +59 -0
- package/dist/shaders/passes/trails.d.ts.map +1 -0
- package/dist/shaders/passes/trails.js +340 -0
- package/dist/shaders/score-api.d.ts +10 -0
- package/dist/shaders/score-api.d.ts.map +1 -0
- package/dist/shaders/score-api.js +114 -0
- package/dist/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 +4535 -296
- 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 +31 -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/audio-api.ts +82 -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 +244 -136
- 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 +340 -0
- package/src/shaders/passes/scene-contract.ts +266 -0
- package/src/shaders/passes/trails.ts +390 -0
- package/src/shadow-cascades.ts +97 -0
- package/src/vmd-loader.ts +2 -2
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { RZ_LIGHT_STRUCT_WGSL } from "../lights";
|
|
2
|
+
import { anchorAliasWgsl } from "../anchor-table";
|
|
3
|
+
import { CAST_API } from "../cast-api";
|
|
4
|
+
import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api";
|
|
5
|
+
import { audioApi } from "../audio-api";
|
|
6
|
+
import { lyricsApi, lyricsTextApi } from "../lyrics-api";
|
|
7
|
+
import { midiApi } from "../midi-api";
|
|
8
|
+
import { gridReadApi } from "./grid";
|
|
1
9
|
// Composite: HDR scene + bloom pyramid → Filmic tone map → gamma → swapchain.
|
|
2
10
|
// Bloom tint/intensity applied at combine (EEVEE treats them as combine-stage params, not prefilter).
|
|
3
11
|
//
|
|
@@ -69,20 +77,9 @@ export function parseEffectAnchors(wgsl, max) {
|
|
|
69
77
|
.map((m) => ({ bone: m[1], trail: m[2] !== undefined }))
|
|
70
78
|
.slice(0, max);
|
|
71
79
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* the layout arithmetic on both sides has to agree exactly, and two literals
|
|
76
|
-
* that must match are two literals that eventually will not.
|
|
77
|
-
*
|
|
78
|
-
* All three are MINIMUMS. Raising one breaks nothing, because effects read
|
|
79
|
-
* through accessors and loop to the count functions; lowering one does.
|
|
80
|
-
*/
|
|
81
|
-
export const EFFECT_SUBJECTS = 4;
|
|
82
|
-
export const EFFECT_ANCHORS = 8;
|
|
83
|
-
export const EFFECT_TRAIL_SAMPLES = 128;
|
|
84
|
-
/** vec4 slot where the trails begin — after the subjects and the anchors. */
|
|
85
|
-
export const EFFECT_TRAIL_BASE = EFFECT_SUBJECTS * 3 + EFFECT_ANCHORS * EFFECT_SUBJECTS * 3;
|
|
80
|
+
// The cast's caps live in cast-layout.ts — re-exported here because half the
|
|
81
|
+
// engine imports them from this file and the constants did not move in meaning.
|
|
82
|
+
export { EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES } from "../cast-layout";
|
|
86
83
|
const COMPOSITE_HEAD = /* wgsl */ `
|
|
87
84
|
// Pipeline-override constant: the engine creates two composite pipelines, one
|
|
88
85
|
// with APPLY_GAMMA=false (gamma=1 fast path) and one with APPLY_GAMMA=true.
|
|
@@ -148,13 +145,39 @@ override APPLY_GAMMA: bool = true;
|
|
|
148
145
|
// vec4 slots: [0 .. 11] four subjects, three each (root+valid, hip, bounds);
|
|
149
146
|
// then MAX_ANCHORS × four subjects, three each (pos+valid, vel, fwd).
|
|
150
147
|
@group(0) @binding(11) var<storage, read> _rzCast: array<vec4f>;
|
|
148
|
+
// The FIELD LAYER: the user's background/foreground mounts, rendered at half
|
|
149
|
+
// resolution in their own pass (see buildFieldShader) and sampled here. Field
|
|
150
|
+
// effects are glows, fog, shafts, gradients — low-frequency by nature — and
|
|
151
|
+
// running them per quarter-pixel was the single largest per-frame cost an
|
|
152
|
+
// effect could add. Bilinear upsampling is invisible at that frequency.
|
|
153
|
+
// The field layer, ONE PAIR PER RESOLUTION. 15/16 are full res, 20/21 half.
|
|
154
|
+
// An effect draws into the pair it declared, so a starfield that upsamples
|
|
155
|
+
// perfectly no longer pays full-resolution pixels because a neighbour needed
|
|
156
|
+
// crisp edges. Both pairs are read every frame and combined full-over-half
|
|
157
|
+
// below; a pair NO effect draws into is bound to a 1x1 transparent texture
|
|
158
|
+
// instead of a target, so nothing has to clear a full-res pair to hand this
|
|
159
|
+
// shader the transparent black it would then read.
|
|
160
|
+
@group(0) @binding(15) var fieldBgTex: texture_2d<f32>;
|
|
161
|
+
@group(0) @binding(16) var fieldFgTex: texture_2d<f32>;
|
|
162
|
+
@group(0) @binding(20) var fieldBgHalfTex: texture_2d<f32>;
|
|
163
|
+
@group(0) @binding(21) var fieldFgHalfTex: texture_2d<f32>;
|
|
151
164
|
|
|
165
|
+
/** Two field layers into one, premultiplied OVER, top in front. A resolution
|
|
166
|
+
* boundary is a layer boundary: within a pair, effects blend in document
|
|
167
|
+
* order; across pairs, full res wins. (No backticks in here — this string is
|
|
168
|
+
* a TS template literal and one ends it mid-shader.) */
|
|
169
|
+
fn rzFieldMerge(top: vec4f, bot: vec4f) -> vec4f {
|
|
170
|
+
return vec4f(top.rgb + bot.rgb * (1.0 - top.a), top.a + bot.a * (1.0 - top.a));
|
|
171
|
+
}
|
|
152
172
|
// Must match FILMIC_LUT_WIDTH in engine.ts (bakeFilmicLut).
|
|
153
173
|
const FILMIC_LUT_W: f32 = 256.0;
|
|
154
174
|
|
|
155
175
|
fn linearDepth(coord: vec2<i32>) -> f32 {
|
|
156
176
|
let z = textureLoad(depthTex, coord, 0);
|
|
157
|
-
// projA
|
|
177
|
+
// projA and projB are m[10] and m[14] of whatever projection the camera built,
|
|
178
|
+
// so this one line inverts both conventions. Non-reversed puts projA just above
|
|
179
|
+
// 1; reversed puts it slightly below 0. Either way (z - projA) keeps its sign
|
|
180
|
+
// across the whole of z in [0,1], so the divisor never crosses zero.
|
|
158
181
|
return clamp(dofU[2].y / (z - dofU[2].x), 0.05, 100000.0);
|
|
159
182
|
}
|
|
160
183
|
|
|
@@ -242,7 +265,21 @@ fn viewTransform(c: vec3f) -> vec3f {
|
|
|
242
265
|
if (mode > 0.5) { return vec3f(srgb_encode(c.r), srgb_encode(c.g), srgb_encode(c.b)); }
|
|
243
266
|
return vec3f(filmic(c.r), filmic(c.g), filmic(c.b));
|
|
244
267
|
}
|
|
245
|
-
|
|
268
|
+
`;
|
|
269
|
+
/**
|
|
270
|
+
* The scene half of the effect API — camera, projection, cast, trails.
|
|
271
|
+
*
|
|
272
|
+
* Split out of COMPOSITE_HEAD so the SIM pass can have it too. One effect file
|
|
273
|
+
* is spliced into every module it has a mount in, so a file with both a grid and
|
|
274
|
+
* a foreground compiles its foreground inside the grid shader, where every
|
|
275
|
+
* rzCameraPos() in it has to resolve. Sharing the block is better than stubbing
|
|
276
|
+
* it twice, and it means a kernel can legitimately ask where a bone is — which
|
|
277
|
+
* is exactly what a wake wants.
|
|
278
|
+
*
|
|
279
|
+
* Depends on `viewU` and `_rzCast` being declared by the including module, and
|
|
280
|
+
* on nothing else: no textures, no samplers.
|
|
281
|
+
*/
|
|
282
|
+
export const EFFECT_SCENE_API = /* wgsl */ `
|
|
246
283
|
// ── The effect API ────────────────────────────────────────────────────────────
|
|
247
284
|
//
|
|
248
285
|
// Named rz*, for the engine. The prefix earns its place twice: user code is
|
|
@@ -288,104 +325,22 @@ fn rzProject(p: vec3f) -> vec3f {
|
|
|
288
325
|
return vec3f(ndc * 0.5 + 0.5, z);
|
|
289
326
|
}
|
|
290
327
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
/** At the hips, the middle of the body — where an aura belongs. */
|
|
296
|
-
center: vec3f,
|
|
297
|
-
/** Bounding sphere: xyz centre, w radius. Deliberately generous — cull with it. */
|
|
298
|
-
bounds: vec4f,
|
|
299
|
-
/** False past the end of the cast, and every field is then zero. */
|
|
300
|
-
valid: bool,
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** One bone an effect asked for, by name, at the top of its own source. */
|
|
304
|
-
struct RzAnchor {
|
|
305
|
-
pos: vec3f,
|
|
306
|
-
/** World units per second, from the previous frame. Direction for a trail,
|
|
307
|
-
* magnitude for anything that should react to how hard someone is moving. */
|
|
308
|
-
vel: vec3f,
|
|
309
|
-
/** The bone's forward axis — which way a foot points, where a head looks. */
|
|
310
|
-
fwd: vec3f,
|
|
311
|
-
/** False when this rig has no such bone. Check it: the alternative is drawing
|
|
312
|
-
* a hand effect at the world origin on every model that spells it differently. */
|
|
313
|
-
valid: bool,
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const RZ_MAX_ANCHORS: i32 = ${EFFECT_ANCHORS};
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Character i. Loop to rzSubjectCount(), never to a constant — the caps here are
|
|
320
|
-
* MINIMUMS and are free to grow, which is only true while nobody hardcodes them.
|
|
321
|
-
*/
|
|
322
|
-
fn rzSubject(i: i32) -> RzSubject {
|
|
323
|
-
var s: RzSubject;
|
|
324
|
-
s.valid = i >= 0 && i < rzSubjectCount();
|
|
325
|
-
if (!s.valid) { return s; }
|
|
326
|
-
let b = i * 3;
|
|
327
|
-
s.root = _rzCast[b].xyz;
|
|
328
|
-
s.center = _rzCast[b + 1].xyz;
|
|
329
|
-
s.bounds = _rzCast[b + 2];
|
|
330
|
-
return s;
|
|
331
|
-
}
|
|
328
|
+
fn rzCamPos() -> vec3f { return rzCameraPos(); }
|
|
329
|
+
fn rzCameraRight() -> vec3f { return viewU[3].xyz; }
|
|
330
|
+
fn rzCameraUp() -> vec3f { return viewU[4].xyz; }
|
|
331
|
+
fn rzCameraForward() -> vec3f { return viewU[5].xyz; }
|
|
332
332
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
*
|
|
338
|
-
* // @anchor 左手首
|
|
339
|
-
* // @anchor 頭
|
|
340
|
-
*
|
|
341
|
-
* gives you slot 0 and slot 1. Any bone name the model has works; valid is
|
|
342
|
-
* false when it does not have it, which is the normal case across rigs that
|
|
343
|
-
* spell things differently.
|
|
344
|
-
*/
|
|
345
|
-
fn rzAnchor(subject: i32, slot: i32) -> RzAnchor {
|
|
346
|
-
var a: RzAnchor;
|
|
347
|
-
a.valid = false;
|
|
348
|
-
if (subject < 0 || subject >= rzSubjectCount() || slot < 0 || slot >= RZ_MAX_ANCHORS) { return a; }
|
|
349
|
-
let b = ${EFFECT_SUBJECTS * 3} + (slot * ${EFFECT_SUBJECTS} + subject) * 3;
|
|
350
|
-
a.valid = _rzCast[b].w > 0.5;
|
|
351
|
-
a.pos = _rzCast[b].xyz;
|
|
352
|
-
a.vel = _rzCast[b + 1].xyz;
|
|
353
|
-
a.fwd = _rzCast[b + 2].xyz;
|
|
354
|
-
return a;
|
|
355
|
-
}
|
|
333
|
+
// The cast — subjects, anchors, trails — is CAST_API, shared verbatim with the
|
|
334
|
+
// particle and trail modules. It used to be written out here, a second time, and
|
|
335
|
+
// the two copies had drifted: this one had rzAnchor and that one did not.
|
|
336
|
+
${CAST_API}
|
|
356
337
|
|
|
357
|
-
|
|
338
|
+
// The hashes, the noise and rzFalloff — EFFECT_MATH_API, the same text the
|
|
339
|
+
// particle and trail modules get. The field module used to be missing most of
|
|
340
|
+
// it, so a helper an author wrote for one mount failed to compile in another
|
|
341
|
+
// for no reason visible in the file.
|
|
342
|
+
${EFFECT_MATH_API}${PARTICLE_STRUCT_WGSL}
|
|
358
343
|
|
|
359
|
-
/**
|
|
360
|
-
* How many path samples this anchor has. Zero unless it was declared with
|
|
361
|
-
* trail, and it climbs from zero as the trail fills after the effect loads.
|
|
362
|
-
*
|
|
363
|
-
* Loop to THIS, never to RZ_TRAIL_SAMPLES: the cap is a minimum and is free to
|
|
364
|
-
* grow, which stays true only while nobody hardcodes it.
|
|
365
|
-
*/
|
|
366
|
-
fn rzTrailCount(subject: i32, slot: i32) -> i32 {
|
|
367
|
-
if (subject < 0 || subject >= rzSubjectCount() || slot < 0 || slot >= RZ_MAX_ANCHORS) { return 0; }
|
|
368
|
-
return i32(_rzCast[${EFFECT_SUBJECTS * 3} + (slot * ${EFFECT_SUBJECTS} + subject) * 3 + 2].w);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Sample i of an anchor's path: xyz where it was, w how many seconds ago.
|
|
373
|
-
*
|
|
374
|
-
* i = 0 is NOW and they run backwards in time, so a ribbon is drawn by walking i
|
|
375
|
-
* upward and fading on .w. Sampled at a fixed rate on the SCENE clock, not the
|
|
376
|
-
* display's — so the path is identical in the editor, in an export, and in a
|
|
377
|
-
* re-export, and its spacing does not change with framerate.
|
|
378
|
-
*
|
|
379
|
-
* This is what a hand trail wants instead of position and velocity. One position
|
|
380
|
-
* and one velocity is a straight segment that jitters, because a velocity is a
|
|
381
|
-
* difference between two frames; a path is what actually happened.
|
|
382
|
-
*/
|
|
383
|
-
fn rzTrail(subject: i32, slot: i32, i: i32) -> vec4f {
|
|
384
|
-
let n = rzTrailCount(subject, slot);
|
|
385
|
-
if (i < 0 || i >= n) { return vec4f(0.0); }
|
|
386
|
-
let base = ${EFFECT_TRAIL_BASE} + (slot * ${EFFECT_SUBJECTS} + subject) * RZ_TRAIL_SAMPLES;
|
|
387
|
-
return _rzCast[base + i];
|
|
388
|
-
}
|
|
389
344
|
|
|
390
345
|
fn bgResolution() -> vec2f { return rzResolution(); }
|
|
391
346
|
fn bgCameraPos() -> vec3f { return rzCameraPos(); }
|
|
@@ -432,6 +387,7 @@ fn rzWorldPos(ray: vec3f, depth: f32) -> vec3f {
|
|
|
432
387
|
}
|
|
433
388
|
|
|
434
389
|
fn bgWorldPos(ray: vec3f, depth: f32) -> vec3f { return rzWorldPos(ray, depth); }
|
|
390
|
+
${RZ_LIGHT_STRUCT_WGSL}
|
|
435
391
|
|
|
436
392
|
/** Color grading, applied to the tonemapped SCENE (not the background — see the
|
|
437
393
|
* call site). The core is ASC CDL, the film-industry interchange standard:
|
|
@@ -555,6 +511,19 @@ const COMPOSITE_BODY = /* wgsl */ `
|
|
|
555
511
|
let su = 0.5 + atan2(dir.x, dir.z) * 0.15915494309; // 1/(2π)
|
|
556
512
|
let sv = 0.5 - asin(clamp(dir.y, -1.0, 1.0)) * 0.31830988618; // 1/π
|
|
557
513
|
bgPm = textureSampleLevel(bgEquirect, bloomSamp, vec2f(su, sv), 0.0).rgb;
|
|
514
|
+
if (bg.w > 2.5) {
|
|
515
|
+
// Mode 3: the texels are scene-linear RADIANCE, not display wallpaper.
|
|
516
|
+
// Same exposure, same view transform, same user gamma as the scene —
|
|
517
|
+
// one film for everything in frame, which is what makes a sun roll off
|
|
518
|
+
// like a sun instead of clipping at texture white. bg.x carries the
|
|
519
|
+
// world STRENGTH (the colour slot is dead in equirect modes). The
|
|
520
|
+
// scene's grade stays scene-only, the documented rule above.
|
|
521
|
+
var sky = max(viewTransform(bgPm * bg.x * viewU[0].x), vec3f(0.0));
|
|
522
|
+
if (APPLY_GAMMA) {
|
|
523
|
+
sky = pow(sky, vec3f(viewU[0].y));
|
|
524
|
+
}
|
|
525
|
+
bgPm = sky;
|
|
526
|
+
}
|
|
558
527
|
}
|
|
559
528
|
BACKGROUND_CALL
|
|
560
529
|
}
|
|
@@ -562,6 +531,11 @@ const COMPOSITE_BODY = /* wgsl */ `
|
|
|
562
531
|
// expression, because the foreground mount composites onto it.
|
|
563
532
|
var outRgb = disp * sceneAlpha + bgPm * (1.0 - sceneAlpha);
|
|
564
533
|
var outA = sceneAlpha + bgA * (1.0 - sceneAlpha);
|
|
534
|
+
// Ribbons are NOT read here any more: they draw inside the scene pass, so
|
|
535
|
+
// they are already in disp — tone mapped, and bloomed, which they never
|
|
536
|
+
// were while this line existed. Sampling them here as well would draw them
|
|
537
|
+
// twice, and the layer nothing clears would go stale the moment an effect
|
|
538
|
+
// was removed.
|
|
565
539
|
FOREGROUND_CALL
|
|
566
540
|
return vec4f(outRgb, outA);
|
|
567
541
|
}
|
|
@@ -571,28 +545,29 @@ const COMPOSITE_BODY = /* wgsl */ `
|
|
|
571
545
|
// OVER onto the base layer. No `if` around it: the pipeline is rebuilt per
|
|
572
546
|
// effect, so this text only exists in variants whose WGSL defines background().
|
|
573
547
|
const BACKGROUND_CALL = /* wgsl */ `
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
548
|
+
// The field layer is PREMULTIPLIED: N effects blend into it in document
|
|
549
|
+
// order, and premultiplied is the only form in which repeated OVER composes
|
|
550
|
+
// associatively — straight alpha would need the divide back out on every
|
|
551
|
+
// draw. So rgb is already scaled by its own alpha and must not be again.
|
|
552
|
+
// With one effect drawing over a cleared target this is identical to the
|
|
553
|
+
// straight form it replaced.
|
|
554
|
+
let bgFx = rzFieldMerge(
|
|
555
|
+
clamp(textureSampleLevel(fieldBgTex, bloomSamp, fragCoord.xy / fullSz, 0.0), vec4f(0.0), vec4f(1.0)),
|
|
556
|
+
clamp(textureSampleLevel(fieldBgHalfTex, bloomSamp, fragCoord.xy / fullSz, 0.0), vec4f(0.0), vec4f(1.0)));
|
|
557
|
+
bgPm = bgFx.rgb + bgPm * (1.0 - bgFx.a);
|
|
577
558
|
bgA = bgFx.a + bgA * (1.0 - bgFx.a);
|
|
578
559
|
`;
|
|
579
560
|
// Same OVER, one layer later — onto the finished frame rather than onto the
|
|
580
561
|
// base. Ungated by design: a foreground runs at every pixel, including the ones
|
|
581
562
|
// the model covers, because covering them is the point.
|
|
582
563
|
const FOREGROUND_CALL = /* wgsl */ `
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
let fgFx = clamp(foreground(dir, fgUv, viewU[6].x, linearDepth(coord)), vec4f(0.0), vec4f(1.0));
|
|
589
|
-
outRgb = fgFx.rgb * fgFx.a + outRgb * (1.0 - fgFx.a);
|
|
564
|
+
// Premultiplied, as the background layer above — same reason.
|
|
565
|
+
let fgFx = rzFieldMerge(
|
|
566
|
+
clamp(textureSampleLevel(fieldFgTex, bloomSamp, fragCoord.xy / fullSz, 0.0), vec4f(0.0), vec4f(1.0)),
|
|
567
|
+
clamp(textureSampleLevel(fieldFgHalfTex, bloomSamp, fragCoord.xy / fullSz, 0.0), vec4f(0.0), vec4f(1.0)));
|
|
568
|
+
outRgb = fgFx.rgb + outRgb * (1.0 - fgFx.a);
|
|
590
569
|
outA = fgFx.a + outA * (1.0 - fgFx.a);
|
|
591
570
|
`;
|
|
592
|
-
// Derivative builtins are illegal in non-uniform control flow (WGSL uniformity
|
|
593
|
-
// analysis rejects the pipeline), so the coverage gate below can only wrap
|
|
594
|
-
// effect code that doesn't use them. Checked textually at build time.
|
|
595
|
-
const USES_DERIVATIVES = /\b(?:fwidth|dpdx|dpdy)(?:Fine|Coarse)?\s*\(/;
|
|
596
571
|
/** The condition on the background block (equirect sample + background effect).
|
|
597
572
|
*
|
|
598
573
|
* Two jobs. It skips the block behind pixels the model fully covers — the
|
|
@@ -601,33 +576,148 @@ const USES_DERIVATIVES = /\b(?:fwidth|dpdx|dpdy)(?:Fine|Coarse)?\s*\(/;
|
|
|
601
576
|
* most). And with no background effect compiled in, it also skips the block
|
|
602
577
|
* entirely unless the equirect needs it.
|
|
603
578
|
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
* in the direction of correctness. (The foreground mount itself sits in uniform
|
|
609
|
-
* flow, so derivatives are always legal there.) */
|
|
579
|
+
* Everything the gate wraps is an explicit-LOD sample or a texture read, both
|
|
580
|
+
* always legal in non-uniform flow. It used to also wrap the user's code, which
|
|
581
|
+
* meant an effect using a derivative builtin had to forfeit the gate; that
|
|
582
|
+
* carve-out went with the field pass, and the last of it is below. */
|
|
610
583
|
function backgroundCondition(effect) {
|
|
611
584
|
// sceneAlpha, not alpha: the bokeh gather spreads coverage, so a pixel the
|
|
612
585
|
// sharp scene fully covered can end up needing background behind its blur.
|
|
586
|
+
// (The old derivative carve-out is gone with the inline user code: the field
|
|
587
|
+
// pass runs the whole quad, which is uniform control flow by construction.)
|
|
613
588
|
const coverage = "sceneAlpha < 0.999";
|
|
614
589
|
if (!effect?.hasBackground)
|
|
615
590
|
return `bg.w > 1.5 && ${coverage}`;
|
|
616
|
-
return
|
|
591
|
+
return coverage;
|
|
617
592
|
}
|
|
618
593
|
export function buildCompositeShader(effect) {
|
|
619
594
|
const body = COMPOSITE_BODY.replace("BACKGROUND_COND", backgroundCondition(effect))
|
|
620
595
|
.replace("BACKGROUND_CALL", effect?.hasBackground ? BACKGROUND_CALL.trim() : "")
|
|
621
596
|
.replace("FOREGROUND_CALL", effect?.hasForeground ? FOREGROUND_CALL.trim() : "");
|
|
622
|
-
|
|
623
|
-
|
|
597
|
+
// The composite is STATIC either way now: the user's code compiles in the
|
|
598
|
+
// field module alone, and the composite only decides whether to sample it.
|
|
599
|
+
return COMPOSITE_HEAD +
|
|
600
|
+
EFFECT_SCENE_API + anchorAliasWgsl(effect?.alias ?? []) + audioApi(0, 13) + midiApi(0, 19) + lyricsApi(0, 24) + body;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* The field pass: the user's background/foreground mounts at half resolution,
|
|
604
|
+
* into two rgba16f targets the composite bilinearly upsamples.
|
|
605
|
+
*
|
|
606
|
+
* The fragment reconstructs the FULL-resolution pixel it stands in for and runs
|
|
607
|
+
* the original derivation verbatim — same ndc, same ray, same uv, same depth
|
|
608
|
+
* read — so an effect cannot tell it moved; it is simply asked half as often
|
|
609
|
+
* in each direction.
|
|
610
|
+
*/
|
|
611
|
+
/**
|
|
612
|
+
* Reading the scene's id attachment from a field effect — the consumer the MRT
|
|
613
|
+
* work exists for.
|
|
614
|
+
*
|
|
615
|
+
* A field effect covers the whole screen and has no idea what it is drawing
|
|
616
|
+
* over. These make it addressable: mask a glow to one character, dissolve one
|
|
617
|
+
* material, outline the thing someone selected. Without them the id buffer is
|
|
618
|
+
* written every frame and read by nobody.
|
|
619
|
+
*
|
|
620
|
+
* MULTISAMPLED AND UNRESOLVED, so it is read the way it is written —
|
|
621
|
+
* textureLoad of sample 0, the same rule linearDepth already follows. An
|
|
622
|
+
* averaged id belongs to nothing.
|
|
623
|
+
*
|
|
624
|
+
* When ids are OFF the buffer does not exist, so the accessors are still
|
|
625
|
+
* DECLARED and answer 0 — the reserved nothing. An effect that masks by id then
|
|
626
|
+
* masks nothing at all, which is a scene that renders rather than a shader that
|
|
627
|
+
* will not compile.
|
|
628
|
+
*/
|
|
629
|
+
function idApi(on, group, binding) {
|
|
630
|
+
if (!on) {
|
|
631
|
+
return /* wgsl */ `
|
|
632
|
+
fn rzObjectAt(uv: vec2f) -> u32 { return 0u; }
|
|
633
|
+
fn rzMaterialAt(uv: vec2f) -> u32 { return 0u; }
|
|
634
|
+
`;
|
|
635
|
+
}
|
|
636
|
+
return /* wgsl */ `
|
|
637
|
+
@group(${group}) @binding(${binding}) var _rzIdTex: texture_multisampled_2d<u32>;
|
|
638
|
+
|
|
639
|
+
/** Which OBJECT drew this pixel — compare against rzSubjectId(i). 0 = nothing. */
|
|
640
|
+
fn rzObjectAt(uv: vec2f) -> u32 {
|
|
641
|
+
let sz = vec2f(textureDimensions(_rzIdTex));
|
|
642
|
+
let p = vec2<i32>(clamp(uv, vec2f(0.0), vec2f(1.0)) * sz);
|
|
643
|
+
return textureLoad(_rzIdTex, clamp(p, vec2<i32>(0), vec2<i32>(sz) - vec2<i32>(1)), 0).y;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** Which MATERIAL drew it, within that object. 0 = nothing. */
|
|
647
|
+
fn rzMaterialAt(uv: vec2f) -> u32 {
|
|
648
|
+
let sz = vec2f(textureDimensions(_rzIdTex));
|
|
649
|
+
let p = vec2<i32>(clamp(uv, vec2f(0.0), vec2f(1.0)) * sz);
|
|
650
|
+
return textureLoad(_rzIdTex, clamp(p, vec2<i32>(0), vec2<i32>(sz) - vec2<i32>(1)), 0).x;
|
|
651
|
+
}
|
|
652
|
+
`;
|
|
653
|
+
}
|
|
654
|
+
export function buildFieldShader(effect) {
|
|
655
|
+
const bgLine = effect.hasBackground
|
|
656
|
+
? "out.bg = clamp(background(dir, uv, _rzFieldClock.x), vec4f(0.0), vec4f(1.0));"
|
|
657
|
+
: "";
|
|
658
|
+
const fgLine = effect.hasForeground
|
|
659
|
+
? "out.fg = clamp(foreground(dir, uv, _rzFieldClock.x, linearDepth(vec2<i32>(min(fx, fullSz - 1.0)))), vec4f(0.0), vec4f(1.0));"
|
|
660
|
+
: "";
|
|
624
661
|
return (COMPOSITE_HEAD +
|
|
662
|
+
EFFECT_SCENE_API +
|
|
663
|
+
anchorAliasWgsl(effect.alias ?? []) +
|
|
664
|
+
audioApi(0, 13) +
|
|
665
|
+
midiApi(0, 19) +
|
|
666
|
+
lyricsApi(0, 24) +
|
|
667
|
+
// The words themselves — the atlas rides the grid's sampler, which is
|
|
668
|
+
// declared just below and resolves module-wide.
|
|
669
|
+
lyricsTextApi(0, 25, "_rzGridSamp") +
|
|
670
|
+
// The persistent grid, always bound — a 1×1 of zeroes when the effect has
|
|
671
|
+
// none, so rzGrid() is a function that always exists rather than one an
|
|
672
|
+
// author has to know whether they are allowed to call.
|
|
673
|
+
gridReadApi(0, 17, 18, effect.gridSize) +
|
|
674
|
+
clockApi("_rzFieldClock.x", "0.0") +
|
|
675
|
+
viewportApi("viewU[6].w") +
|
|
676
|
+
trailSlotsApi(effect.trailCount) +
|
|
677
|
+
idApi(effect.ids === true, 0, 23) +
|
|
625
678
|
"\n// ── user effect (setEffect) ──\n" +
|
|
626
679
|
effect.paramsDecl +
|
|
627
680
|
"\n" +
|
|
628
681
|
effect.wgsl +
|
|
629
682
|
"\n" +
|
|
630
|
-
|
|
683
|
+
/* wgsl */ `
|
|
684
|
+
@group(0) @binding(14) var<uniform> fieldU: vec4f;
|
|
685
|
+
/**
|
|
686
|
+
* THIS EFFECT'S OWN clock, seconds since it was installed.
|
|
687
|
+
*
|
|
688
|
+
* Per effect, and that is the whole point of it existing. The time argument
|
|
689
|
+
* used to come from viewU[6].x, which is measured from the FIRST installed
|
|
690
|
+
* effect's epoch — so every later effect started mid-stream, and an effect
|
|
691
|
+
* whose lightEmit read its own epoch disagreed with its own background()
|
|
692
|
+
* about what time it was. One buffer per effect, one answer.
|
|
693
|
+
*/
|
|
694
|
+
@group(0) @binding(22) var<uniform> _rzFieldClock: vec4f;
|
|
695
|
+
|
|
696
|
+
@vertex fn fieldVs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
697
|
+
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
698
|
+
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
699
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
struct FieldOut {
|
|
703
|
+
@location(0) bg: vec4f,
|
|
704
|
+
@location(1) fg: vec4f,
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
@fragment fn fieldFs(@builtin(position) fragCoord: vec4f) -> FieldOut {
|
|
708
|
+
let fullSz = fieldU.zw;
|
|
709
|
+
let fx = fragCoord.xy * (fullSz / max(fieldU.xy, vec2f(1.0)));
|
|
710
|
+
let ndc = vec2f(fx.x / fullSz.x * 2.0 - 1.0, 1.0 - fx.y / fullSz.y * 2.0);
|
|
711
|
+
let dir = normalize(viewU[5].xyz + ndc.x * viewU[3].w * viewU[3].xyz + ndc.y * viewU[4].w * viewU[4].xyz);
|
|
712
|
+
let uv = vec2f(fx.x / fullSz.x, 1.0 - fx.y / fullSz.y);
|
|
713
|
+
var out: FieldOut;
|
|
714
|
+
out.bg = vec4f(0.0);
|
|
715
|
+
out.fg = vec4f(0.0);
|
|
716
|
+
${bgLine}
|
|
717
|
+
${fgLine}
|
|
718
|
+
return out;
|
|
719
|
+
}
|
|
720
|
+
`);
|
|
631
721
|
}
|
|
632
722
|
/** Kept for compatibility with existing imports (the base, no-effect shader). */
|
|
633
723
|
export const COMPOSITE_SHADER_WGSL = buildCompositeShader(null);
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const CULL_COMPUTE_WGSL = "\nstruct DrawMeta {\n lo: vec3f, // model-space AABB min \u2014 read only for RIGID models\n model: u32, // index into models[]\n hi: vec3f, // model-space AABB max\n flags: u32, // bit0 = casts shadow\n};\n\nstruct ModelRec {\n xform: mat4x4f, // model space -> world; meaningful only when RIGID\n sphere: vec4f, // world-space centre.xyz + radius.w; meaningful when SKINNED\n flags: u32, // bit0 = visible, bit1 = rigid\n pad0: u32,\n pad1: u32,\n pad2: u32,\n};\n\n// Camera planes at 0..5, light planes at 6..11, mirror planes at 12..17.\n// counts.x = draw count, counts.y = 0 to pass everything (setCullEnabled).\nstruct Frusta {\n planes: array<vec4f, 18>,\n counts: vec4u,\n};\n\n@group(0) @binding(0) var<storage, read> metas: array<DrawMeta>;\n@group(0) @binding(1) var<storage, read> models: array<ModelRec>;\n@group(0) @binding(2) var<uniform> frusta: Frusta;\n@group(0) @binding(3) var<storage, read_write> cameraArgs: array<u32>;\n@group(0) @binding(4) var<storage, read_write> shadowArgs: array<u32>;\n// 1 = the user hid this material, or a material morph drove its alpha to zero.\n// Here rather than in the encode loop because a render bundle bakes its draw\n// list: a face VMD rewrites the morph-hidden set every frame, so a bundle that\n// invalidated on it would re-record every frame and cost more than it saves.\n@group(0) @binding(5) var<storage, read> hidden: array<u32>;\n@group(0) @binding(6) var<storage, read_write> mirrorArgs: array<u32>;\n\nconst DRAW_CASTS_SHADOW: u32 = 1u;\nconst MODEL_VISIBLE: u32 = 1u;\nconst MODEL_RIGID: u32 = 2u;\n\n// Signed distance of the AABB's most-positive corner: the centre distance plus\n// the extent projected onto |n|. Negative means every corner is behind the\n// plane, which is the only case that can reject.\nfn aabbVisible(base: u32, c: vec3f, e: vec3f) -> bool {\n for (var i = 0u; i < 6u; i = i + 1u) {\n let p = frusta.planes[base + i];\n if (dot(p.xyz, c) + p.w + dot(e, abs(p.xyz)) < 0.0) { return false; }\n }\n return true;\n}\n\nfn sphereVisible(base: u32, s: vec4f) -> bool {\n for (var i = 0u; i < 6u; i = i + 1u) {\n let p = frusta.planes[base + i];\n if (dot(p.xyz, s.xyz) + p.w + s.w < 0.0) { return false; }\n }\n return true;\n}\n\n@compute @workgroup_size(64)\nfn cs(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i >= frusta.counts.x) { return; }\n\n // Named dm, not meta: \"meta\" is a WGSL reserved word (reserved for future use,\n // so it parses nowhere). No backticks anywhere in this string either \u2014 it is a\n // template literal, and one would end the shader mid-comment.\n let dm = metas[i];\n let m = models[dm.model];\n let visible = (m.flags & MODEL_VISIBLE) != 0u;\n\n var inCamera = false;\n var inLight = false;\n var inMirror = false;\n if (visible) {\n if ((m.flags & MODEL_RIGID) != 0u) {\n let c = (dm.lo + dm.hi) * 0.5;\n let e = (dm.hi - dm.lo) * 0.5;\n let wc = (m.xform * vec4f(c, 1.0)).xyz;\n // Extent of the transformed box: the abs of the linear part applied to the\n // half-extent. Exact for the axis-aligned case, conservative for a rotation.\n let a = mat3x3f(abs(m.xform[0].xyz), abs(m.xform[1].xyz), abs(m.xform[2].xyz));\n let we = a * e;\n inCamera = aabbVisible(0u, wc, we);\n inLight = aabbVisible(6u, wc, we);\n inMirror = aabbVisible(12u, wc, we);\n } else {\n inCamera = sphereVisible(0u, m.sphere);\n inLight = sphereVisible(6u, m.sphere);\n inMirror = sphereVisible(12u, m.sphere);\n }\n }\n\n // Culling off (setCullEnabled) passes everything, so a scene can be A/B'd\n // against its own unculled self without rebuilding anything. The pass still\n // runs, so the diagnostics keep reporting. It bypasses the FRUSTUM only \u2014\n // a hidden material is not culled, it is switched off, and answering \"is this\n // gone because of culling?\" requires the two to stay separable.\n let off = frusta.counts.y == 0u;\n let shown = hidden[i] == 0u;\n let castsShadow = (dm.flags & DRAW_CASTS_SHADOW) != 0u;\n cameraArgs[i * 5u + 1u] = select(0u, 1u, (inCamera || off) && shown);\n shadowArgs[i * 5u + 1u] = select(0u, 1u, ((inLight && castsShadow) || off) && shown);\n mirrorArgs[i * 5u + 1u] = select(0u, 1u, (inMirror || off) && shown);\n}\n";
|
|
2
|
+
//# sourceMappingURL=cull.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cull.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/cull.ts"],"names":[],"mappings":"AAiCA,eAAO,MAAM,iBAAiB,uwIAyG7B,CAAA"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// GPU frustum cull writing indirect draw arguments.
|
|
2
|
+
//
|
|
3
|
+
// One dispatch covers every material draw in the scene and writes THREE
|
|
4
|
+
// argument buffers — the camera pass, the shadow pass and the mirror pass —
|
|
5
|
+
// because they differ only in which frustum they test against, and splitting
|
|
6
|
+
// them into separate dispatches would read the same metadata each time. See
|
|
7
|
+
// docs/frame-graph.md.
|
|
8
|
+
//
|
|
9
|
+
// The MIRROR frustum is the camera frustum reflected about the floor plane: an
|
|
10
|
+
// object is visible in the mirror exactly when its reflection is visible to
|
|
11
|
+
// the camera, i.e. when the object itself lies in the reflected frustum. The
|
|
12
|
+
// camera args would be WRONG for it — a close-up of the floor shows a dancer's
|
|
13
|
+
// reflection while the dancer herself is out of frame. When no reflection is
|
|
14
|
+
// active the CPU writes the camera planes into the mirror slots, so the args
|
|
15
|
+
// stay sane for bundles that never execute.
|
|
16
|
+
//
|
|
17
|
+
// The compute writes ONLY `instanceCount` (word 1 of each 5-word record). The
|
|
18
|
+
// other four words — indexCount, firstIndex, baseVertex, firstInstance — are
|
|
19
|
+
// seeded by the CPU when the draw list changes, which is the same moment the
|
|
20
|
+
// metadata buffer is uploaded. Nothing about them varies per frame, so shipping
|
|
21
|
+
// them through the compute would be a per-frame write in exchange for nothing.
|
|
22
|
+
//
|
|
23
|
+
// Two bound kinds, because a skinned mesh has no usable model-space bounds:
|
|
24
|
+
// RIGID — every bone of the model shares one skin matrix (a stage, or a
|
|
25
|
+
// character still in its bind pose), so each material's load-time
|
|
26
|
+
// model-space AABB is valid and gets transformed by that matrix.
|
|
27
|
+
// This is what makes a 200-material stage worth culling at all.
|
|
28
|
+
// SKINNED — animation invalidates per-material bounds, so the whole model is
|
|
29
|
+
// one world-space sphere derived from its posed bone positions.
|
|
30
|
+
//
|
|
31
|
+
// Planes are NORMALIZED on the CPU. The AABB test would tolerate unnormalized
|
|
32
|
+
// planes (distance and extent scale together), the sphere test would not.
|
|
33
|
+
export const CULL_COMPUTE_WGSL = /* wgsl */ `
|
|
34
|
+
struct DrawMeta {
|
|
35
|
+
lo: vec3f, // model-space AABB min — read only for RIGID models
|
|
36
|
+
model: u32, // index into models[]
|
|
37
|
+
hi: vec3f, // model-space AABB max
|
|
38
|
+
flags: u32, // bit0 = casts shadow
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
struct ModelRec {
|
|
42
|
+
xform: mat4x4f, // model space -> world; meaningful only when RIGID
|
|
43
|
+
sphere: vec4f, // world-space centre.xyz + radius.w; meaningful when SKINNED
|
|
44
|
+
flags: u32, // bit0 = visible, bit1 = rigid
|
|
45
|
+
pad0: u32,
|
|
46
|
+
pad1: u32,
|
|
47
|
+
pad2: u32,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Camera planes at 0..5, light planes at 6..11, mirror planes at 12..17.
|
|
51
|
+
// counts.x = draw count, counts.y = 0 to pass everything (setCullEnabled).
|
|
52
|
+
struct Frusta {
|
|
53
|
+
planes: array<vec4f, 18>,
|
|
54
|
+
counts: vec4u,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
@group(0) @binding(0) var<storage, read> metas: array<DrawMeta>;
|
|
58
|
+
@group(0) @binding(1) var<storage, read> models: array<ModelRec>;
|
|
59
|
+
@group(0) @binding(2) var<uniform> frusta: Frusta;
|
|
60
|
+
@group(0) @binding(3) var<storage, read_write> cameraArgs: array<u32>;
|
|
61
|
+
@group(0) @binding(4) var<storage, read_write> shadowArgs: array<u32>;
|
|
62
|
+
// 1 = the user hid this material, or a material morph drove its alpha to zero.
|
|
63
|
+
// Here rather than in the encode loop because a render bundle bakes its draw
|
|
64
|
+
// list: a face VMD rewrites the morph-hidden set every frame, so a bundle that
|
|
65
|
+
// invalidated on it would re-record every frame and cost more than it saves.
|
|
66
|
+
@group(0) @binding(5) var<storage, read> hidden: array<u32>;
|
|
67
|
+
@group(0) @binding(6) var<storage, read_write> mirrorArgs: array<u32>;
|
|
68
|
+
|
|
69
|
+
const DRAW_CASTS_SHADOW: u32 = 1u;
|
|
70
|
+
const MODEL_VISIBLE: u32 = 1u;
|
|
71
|
+
const MODEL_RIGID: u32 = 2u;
|
|
72
|
+
|
|
73
|
+
// Signed distance of the AABB's most-positive corner: the centre distance plus
|
|
74
|
+
// the extent projected onto |n|. Negative means every corner is behind the
|
|
75
|
+
// plane, which is the only case that can reject.
|
|
76
|
+
fn aabbVisible(base: u32, c: vec3f, e: vec3f) -> bool {
|
|
77
|
+
for (var i = 0u; i < 6u; i = i + 1u) {
|
|
78
|
+
let p = frusta.planes[base + i];
|
|
79
|
+
if (dot(p.xyz, c) + p.w + dot(e, abs(p.xyz)) < 0.0) { return false; }
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn sphereVisible(base: u32, s: vec4f) -> bool {
|
|
85
|
+
for (var i = 0u; i < 6u; i = i + 1u) {
|
|
86
|
+
let p = frusta.planes[base + i];
|
|
87
|
+
if (dot(p.xyz, s.xyz) + p.w + s.w < 0.0) { return false; }
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@compute @workgroup_size(64)
|
|
93
|
+
fn cs(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
94
|
+
let i = gid.x;
|
|
95
|
+
if (i >= frusta.counts.x) { return; }
|
|
96
|
+
|
|
97
|
+
// Named dm, not meta: "meta" is a WGSL reserved word (reserved for future use,
|
|
98
|
+
// so it parses nowhere). No backticks anywhere in this string either — it is a
|
|
99
|
+
// template literal, and one would end the shader mid-comment.
|
|
100
|
+
let dm = metas[i];
|
|
101
|
+
let m = models[dm.model];
|
|
102
|
+
let visible = (m.flags & MODEL_VISIBLE) != 0u;
|
|
103
|
+
|
|
104
|
+
var inCamera = false;
|
|
105
|
+
var inLight = false;
|
|
106
|
+
var inMirror = false;
|
|
107
|
+
if (visible) {
|
|
108
|
+
if ((m.flags & MODEL_RIGID) != 0u) {
|
|
109
|
+
let c = (dm.lo + dm.hi) * 0.5;
|
|
110
|
+
let e = (dm.hi - dm.lo) * 0.5;
|
|
111
|
+
let wc = (m.xform * vec4f(c, 1.0)).xyz;
|
|
112
|
+
// Extent of the transformed box: the abs of the linear part applied to the
|
|
113
|
+
// half-extent. Exact for the axis-aligned case, conservative for a rotation.
|
|
114
|
+
let a = mat3x3f(abs(m.xform[0].xyz), abs(m.xform[1].xyz), abs(m.xform[2].xyz));
|
|
115
|
+
let we = a * e;
|
|
116
|
+
inCamera = aabbVisible(0u, wc, we);
|
|
117
|
+
inLight = aabbVisible(6u, wc, we);
|
|
118
|
+
inMirror = aabbVisible(12u, wc, we);
|
|
119
|
+
} else {
|
|
120
|
+
inCamera = sphereVisible(0u, m.sphere);
|
|
121
|
+
inLight = sphereVisible(6u, m.sphere);
|
|
122
|
+
inMirror = sphereVisible(12u, m.sphere);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Culling off (setCullEnabled) passes everything, so a scene can be A/B'd
|
|
127
|
+
// against its own unculled self without rebuilding anything. The pass still
|
|
128
|
+
// runs, so the diagnostics keep reporting. It bypasses the FRUSTUM only —
|
|
129
|
+
// a hidden material is not culled, it is switched off, and answering "is this
|
|
130
|
+
// gone because of culling?" requires the two to stay separable.
|
|
131
|
+
let off = frusta.counts.y == 0u;
|
|
132
|
+
let shown = hidden[i] == 0u;
|
|
133
|
+
let castsShadow = (dm.flags & DRAW_CASTS_SHADOW) != 0u;
|
|
134
|
+
cameraArgs[i * 5u + 1u] = select(0u, 1u, (inCamera || off) && shown);
|
|
135
|
+
shadowArgs[i * 5u + 1u] = select(0u, 1u, ((inLight && castsShadow) || off) && shown);
|
|
136
|
+
mirrorArgs[i * 5u + 1u] = select(0u, 1u, (inMirror || off) && shown);
|
|
137
|
+
}
|
|
138
|
+
`;
|
|
@@ -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"}
|