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/dist/shaders/lights.js
CHANGED
|
@@ -37,6 +37,7 @@ import { midiApi } from "./midi-api";
|
|
|
37
37
|
import { lyricsApi } from "./lyrics-api";
|
|
38
38
|
import { clockApi, trailSlotsApi, viewportApi } from "./passes/hosted-api";
|
|
39
39
|
import { idApi } from "./id-api";
|
|
40
|
+
import { sceneLightApi, worldAmbientWgsl } from "./scene-light-api";
|
|
40
41
|
/** Floats before the first record. One is the count; the rest keep the records
|
|
41
42
|
* vec4-aligned, which is what lets a future pass read them as vec4s. */
|
|
42
43
|
export const LIGHT_HEADER = 4;
|
|
@@ -80,29 +81,8 @@ struct RzLight {
|
|
|
80
81
|
radius: f32,
|
|
81
82
|
}
|
|
82
83
|
`;
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
* HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
|
|
86
|
-
* ibl.ts for the folding; the shader is a plain polynomial in the normal).
|
|
87
|
-
*
|
|
88
|
-
* One string included by every module that declares LightUniforms with the sh
|
|
89
|
-
* block — the hosted-api lesson: a helper defined in some modules and not
|
|
90
|
-
* others is a compile error waiting for the first file that crosses them.
|
|
91
|
-
*/
|
|
92
|
-
export const WORLD_AMBIENT_WGSL = /* wgsl */ `
|
|
93
|
-
fn rzWorldAmbient(n: vec3f) -> vec3f {
|
|
94
|
-
if (light.sh[0].w < 0.5) { return light.ambientColor.xyz; }
|
|
95
|
-
let x = n.x;
|
|
96
|
-
let y = n.y;
|
|
97
|
-
let z = n.z;
|
|
98
|
-
let c = light.sh[0].xyz
|
|
99
|
-
+ light.sh[1].xyz * y + light.sh[2].xyz * z + light.sh[3].xyz * x
|
|
100
|
-
+ light.sh[4].xyz * (x * y) + light.sh[5].xyz * (y * z)
|
|
101
|
-
+ light.sh[6].xyz * (3.0 * z * z - 1.0) + light.sh[7].xyz * (x * z)
|
|
102
|
-
+ light.sh[8].xyz * (x * x - y * y);
|
|
103
|
-
return max(c, vec3f(0.0));
|
|
104
|
-
}
|
|
105
|
-
`;
|
|
84
|
+
/** rzWorldAmbient against the materials' and the ground's `light` uniform. */
|
|
85
|
+
export const WORLD_AMBIENT_WGSL = worldAmbientWgsl("light");
|
|
106
86
|
/** Does this source define the emit mount? */
|
|
107
87
|
export function hasLightEmit(wgsl) {
|
|
108
88
|
return /\bfn\s+lightEmit\s*\(/.test(wgsl);
|
|
@@ -173,7 +153,7 @@ ${viewportApi("viewU[6].w")}
|
|
|
173
153
|
${trailSlotsApi(cast.trailCount)}
|
|
174
154
|
// The id accessors, stubbed: this module cannot read an attachment the
|
|
175
155
|
// scene pass writes. See id-api.ts — the author's whole file compiles here.
|
|
176
|
-
${idApi(false, 0, 0) + castDistanceStub()}
|
|
156
|
+
${idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0)}
|
|
177
157
|
// The dials the author declared, if any. A lamp is exactly the thing someone
|
|
178
158
|
// retunes — its colour and its reach — so an emitter reads params like every
|
|
179
159
|
// other mount rather than being the one place a #param resolves to nothing.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA4BA,eAAO,MAAM,oBAAoB,QAmJhC,CAAC;AA8HF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,orMAgHzB,CAAA;AAQD,eAAO,MAAM,4BAA4B,QAC0D,CAAA"}
|
|
@@ -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
|
// Shared WGSL blocks concatenated by every material shader.
|
|
6
7
|
// Splits the boilerplate (uniform structs, bind group layout, skinning VS, PCF shadow)
|
|
@@ -171,8 +172,8 @@ fn safe_normal(nIn: vec3f) -> vec3f {
|
|
|
171
172
|
|
|
172
173
|
`;
|
|
173
174
|
// ─── Shadow sampler (3×3 PCF, cascade-selected) ─────────────────────
|
|
174
|
-
// Normal-bias 0.08, depth-bias 0.001 NDC
|
|
175
|
-
//
|
|
175
|
+
// Normal-bias 0.08, depth-bias 0.001 NDC; the kernel is pcf9 in scene-light-api.ts,
|
|
176
|
+
// shared with effects. Texel sizes are interpolated
|
|
176
177
|
// from SHADOW_CASCADES so they cannot go stale the way the hardcoded 1/2048
|
|
177
178
|
// once did (PCF taps landed TWO texels apart after the map grew to 4096,
|
|
178
179
|
// quantizing self-shadow edges into crawling gray squares).
|
|
@@ -183,21 +184,6 @@ fn safe_normal(nIn: vec3f) -> vec3f {
|
|
|
183
184
|
// merely a default: the old single-volume path clamped, and past the light's
|
|
184
185
|
// far plane the comparison failed against every stored depth, silently
|
|
185
186
|
// shadowing any stage deeper than the box.
|
|
186
|
-
const pcf9 = (map, ts) => /* wgsl */ `
|
|
187
|
-
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
188
|
-
let cmpZ = ndc.z - 0.001;
|
|
189
|
-
let ts = ${ts};
|
|
190
|
-
let s00 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
191
|
-
let s10 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
192
|
-
let s20 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
193
|
-
let s01 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
194
|
-
let s11 = textureSampleCompareLevel(${map}, shadowSampler, suv, cmpZ);
|
|
195
|
-
let s21 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
196
|
-
let s02 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
197
|
-
let s12 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
198
|
-
let s22 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
199
|
-
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
200
|
-
`;
|
|
201
187
|
const SAMPLE_SHADOW_WGSL = /* wgsl */ `
|
|
202
188
|
|
|
203
189
|
fn sampleShadowNear(ndc: vec3f) -> f32 {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AA4BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAgC+B;AAC/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EAAE,CAIhG;AAID,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAEzG,KAAK,qBAAqB,GAAG;IAC3B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,aAAa,EAAE,OAAO,CAAA;IACtB,oEAAoE;IACpE,aAAa,EAAE,OAAO,CAAA;IACtB;6DACyD;IACzD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAA;IAChB;mFAC+E;IAC/E,GAAG,CAAC,EAAE,OAAO,CAAA;IACb;;;yEAGqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB;;8EAE0E;IAC1E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB,CAAA;AA+LD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,+ufAsI5B,CAAA;AA8MD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAclF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAyGtE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
|
|
@@ -5,6 +5,7 @@ import { CAST_API, CAST_MASK_ALL, subjectMaskApi } from "../cast-api";
|
|
|
5
5
|
import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api";
|
|
6
6
|
import { audioApi } from "../audio-api";
|
|
7
7
|
import { idApi } from "../id-api";
|
|
8
|
+
import { sceneLightApi } from "../scene-light-api";
|
|
8
9
|
import { castDistanceApi, CAST_FIELD_DIV } from "./cast-distance";
|
|
9
10
|
import { lyricsApi, lyricsTextApi } from "../lyrics-api";
|
|
10
11
|
import { midiApi } from "../midi-api";
|
|
@@ -681,6 +682,7 @@ export function buildFieldShader(effect) {
|
|
|
681
682
|
viewportApi("viewU[6].w") +
|
|
682
683
|
trailSlotsApi(effect.trailCount) +
|
|
683
684
|
idApi(effect.ids === true, 0, 23) +
|
|
685
|
+
sceneLightApi(false, 0, 0) +
|
|
684
686
|
// Distance to the cast, in SCREEN pixels: the field is half-res, so a field
|
|
685
687
|
// texel is CAST_FIELD_DIV of them and the accessor scales on the way out.
|
|
686
688
|
// An author writes the width they mean and never learns how it is built.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/grid.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAmC7C,eAAO,MAAM,QAAQ,OAAO,CAAA;AAC5B,uEAAuE;AACvE,eAAO,MAAM,UAAU,EAAE,gBAAgC,CAAA;AAEzD,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;AAgBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,SAAK,GAAG,MAAM,CAqFpG"}
|
|
@@ -7,6 +7,7 @@ import { subjectMaskApi } from "../cast-api";
|
|
|
7
7
|
import { SCENE_TAP_STUB } from "../scene-tap";
|
|
8
8
|
import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api";
|
|
9
9
|
import { idApi } from "../id-api";
|
|
10
|
+
import { sceneLightApi } from "../scene-light-api";
|
|
10
11
|
// A persistent grid an effect can step and read: the one thing an effect could
|
|
11
12
|
// not have before, which is MEMORY.
|
|
12
13
|
//
|
|
@@ -160,6 +161,7 @@ fn rzGrid(uv: vec2f) -> vec4f { return rzGridPrev(uv); }
|
|
|
160
161
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
161
162
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
162
163
|
idApi(false, 0, 0) +
|
|
164
|
+
sceneLightApi(false, 0, 0) +
|
|
163
165
|
// Inlined rather than imported from cast-distance, and NOT for tidiness.
|
|
164
166
|
//
|
|
165
167
|
// This module and composite.ts import each other — grid takes
|
|
@@ -14,7 +14,9 @@ export type CastLayout = {
|
|
|
14
14
|
alias: number[];
|
|
15
15
|
};
|
|
16
16
|
/** How the author's quads combine with the scene. */
|
|
17
|
-
|
|
17
|
+
/** `cutout` shades exactly like `alpha`; the difference is entirely in the
|
|
18
|
+
* pipeline — depth write on, alpha-to-coverage on. See directives.ts. */
|
|
19
|
+
type ParticleBlend = "alpha" | "additive" | "cutout";
|
|
18
20
|
type ParticleSource = {
|
|
19
21
|
/** The author's WGSL verbatim. */
|
|
20
22
|
wgsl: string;
|
|
@@ -27,14 +29,30 @@ type ParticleSource = {
|
|
|
27
29
|
* rule reads the same dial the shading does, and rain's fall speed is used
|
|
28
30
|
* in the compute half. */
|
|
29
31
|
paramsDecl: string;
|
|
32
|
+
/** The effect defines `particleCover(p, uv) -> f32`: the shape alone, cheap.
|
|
33
|
+
* A cutout then draws TWICE — coverage to depth first, then the shading at
|
|
34
|
+
* depthCompare equal — so the lighting runs only for the visible layer. A
|
|
35
|
+
* discard forbids the hardware's own early rejection on a tile-based GPU,
|
|
36
|
+
* where every stacked fragment is otherwise shaded in full; the prepass is
|
|
37
|
+
* the shape of that rejection, done by hand and cheaply. */
|
|
38
|
+
cover: boolean;
|
|
39
|
+
/** The side of this effect's grid, or 0 when it declared none. Particles READ
|
|
40
|
+
* the grid; the kernel that steps it lives in the grid pass. A blade of grass
|
|
41
|
+
* bending where a foot pressed is the whole case: the bend has to persist and
|
|
42
|
+
* recover, which is memory, and memory is what the grid is for. */
|
|
43
|
+
gridSize: number;
|
|
30
44
|
};
|
|
45
|
+
/** Where the shading stage's five scene-light bindings start — see scene-light-api.ts. */
|
|
46
|
+
export declare const PARTICLE_LIGHT_BINDING = 10;
|
|
31
47
|
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
32
48
|
export declare const PARTICLE_STRIDE = 48;
|
|
33
|
-
/** Does the source define the particle contract? All three are required
|
|
49
|
+
/** Does the source define the particle contract? All three are required
|
|
50
|
+
* together; `cover` is optional and only means anything to a cutout. */
|
|
34
51
|
export declare function particleEntryPoints(wgsl: string): {
|
|
35
52
|
init: boolean;
|
|
36
53
|
step: boolean;
|
|
37
54
|
shade: boolean;
|
|
55
|
+
cover: boolean;
|
|
38
56
|
};
|
|
39
57
|
/**
|
|
40
58
|
* Spawn, age, recycle.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/particles.ts"],"names":[],"mappings":"AA8BA,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb;;mFAE+E;IAC/E,UAAU,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAkCD,qDAAqD;AACrD;0EAC0E;AAC1E,KAAK,aAAa,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAA;AAEpD,KAAK,cAAc,GAAG;IACpB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;IACd;;+BAE2B;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;iEAK6D;IAC7D,KAAK,EAAE,OAAO,CAAA;IACd;;;wEAGoE;IACpE,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,0FAA0F;AAC1F,eAAO,MAAM,sBAAsB,KAAK,CAAA;AAExC,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAA;AAwDjC;yEACyE;AACzE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAOlH;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAmDxF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CA0LvF"}
|
|
@@ -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
|
/**
|
|
13
15
|
* The trail accessors, in the PARTICLE module.
|
|
14
16
|
*
|
|
@@ -39,6 +41,8 @@ fn _rzCastLive() -> i32 {
|
|
|
39
41
|
trailSlotsApi(cast.trailCount) +
|
|
40
42
|
anchorAliasWgsl(cast.alias));
|
|
41
43
|
}
|
|
44
|
+
/** Where the shading stage's five scene-light bindings start — see scene-light-api.ts. */
|
|
45
|
+
export const PARTICLE_LIGHT_BINDING = 10;
|
|
42
46
|
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
43
47
|
export const PARTICLE_STRIDE = 48;
|
|
44
48
|
const CAMERA_STRUCT = /* wgsl */ `
|
|
@@ -91,12 +95,14 @@ fn rzProject(p: vec3f) -> vec3f {
|
|
|
91
95
|
}
|
|
92
96
|
fn rzCamPos() -> vec3f { return cam.camPos; }
|
|
93
97
|
`;
|
|
94
|
-
/** Does the source define the particle contract? All three are required
|
|
98
|
+
/** Does the source define the particle contract? All three are required
|
|
99
|
+
* together; `cover` is optional and only means anything to a cutout. */
|
|
95
100
|
export function particleEntryPoints(wgsl) {
|
|
96
101
|
return {
|
|
97
102
|
init: /\bfn\s+particleInit\s*\(/.test(wgsl),
|
|
98
103
|
step: /\bfn\s+particleStep\s*\(/.test(wgsl),
|
|
99
104
|
shade: /\bfn\s+particleShade\s*\(/.test(wgsl),
|
|
105
|
+
cover: /\bfn\s+particleCover\s*\(/.test(wgsl),
|
|
100
106
|
};
|
|
101
107
|
}
|
|
102
108
|
/**
|
|
@@ -123,10 +129,15 @@ ${src.paramsDecl}
|
|
|
123
129
|
audioApi(0, 4) +
|
|
124
130
|
midiApi(0, 5) +
|
|
125
131
|
lyricsApi(0, 6) +
|
|
132
|
+
// Always, even with no grid — then it samples a 1x1 of zeroes. Same reason
|
|
133
|
+
// composite compiles it unconditionally, plus one this module owns: an
|
|
134
|
+
// effect's whole file is spliced here, so a `gridStep` written for the grid
|
|
135
|
+
// pass has to RESOLVE here too, and the stubs that let it are in here.
|
|
136
|
+
gridReadApi(0, 8, 9, src.gridSize) +
|
|
126
137
|
PRELUDE +
|
|
127
138
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
128
139
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
129
|
-
idApi(false, 0, 0) + castDistanceStub() +
|
|
140
|
+
idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) +
|
|
130
141
|
"\n// ── user effect ──\n" +
|
|
131
142
|
src.wgsl +
|
|
132
143
|
/* wgsl */ `
|
|
@@ -168,7 +179,13 @@ fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
168
179
|
*/
|
|
169
180
|
export function buildParticleRenderShader(src, cast) {
|
|
170
181
|
return (`override BLOOM: bool = ${src.bloom ? "true" : "false"};
|
|
171
|
-
override ADDITIVE: bool = ${src.blend === "additive" ? "true" : "false"}
|
|
182
|
+
override ADDITIVE: bool = ${src.blend === "additive" ? "true" : "false"};
|
|
183
|
+
override CUTOUT: bool = ${src.blend === "cutout" ? "true" : "false"};
|
|
184
|
+
// A cutout with a prepass: the depth is already written and this pass draws at
|
|
185
|
+
// depthCompare equal, so it neither discards nor masks — the depth test IS the
|
|
186
|
+
// coverage, per sample, and a shader with no discard keeps the hardware's
|
|
187
|
+
// early rejection.
|
|
188
|
+
override PREPASSED: bool = ${src.blend === "cutout" && src.cover ? "true" : "false"};\n` +
|
|
172
189
|
PARTICLE_STRUCT_WGSL +
|
|
173
190
|
CAMERA_STRUCT +
|
|
174
191
|
PARTICLE_UNIFORMS +
|
|
@@ -183,10 +200,18 @@ ${src.paramsDecl}
|
|
|
183
200
|
audioApi(0, 4) +
|
|
184
201
|
midiApi(0, 5) +
|
|
185
202
|
lyricsApi(0, 6) +
|
|
203
|
+
// Always, even with no grid — then it samples a 1x1 of zeroes. Same reason
|
|
204
|
+
// composite compiles it unconditionally, plus one this module owns: an
|
|
205
|
+
// effect's whole file is spliced here, so a `gridStep` written for the grid
|
|
206
|
+
// pass has to RESOLVE here too, and the stubs that let it are in here.
|
|
207
|
+
gridReadApi(0, 8, 9, src.gridSize) +
|
|
186
208
|
PRELUDE +
|
|
187
209
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
188
210
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
189
211
|
idApi(false, 0, 0) + castDistanceStub() +
|
|
212
|
+
// The sun's shadow and the world's ambient, for real: the shadow pass has
|
|
213
|
+
// run by the time this draws, inside the scene pass. See scene-light-api.ts.
|
|
214
|
+
sceneLightApi(true, 0, PARTICLE_LIGHT_BINDING) +
|
|
190
215
|
"\n// ── user effect ──\n" +
|
|
191
216
|
src.wgsl +
|
|
192
217
|
/* wgsl */ `
|
|
@@ -239,6 +264,12 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
|
|
|
239
264
|
|
|
240
265
|
struct FSOut {
|
|
241
266
|
@location(0) color: vec4f,
|
|
267
|
+
// Which of the pixel's MSAA samples this fragment covers. Alpha-to-coverage
|
|
268
|
+
// by hand: the hardware kind reads alpha off the first colour target, and
|
|
269
|
+
// the HDR target is rg11b10, which has no alpha to read. Every particle
|
|
270
|
+
// writes it; a blended one writes all four, which is what not declaring it
|
|
271
|
+
// would have meant.
|
|
272
|
+
@builtin(sample_mask) samples: u32,
|
|
242
273
|
// The scene's aux target: (bloom mask, coverage). Materials write it, so a
|
|
243
274
|
// particle that skipped it would punch a hole in the mask of whatever it drew
|
|
244
275
|
// over.
|
|
@@ -266,8 +297,25 @@ fn fs(in: VSOut) -> FSOut {
|
|
|
266
297
|
c.a *= pu.weight;
|
|
267
298
|
// Weight 0 therefore discards every fragment, so an effect faded out costs
|
|
268
299
|
// nothing past the vertex stage even on the frame the draw is still issued.
|
|
269
|
-
|
|
300
|
+
// Not after a prepass: that pass already decided coverage into depth, and
|
|
301
|
+
// a discard here would cost this pass the early rejection it exists for.
|
|
302
|
+
if (!PREPASSED && c.a <= 0.0) { discard; }
|
|
270
303
|
var out: FSOut;
|
|
304
|
+
out.samples = 0xFu;
|
|
305
|
+
if (PREPASSED) {
|
|
306
|
+
c.a = 1.0;
|
|
307
|
+
} else if (CUTOUT) {
|
|
308
|
+
// Alpha becomes coverage: 0..4 of the pixel's samples, and the fragment is
|
|
309
|
+
// then opaque wherever it lands. The pattern is turned by pixel position
|
|
310
|
+
// so a half-covered edge is not the same two samples on every pixel along
|
|
311
|
+
// it, which is what makes a2c edges look dithered rather than stepped.
|
|
312
|
+
let n = u32(clamp(c.a, 0.0, 1.0) * 4.0 + 0.5);
|
|
313
|
+
if (n == 0u) { discard; }
|
|
314
|
+
let low = (1u << n) - 1u;
|
|
315
|
+
let turn = (u32(in.clip.x) + u32(in.clip.y)) & 3u;
|
|
316
|
+
out.samples = ((low << turn) | (low >> (4u - turn))) & 0xFu;
|
|
317
|
+
c.a = 1.0;
|
|
318
|
+
}
|
|
271
319
|
// PREMULTIPLIED: the scene's colour target blends with srcFactor \"one\", so a
|
|
272
320
|
// straight-alpha fragment would come out over-bright wherever it is
|
|
273
321
|
// translucent — which is most of a soft particle.
|
|
@@ -288,5 +336,29 @@ fn fs(in: VSOut) -> FSOut {
|
|
|
288
336
|
out.mask = vec4f(mg.x, mg.y, 0.0, c.a);
|
|
289
337
|
${sceneIdPadWgsl("out")} return out;
|
|
290
338
|
}
|
|
291
|
-
`
|
|
339
|
+
` +
|
|
340
|
+
(src.blend === "cutout" && src.cover
|
|
341
|
+
? /* wgsl */ `
|
|
342
|
+
/**
|
|
343
|
+
* The prepass: the shape to depth, nothing to colour. Every output is still
|
|
344
|
+
* declared — the pipeline takes the colour targets at writeMask 0 — and the
|
|
345
|
+
* sample mask is exactly what the single-pass cutout would have written, so
|
|
346
|
+
* the shading pass's equal test lands on the same samples.
|
|
347
|
+
*/
|
|
348
|
+
@fragment
|
|
349
|
+
fn fsDepth(in: VSOut) -> FSOut {
|
|
350
|
+
let p = particles[in.id];
|
|
351
|
+
let a = particleCover(p, in.uv) * pu.weight;
|
|
352
|
+
let n = u32(clamp(a, 0.0, 1.0) * 4.0 + 0.5);
|
|
353
|
+
if (n == 0u) { discard; }
|
|
354
|
+
let low = (1u << n) - 1u;
|
|
355
|
+
let turn = (u32(in.clip.x) + u32(in.clip.y)) & 3u;
|
|
356
|
+
var out: FSOut;
|
|
357
|
+
out.samples = ((low << turn) | (low >> (4u - turn))) & 0xFu;
|
|
358
|
+
out.color = vec4f(0.0);
|
|
359
|
+
out.mask = vec4f(0.0);
|
|
360
|
+
${sceneIdPadWgsl("out")} return out;
|
|
361
|
+
}
|
|
362
|
+
`
|
|
363
|
+
: ""));
|
|
292
364
|
}
|
|
@@ -11,6 +11,9 @@ type TrailSource = {
|
|
|
11
11
|
/** Additive, like most glowing ribbons, or straight alpha. */
|
|
12
12
|
blend: "alpha" | "additive";
|
|
13
13
|
bloom: boolean;
|
|
14
|
+
/** The side of this effect's grid, or 0 when it declared none. See the note
|
|
15
|
+
* on the same field in particles.ts: a ribbon reads the grid, never steps it. */
|
|
16
|
+
gridSize: number;
|
|
14
17
|
};
|
|
15
18
|
/**
|
|
16
19
|
* Sub-segments drawn between each pair of recorded samples.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"trails.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/trails.ts"],"names":[],"mappings":"AA8BA,KAAK,WAAW,GAAG;IACjB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAA;IACb;oEACgE;IAChE,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,GAAG,UAAU,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAA;IACd;sFACkF;IAClF,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAKjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;;kFAI8E;IAC9E,SAAS,EAAE,OAAO,CAAA;CACnB,GACA,MAAM,CA6ZR"}
|
|
@@ -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
|
/**
|
|
13
15
|
* Sub-segments drawn between each pair of recorded samples.
|
|
14
16
|
*
|
|
@@ -240,9 +242,13 @@ fn rzTurnRadius(a: vec3f, b: vec3f, c: vec3f) -> f32 {
|
|
|
240
242
|
audioApi(0, 4) +
|
|
241
243
|
midiApi(0, 5) +
|
|
242
244
|
lyricsApi(0, 6) +
|
|
245
|
+
// Always, with or without a grid — and required either way, because an
|
|
246
|
+
// effect's whole file is spliced here and a `gridStep` written for the grid
|
|
247
|
+
// pass has to resolve in this module too.
|
|
248
|
+
gridReadApi(0, 8, 9, src.gridSize) +
|
|
243
249
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
244
250
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
245
|
-
idApi(false, 0, 0) + castDistanceStub() +
|
|
251
|
+
idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) +
|
|
246
252
|
"\n// ── user effect ──\n" +
|
|
247
253
|
src.wgsl +
|
|
248
254
|
/* wgsl */ `
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The world's light at a surface facing n — the flat colour, or the installed
|
|
3
|
+
* HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
|
|
4
|
+
* ibl.ts for the folding; the shader is a plain polynomial in the normal).
|
|
5
|
+
*
|
|
6
|
+
* Written against whichever name the module gives the light uniform, so the
|
|
7
|
+
* materials, the ground and an effect all evaluate one polynomial.
|
|
8
|
+
*/
|
|
9
|
+
export declare const worldAmbientWgsl: (u: string) => string;
|
|
10
|
+
/**
|
|
11
|
+
* 3×3 PCF over one cascade's map, for a point already in that cascade's NDC.
|
|
12
|
+
*
|
|
13
|
+
* Shared with the materials, so an effect's shadow and her own shadow soften
|
|
14
|
+
* alike. The sampler is linear-filtered, which makes each tap a 2×2 bilinear
|
|
15
|
+
* compare. Unrolled — Safari's Metal backend doesn't unroll nested shadow
|
|
16
|
+
* loops reliably. The body expects `ndc` in scope and returns.
|
|
17
|
+
*/
|
|
18
|
+
export declare const pcf9: (map: string, ts: string, sampler?: string) => string;
|
|
19
|
+
/** Bindings the real accessors take, from `binding` upward. */
|
|
20
|
+
export declare const SCENE_LIGHT_API_BINDINGS = 5;
|
|
21
|
+
/**
|
|
22
|
+
* `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
|
|
23
|
+
* caster's shadow, soft in between.
|
|
24
|
+
*
|
|
25
|
+
* `rzWorldAmbient(n)`: the world's light arriving at a surface facing `n` —
|
|
26
|
+
* the materials' own ambient, so an effect's shade can match hers.
|
|
27
|
+
*
|
|
28
|
+
* With `on`, declares five bindings from `binding`:
|
|
29
|
+
* +0 the light uniform (ambient, the sun — direction, and in .w the shadow
|
|
30
|
+
* switch — and the HDRI's irradiance)
|
|
31
|
+
* +1 the cascade view-projections, then the caster sphere
|
|
32
|
+
* +2 the near cascade's map +3 the far cascade's map
|
|
33
|
+
* +4 the comparison sampler
|
|
34
|
+
*
|
|
35
|
+
* It takes a POINT and no normal. The materials' sampleShadow also rejects a
|
|
36
|
+
* surface facing away from the sun, which is right for skin and wrong for a
|
|
37
|
+
* blade of grass or a petal: those are thin, lit through, and shaded by their
|
|
38
|
+
* own N·L. The point is nudged toward the sun instead, which lifts a blade's
|
|
39
|
+
* root off a stage floor that casts into the same map.
|
|
40
|
+
*/
|
|
41
|
+
export declare function sceneLightApi(on: boolean, group: number, binding: number): string;
|
|
42
|
+
//# sourceMappingURL=scene-light-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scene-light-api.d.ts","sourceRoot":"","sources":["../../src/shaders/scene-light-api.ts"],"names":[],"mappings":"AAkBA;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,GAAG,MAAM,WAazC,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,GAAI,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,gBAAyB,WActE,CAAA;AAED,+DAA+D;AAC/D,eAAO,MAAM,wBAAwB,IAAI,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA2DjF"}
|
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
* The world's light at a surface facing n — the flat colour, or the installed
|
|
20
|
+
* HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
|
|
21
|
+
* ibl.ts for the folding; the shader is a plain polynomial in the normal).
|
|
22
|
+
*
|
|
23
|
+
* Written against whichever name the module gives the light uniform, so the
|
|
24
|
+
* materials, the ground and an effect all evaluate one polynomial.
|
|
25
|
+
*/
|
|
26
|
+
export const worldAmbientWgsl = (u) => /* wgsl */ `
|
|
27
|
+
fn rzWorldAmbient(n: vec3f) -> vec3f {
|
|
28
|
+
if (${u}.sh[0].w < 0.5) { return ${u}.ambientColor.xyz; }
|
|
29
|
+
let x = n.x;
|
|
30
|
+
let y = n.y;
|
|
31
|
+
let z = n.z;
|
|
32
|
+
let c = ${u}.sh[0].xyz
|
|
33
|
+
+ ${u}.sh[1].xyz * y + ${u}.sh[2].xyz * z + ${u}.sh[3].xyz * x
|
|
34
|
+
+ ${u}.sh[4].xyz * (x * y) + ${u}.sh[5].xyz * (y * z)
|
|
35
|
+
+ ${u}.sh[6].xyz * (3.0 * z * z - 1.0) + ${u}.sh[7].xyz * (x * z)
|
|
36
|
+
+ ${u}.sh[8].xyz * (x * x - y * y);
|
|
37
|
+
return max(c, vec3f(0.0));
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
/**
|
|
41
|
+
* 3×3 PCF over one cascade's map, for a point already in that cascade's NDC.
|
|
42
|
+
*
|
|
43
|
+
* Shared with the materials, so an effect's shadow and her own shadow soften
|
|
44
|
+
* alike. The sampler is linear-filtered, which makes each tap a 2×2 bilinear
|
|
45
|
+
* compare. Unrolled — Safari's Metal backend doesn't unroll nested shadow
|
|
46
|
+
* loops reliably. The body expects `ndc` in scope and returns.
|
|
47
|
+
*/
|
|
48
|
+
export const pcf9 = (map, ts, sampler = "shadowSampler") => /* wgsl */ `
|
|
49
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
50
|
+
let cmpZ = ndc.z - 0.001;
|
|
51
|
+
let ts = ${ts};
|
|
52
|
+
let s00 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, -ts), cmpZ);
|
|
53
|
+
let s10 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(0.0, -ts), cmpZ);
|
|
54
|
+
let s20 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, -ts), cmpZ);
|
|
55
|
+
let s01 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, 0.0), cmpZ);
|
|
56
|
+
let s11 = textureSampleCompareLevel(${map}, ${sampler}, suv, cmpZ);
|
|
57
|
+
let s21 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, 0.0), cmpZ);
|
|
58
|
+
let s02 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, ts), cmpZ);
|
|
59
|
+
let s12 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(0.0, ts), cmpZ);
|
|
60
|
+
let s22 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, ts), cmpZ);
|
|
61
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
62
|
+
`;
|
|
63
|
+
/** Bindings the real accessors take, from `binding` upward. */
|
|
64
|
+
export const SCENE_LIGHT_API_BINDINGS = 5;
|
|
65
|
+
/**
|
|
66
|
+
* `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
|
|
67
|
+
* caster's shadow, soft in between.
|
|
68
|
+
*
|
|
69
|
+
* `rzWorldAmbient(n)`: the world's light arriving at a surface facing `n` —
|
|
70
|
+
* the materials' own ambient, so an effect's shade can match hers.
|
|
71
|
+
*
|
|
72
|
+
* With `on`, declares five bindings from `binding`:
|
|
73
|
+
* +0 the light uniform (ambient, the sun — direction, and in .w the shadow
|
|
74
|
+
* switch — and the HDRI's irradiance)
|
|
75
|
+
* +1 the cascade view-projections, then the caster sphere
|
|
76
|
+
* +2 the near cascade's map +3 the far cascade's map
|
|
77
|
+
* +4 the comparison sampler
|
|
78
|
+
*
|
|
79
|
+
* It takes a POINT and no normal. The materials' sampleShadow also rejects a
|
|
80
|
+
* surface facing away from the sun, which is right for skin and wrong for a
|
|
81
|
+
* blade of grass or a petal: those are thin, lit through, and shaded by their
|
|
82
|
+
* own N·L. The point is nudged toward the sun instead, which lifts a blade's
|
|
83
|
+
* root off a stage floor that casts into the same map.
|
|
84
|
+
*/
|
|
85
|
+
export function sceneLightApi(on, group, binding) {
|
|
86
|
+
if (!on) {
|
|
87
|
+
return /* wgsl */ `
|
|
88
|
+
fn rzShadow(p: vec3f) -> f32 { return 1.0; }
|
|
89
|
+
fn rzWorldAmbient(n: vec3f) -> vec3f { return vec3f(0.0); }
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
const n = SHADOW_CASCADES.length;
|
|
93
|
+
return /* wgsl */ `
|
|
94
|
+
// The light uniform, as the materials lay it out; lights[0] is the sun.
|
|
95
|
+
struct _RzSceneLight { direction: vec4f, color: vec4f, }
|
|
96
|
+
struct _RzSceneLightU { ambientColor: vec4f, lights: array<_RzSceneLight, 4>, sh: array<vec4f, 9>, }
|
|
97
|
+
// The cascades' matrices, inner to outer, then every caster in one sphere
|
|
98
|
+
// (x, y, z, radius): radius 0 = nothing casts, negative = do not test.
|
|
99
|
+
struct _RzShadowBlock { viewProj: array<mat4x4f, ${n}>, casters: vec4f, }
|
|
100
|
+
@group(${group}) @binding(${binding}) var<uniform> _rzLight: _RzSceneLightU;
|
|
101
|
+
@group(${group}) @binding(${binding + 1}) var<uniform> _rzShadowVP: _RzShadowBlock;
|
|
102
|
+
@group(${group}) @binding(${binding + 2}) var _rzShadowNear: texture_depth_2d;
|
|
103
|
+
@group(${group}) @binding(${binding + 3}) var _rzShadowFar: texture_depth_2d;
|
|
104
|
+
@group(${group}) @binding(${binding + 4}) var _rzShadowCmp: sampler_comparison;
|
|
105
|
+
|
|
106
|
+
fn _rzShadowNearTaps(ndc: vec3f) -> f32 {
|
|
107
|
+
${pcf9("_rzShadowNear", `1.0 / ${SHADOW_CASCADES[0].mapSize}.0`, "_rzShadowCmp")}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fn _rzShadowFarTaps(ndc: vec3f) -> f32 {
|
|
111
|
+
${pcf9("_rzShadowFar", `1.0 / ${SHADOW_CASCADES[n - 1].mapSize}.0`, "_rzShadowCmp")}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn rzShadow(p: vec3f) -> f32 {
|
|
115
|
+
// The scene's one shadow switch, on the sun — see sampleShadow.
|
|
116
|
+
let castAmt = _rzLight.lights[0].direction.w;
|
|
117
|
+
if (castAmt <= 0.0) { return 1.0; }
|
|
118
|
+
let toSun = normalize(-_rzLight.lights[0].direction.xyz);
|
|
119
|
+
// Can anything cast onto this point at all? The ground's test, for the same
|
|
120
|
+
// two reasons: most of a lawn is nowhere near her, and a scene with no
|
|
121
|
+
// models never renders the map, whose untouched zeroes compare as shadowed.
|
|
122
|
+
let cs = _rzShadowVP.casters;
|
|
123
|
+
if (cs.w == 0.0) { return 1.0; }
|
|
124
|
+
if (cs.w > 0.0) {
|
|
125
|
+
let toCaster = cs.xyz - p;
|
|
126
|
+
let along = dot(toCaster, toSun);
|
|
127
|
+
let perp = length(toCaster - toSun * along);
|
|
128
|
+
if (along <= -cs.w || perp > cs.w) { return 1.0; }
|
|
129
|
+
}
|
|
130
|
+
let q = p + toSun * 0.08;
|
|
131
|
+
let c0 = _rzShadowVP.viewProj[0] * vec4f(q, 1.0);
|
|
132
|
+
let n0 = c0.xyz / max(c0.w, 1e-6);
|
|
133
|
+
if (all(abs(n0.xy) < vec2f(0.98)) && n0.z > 0.0 && n0.z < 1.0) {
|
|
134
|
+
return mix(1.0, _rzShadowNearTaps(n0), castAmt);
|
|
135
|
+
}
|
|
136
|
+
let c1 = _rzShadowVP.viewProj[${n - 1}] * vec4f(q, 1.0);
|
|
137
|
+
let n1 = c1.xyz / max(c1.w, 1e-6);
|
|
138
|
+
if (all(abs(n1.xy) < vec2f(0.98)) && n1.z > 0.0 && n1.z < 1.0) {
|
|
139
|
+
return mix(1.0, _rzShadowFarTaps(n1), castAmt);
|
|
140
|
+
}
|
|
141
|
+
return 1.0;
|
|
142
|
+
}
|
|
143
|
+
${worldAmbientWgsl("_rzLight")}`;
|
|
144
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 3×3 PCF over one cascade's map, for a point already in that cascade's NDC.
|
|
3
|
+
*
|
|
4
|
+
* Shared with the materials, so an effect's shadow and her own shadow soften
|
|
5
|
+
* alike. The sampler is linear-filtered, which makes each tap a 2×2 bilinear
|
|
6
|
+
* compare. Unrolled — Safari's Metal backend doesn't unroll nested shadow
|
|
7
|
+
* loops reliably. The body expects `ndc` in scope and returns.
|
|
8
|
+
*/
|
|
9
|
+
export declare const pcf9: (map: string, ts: string, sampler?: string) => string;
|
|
10
|
+
/** Bindings the real accessor takes, from `binding` upward. */
|
|
11
|
+
export declare const SHADOW_API_BINDINGS = 5;
|
|
12
|
+
/**
|
|
13
|
+
* `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
|
|
14
|
+
* caster's shadow, soft in between.
|
|
15
|
+
*
|
|
16
|
+
* With `on`, declares five bindings from `binding`:
|
|
17
|
+
* +0 the light uniform (the sun's direction, and in .w the shadow switch)
|
|
18
|
+
* +1 the cascade view-projections, then the caster sphere
|
|
19
|
+
* +2 the near cascade's map +3 the far cascade's map
|
|
20
|
+
* +4 the comparison sampler
|
|
21
|
+
*
|
|
22
|
+
* It takes a POINT and no normal. The materials' sampleShadow also rejects a
|
|
23
|
+
* surface facing away from the sun, which is right for skin and wrong for a
|
|
24
|
+
* blade of grass or a petal: those are thin, lit through, and shaded by their
|
|
25
|
+
* own N·L. The point is nudged toward the sun instead, which lifts a blade's
|
|
26
|
+
* root off a stage floor that casts into the same map.
|
|
27
|
+
*/
|
|
28
|
+
export declare function shadowApi(on: boolean, group: number, binding: number): string;
|
|
29
|
+
//# sourceMappingURL=shadow-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadow-api.d.ts","sourceRoot":"","sources":["../../src/shaders/shadow-api.ts"],"names":[],"mappings":"AAcA;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,GAAI,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,gBAAyB,WActE,CAAA;AAED,+DAA+D;AAC/D,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAyD7E"}
|