reze-engine 0.58.0 → 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 +8 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +51 -12
- 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 +2 -0
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +7 -1
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +2 -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 +52 -11
- 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 +8 -1
- package/src/shaders/passes/trails.ts +2 -1
- package/src/shaders/scene-light-api.ts +148 -0
|
@@ -10,6 +10,7 @@ import { EFFECT_SUBJECT_VEC4S } from "../cast-layout";
|
|
|
10
10
|
import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api";
|
|
11
11
|
import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract";
|
|
12
12
|
import { idApi } from "../id-api";
|
|
13
|
+
import { sceneLightApi } from "../scene-light-api";
|
|
13
14
|
/**
|
|
14
15
|
* Sub-segments drawn between each pair of recorded samples.
|
|
15
16
|
*
|
|
@@ -247,7 +248,7 @@ fn rzTurnRadius(a: vec3f, b: vec3f, c: vec3f) -> f32 {
|
|
|
247
248
|
gridReadApi(0, 8, 9, src.gridSize) +
|
|
248
249
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
249
250
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
250
|
-
idApi(false, 0, 0) + castDistanceStub() +
|
|
251
|
+
idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) +
|
|
251
252
|
"\n// ── user effect ──\n" +
|
|
252
253
|
src.wgsl +
|
|
253
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"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { SHADOW_CASCADES } from "../shadow-cascades";
|
|
2
|
+
// The sun's shadow, as an effect author sees it.
|
|
3
|
+
//
|
|
4
|
+
// A lawn standing in a character's shadow has to darken where the floor under
|
|
5
|
+
// it does, or the figure floats: the ground catcher draws her shadow, and the
|
|
6
|
+
// blades drawn over it hide it. The map is already there — the materials and
|
|
7
|
+
// the ground read it every frame — so this hands the same lookup to effects.
|
|
8
|
+
//
|
|
9
|
+
// Bound for real in the particle SHADING stage only. That is where geometry an
|
|
10
|
+
// effect draws inside the scene pass is shaded, and the shadow pass has already
|
|
11
|
+
// run by then. Every other module that compiles the author's file gets the
|
|
12
|
+
// stub, which answers 1: lit is what a point with no occlusion information is,
|
|
13
|
+
// the same answer sampleShadow gives outside every cascade.
|
|
14
|
+
/**
|
|
15
|
+
* 3×3 PCF over one cascade's map, for a point already in that cascade's NDC.
|
|
16
|
+
*
|
|
17
|
+
* Shared with the materials, so an effect's shadow and her own shadow soften
|
|
18
|
+
* alike. The sampler is linear-filtered, which makes each tap a 2×2 bilinear
|
|
19
|
+
* compare. Unrolled — Safari's Metal backend doesn't unroll nested shadow
|
|
20
|
+
* loops reliably. The body expects `ndc` in scope and returns.
|
|
21
|
+
*/
|
|
22
|
+
export const pcf9 = (map, ts, sampler = "shadowSampler") => /* wgsl */ `
|
|
23
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
24
|
+
let cmpZ = ndc.z - 0.001;
|
|
25
|
+
let ts = ${ts};
|
|
26
|
+
let s00 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, -ts), cmpZ);
|
|
27
|
+
let s10 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(0.0, -ts), cmpZ);
|
|
28
|
+
let s20 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, -ts), cmpZ);
|
|
29
|
+
let s01 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, 0.0), cmpZ);
|
|
30
|
+
let s11 = textureSampleCompareLevel(${map}, ${sampler}, suv, cmpZ);
|
|
31
|
+
let s21 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, 0.0), cmpZ);
|
|
32
|
+
let s02 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, ts), cmpZ);
|
|
33
|
+
let s12 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(0.0, ts), cmpZ);
|
|
34
|
+
let s22 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, ts), cmpZ);
|
|
35
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
36
|
+
`;
|
|
37
|
+
/** Bindings the real accessor takes, from `binding` upward. */
|
|
38
|
+
export const SHADOW_API_BINDINGS = 5;
|
|
39
|
+
/**
|
|
40
|
+
* `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
|
|
41
|
+
* caster's shadow, soft in between.
|
|
42
|
+
*
|
|
43
|
+
* With `on`, declares five bindings from `binding`:
|
|
44
|
+
* +0 the light uniform (the sun's direction, and in .w the shadow switch)
|
|
45
|
+
* +1 the cascade view-projections, then the caster sphere
|
|
46
|
+
* +2 the near cascade's map +3 the far cascade's map
|
|
47
|
+
* +4 the comparison sampler
|
|
48
|
+
*
|
|
49
|
+
* It takes a POINT and no normal. The materials' sampleShadow also rejects a
|
|
50
|
+
* surface facing away from the sun, which is right for skin and wrong for a
|
|
51
|
+
* blade of grass or a petal: those are thin, lit through, and shaded by their
|
|
52
|
+
* own N·L. The point is nudged toward the sun instead, which lifts a blade's
|
|
53
|
+
* root off a stage floor that casts into the same map.
|
|
54
|
+
*/
|
|
55
|
+
export function shadowApi(on, group, binding) {
|
|
56
|
+
if (!on) {
|
|
57
|
+
return /* wgsl */ `
|
|
58
|
+
fn rzShadow(p: vec3f) -> f32 { return 1.0; }
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
const n = SHADOW_CASCADES.length;
|
|
62
|
+
return /* wgsl */ `
|
|
63
|
+
// The first two vec4s of the light uniform: world ambient, then the sun.
|
|
64
|
+
struct _RzShadowSun { ambient: vec4f, direction: vec4f, }
|
|
65
|
+
// The cascades' matrices, inner to outer, then every caster in one sphere
|
|
66
|
+
// (x, y, z, radius): radius 0 = nothing casts, negative = do not test.
|
|
67
|
+
struct _RzShadowBlock { viewProj: array<mat4x4f, ${n}>, casters: vec4f, }
|
|
68
|
+
@group(${group}) @binding(${binding}) var<uniform> _rzShadowSun: _RzShadowSun;
|
|
69
|
+
@group(${group}) @binding(${binding + 1}) var<uniform> _rzShadowVP: _RzShadowBlock;
|
|
70
|
+
@group(${group}) @binding(${binding + 2}) var _rzShadowNear: texture_depth_2d;
|
|
71
|
+
@group(${group}) @binding(${binding + 3}) var _rzShadowFar: texture_depth_2d;
|
|
72
|
+
@group(${group}) @binding(${binding + 4}) var _rzShadowCmp: sampler_comparison;
|
|
73
|
+
|
|
74
|
+
fn _rzShadowNearTaps(ndc: vec3f) -> f32 {
|
|
75
|
+
${pcf9("_rzShadowNear", `1.0 / ${SHADOW_CASCADES[0].mapSize}.0`, "_rzShadowCmp")}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn _rzShadowFarTaps(ndc: vec3f) -> f32 {
|
|
79
|
+
${pcf9("_rzShadowFar", `1.0 / ${SHADOW_CASCADES[n - 1].mapSize}.0`, "_rzShadowCmp")}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn rzShadow(p: vec3f) -> f32 {
|
|
83
|
+
// The scene's one shadow switch, on the sun — see sampleShadow.
|
|
84
|
+
let castAmt = _rzShadowSun.direction.w;
|
|
85
|
+
if (castAmt <= 0.0) { return 1.0; }
|
|
86
|
+
let toSun = normalize(-_rzShadowSun.direction.xyz);
|
|
87
|
+
// Can anything cast onto this point at all? The ground's test, for the same
|
|
88
|
+
// two reasons: most of a lawn is nowhere near her, and a scene with no
|
|
89
|
+
// models never renders the map, whose untouched zeroes compare as shadowed.
|
|
90
|
+
let cs = _rzShadowVP.casters;
|
|
91
|
+
if (cs.w == 0.0) { return 1.0; }
|
|
92
|
+
if (cs.w > 0.0) {
|
|
93
|
+
let toCaster = cs.xyz - p;
|
|
94
|
+
let along = dot(toCaster, toSun);
|
|
95
|
+
let perp = length(toCaster - toSun * along);
|
|
96
|
+
if (along <= -cs.w || perp > cs.w) { return 1.0; }
|
|
97
|
+
}
|
|
98
|
+
let q = p + toSun * 0.08;
|
|
99
|
+
let c0 = _rzShadowVP.viewProj[0] * vec4f(q, 1.0);
|
|
100
|
+
let n0 = c0.xyz / max(c0.w, 1e-6);
|
|
101
|
+
if (all(abs(n0.xy) < vec2f(0.98)) && n0.z > 0.0 && n0.z < 1.0) {
|
|
102
|
+
return mix(1.0, _rzShadowNearTaps(n0), castAmt);
|
|
103
|
+
}
|
|
104
|
+
let c1 = _rzShadowVP.viewProj[${n - 1}] * vec4f(q, 1.0);
|
|
105
|
+
let n1 = c1.xyz / max(c1.w, 1e-6);
|
|
106
|
+
if (all(abs(n1.xy) < vec2f(0.98)) && n1.z > 0.0 && n1.z < 1.0) {
|
|
107
|
+
return mix(1.0, _rzShadowFarTaps(n1), castAmt);
|
|
108
|
+
}
|
|
109
|
+
return 1.0;
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
}
|
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -113,6 +113,7 @@ import {
|
|
|
113
113
|
import {
|
|
114
114
|
buildParticleComputeShader,
|
|
115
115
|
buildParticleRenderShader,
|
|
116
|
+
PARTICLE_LIGHT_BINDING,
|
|
116
117
|
particleEntryPoints,
|
|
117
118
|
PARTICLE_STRIDE,
|
|
118
119
|
} from "./shaders/passes/particles"
|
|
@@ -5300,7 +5301,7 @@ export class Engine {
|
|
|
5300
5301
|
// Declaring one set of flags for both layouts is what made the pipeline
|
|
5301
5302
|
// layout invalid, and the error surfaces later and unhelpfully as "invalid
|
|
5302
5303
|
// due to a previous error".
|
|
5303
|
-
const layoutFor = (storage: GPUBufferBindingType, visibility: number) =>
|
|
5304
|
+
const layoutFor = (storage: GPUBufferBindingType, visibility: number, shadow: boolean) =>
|
|
5304
5305
|
this.device.createBindGroupLayout({
|
|
5305
5306
|
entries: [
|
|
5306
5307
|
{ binding: 0, visibility, buffer: { type: storage } },
|
|
@@ -5320,13 +5321,25 @@ export class Engine {
|
|
|
5320
5321
|
// author calls it or not. With no grid it is the 1x1 of zeroes.
|
|
5321
5322
|
{ binding: 8, visibility, texture: { sampleType: "float" as const } },
|
|
5322
5323
|
{ binding: 9, visibility, sampler: { type: "filtering" as const } },
|
|
5324
|
+
// The scene's light, for rzShadow and rzWorldAmbient — the shading
|
|
5325
|
+
// stage only; the compute module compiles the stubs. See
|
|
5326
|
+
// scene-light-api.ts.
|
|
5327
|
+
...(shadow
|
|
5328
|
+
? [
|
|
5329
|
+
{ binding: PARTICLE_LIGHT_BINDING, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" as const } },
|
|
5330
|
+
{ binding: PARTICLE_LIGHT_BINDING + 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" as const } },
|
|
5331
|
+
{ binding: PARTICLE_LIGHT_BINDING + 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" as const } },
|
|
5332
|
+
{ binding: PARTICLE_LIGHT_BINDING + 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" as const } },
|
|
5333
|
+
{ binding: PARTICLE_LIGHT_BINDING + 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" as const } },
|
|
5334
|
+
]
|
|
5335
|
+
: []),
|
|
5323
5336
|
],
|
|
5324
5337
|
})
|
|
5325
5338
|
/** One per GRID PARITY, for the reason rebuildFieldBindGroup gives: the grid
|
|
5326
5339
|
* alternates which texture is current, and rebuilding a single group every
|
|
5327
5340
|
* frame is waste for a change that only ever toggles between two known
|
|
5328
5341
|
* states. Both entries are the same view when there is no grid. */
|
|
5329
|
-
const bindFor = (layout: GPUBindGroupLayout, camera: GPUBuffer) =>
|
|
5342
|
+
const bindFor = (layout: GPUBindGroupLayout, camera: GPUBuffer, shadow: boolean) =>
|
|
5330
5343
|
[0, 1].map((parity) =>
|
|
5331
5344
|
this.device.createBindGroup({
|
|
5332
5345
|
layout,
|
|
@@ -5341,12 +5354,21 @@ export class Engine {
|
|
|
5341
5354
|
...(params.buffer ? [{ binding: 7, resource: { buffer: params.buffer } }] : []),
|
|
5342
5355
|
{ binding: 8, resource: grid ? grid.read[parity] : this.simFallbackView },
|
|
5343
5356
|
{ binding: 9, resource: this.simSampler },
|
|
5357
|
+
...(shadow
|
|
5358
|
+
? [
|
|
5359
|
+
{ binding: PARTICLE_LIGHT_BINDING, resource: { buffer: this.lightUniformBuffer } },
|
|
5360
|
+
{ binding: PARTICLE_LIGHT_BINDING + 1, resource: { buffer: this.shadowLightVPBuffer } },
|
|
5361
|
+
{ binding: PARTICLE_LIGHT_BINDING + 2, resource: this.shadowMapDepthViews[0] },
|
|
5362
|
+
{ binding: PARTICLE_LIGHT_BINDING + 3, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
5363
|
+
{ binding: PARTICLE_LIGHT_BINDING + 4, resource: this.shadowComparisonSampler },
|
|
5364
|
+
]
|
|
5365
|
+
: []),
|
|
5344
5366
|
],
|
|
5345
5367
|
}),
|
|
5346
5368
|
) as [GPUBindGroup, GPUBindGroup]
|
|
5347
5369
|
|
|
5348
|
-
const computeLayout = layoutFor("storage", GPUShaderStage.COMPUTE)
|
|
5349
|
-
const renderLayout = layoutFor("read-only-storage", GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT)
|
|
5370
|
+
const computeLayout = layoutFor("storage", GPUShaderStage.COMPUTE, false)
|
|
5371
|
+
const renderLayout = layoutFor("read-only-storage", GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, true)
|
|
5350
5372
|
|
|
5351
5373
|
// Additive keeps the destination and adds to it, and leaves alpha alone, so
|
|
5352
5374
|
// a glow does not claim coverage it never occluded. The MASK sums with it —
|
|
@@ -5422,16 +5444,16 @@ export class Engine {
|
|
|
5422
5444
|
counts: uniformView.uints,
|
|
5423
5445
|
compute,
|
|
5424
5446
|
computeLayout,
|
|
5425
|
-
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer),
|
|
5447
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false),
|
|
5426
5448
|
depth,
|
|
5427
5449
|
render,
|
|
5428
5450
|
renderLayout,
|
|
5429
|
-
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer),
|
|
5430
|
-
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer),
|
|
5451
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true),
|
|
5452
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true),
|
|
5431
5453
|
rebind: () => ({
|
|
5432
|
-
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer),
|
|
5433
|
-
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer),
|
|
5434
|
-
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer),
|
|
5454
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false),
|
|
5455
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true),
|
|
5456
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true),
|
|
5435
5457
|
}),
|
|
5436
5458
|
},
|
|
5437
5459
|
}
|
|
@@ -7387,8 +7409,10 @@ export class Engine {
|
|
|
7387
7409
|
},
|
|
7388
7410
|
})
|
|
7389
7411
|
|
|
7412
|
+
// The matrices, then one vec4: the caster sphere, which effects' rzShadow
|
|
7413
|
+
// tests before any tap. The materials and the ground bind the matrices only.
|
|
7390
7414
|
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
7391
|
-
size: 64 * SHADOW_CASCADES.length,
|
|
7415
|
+
size: 64 * SHADOW_CASCADES.length + 16,
|
|
7392
7416
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
7393
7417
|
})
|
|
7394
7418
|
this.shadowCascadeVPBuffers = SHADOW_CASCADES.map((_, i) =>
|
|
@@ -12380,6 +12404,22 @@ export class Engine {
|
|
|
12380
12404
|
this.device.queue.writeBuffer(this.groundShadowMaterialBuffer, 40, gb.subarray(10, 11) as Float32Array<ArrayBuffer>)
|
|
12381
12405
|
}
|
|
12382
12406
|
|
|
12407
|
+
/** The caster sphere as last written after the cascade matrices; NaN so the
|
|
12408
|
+
* first frame always writes. */
|
|
12409
|
+
private shadowCastersWritten = new Float32Array([NaN, NaN, NaN, NaN])
|
|
12410
|
+
|
|
12411
|
+
/**
|
|
12412
|
+
* The same sphere, into the cascade block after its matrices — rzShadow's
|
|
12413
|
+
* copy. There whether or not a ground exists: a lawn is often the only floor.
|
|
12414
|
+
*/
|
|
12415
|
+
private writeShadowCasters(): void {
|
|
12416
|
+
const w = this.shadowCastersWritten
|
|
12417
|
+
const c = this.casterSphere
|
|
12418
|
+
if (w[0] === c[0] && w[1] === c[1] && w[2] === c[2] && w[3] === c[3]) return
|
|
12419
|
+
w.set(c)
|
|
12420
|
+
this.device.queue.writeBuffer(this.shadowLightVPBuffer, 64 * SHADOW_CASCADES.length, c as Float32Array<ArrayBuffer>)
|
|
12421
|
+
}
|
|
12422
|
+
|
|
12383
12423
|
/**
|
|
12384
12424
|
* Push this frame's caster sphere into the ground's uniform.
|
|
12385
12425
|
*
|
|
@@ -15376,6 +15416,7 @@ export class Engine {
|
|
|
15376
15416
|
if (hasModels) this.dispatchCull(encoder)
|
|
15377
15417
|
// After the cull, which is what recomputes the spheres it unions.
|
|
15378
15418
|
this.writeGroundCasterSphere()
|
|
15419
|
+
this.writeShadowCasters()
|
|
15379
15420
|
this.writeGroundDress()
|
|
15380
15421
|
|
|
15381
15422
|
// After the cull, because a rebuild there can reallocate the argument
|
package/src/shaders/lights.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { midiApi } from "./midi-api"
|
|
|
38
38
|
import { lyricsApi } from "./lyrics-api"
|
|
39
39
|
import { clockApi, trailSlotsApi, viewportApi } from "./passes/hosted-api"
|
|
40
40
|
import { idApi } from "./id-api"
|
|
41
|
+
import { sceneLightApi, worldAmbientWgsl } from "./scene-light-api"
|
|
41
42
|
|
|
42
43
|
/** Floats before the first record. One is the count; the rest keep the records
|
|
43
44
|
* vec4-aligned, which is what lets a future pass read them as vec4s. */
|
|
@@ -84,29 +85,8 @@ struct RzLight {
|
|
|
84
85
|
}
|
|
85
86
|
`
|
|
86
87
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
* HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
|
|
90
|
-
* ibl.ts for the folding; the shader is a plain polynomial in the normal).
|
|
91
|
-
*
|
|
92
|
-
* One string included by every module that declares LightUniforms with the sh
|
|
93
|
-
* block — the hosted-api lesson: a helper defined in some modules and not
|
|
94
|
-
* others is a compile error waiting for the first file that crosses them.
|
|
95
|
-
*/
|
|
96
|
-
export const WORLD_AMBIENT_WGSL = /* wgsl */ `
|
|
97
|
-
fn rzWorldAmbient(n: vec3f) -> vec3f {
|
|
98
|
-
if (light.sh[0].w < 0.5) { return light.ambientColor.xyz; }
|
|
99
|
-
let x = n.x;
|
|
100
|
-
let y = n.y;
|
|
101
|
-
let z = n.z;
|
|
102
|
-
let c = light.sh[0].xyz
|
|
103
|
-
+ light.sh[1].xyz * y + light.sh[2].xyz * z + light.sh[3].xyz * x
|
|
104
|
-
+ light.sh[4].xyz * (x * y) + light.sh[5].xyz * (y * z)
|
|
105
|
-
+ light.sh[6].xyz * (3.0 * z * z - 1.0) + light.sh[7].xyz * (x * z)
|
|
106
|
-
+ light.sh[8].xyz * (x * x - y * y);
|
|
107
|
-
return max(c, vec3f(0.0));
|
|
108
|
-
}
|
|
109
|
-
`
|
|
88
|
+
/** rzWorldAmbient against the materials' and the ground's `light` uniform. */
|
|
89
|
+
export const WORLD_AMBIENT_WGSL = worldAmbientWgsl("light")
|
|
110
90
|
|
|
111
91
|
/** Does this source define the emit mount? */
|
|
112
92
|
export function hasLightEmit(wgsl: string): boolean {
|
|
@@ -184,7 +164,7 @@ ${viewportApi("viewU[6].w")}
|
|
|
184
164
|
${trailSlotsApi(cast.trailCount)}
|
|
185
165
|
// The id accessors, stubbed: this module cannot read an attachment the
|
|
186
166
|
// scene pass writes. See id-api.ts — the author's whole file compiles here.
|
|
187
|
-
${idApi(false, 0, 0) + castDistanceStub()}
|
|
167
|
+
${idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0)}
|
|
188
168
|
// The dials the author declared, if any. A lamp is exactly the thing someone
|
|
189
169
|
// retunes — its colour and its reach — so an emitter reads params like every
|
|
190
170
|
// other mount rather than being the one place a #param resolves to nothing.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sceneFsOutWgsl } from "../passes/scene-contract"
|
|
2
2
|
import { lightsApi } from "../lights"
|
|
3
3
|
import { SHADOW_CASCADES } from "../../shadow-cascades"
|
|
4
|
+
import { pcf9 } from "../scene-light-api"
|
|
4
5
|
import { WORLD_AMBIENT_WGSL } from "../lights"
|
|
5
6
|
|
|
6
7
|
// Shared WGSL blocks concatenated by every material shader.
|
|
@@ -175,8 +176,8 @@ fn safe_normal(nIn: vec3f) -> vec3f {
|
|
|
175
176
|
`;
|
|
176
177
|
|
|
177
178
|
// ─── Shadow sampler (3×3 PCF, cascade-selected) ─────────────────────
|
|
178
|
-
// Normal-bias 0.08, depth-bias 0.001 NDC
|
|
179
|
-
//
|
|
179
|
+
// Normal-bias 0.08, depth-bias 0.001 NDC; the kernel is pcf9 in scene-light-api.ts,
|
|
180
|
+
// shared with effects. Texel sizes are interpolated
|
|
180
181
|
// from SHADOW_CASCADES so they cannot go stale the way the hardcoded 1/2048
|
|
181
182
|
// once did (PCF taps landed TWO texels apart after the map grew to 4096,
|
|
182
183
|
// quantizing self-shadow edges into crawling gray squares).
|
|
@@ -188,21 +189,6 @@ fn safe_normal(nIn: vec3f) -> vec3f {
|
|
|
188
189
|
// far plane the comparison failed against every stored depth, silently
|
|
189
190
|
// shadowing any stage deeper than the box.
|
|
190
191
|
|
|
191
|
-
const pcf9 = (map: string, ts: string) => /* wgsl */ `
|
|
192
|
-
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
193
|
-
let cmpZ = ndc.z - 0.001;
|
|
194
|
-
let ts = ${ts};
|
|
195
|
-
let s00 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
196
|
-
let s10 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
197
|
-
let s20 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
198
|
-
let s01 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
199
|
-
let s11 = textureSampleCompareLevel(${map}, shadowSampler, suv, cmpZ);
|
|
200
|
-
let s21 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
201
|
-
let s02 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
202
|
-
let s12 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
203
|
-
let s22 = textureSampleCompareLevel(${map}, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
204
|
-
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
205
|
-
`
|
|
206
192
|
|
|
207
193
|
const SAMPLE_SHADOW_WGSL = /* wgsl */ `
|
|
208
194
|
|
|
@@ -6,6 +6,7 @@ import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewpor
|
|
|
6
6
|
import { EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES } from "../cast-layout"
|
|
7
7
|
import { audioApi } from "../audio-api"
|
|
8
8
|
import { idApi } from "../id-api"
|
|
9
|
+
import { sceneLightApi } from "../scene-light-api"
|
|
9
10
|
import { castDistanceApi, CAST_FIELD_DIV } from "./cast-distance"
|
|
10
11
|
import { lyricsApi, lyricsTextApi } from "../lyrics-api"
|
|
11
12
|
import { midiApi } from "../midi-api"
|
|
@@ -722,6 +723,7 @@ export function buildFieldShader(effect: CompositeEffectSource): string {
|
|
|
722
723
|
viewportApi("viewU[6].w") +
|
|
723
724
|
trailSlotsApi(effect.trailCount) +
|
|
724
725
|
idApi(effect.ids === true, 0, 23) +
|
|
726
|
+
sceneLightApi(false, 0, 0) +
|
|
725
727
|
// Distance to the cast, in SCREEN pixels: the field is half-res, so a field
|
|
726
728
|
// texel is CAST_FIELD_DIV of them and the accessor scales on the way out.
|
|
727
729
|
// An author writes the width they mean and never learns how it is built.
|
|
@@ -8,6 +8,7 @@ import { SCENE_TAP_STUB } from "../scene-tap"
|
|
|
8
8
|
import { type CastLayout } from "./particles"
|
|
9
9
|
import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api"
|
|
10
10
|
import { idApi } from "../id-api"
|
|
11
|
+
import { sceneLightApi } from "../scene-light-api"
|
|
11
12
|
|
|
12
13
|
// A persistent grid an effect can step and read: the one thing an effect could
|
|
13
14
|
// not have before, which is MEMORY.
|
|
@@ -168,6 +169,7 @@ fn rzGrid(uv: vec2f) -> vec4f { return rzGridPrev(uv); }
|
|
|
168
169
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
169
170
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
170
171
|
idApi(false, 0, 0) +
|
|
172
|
+
sceneLightApi(false, 0, 0) +
|
|
171
173
|
// Inlined rather than imported from cast-distance, and NOT for tidiness.
|
|
172
174
|
//
|
|
173
175
|
// This module and composite.ts import each other — grid takes
|
|
@@ -10,6 +10,7 @@ import { EFFECT_SUBJECT_VEC4S } from "../cast-layout"
|
|
|
10
10
|
import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api"
|
|
11
11
|
import { sceneIdFieldWgsl, sceneIdPadWgsl } from "./scene-contract"
|
|
12
12
|
import { idApi } from "../id-api"
|
|
13
|
+
import { sceneLightApi } from "../scene-light-api"
|
|
13
14
|
// GPU particles for user effects: a compute step and an instanced quad draw.
|
|
14
15
|
//
|
|
15
16
|
// Its own shader MODULE rather than more source spliced into composite.ts, for
|
|
@@ -106,6 +107,9 @@ type ParticleSource = {
|
|
|
106
107
|
gridSize: number
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/** Where the shading stage's five scene-light bindings start — see scene-light-api.ts. */
|
|
111
|
+
export const PARTICLE_LIGHT_BINDING = 10
|
|
112
|
+
|
|
109
113
|
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
110
114
|
export const PARTICLE_STRIDE = 48
|
|
111
115
|
|
|
@@ -207,7 +211,7 @@ ${src.paramsDecl}
|
|
|
207
211
|
PRELUDE +
|
|
208
212
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
209
213
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
210
|
-
idApi(false, 0, 0) + castDistanceStub() +
|
|
214
|
+
idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) +
|
|
211
215
|
"\n// ── user effect ──\n" +
|
|
212
216
|
src.wgsl +
|
|
213
217
|
/* wgsl */ `
|
|
@@ -282,6 +286,9 @@ ${src.paramsDecl}
|
|
|
282
286
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
283
287
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
284
288
|
idApi(false, 0, 0) + castDistanceStub() +
|
|
289
|
+
// The sun's shadow and the world's ambient, for real: the shadow pass has
|
|
290
|
+
// run by the time this draws, inside the scene pass. See scene-light-api.ts.
|
|
291
|
+
sceneLightApi(true, 0, PARTICLE_LIGHT_BINDING) +
|
|
285
292
|
"\n// ── user effect ──\n" +
|
|
286
293
|
src.wgsl +
|
|
287
294
|
/* wgsl */ `
|