reze-engine 0.58.0 → 0.58.2
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/effect-points.d.ts +30 -0
- package/dist/effect-points.d.ts.map +1 -0
- package/dist/effect-points.js +49 -0
- package/dist/engine.d.ts +30 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +149 -19
- package/dist/light-grid.d.ts +25 -0
- package/dist/light-grid.d.ts.map +1 -0
- package/dist/light-grid.js +91 -0
- package/dist/model.d.ts +9 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +6 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +11 -6
- package/dist/shaders/cast-api.d.ts +1 -1
- package/dist/shaders/cast-layout.d.ts +5 -3
- package/dist/shaders/cast-layout.d.ts.map +1 -1
- package/dist/shaders/cast-layout.js +5 -3
- package/dist/shaders/directives.d.ts +3 -0
- package/dist/shaders/directives.d.ts.map +1 -1
- package/dist/shaders/directives.js +5 -0
- package/dist/shaders/lights.d.ts +16 -19
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +121 -88
- 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 +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +4 -0
- package/dist/shaders/passes/grid.d.ts.map +1 -1
- package/dist/shaders/passes/grid.js +4 -0
- package/dist/shaders/passes/particles.d.ts +4 -0
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +13 -1
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +3 -1
- package/dist/shaders/points-api.d.ts +11 -0
- package/dist/shaders/points-api.d.ts.map +1 -0
- package/dist/shaders/points-api.js +58 -0
- 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/effect-points.ts +67 -0
- package/src/engine.ts +163 -18
- package/src/light-grid.ts +139 -0
- package/src/model.ts +12 -0
- package/src/pmx-loader.ts +13 -6
- package/src/shaders/cast-layout.ts +5 -3
- package/src/shaders/directives.ts +8 -0
- package/src/shaders/lights.ts +121 -88
- package/src/shaders/materials/common.ts +3 -17
- package/src/shaders/passes/composite.ts +4 -0
- package/src/shaders/passes/grid.ts +4 -0
- package/src/shaders/passes/particles.ts +14 -1
- package/src/shaders/passes/trails.ts +3 -1
- package/src/shaders/points-api.ts +66 -0
- package/src/shaders/scene-light-api.ts +148 -0
package/src/shaders/lights.ts
CHANGED
|
@@ -11,18 +11,27 @@ import { subjectMaskApi } from "./cast-api"
|
|
|
11
11
|
// renderer that bolted a second key light onto a toon shader has shipped. A
|
|
12
12
|
// light here brightens; it does not restate the shading.
|
|
13
13
|
//
|
|
14
|
-
// So there are no per-light shadows
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// So there are no per-light shadows and no area lights. A fragment walks the
|
|
15
|
+
// DOCUMENT's lamps through a world-space grid (see light-grid.ts): its cell
|
|
16
|
+
// names the lamps that can reach it, so a stage rig of fifty costs each pixel
|
|
17
|
+
// the handful standing near it. The lamps an EFFECT emits are placed by a
|
|
18
|
+
// compute pass every frame, which the grid never sees, and are walked in full.
|
|
17
19
|
//
|
|
18
|
-
// LAYOUT
|
|
19
|
-
//
|
|
20
|
+
// LAYOUT, in 32-bit words, read by the shader as vec4u. A 16-word header:
|
|
21
|
+
//
|
|
22
|
+
// [0] light count (f32) [1] how many are the document's (f32)
|
|
23
|
+
// [4..6] grid origin (f32) [7] 1 / cell edge (f32)
|
|
24
|
+
// [8..10] grid dims (u32) [12..15] the OUTSIDE mask (u32 bits)
|
|
25
|
+
//
|
|
26
|
+
// then MAX_LIGHTS records of 16 floats — four vec4s:
|
|
20
27
|
//
|
|
21
28
|
// [0..2] position, world space [3] radius
|
|
22
29
|
// [4..6] colour PREMULTIPLIED by intensity [7] type
|
|
23
30
|
// [8..10] aim, unit, pointing away from the light [11] cos of the outer angle
|
|
24
31
|
// [12] cos of the inner angle [13..15] spare
|
|
25
32
|
//
|
|
33
|
+
// then the grid: LIGHT_MASK_WORDS words of lamp bits per cell.
|
|
34
|
+
//
|
|
26
35
|
// Colour carries intensity because nothing reads them apart: every use is the
|
|
27
36
|
// product, and storing two numbers that are only ever multiplied is two numbers
|
|
28
37
|
// that can disagree.
|
|
@@ -38,10 +47,12 @@ import { midiApi } from "./midi-api"
|
|
|
38
47
|
import { lyricsApi } from "./lyrics-api"
|
|
39
48
|
import { clockApi, trailSlotsApi, viewportApi } from "./passes/hosted-api"
|
|
40
49
|
import { idApi } from "./id-api"
|
|
50
|
+
import { sceneLightApi, worldAmbientWgsl } from "./scene-light-api"
|
|
51
|
+
import { pointsApi } from "./points-api"
|
|
41
52
|
|
|
42
|
-
/**
|
|
43
|
-
*
|
|
44
|
-
export const LIGHT_HEADER =
|
|
53
|
+
/** Words before the first record: the counts, the grid's placement and the
|
|
54
|
+
* outside mask, four vec4s — see the layout above. */
|
|
55
|
+
export const LIGHT_HEADER = 16
|
|
45
56
|
/** Floats per light — see the layout above. */
|
|
46
57
|
export const LIGHT_STRIDE = 16
|
|
47
58
|
/**
|
|
@@ -55,15 +66,20 @@ export const LIGHT_STRIDE = 16
|
|
|
55
66
|
* interior brings a hundred or more, and the ceilings of sixteen and then
|
|
56
67
|
* forty-eight each cut a real scene in half. Past this the extras are dropped.
|
|
57
68
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* size affordable at all; clustering is what replaces the linear walk when a
|
|
62
|
-
* scene wants every one of them close enough to matter.
|
|
69
|
+
* A fragment pays for the document lamps its grid cell names, so the cost
|
|
70
|
+
* follows how many reach a place rather than how many the scene holds. It is
|
|
71
|
+
* also why the cap is 128: one vec4u of bits per cell.
|
|
63
72
|
*/
|
|
64
73
|
export const MAX_LIGHTS = 128
|
|
65
|
-
/**
|
|
66
|
-
export const
|
|
74
|
+
/** Words of lamp bits per grid cell — one bit per lamp the cap allows. */
|
|
75
|
+
export const LIGHT_MASK_WORDS = MAX_LIGHTS / 32
|
|
76
|
+
/** The grid's cell budget. 32k cells over a stage a couple of hundred units
|
|
77
|
+
* across is a cell of about four — finer than the lamps it separates. */
|
|
78
|
+
export const LIGHT_GRID_CELLS = 32768
|
|
79
|
+
/** Where the grid starts, in words. */
|
|
80
|
+
export const LIGHT_GRID_BASE = LIGHT_HEADER + MAX_LIGHTS * LIGHT_STRIDE
|
|
81
|
+
/** Words in the whole buffer: header, records, grid. */
|
|
82
|
+
export const LIGHTS_FLOATS = LIGHT_GRID_BASE + LIGHT_GRID_CELLS * LIGHT_MASK_WORDS
|
|
67
83
|
|
|
68
84
|
/**
|
|
69
85
|
* The RzLight struct, declared in EVERY module a user's source is spliced into.
|
|
@@ -84,29 +100,8 @@ struct RzLight {
|
|
|
84
100
|
}
|
|
85
101
|
`
|
|
86
102
|
|
|
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
|
-
`
|
|
103
|
+
/** rzWorldAmbient against the materials' and the ground's `light` uniform. */
|
|
104
|
+
export const WORLD_AMBIENT_WGSL = worldAmbientWgsl("light")
|
|
110
105
|
|
|
111
106
|
/** Does this source define the emit mount? */
|
|
112
107
|
export function hasLightEmit(wgsl: string): boolean {
|
|
@@ -184,7 +179,7 @@ ${viewportApi("viewU[6].w")}
|
|
|
184
179
|
${trailSlotsApi(cast.trailCount)}
|
|
185
180
|
// The id accessors, stubbed: this module cannot read an attachment the
|
|
186
181
|
// scene pass writes. See id-api.ts — the author's whole file compiles here.
|
|
187
|
-
${idApi(false, 0, 0) + castDistanceStub()}
|
|
182
|
+
${idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) + pointsApi(false)}
|
|
188
183
|
// The dials the author declared, if any. A lamp is exactly the thing someone
|
|
189
184
|
// retunes — its colour and its reach — so an emitter reads params like every
|
|
190
185
|
// other mount rather than being the one place a #param resolves to nothing.
|
|
@@ -249,51 +244,55 @@ fn lightEmitMain(@builtin(global_invocation_id) gid: vec3u) {
|
|
|
249
244
|
|
|
250
245
|
/** The rz*Light accessors, with the buffer declared at the given binding. */
|
|
251
246
|
export function lightsApi(group: number, binding: number): string {
|
|
247
|
+
const R = LIGHT_HEADER / 4
|
|
248
|
+
const S = LIGHT_STRIDE / 4
|
|
252
249
|
return /* wgsl */ `
|
|
253
|
-
|
|
250
|
+
// vec4u, so a record is four loads and a cell's lamp bits are one. Floats come
|
|
251
|
+
// out through bitcast; the bits must NOT pass through f32 on the way, where a
|
|
252
|
+
// mask that happens to spell a NaN is not guaranteed to survive a load.
|
|
253
|
+
@group(${group}) @binding(${binding}) var<storage, read> _rzLights: array<vec4u>;
|
|
254
254
|
|
|
255
255
|
const RZ_MAX_LIGHTS: u32 = ${MAX_LIGHTS}u;
|
|
256
256
|
|
|
257
257
|
/** How many positional lights the scene has. Zero is the ordinary case. */
|
|
258
|
-
fn rzLightCount() -> u32 { return min(u32(_rzLights[0]), RZ_MAX_LIGHTS); }
|
|
258
|
+
fn rzLightCount() -> u32 { return min(u32(bitcast<f32>(_rzLights[0].x)), RZ_MAX_LIGHTS); }
|
|
259
|
+
|
|
260
|
+
/** How many of them the document placed — the ones the grid indexes. */
|
|
261
|
+
fn _rzLightDocCount() -> u32 { return min(u32(bitcast<f32>(_rzLights[0].y)), rzLightCount()); }
|
|
262
|
+
|
|
263
|
+
/** One vec4 of light i's record. */
|
|
264
|
+
fn _rzLightVec(i: u32, k: u32) -> vec4f { return bitcast<vec4f>(_rzLights[${R}u + i * ${S}u + k]); }
|
|
259
265
|
|
|
260
266
|
/** Light i's world position. */
|
|
261
|
-
fn rzLightPos(i: u32) -> vec3f {
|
|
262
|
-
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u;
|
|
263
|
-
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
264
|
-
}
|
|
267
|
+
fn rzLightPos(i: u32) -> vec3f { return _rzLightVec(i, 0u).xyz; }
|
|
265
268
|
|
|
266
269
|
/** How far light i reaches. Its falloff is zero AT this distance, not merely
|
|
267
|
-
* small,
|
|
268
|
-
fn rzLightRadius(i: u32) -> f32 { return
|
|
270
|
+
* small, which is the bound the grid is built from. */
|
|
271
|
+
fn rzLightRadius(i: u32) -> f32 { return _rzLightVec(i, 0u).w; }
|
|
269
272
|
|
|
270
273
|
/** Light i's colour, already multiplied by its intensity. */
|
|
271
|
-
fn rzLightColor(i: u32) -> vec3f {
|
|
272
|
-
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 4u;
|
|
273
|
-
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
274
|
-
}
|
|
274
|
+
fn rzLightColor(i: u32) -> vec3f { return _rzLightVec(i, 1u).xyz; }
|
|
275
275
|
|
|
276
276
|
/** Where light i points, away from itself. The zero vector for a point light. */
|
|
277
|
-
fn rzLightAim(i: u32) -> vec3f {
|
|
278
|
-
let b = ${LIGHT_HEADER}u + i * ${LIGHT_STRIDE}u + 8u;
|
|
279
|
-
return vec3f(_rzLights[b], _rzLights[b + 1u], _rzLights[b + 2u]);
|
|
280
|
-
}
|
|
277
|
+
fn rzLightAim(i: u32) -> vec3f { return _rzLightVec(i, 2u).xyz; }
|
|
281
278
|
|
|
282
279
|
/** The cosines a spot fades between: x its outer edge, y its inner one. A point
|
|
283
280
|
* light stores (-1, -1), which saturates the cone term to 1. */
|
|
284
|
-
fn rzLightCone(i: u32) -> vec2f {
|
|
285
|
-
|
|
286
|
-
|
|
281
|
+
fn rzLightCone(i: u32) -> vec2f { return vec2f(_rzLightVec(i, 2u).w, _rzLightVec(i, 3u).x); }
|
|
282
|
+
|
|
283
|
+
/** The document lamps that can reach p: its grid cell's bits, or the outside
|
|
284
|
+
* mask beyond the grid. Written so a NaN position fails the inside test. */
|
|
285
|
+
fn _rzLightCellMask(p: vec3f) -> vec4u {
|
|
286
|
+
let g = bitcast<vec4f>(_rzLights[1]);
|
|
287
|
+
let dims = _rzLights[2].xyz;
|
|
288
|
+
let c = floor((p - g.xyz) * g.w);
|
|
289
|
+
if (!(all(c >= vec3f(0.0)) && all(c < vec3f(dims)))) { return _rzLights[3]; }
|
|
290
|
+
let ci = vec3u(c);
|
|
291
|
+
return _rzLights[${LIGHT_GRID_BASE / 4}u + (ci.z * dims.y + ci.y) * dims.x + ci.x];
|
|
287
292
|
}
|
|
288
293
|
|
|
289
294
|
/**
|
|
290
|
-
*
|
|
291
|
-
* finished colour. Multiply by whatever the surface's albedo is.
|
|
292
|
-
*
|
|
293
|
-
* WITH NO LIGHTS THIS RETURNS EXACTLY ZERO and the loop never runs, so a scene
|
|
294
|
-
* that declares none is arithmetically identical to one compiled before lights
|
|
295
|
-
* existed. That is the property the whole feature is gated on: adding this to
|
|
296
|
-
* every material must cost nothing until someone asks for a light.
|
|
295
|
+
* One light's contribution at a surface point.
|
|
297
296
|
*
|
|
298
297
|
* FALLOFF IS RELATIVE TO THE RADIUS, and deliberately not physical.
|
|
299
298
|
*
|
|
@@ -307,33 +306,67 @@ fn rzLightCone(i: u32) -> vec2f {
|
|
|
307
306
|
* zero, and the curve between them is the same shape whatever the scene's
|
|
308
307
|
* scale. Both dials now mean what they say, which for a composer beats being
|
|
309
308
|
* right about photons. (1 - t²)² — smooth at both ends, exactly 0 at the
|
|
310
|
-
* radius, so the bound
|
|
309
|
+
* radius, so the bound the grid is built from is real.
|
|
310
|
+
*/
|
|
311
|
+
fn _rzLightOne(i: u32, p: vec3f, n: vec3f) -> vec3f {
|
|
312
|
+
let pr = _rzLightVec(i, 0u);
|
|
313
|
+
let d = pr.xyz - p;
|
|
314
|
+
let dist = length(d);
|
|
315
|
+
// Out of reach before anything else is computed. The grid is conservative —
|
|
316
|
+
// a cell's bit means the lamp CAN reach part of it — so this still runs.
|
|
317
|
+
if (dist >= pr.w) { return vec3f(0.0); }
|
|
318
|
+
let toLight = d / max(dist, 1e-4);
|
|
319
|
+
// Facing the light, and nothing behind it. No wrap or half-lambert: this
|
|
320
|
+
// layer adds light, and a wrapped term would lift the shadow side, which is
|
|
321
|
+
// the ramp's business and not this one's.
|
|
322
|
+
let ndl = max(dot(n, toLight), 0.0);
|
|
323
|
+
if (ndl <= 0.0) { return vec3f(0.0); }
|
|
324
|
+
let t = clamp(dist / max(pr.w, 1e-4), 0.0, 1.0);
|
|
325
|
+
let falloff = 1.0 - t * t;
|
|
326
|
+
// How far inside the cone this point sits: 1 within the inner angle, 0 past
|
|
327
|
+
// the outer one, squared for the same soft edge the falloff has. A point
|
|
328
|
+
// light's (-1, -1) divides by the floor and clamps to 1, so it pays one
|
|
329
|
+
// dot product and no branch.
|
|
330
|
+
let cone = rzLightCone(i);
|
|
331
|
+
let aim = clamp((dot(-toLight, rzLightAim(i)) - cone.x) / max(cone.y - cone.x, 1e-4), 0.0, 1.0);
|
|
332
|
+
return rzLightColor(i) * (ndl * falloff * falloff * aim * aim);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** The lamps named by one word of a cell's bits, lowest first. */
|
|
336
|
+
fn _rzLightWord(bits0: u32, base: u32, p: vec3f, n: vec3f) -> vec3f {
|
|
337
|
+
var acc = vec3f(0.0);
|
|
338
|
+
var bits = bits0;
|
|
339
|
+
loop {
|
|
340
|
+
if (bits == 0u) { break; }
|
|
341
|
+
let i = base + firstTrailingBit(bits);
|
|
342
|
+
bits = bits & (bits - 1u);
|
|
343
|
+
acc = acc + _rzLightOne(i, p, n);
|
|
344
|
+
}
|
|
345
|
+
return acc;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Every positional light's contribution at a surface point, as light — not as a
|
|
350
|
+
* finished colour. Multiply by whatever the surface's albedo is.
|
|
351
|
+
*
|
|
352
|
+
* WITH NO LIGHTS THIS RETURNS EXACTLY ZERO and neither walk runs, so a scene
|
|
353
|
+
* that declares none is arithmetically identical to one compiled before lights
|
|
354
|
+
* existed. That is the property the whole feature is gated on: adding this to
|
|
355
|
+
* every material must cost nothing until someone asks for a light.
|
|
311
356
|
*/
|
|
312
357
|
fn rzLightsDiffuse(p: vec3f, n: vec3f) -> vec3f {
|
|
313
358
|
var acc = vec3f(0.0);
|
|
314
359
|
let count = rzLightCount();
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// the ramp's business and not this one's.
|
|
326
|
-
let ndl = max(dot(n, toLight), 0.0);
|
|
327
|
-
if (ndl <= 0.0) { continue; }
|
|
328
|
-
let t = clamp(dist / max(rzLightRadius(i), 1e-4), 0.0, 1.0);
|
|
329
|
-
let falloff = 1.0 - t * t;
|
|
330
|
-
// How far inside the cone this point sits: 1 within the inner angle, 0 past
|
|
331
|
-
// the outer one, squared for the same soft edge the falloff has. A point
|
|
332
|
-
// light's (-1, -1) divides by the floor and clamps to 1, so it pays one
|
|
333
|
-
// dot product and no branch.
|
|
334
|
-
let cone = rzLightCone(i);
|
|
335
|
-
let aim = clamp((dot(-toLight, rzLightAim(i)) - cone.x) / max(cone.y - cone.x, 1e-4), 0.0, 1.0);
|
|
336
|
-
acc = acc + rzLightColor(i) * (ndl * falloff * falloff * aim * aim);
|
|
360
|
+
let docs = _rzLightDocCount();
|
|
361
|
+
// The document's lamps: only those the grid says can reach this cell.
|
|
362
|
+
if (docs > 0u) {
|
|
363
|
+
let m = _rzLightCellMask(p);
|
|
364
|
+
acc = acc + _rzLightWord(m.x, 0u, p, n) + _rzLightWord(m.y, 32u, p, n) +
|
|
365
|
+
_rzLightWord(m.z, 64u, p, n) + _rzLightWord(m.w, 96u, p, n);
|
|
366
|
+
}
|
|
367
|
+
// The effects' lamps, which move every frame on the GPU: all of them.
|
|
368
|
+
for (var i = docs; i < count; i = i + 1u) {
|
|
369
|
+
acc = acc + _rzLightOne(i, p, n);
|
|
337
370
|
}
|
|
338
371
|
return acc;
|
|
339
372
|
}
|
|
@@ -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,8 @@ 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"
|
|
10
|
+
import { pointsApi } from "../points-api"
|
|
9
11
|
import { castDistanceApi, CAST_FIELD_DIV } from "./cast-distance"
|
|
10
12
|
import { lyricsApi, lyricsTextApi } from "../lyrics-api"
|
|
11
13
|
import { midiApi } from "../midi-api"
|
|
@@ -722,6 +724,8 @@ export function buildFieldShader(effect: CompositeEffectSource): string {
|
|
|
722
724
|
viewportApi("viewU[6].w") +
|
|
723
725
|
trailSlotsApi(effect.trailCount) +
|
|
724
726
|
idApi(effect.ids === true, 0, 23) +
|
|
727
|
+
sceneLightApi(false, 0, 0) +
|
|
728
|
+
pointsApi(false) +
|
|
725
729
|
// Distance to the cast, in SCREEN pixels: the field is half-res, so a field
|
|
726
730
|
// texel is CAST_FIELD_DIV of them and the accessor scales on the way out.
|
|
727
731
|
// An author writes the width they mean and never learns how it is built.
|
|
@@ -8,6 +8,8 @@ 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"
|
|
12
|
+
import { pointsApi } from "../points-api"
|
|
11
13
|
|
|
12
14
|
// A persistent grid an effect can step and read: the one thing an effect could
|
|
13
15
|
// not have before, which is MEMORY.
|
|
@@ -168,6 +170,8 @@ fn rzGrid(uv: vec2f) -> vec4f { return rzGridPrev(uv); }
|
|
|
168
170
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
169
171
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
170
172
|
idApi(false, 0, 0) +
|
|
173
|
+
sceneLightApi(false, 0, 0) +
|
|
174
|
+
pointsApi(false) +
|
|
171
175
|
// Inlined rather than imported from cast-distance, and NOT for tidiness.
|
|
172
176
|
//
|
|
173
177
|
// This module and composite.ts import each other — grid takes
|
|
@@ -10,6 +10,8 @@ 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"
|
|
14
|
+
import { pointsApi } from "../points-api"
|
|
13
15
|
// GPU particles for user effects: a compute step and an instanced quad draw.
|
|
14
16
|
//
|
|
15
17
|
// Its own shader MODULE rather than more source spliced into composite.ts, for
|
|
@@ -106,6 +108,11 @@ type ParticleSource = {
|
|
|
106
108
|
gridSize: number
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
/** Where the shading stage's five scene-light bindings start — see scene-light-api.ts. */
|
|
112
|
+
export const PARTICLE_LIGHT_BINDING = 10
|
|
113
|
+
/** The effect's named points (`#points`), in both stages — see points-api.ts. */
|
|
114
|
+
export const PARTICLE_POINTS_BINDING = 15
|
|
115
|
+
|
|
109
116
|
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
110
117
|
export const PARTICLE_STRIDE = 48
|
|
111
118
|
|
|
@@ -207,7 +214,9 @@ ${src.paramsDecl}
|
|
|
207
214
|
PRELUDE +
|
|
208
215
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
209
216
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
210
|
-
idApi(false, 0, 0) + castDistanceStub() +
|
|
217
|
+
idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) +
|
|
218
|
+
// The named points, for real: particleInit is where a flame is put on a wick.
|
|
219
|
+
pointsApi(true, 0, PARTICLE_POINTS_BINDING) +
|
|
211
220
|
"\n// ── user effect ──\n" +
|
|
212
221
|
src.wgsl +
|
|
213
222
|
/* wgsl */ `
|
|
@@ -282,6 +291,10 @@ ${src.paramsDecl}
|
|
|
282
291
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
283
292
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
284
293
|
idApi(false, 0, 0) + castDistanceStub() +
|
|
294
|
+
// The sun's shadow and the world's ambient, for real: the shadow pass has
|
|
295
|
+
// run by the time this draws, inside the scene pass. See scene-light-api.ts.
|
|
296
|
+
sceneLightApi(true, 0, PARTICLE_LIGHT_BINDING) +
|
|
297
|
+
pointsApi(true, 0, PARTICLE_POINTS_BINDING) +
|
|
285
298
|
"\n// ── user effect ──\n" +
|
|
286
299
|
src.wgsl +
|
|
287
300
|
/* wgsl */ `
|
|
@@ -10,6 +10,8 @@ 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"
|
|
14
|
+
import { pointsApi } from "../points-api"
|
|
13
15
|
// Ribbons along a bone's recorded path, drawn as geometry.
|
|
14
16
|
//
|
|
15
17
|
// This is the effect the whole geometry path was built for. As a fullscreen
|
|
@@ -301,7 +303,7 @@ fn rzTurnRadius(a: vec3f, b: vec3f, c: vec3f) -> f32 {
|
|
|
301
303
|
gridReadApi(0, 8, 9, src.gridSize) +
|
|
302
304
|
// Stubbed: this module cannot read an attachment the scene pass writes — see
|
|
303
305
|
// id-api.ts. The author's whole file compiles here, so the names must exist.
|
|
304
|
-
idApi(false, 0, 0) + castDistanceStub() +
|
|
306
|
+
idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) + pointsApi(false) +
|
|
305
307
|
"\n// ── user effect ──\n" +
|
|
306
308
|
src.wgsl +
|
|
307
309
|
/* wgsl */ `
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Named points on the models, for an effect that puts something at each one.
|
|
2
|
+
//
|
|
3
|
+
// #points flame
|
|
4
|
+
//
|
|
5
|
+
// gives the effect every bone whose name starts with `flame`, on every model in
|
|
6
|
+
// the scene — a stage, its parts, a prop, a character — as RzPoint: where the
|
|
7
|
+
// bone stands and where its tail points, in world space, this frame. A candle
|
|
8
|
+
// stage carries a bone per wick; a flame effect draws one flame on each, sized
|
|
9
|
+
// and stood up by the tail.
|
|
10
|
+
//
|
|
11
|
+
// BONES, because a bone is MMD's own word for a named point in a model. A stage
|
|
12
|
+
// that brings its candles as bones needs no side file, a hand-made stage can
|
|
13
|
+
// add them in any PMX editor, and a candle a character carries moves with the
|
|
14
|
+
// hand that holds it.
|
|
15
|
+
//
|
|
16
|
+
// Not the cast's anchors: those name up to sixteen bones on four characters,
|
|
17
|
+
// one bone per name. A stage is not a character, and fifty wicks share a
|
|
18
|
+
// prefix rather than each having a name worth declaring.
|
|
19
|
+
|
|
20
|
+
/** How many points one effect can read. A candlelit hall is fifty. */
|
|
21
|
+
export const MAX_EFFECT_POINTS = 256
|
|
22
|
+
|
|
23
|
+
/** Floats in a points buffer: a vec4 header (count), then two vec4s a point. */
|
|
24
|
+
export const POINTS_FLOATS = 4 + MAX_EFFECT_POINTS * 8
|
|
25
|
+
|
|
26
|
+
const STRUCT = /* wgsl */ `
|
|
27
|
+
/** One named point: where the bone stands, and where its tail points to. */
|
|
28
|
+
struct RzPoint {
|
|
29
|
+
pos: vec3f,
|
|
30
|
+
tip: vec3f,
|
|
31
|
+
}
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* rzPointCount / rzPoint. `bound` is false in every module that cannot read the
|
|
36
|
+
* buffer — the author's whole file is spliced into each module it has a mount
|
|
37
|
+
* in, so the names must resolve there too, and there they read as no points.
|
|
38
|
+
*/
|
|
39
|
+
export function pointsApi(bound: boolean, group = 0, binding = 0): string {
|
|
40
|
+
if (!bound) {
|
|
41
|
+
return (
|
|
42
|
+
STRUCT +
|
|
43
|
+
/* wgsl */ `
|
|
44
|
+
fn rzPointCount() -> u32 { return 0u; }
|
|
45
|
+
fn rzPoint(i: u32) -> RzPoint { var p: RzPoint; return p; }
|
|
46
|
+
`
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
return (
|
|
50
|
+
STRUCT +
|
|
51
|
+
/* wgsl */ `
|
|
52
|
+
@group(${group}) @binding(${binding}) var<storage, read> _rzPoints: array<vec4f>;
|
|
53
|
+
|
|
54
|
+
/** How many named points the scene has for this effect, this frame. */
|
|
55
|
+
fn rzPointCount() -> u32 { return min(u32(_rzPoints[0].x), ${MAX_EFFECT_POINTS}u); }
|
|
56
|
+
|
|
57
|
+
/** Point i, world space. */
|
|
58
|
+
fn rzPoint(i: u32) -> RzPoint {
|
|
59
|
+
var p: RzPoint;
|
|
60
|
+
p.pos = _rzPoints[1u + i * 2u].xyz;
|
|
61
|
+
p.tip = _rzPoints[2u + i * 2u].xyz;
|
|
62
|
+
return p;
|
|
63
|
+
}
|
|
64
|
+
`
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { SHADOW_CASCADES } from "../shadow-cascades"
|
|
2
|
+
// The scene's own light, as an effect author sees it: the sun's shadow and the
|
|
3
|
+
// world's ambient.
|
|
4
|
+
//
|
|
5
|
+
// A lawn standing in a character's shadow has to darken where the floor under
|
|
6
|
+
// it does, or the figure floats: the ground catcher draws her shadow, and the
|
|
7
|
+
// blades drawn over it hide it. And the shade has to be the SCENE'S shade — her
|
|
8
|
+
// shaded side takes the world's colour, and a lawn shaded by its own guess
|
|
9
|
+
// reads as a patch of different flowers beside her. Both are already computed
|
|
10
|
+
// for the materials every frame; this hands the same lookups to effects.
|
|
11
|
+
//
|
|
12
|
+
// Bound for real in the particle SHADING stage only. That is where geometry an
|
|
13
|
+
// effect draws inside the scene pass is shaded, and the shadow pass has already
|
|
14
|
+
// run by then. Every other module that compiles the author's file gets stubs:
|
|
15
|
+
// rzShadow answers 1, lit, which is what a point with no occlusion information
|
|
16
|
+
// is — the same answer sampleShadow gives outside every cascade — and
|
|
17
|
+
// rzWorldAmbient answers black, no light.
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The world's light at a surface facing n — the flat colour, or the installed
|
|
21
|
+
* HDRI's irradiance (sh[0].w = 1), evaluated from folded SH coefficients (see
|
|
22
|
+
* ibl.ts for the folding; the shader is a plain polynomial in the normal).
|
|
23
|
+
*
|
|
24
|
+
* Written against whichever name the module gives the light uniform, so the
|
|
25
|
+
* materials, the ground and an effect all evaluate one polynomial.
|
|
26
|
+
*/
|
|
27
|
+
export const worldAmbientWgsl = (u: string) => /* wgsl */ `
|
|
28
|
+
fn rzWorldAmbient(n: vec3f) -> vec3f {
|
|
29
|
+
if (${u}.sh[0].w < 0.5) { return ${u}.ambientColor.xyz; }
|
|
30
|
+
let x = n.x;
|
|
31
|
+
let y = n.y;
|
|
32
|
+
let z = n.z;
|
|
33
|
+
let c = ${u}.sh[0].xyz
|
|
34
|
+
+ ${u}.sh[1].xyz * y + ${u}.sh[2].xyz * z + ${u}.sh[3].xyz * x
|
|
35
|
+
+ ${u}.sh[4].xyz * (x * y) + ${u}.sh[5].xyz * (y * z)
|
|
36
|
+
+ ${u}.sh[6].xyz * (3.0 * z * z - 1.0) + ${u}.sh[7].xyz * (x * z)
|
|
37
|
+
+ ${u}.sh[8].xyz * (x * x - y * y);
|
|
38
|
+
return max(c, vec3f(0.0));
|
|
39
|
+
}
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 3×3 PCF over one cascade's map, for a point already in that cascade's NDC.
|
|
44
|
+
*
|
|
45
|
+
* Shared with the materials, so an effect's shadow and her own shadow soften
|
|
46
|
+
* alike. The sampler is linear-filtered, which makes each tap a 2×2 bilinear
|
|
47
|
+
* compare. Unrolled — Safari's Metal backend doesn't unroll nested shadow
|
|
48
|
+
* loops reliably. The body expects `ndc` in scope and returns.
|
|
49
|
+
*/
|
|
50
|
+
export const pcf9 = (map: string, ts: string, sampler = "shadowSampler") => /* wgsl */ `
|
|
51
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
52
|
+
let cmpZ = ndc.z - 0.001;
|
|
53
|
+
let ts = ${ts};
|
|
54
|
+
let s00 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, -ts), cmpZ);
|
|
55
|
+
let s10 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(0.0, -ts), cmpZ);
|
|
56
|
+
let s20 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, -ts), cmpZ);
|
|
57
|
+
let s01 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, 0.0), cmpZ);
|
|
58
|
+
let s11 = textureSampleCompareLevel(${map}, ${sampler}, suv, cmpZ);
|
|
59
|
+
let s21 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, 0.0), cmpZ);
|
|
60
|
+
let s02 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, ts), cmpZ);
|
|
61
|
+
let s12 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(0.0, ts), cmpZ);
|
|
62
|
+
let s22 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, ts), cmpZ);
|
|
63
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
64
|
+
`
|
|
65
|
+
|
|
66
|
+
/** Bindings the real accessors take, from `binding` upward. */
|
|
67
|
+
export const SCENE_LIGHT_API_BINDINGS = 5
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
|
|
71
|
+
* caster's shadow, soft in between.
|
|
72
|
+
*
|
|
73
|
+
* `rzWorldAmbient(n)`: the world's light arriving at a surface facing `n` —
|
|
74
|
+
* the materials' own ambient, so an effect's shade can match hers.
|
|
75
|
+
*
|
|
76
|
+
* With `on`, declares five bindings from `binding`:
|
|
77
|
+
* +0 the light uniform (ambient, the sun — direction, and in .w the shadow
|
|
78
|
+
* switch — and the HDRI's irradiance)
|
|
79
|
+
* +1 the cascade view-projections, then the caster sphere
|
|
80
|
+
* +2 the near cascade's map +3 the far cascade's map
|
|
81
|
+
* +4 the comparison sampler
|
|
82
|
+
*
|
|
83
|
+
* It takes a POINT and no normal. The materials' sampleShadow also rejects a
|
|
84
|
+
* surface facing away from the sun, which is right for skin and wrong for a
|
|
85
|
+
* blade of grass or a petal: those are thin, lit through, and shaded by their
|
|
86
|
+
* own N·L. The point is nudged toward the sun instead, which lifts a blade's
|
|
87
|
+
* root off a stage floor that casts into the same map.
|
|
88
|
+
*/
|
|
89
|
+
export function sceneLightApi(on: boolean, group: number, binding: number): string {
|
|
90
|
+
if (!on) {
|
|
91
|
+
return /* wgsl */ `
|
|
92
|
+
fn rzShadow(p: vec3f) -> f32 { return 1.0; }
|
|
93
|
+
fn rzWorldAmbient(n: vec3f) -> vec3f { return vec3f(0.0); }
|
|
94
|
+
`
|
|
95
|
+
}
|
|
96
|
+
const n = SHADOW_CASCADES.length
|
|
97
|
+
return /* wgsl */ `
|
|
98
|
+
// The light uniform, as the materials lay it out; lights[0] is the sun.
|
|
99
|
+
struct _RzSceneLight { direction: vec4f, color: vec4f, }
|
|
100
|
+
struct _RzSceneLightU { ambientColor: vec4f, lights: array<_RzSceneLight, 4>, sh: array<vec4f, 9>, }
|
|
101
|
+
// The cascades' matrices, inner to outer, then every caster in one sphere
|
|
102
|
+
// (x, y, z, radius): radius 0 = nothing casts, negative = do not test.
|
|
103
|
+
struct _RzShadowBlock { viewProj: array<mat4x4f, ${n}>, casters: vec4f, }
|
|
104
|
+
@group(${group}) @binding(${binding}) var<uniform> _rzLight: _RzSceneLightU;
|
|
105
|
+
@group(${group}) @binding(${binding + 1}) var<uniform> _rzShadowVP: _RzShadowBlock;
|
|
106
|
+
@group(${group}) @binding(${binding + 2}) var _rzShadowNear: texture_depth_2d;
|
|
107
|
+
@group(${group}) @binding(${binding + 3}) var _rzShadowFar: texture_depth_2d;
|
|
108
|
+
@group(${group}) @binding(${binding + 4}) var _rzShadowCmp: sampler_comparison;
|
|
109
|
+
|
|
110
|
+
fn _rzShadowNearTaps(ndc: vec3f) -> f32 {
|
|
111
|
+
${pcf9("_rzShadowNear", `1.0 / ${SHADOW_CASCADES[0].mapSize}.0`, "_rzShadowCmp")}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn _rzShadowFarTaps(ndc: vec3f) -> f32 {
|
|
115
|
+
${pcf9("_rzShadowFar", `1.0 / ${SHADOW_CASCADES[n - 1].mapSize}.0`, "_rzShadowCmp")}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn rzShadow(p: vec3f) -> f32 {
|
|
119
|
+
// The scene's one shadow switch, on the sun — see sampleShadow.
|
|
120
|
+
let castAmt = _rzLight.lights[0].direction.w;
|
|
121
|
+
if (castAmt <= 0.0) { return 1.0; }
|
|
122
|
+
let toSun = normalize(-_rzLight.lights[0].direction.xyz);
|
|
123
|
+
// Can anything cast onto this point at all? The ground's test, for the same
|
|
124
|
+
// two reasons: most of a lawn is nowhere near her, and a scene with no
|
|
125
|
+
// models never renders the map, whose untouched zeroes compare as shadowed.
|
|
126
|
+
let cs = _rzShadowVP.casters;
|
|
127
|
+
if (cs.w == 0.0) { return 1.0; }
|
|
128
|
+
if (cs.w > 0.0) {
|
|
129
|
+
let toCaster = cs.xyz - p;
|
|
130
|
+
let along = dot(toCaster, toSun);
|
|
131
|
+
let perp = length(toCaster - toSun * along);
|
|
132
|
+
if (along <= -cs.w || perp > cs.w) { return 1.0; }
|
|
133
|
+
}
|
|
134
|
+
let q = p + toSun * 0.08;
|
|
135
|
+
let c0 = _rzShadowVP.viewProj[0] * vec4f(q, 1.0);
|
|
136
|
+
let n0 = c0.xyz / max(c0.w, 1e-6);
|
|
137
|
+
if (all(abs(n0.xy) < vec2f(0.98)) && n0.z > 0.0 && n0.z < 1.0) {
|
|
138
|
+
return mix(1.0, _rzShadowNearTaps(n0), castAmt);
|
|
139
|
+
}
|
|
140
|
+
let c1 = _rzShadowVP.viewProj[${n - 1}] * vec4f(q, 1.0);
|
|
141
|
+
let n1 = c1.xyz / max(c1.w, 1e-6);
|
|
142
|
+
if (all(abs(n1.xy) < vec2f(0.98)) && n1.z > 0.0 && n1.z < 1.0) {
|
|
143
|
+
return mix(1.0, _rzShadowFarTaps(n1), castAmt);
|
|
144
|
+
}
|
|
145
|
+
return 1.0;
|
|
146
|
+
}
|
|
147
|
+
${worldAmbientWgsl("_rzLight")}`
|
|
148
|
+
}
|