reze-engine 0.62.1 → 0.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.d.ts +95 -57
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +466 -162
- package/dist/graph/compile.d.ts.map +1 -1
- package/dist/graph/compile.js +4 -1
- package/dist/graph/registry.d.ts +3 -0
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +133 -0
- package/dist/graph/schema.d.ts +12 -0
- package/dist/graph/schema.d.ts.map +1 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +11 -20
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/shaders/lights.d.ts +3 -2
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +16 -3
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +63 -56
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +210 -0
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +13 -1
- package/dist/shaders/passes/cull.d.ts +1 -1
- package/dist/shaders/passes/cull.d.ts.map +1 -1
- package/dist/shaders/passes/cull.js +7 -3
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +32 -55
- package/dist/shaders/passes/particles.d.ts +11 -1
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +9 -4
- package/dist/shaders/scene-light-api.d.ts +18 -10
- package/dist/shaders/scene-light-api.d.ts.map +1 -1
- package/dist/shaders/scene-light-api.js +58 -46
- package/dist/shadow-cascades.d.ts +47 -20
- package/dist/shadow-cascades.d.ts.map +1 -1
- package/dist/shadow-cascades.js +36 -13
- package/dist/unity/character.d.ts +59 -0
- package/dist/unity/character.d.ts.map +1 -0
- package/dist/unity/character.js +120 -0
- package/dist/unity/globals.d.ts +93 -0
- package/dist/unity/globals.d.ts.map +1 -0
- package/dist/unity/globals.js +340 -0
- package/dist/unity/host.d.ts +137 -0
- package/dist/unity/host.d.ts.map +1 -0
- package/dist/unity/host.js +489 -0
- package/dist/unity/looks.d.ts +88 -0
- package/dist/unity/looks.d.ts.map +1 -0
- package/dist/unity/looks.js +295 -0
- package/dist/unity/skin.d.ts +9 -0
- package/dist/unity/skin.d.ts.map +1 -0
- package/dist/unity/skin.js +117 -0
- package/dist/unity/stage.d.ts +148 -0
- package/dist/unity/stage.d.ts.map +1 -0
- package/dist/unity/stage.js +375 -0
- package/dist/unity/state.d.ts +22 -0
- package/dist/unity/state.d.ts.map +1 -0
- package/dist/unity/state.js +170 -0
- package/package.json +1 -1
- package/src/engine.ts +504 -176
- package/src/graph/compile.ts +3 -1
- package/src/graph/registry.ts +142 -0
- package/src/graph/schema.ts +9 -0
- package/src/graph/slots.ts +11 -20
- package/src/index.ts +11 -0
- package/src/shaders/lights.ts +16 -3
- package/src/shaders/materials/common.ts +63 -57
- package/src/shaders/materials/nodes.ts +210 -0
- package/src/shaders/passes/composite.ts +13 -1
- package/src/shaders/passes/cull.ts +7 -3
- package/src/shaders/passes/ground.ts +32 -55
- package/src/shaders/passes/particles.ts +19 -5
- package/src/shaders/scene-light-api.ts +59 -46
- package/src/shadow-cascades.ts +61 -32
- package/src/unity/character.ts +169 -0
- package/src/unity/globals.ts +443 -0
- package/src/unity/host.ts +587 -0
- package/src/unity/looks.ts +372 -0
- package/src/unity/skin.ts +128 -0
- package/src/unity/stage.ts +568 -0
- package/src/unity/state.ts +172 -0
|
@@ -348,6 +348,215 @@ fn rzWorldSpecularLod(dir: vec3f, roughness: f32, unity: f32) -> vec3f {
|
|
|
348
348
|
return textureSampleLevel(worldEnvTexture, diffuseSampler, vec2f(u, v), lod).rgb * light.ambientColor.w;
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
// ── Aether Gazer's character shading, step by step ──
|
|
352
|
+
// From the decompiled ForwardBase pass of SimPipeline/Character/Debug. Its
|
|
353
|
+
// spaces are the game's: a view space looking down -Z (Unity's camera space),
|
|
354
|
+
// which the engine's +Z-forward view is turned into by negating z.
|
|
355
|
+
|
|
356
|
+
/** CharacterEffect's key light, in sim_TowardMatrix's frame: the camera's
|
|
357
|
+
* backward direction flattened to the ground (d), up, and up × d. The
|
|
358
|
+
* direction points toward the light. */
|
|
359
|
+
fn ag_key_light(inclination: f32, azimuth: f32) -> vec3f {
|
|
360
|
+
let f = vec3f(camera.view[0].z, camera.view[1].z, camera.view[2].z);
|
|
361
|
+
let flat = vec3f(-f.x, 0.0, -f.z);
|
|
362
|
+
let len = length(flat);
|
|
363
|
+
let d = select(vec3f(0.0, 0.0, 1.0), flat / max(len, 1e-5), len > 1e-5);
|
|
364
|
+
let incl = radians(inclination);
|
|
365
|
+
let az = radians(-azimuth);
|
|
366
|
+
let c = cos(incl);
|
|
367
|
+
let k = vec3f(sin(az) * -c, sin(incl), cos(az) * c);
|
|
368
|
+
return normalize(vec3f(d.z, 0.0, -d.x) * k.x + vec3f(0.0, 1.0, 0.0) * k.y + d * k.z);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** u on the shadow ramp: N·L, darkened by the mask's occlusion
|
|
372
|
+
* (1 - min((1 - ao) * scale, 1)), and min'd with the sun's cast shadow. */
|
|
373
|
+
fn ag_shade_term(n: vec3f, l: vec3f, ao: f32, aoScale: f32, receive: f32, wp: vec3f) -> f32 {
|
|
374
|
+
let ndl = max(dot(n, l), 0.0);
|
|
375
|
+
var t = (1.0 - min((1.0 - ao) * aoScale, 1.0)) * ndl;
|
|
376
|
+
if (receive > 0.5) { t = min(t, rzSunOcclusion(wp, n)); }
|
|
377
|
+
return t;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** A ramp image's uv, kept off the image's edge texels so a repeating sampler
|
|
381
|
+
* does not blend the far end in. The row is Unity's v, which runs up from the
|
|
382
|
+
* image's bottom row; the image is uploaded top row first. */
|
|
383
|
+
fn ag_ramp_uv(value: f32, row: f32) -> vec3f {
|
|
384
|
+
return vec3f(clamp(value, 0.002, 0.998), clamp(1.0 - row, 0.002, 0.998), 0.0);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** The view-space reflection, as the game builds it: the view ray mirrored on
|
|
388
|
+
* the normal, into camera space, halved and pushed half a unit toward the
|
|
389
|
+
* eye, normalised. xy*0.5+0.5 is the sphere image's uv (xyz here, z unused),
|
|
390
|
+
* and z how much it faces the eye (w). */
|
|
391
|
+
fn ag_view_reflection(n: vec3f, v: vec3f) -> vec4f {
|
|
392
|
+
let r = reflect(-v, n);
|
|
393
|
+
let rv = vec3f(
|
|
394
|
+
camera.view[0].x * r.x + camera.view[1].x * r.y + camera.view[2].x * r.z,
|
|
395
|
+
camera.view[0].y * r.x + camera.view[1].y * r.y + camera.view[2].y * r.z,
|
|
396
|
+
-(camera.view[0].z * r.x + camera.view[1].z * r.y + camera.view[2].z * r.z),
|
|
397
|
+
);
|
|
398
|
+
let m = normalize(rv * 0.5 + vec3f(0.0, 0.0, 0.5));
|
|
399
|
+
// A sphere image is addressed as Unity reads it, v up: flipped for the
|
|
400
|
+
// top-row-first upload.
|
|
401
|
+
return vec4f(m.x * 0.5 + 0.5, 0.5 - m.y * 0.5, 0.0, m.z);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** The matcap's lift: max(mask * (matcap * strength - 1) + 1, 1). */
|
|
405
|
+
fn ag_matcap(color: vec3f, matcap: vec3f, strength: f32, mask: f32) -> vec3f {
|
|
406
|
+
return color * max(mask * (matcap * strength - vec3f(1.0)) + vec3f(1.0), vec3f(1.0));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** The rim: smoothstep'd to the silhouette by facing, limited to the two rim
|
|
410
|
+
* directions (CharacterEffect: (cos a·s, sin a·s, z), a = azimuth·π,
|
|
411
|
+
* s = sqrt(1 - z²)), laid on as a colour dodge: min(max(c, 2), c / (1 - 2·rim)). */
|
|
412
|
+
fn ag_rim(color: vec3f, facing: f32, uvw: vec3f, rimColor: vec3f, threshold: f32, fade: f32, range: f32,
|
|
413
|
+
inclination: f32, azimuth1: f32, azimuth2: f32, mask: f32) -> vec3f {
|
|
414
|
+
let m = vec3f(uvw.x * 2.0 - 1.0, 1.0 - uvw.y * 2.0, facing);
|
|
415
|
+
let t = saturate((facing - (threshold + fade)) / (threshold - (threshold + fade)));
|
|
416
|
+
let edge = t * t * (3.0 - 2.0 * t);
|
|
417
|
+
let s = sqrt(max(1.0 - inclination * inclination, 0.0));
|
|
418
|
+
let a1 = azimuth1 * 3.1415927;
|
|
419
|
+
let a2 = azimuth2 * 3.1415927;
|
|
420
|
+
let d1 = vec3f(cos(a1) * s, sin(a1) * s, inclination);
|
|
421
|
+
let d2 = vec3f(cos(a2) * s, sin(a2) * s, inclination);
|
|
422
|
+
let along = saturate(max(dot(m, d1), dot(m, d2)) + range);
|
|
423
|
+
let rim = min(edge, along) * rimColor;
|
|
424
|
+
let f = min(rim * mask * 2.0, vec3f(0.99));
|
|
425
|
+
return min(max(color, vec3f(2.0)), color / (vec3f(1.0) - f));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** The fill: inner to outer by pow(1 - facing², softness + 2), laid over by
|
|
429
|
+
* its amount. */
|
|
430
|
+
fn ag_fill(color: vec3f, facing: f32, inner: vec3f, outer: vec3f, amount: f32, softness: f32) -> vec3f {
|
|
431
|
+
let k = pow(max(1.0 - facing * facing, 0.0), softness + 2.0);
|
|
432
|
+
return mix(color, mix(inner, outer, k), amount);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── Aether Gazer's PBR character shading (SimPipeline/Character/PBR/Uber) ──
|
|
436
|
+
// The ForwardBase pass of the game's current character shader, decompiled and
|
|
437
|
+
// written back out step by step. Its ramp image carries the whole tone of the
|
|
438
|
+
// look in rows: the diffuse ramp in the row the material picks, the specular
|
|
439
|
+
// ramp at 0.95, the environment's floor at 0.85.
|
|
440
|
+
|
|
441
|
+
/** A ramp image on a group slot, read at a computed coordinate — mip 0 only,
|
|
442
|
+
* as the game reads it, and so legal wherever the shading branches. */
|
|
443
|
+
fn _ag_ramp(slot: u32, uv: vec2f) -> vec4f {
|
|
444
|
+
// Unity's v runs up from the bottom row; the image is uploaded top row first.
|
|
445
|
+
let c = clamp(vec2f(uv.x, 1.0 - uv.y), vec2f(0.002), vec2f(0.998));
|
|
446
|
+
switch slot {
|
|
447
|
+
case 1u: { return textureSampleLevel(groupTexture1, diffuseSampler, c, 0.0); }
|
|
448
|
+
case 2u: { return textureSampleLevel(groupTexture2, diffuseSampler, c, 0.0); }
|
|
449
|
+
case 3u: { return textureSampleLevel(groupTexture3, diffuseSampler, c, 0.0); }
|
|
450
|
+
default: { return textureSampleLevel(groupTexture0, diffuseSampler, c, 0.0); }
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* The whole lighting of one Uber material under the character's key light:
|
|
456
|
+
* diffuse = base·(1−metal) · ramp(min(shadow, occlusion·N·L), row)
|
|
457
|
+
* specular = GGX through the ramp's 0.95 row, × F0, plus the rim, × the
|
|
458
|
+
* shade term
|
|
459
|
+
* ambient = the object's SH at n × diffuse
|
|
460
|
+
* reflection = the scene's environment along the reflected view, at a
|
|
461
|
+
* roughness mip, × the mobile environment-BRDF fit
|
|
462
|
+
* (F0·A + ramp(N·L·B, 0.85))
|
|
463
|
+
* Lamps and fog are the engine's, laid on after.
|
|
464
|
+
*/
|
|
465
|
+
fn ag_uber(slot: u32, base: vec3f, metallic: f32, roughness: f32, occlusion: f32, n: vec3f, l: vec3f,
|
|
466
|
+
row: f32, receive: f32, rimMask: f32, rimMid: f32, rimWidth: f32, rimTint: vec3f,
|
|
467
|
+
rimIntensity: f32, rimAlbedo: f32, rimInLight: f32, emissive: vec3f, reflection: f32,
|
|
468
|
+
shade: f32, face: f32, wp: vec3f, v: vec3f) -> vec3f {
|
|
469
|
+
let diffuse = base * (1.0 - metallic);
|
|
470
|
+
let f0 = mix(vec3f(0.04), base, metallic);
|
|
471
|
+
let pr = clamp(roughness, 0.0, 1.0);
|
|
472
|
+
let r = max(pr * pr, 0.0078125);
|
|
473
|
+
let a2 = r * r;
|
|
474
|
+
let h = normalize(v + l);
|
|
475
|
+
let ndl = saturate(dot(n, l));
|
|
476
|
+
let ndv = saturate(dot(v, n));
|
|
477
|
+
let ndh = saturate(dot(n, h));
|
|
478
|
+
let vdh = saturate(dot(v, h));
|
|
479
|
+
let ldh = saturate(dot(l, h));
|
|
480
|
+
|
|
481
|
+
// where on the ramp: N·L under occlusion, and the sun's cast shadow
|
|
482
|
+
var shadow = 1.0;
|
|
483
|
+
if (receive > 0.5) { shadow = rzSunOcclusion(wp, n); }
|
|
484
|
+
// A face replaces N·L with its SDF shade; anything else reads N·L.
|
|
485
|
+
let u = min(shadow, occlusion * select(ndl, shade, shade >= 0.0));
|
|
486
|
+
let ramp = _ag_ramp(slot, vec2f(u, row)).rgb;
|
|
487
|
+
|
|
488
|
+
// GGX, as the pass writes it, then through the ramp's specular row
|
|
489
|
+
let dd = ndh * ndh * (a2 - 1.0) + 1.0;
|
|
490
|
+
let D = select(a2 / (dd * dd), 1.0, dd * dd == a2);
|
|
491
|
+
let vis = min(0.5 / (ndl * (ndv * (1.0 - r) + r) + ndv * (ndl * (1.0 - r) + r)), 1.0);
|
|
492
|
+
let vis2 = min(0.5 / (ldh * (vdh * (1.0 - r) + r) + vdh * (ldh * (1.0 - r) + r)), 1.0);
|
|
493
|
+
let x = saturate(D * vis * a2 / vis2);
|
|
494
|
+
let specRamp = _ag_ramp(slot, vec2f(x, 0.95)).rgb;
|
|
495
|
+
let spec = clamp(vis * specRamp * min(1.0 / a2, 2048.0), vec3f(0.0), vec3f(10.0));
|
|
496
|
+
|
|
497
|
+
// the rim: toward the silhouette on the lit side, tinted toward the albedo
|
|
498
|
+
let lit = select(0.0, 1.0, u > 0.001);
|
|
499
|
+
let t = saturate(((ndl * 0.25 + 0.75) * (1.0 - ndv) * lit - (rimMid - rimWidth)) / max(2.0 * rimWidth, 1e-4));
|
|
500
|
+
let edge = t * t * (3.0 - 2.0 * t);
|
|
501
|
+
let tint = min(rimTint * rimIntensity * mix(vec3f(1.0), base, rimAlbedo) * 2.0 * rimMask, vec3f(0.99));
|
|
502
|
+
let rim = tint * edge * (1.0 + rimInLight * (u - 1.0));
|
|
503
|
+
|
|
504
|
+
// A face (FACE_MODE) takes no GGX: its light is the ramp, the rim and its
|
|
505
|
+
// optional SDF highlight (off unless _FaceSpecular).
|
|
506
|
+
let isFace = face > 0.5;
|
|
507
|
+
let direct = (select(spec * f0, vec3f(0.0), isFace) + rim) * u + diffuse * ramp;
|
|
508
|
+
|
|
509
|
+
// the environment, with the game's fit of the split-sum
|
|
510
|
+
let a004 = min((1.0 - pr) * (1.0 - pr), exp2(-9.28 * ndv)) * (1.0 - pr) + (0.0425 - 0.0275 * pr);
|
|
511
|
+
let A = -a004 * 1.04 + (1.04 - 0.572 * pr);
|
|
512
|
+
let ns = saturate(dot(n, mix(reflect(-l, n), h, 0.8)));
|
|
513
|
+
let B = _ag_ramp(slot, vec2f(ns * (a004 * 1.04 + (0.022 * pr - 0.04)), 0.85)).r;
|
|
514
|
+
let env = rzWorldSpecularLod(reflect(-v, n), pr, 1.0) * reflection;
|
|
515
|
+
|
|
516
|
+
// ... and the environment at a flat 0.0157 rather than through the BRDF fit.
|
|
517
|
+
let envTerm = select((f0 * A + B) * env, env * 0.0157, isFace);
|
|
518
|
+
return emissive + rzObjectAmbient(n) * diffuse + envTerm + direct;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* The face's shade from its SDF image (Uber FACE_MODE, _SDFType 0), in place of
|
|
524
|
+
* N·L: the key light taken into the face's frame and flattened, the field read
|
|
525
|
+
* at the uv and at its mirror, the side the light is on choosing which, both
|
|
526
|
+
* compared against the light's front-to-back angle with _BlendSmoothness, then
|
|
527
|
+
* softened by how level the light is and a half-Lambert. The face frame is the
|
|
528
|
+
* head bone's, turned to the game's convention (x to the face's right, z out of
|
|
529
|
+
* it). An SDF is painted in the face's uvs, so it belongs to models that share
|
|
530
|
+
* the game's face layout; a white slot reads as a face lit evenly until the
|
|
531
|
+
* light goes behind it.
|
|
532
|
+
*/
|
|
533
|
+
fn ag_face_sdf(slot: u32, uv: vec2f, l: vec3f, n: vec3f, smoothness: f32, invert: f32) -> f32 {
|
|
534
|
+
let hb = skinMats[u32(max(material.headBoneIndex, 0.0))];
|
|
535
|
+
let fx = -normalize(hb[0].xyz);
|
|
536
|
+
let fy = normalize(hb[1].xyz);
|
|
537
|
+
let fz = -normalize(hb[2].xyz);
|
|
538
|
+
let lf = normalize(vec3f(dot(l, fx), dot(l, fy), dot(l, fz)));
|
|
539
|
+
let fl = lf.xz * inverseSqrt(max(dot(lf.xz, lf.xz), 1.1754944e-38));
|
|
540
|
+
let u0 = mix(uv.x, 1.0 - uv.x, invert);
|
|
541
|
+
let s1 = _ag_ramp(slot, vec2f(u0, 1.0 - uv.y));
|
|
542
|
+
let s2 = _ag_ramp(slot, vec2f(1.0 - u0, 1.0 - uv.y));
|
|
543
|
+
let left = fl.x < 0.0;
|
|
544
|
+
let a = select(s2.x, s1.x, left);
|
|
545
|
+
let b = select(s1.x, s2.x, left);
|
|
546
|
+
let flag = select(0.0, 1.0, s1.w > 0.5);
|
|
547
|
+
let k = max(smoothness, 0.001);
|
|
548
|
+
let th = fl.y * -0.5 + 0.5;
|
|
549
|
+
let w = 1.0 - th;
|
|
550
|
+
var m1 = max(saturate((a - th) / k + 0.5), saturate((b - w - 1.0) / k + 0.5));
|
|
551
|
+
m1 = min(m1, min(saturate((2.0 - a - w) / k + 0.5), saturate((2.0 - b - w) / k + 0.5)));
|
|
552
|
+
var m2 = max(saturate((b - th - 1.0) / k + 0.5), saturate((a - w) / k + 0.5));
|
|
553
|
+
m2 = min(m2, min(saturate((a + w) / k + 0.5), saturate((b + w) / k + 0.5)));
|
|
554
|
+
var r = flag * (m1 - m2) + m2;
|
|
555
|
+
r *= (1.0 - abs(lf.z)) * 0.5 + 0.5;
|
|
556
|
+
r *= saturate(dot(n, l)) * 0.5 + 0.5;
|
|
557
|
+
return r;
|
|
558
|
+
}
|
|
559
|
+
|
|
351
560
|
fn group_tex0(uv: vec2f) -> vec4f { return textureSample(groupTexture0, diffuseSampler, uv); }
|
|
352
561
|
fn group_tex1(uv: vec2f) -> vec4f { return textureSample(groupTexture1, diffuseSampler, uv); }
|
|
353
562
|
fn group_tex2(uv: vec2f) -> vec4f { return textureSample(groupTexture2, diffuseSampler, uv); }
|
|
@@ -1056,6 +1265,7 @@ struct _RzLampPair { d: vec3f, s: vec3f };
|
|
|
1056
1265
|
|
|
1057
1266
|
fn _rzLampOne(i: u32, p: vec3f, n: vec3f, v: vec3f, ndv: f32, roughness: f32, unity: f32) -> _RzLampPair {
|
|
1058
1267
|
var out = _RzLampPair(vec3f(0.0), vec3f(0.0));
|
|
1268
|
+
if (!_rzLightReaches(i)) { return out; }
|
|
1059
1269
|
let pr = _rzLightVec(i, 0u);
|
|
1060
1270
|
let d = pr.xyz - p;
|
|
1061
1271
|
let dist = length(d);
|
|
@@ -302,11 +302,23 @@ fn agxTransform(c: vec3f) -> vec3f {
|
|
|
302
302
|
return vec3f(srgb_encode(linear.r), srgb_encode(linear.g), srgb_encode(linear.b));
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
/**
|
|
305
|
+
/**
|
|
306
|
+
* Aether Gazer's own curve, from its final pass (Hidden/SimPipeline/Final,
|
|
307
|
+
* decompiled): (1 - e^(-exposure x))^contrast, at the 2.5 and 1.4 its scenes
|
|
308
|
+
* set, into a linear target the swapchain then encodes. The game's ACES branch
|
|
309
|
+
* is off in every scene measured.
|
|
310
|
+
*/
|
|
311
|
+
fn agTransform(c: vec3f) -> vec3f {
|
|
312
|
+
let t = pow(max(vec3f(1.0) - exp(-2.5 * max(c, vec3f(0.0))), vec3f(0.0)), vec3f(1.4));
|
|
313
|
+
return vec3f(srgb_encode(t.r), srgb_encode(t.g), srgb_encode(t.b));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Which display transform, chosen per frame at viewU[6].y (0 filmic, 1 standard, 2 agx, 3 aether-gazer).
|
|
306
317
|
* A uniform branch rather than a pipeline variant: switching is rare, and both
|
|
307
318
|
* arms are cheap enough that specialising the pipeline would buy nothing. */
|
|
308
319
|
fn viewTransform(c: vec3f) -> vec3f {
|
|
309
320
|
let mode = viewU[6].y;
|
|
321
|
+
if (mode > 2.5) { return agTransform(c); }
|
|
310
322
|
if (mode > 1.5) { return agxTransform(c); }
|
|
311
323
|
if (mode > 0.5) { return vec3f(srgb_encode(c.r), srgb_encode(c.g), srgb_encode(c.b)); }
|
|
312
324
|
return vec3f(filmic(c.r), filmic(c.g), filmic(c.b));
|
|
@@ -130,10 +130,14 @@ fn cs(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
130
130
|
// a hidden material is not culled, it is switched off, and answering "is this
|
|
131
131
|
// gone because of culling?" requires the two to stay separable.
|
|
132
132
|
let off = frusta.counts.y == 0u;
|
|
133
|
-
|
|
133
|
+
// hidden: bit 0 switched off; bit 1 drawn by the native-material host
|
|
134
|
+
// (unity/host.ts) rather than by its graph — no camera or mirror draw here,
|
|
135
|
+
// and still its shadow, which the engine's pass casts for every material.
|
|
136
|
+
let shown = (hidden[i] & 1u) == 0u;
|
|
137
|
+
let graph = (hidden[i] & 2u) == 0u;
|
|
134
138
|
let castsShadow = (dm.flags & DRAW_CASTS_SHADOW) != 0u;
|
|
135
|
-
cameraArgs[i * 5u + 1u] = select(0u, 1u, (inCamera || off) && shown);
|
|
139
|
+
cameraArgs[i * 5u + 1u] = select(0u, 1u, (inCamera || off) && shown && graph);
|
|
136
140
|
shadowArgs[i * 5u + 1u] = select(0u, 1u, ((inLight && castsShadow) || off) && shown);
|
|
137
|
-
mirrorArgs[i * 5u + 1u] = select(0u, 1u, (inMirror || off) && shown);
|
|
141
|
+
mirrorArgs[i * 5u + 1u] = select(0u, 1u, (inMirror || off) && shown && graph);
|
|
138
142
|
}
|
|
139
143
|
`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sceneFsOutWgsl, sceneIdWriteWgsl } from "./scene-contract"
|
|
2
2
|
import { lightsApi } from "../lights"
|
|
3
|
-
import { SHADOW_CASCADES } from "../../shadow-cascades"
|
|
3
|
+
import { SHADOW_ATLAS_SIZE, SHADOW_CASCADES } from "../../shadow-cascades"
|
|
4
|
+
import { SHADOW_MARGIN } from "../scene-light-api"
|
|
4
5
|
import { WORLD_AMBIENT_WGSL } from "../lights"
|
|
5
6
|
|
|
6
7
|
// Ground shadow-catcher: receives directional shadow, grid lines, frosted noise,
|
|
@@ -36,6 +37,9 @@ const SOFT_MAX_SPREAD = 14
|
|
|
36
37
|
* normalised, so there is no `/ 9.0` left at the call site.
|
|
37
38
|
*/
|
|
38
39
|
function pcfWgsl(map: string, uv: string, texel: string, z: string, acc: string, pad: string, soft: boolean): string {
|
|
40
|
+
// Every tap stays inside the cascade's own tile (tileLo..tileHi, in scope at
|
|
41
|
+
// the call): the soft disk reaches further than the selection margin, and a
|
|
42
|
+
// tap in the neighbouring tile compares against another cascade's depths.
|
|
39
43
|
const golden = 2.39996323
|
|
40
44
|
// ONE BRANCH IS COMPILED, NOT BOTH.
|
|
41
45
|
//
|
|
@@ -58,7 +62,7 @@ function pcfWgsl(map: string, uv: string, texel: string, z: string, acc: string,
|
|
|
58
62
|
` for (var x = -1; x <= 1; x++) {`,
|
|
59
63
|
// ...Level, not the implicit-derivative form: identical on a single-mip
|
|
60
64
|
// shadow map, and legal inside a branch.
|
|
61
|
-
` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, ${uv} + vec2f(f32(x), f32(y)) * st, ${z});`,
|
|
65
|
+
` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, clamp(${uv} + vec2f(f32(x), f32(y)) * st, tileLo, tileHi), ${z});`,
|
|
62
66
|
` }`,
|
|
63
67
|
`}`,
|
|
64
68
|
`${acc} *= ${1 / 9};`,
|
|
@@ -71,7 +75,7 @@ function pcfWgsl(map: string, uv: string, texel: string, z: string, acc: string,
|
|
|
71
75
|
// keeps successive taps from lining up into spokes.
|
|
72
76
|
` let r = sqrt((fs + 0.5) * ${1 / SOFT_TAPS});`,
|
|
73
77
|
` let a = fs * ${golden} + rot;`,
|
|
74
|
-
` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, ${uv} + vec2f(cos(a), sin(a)) * (r * radius), ${z});`,
|
|
78
|
+
` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, clamp(${uv} + vec2f(cos(a), sin(a)) * (r * radius), tileLo, tileHi), ${z});`,
|
|
75
79
|
`}`,
|
|
76
80
|
`${acc} *= ${1 / SOFT_TAPS};`,
|
|
77
81
|
]
|
|
@@ -89,12 +93,10 @@ struct Light { direction: vec4f, color: vec4f, };
|
|
|
89
93
|
struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, sh: array<vec4f, 9>, };
|
|
90
94
|
struct GroundShadowMat {
|
|
91
95
|
diffuseColor: vec3f, fadeStart: f32,
|
|
92
|
-
fadeEnd: f32, shadowStrength: f32,
|
|
96
|
+
fadeEnd: f32, shadowStrength: f32, _pad6: f32, gridSpacing: f32,
|
|
93
97
|
gridLineWidth: f32, gridLineOpacity: f32, noiseStrength: f32, opacity: f32,
|
|
94
98
|
gridLineColor: vec3f, mirror: f32,
|
|
95
|
-
|
|
96
|
-
// with no stage the far map is never drawn into, so its taps are known.
|
|
97
|
-
mirrorBlur: f32, farCascade: f32, shadowSoftness: f32, groundY: f32,
|
|
99
|
+
mirrorBlur: f32, _pad17: f32, shadowSoftness: f32, groundY: f32,
|
|
98
100
|
// Every shadow caster in one sphere, refreshed per frame. w = radius; 0 means
|
|
99
101
|
// nothing casts, negative means "do not use this" (a rigid caster has no
|
|
100
102
|
// sphere, so a scene with a stage keeps the taps). See rzShadowPossible.
|
|
@@ -105,12 +107,11 @@ struct GroundShadowMat {
|
|
|
105
107
|
struct LightVP { viewProj: array<mat4x4f, ${SHADOW_CASCADES.length}>, };
|
|
106
108
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
107
109
|
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
108
|
-
|
|
110
|
+
// The sun's shadow atlas — see shadow-cascades.ts.
|
|
111
|
+
@group(0) @binding(2) var shadowAtlas: texture_depth_2d;
|
|
109
112
|
@group(0) @binding(3) var shadowSampler: sampler_comparison;
|
|
110
113
|
@group(0) @binding(4) var<uniform> material: GroundShadowMat;
|
|
111
114
|
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
112
|
-
// The far cascade's map, binding 7 as in the materials' layout.
|
|
113
|
-
@group(0) @binding(7) var shadowMapFar: texture_depth_2d;
|
|
114
115
|
// The floor mirror (step 7D): the reflection target, the camera that rendered
|
|
115
116
|
// it, and an ordinary sampler — the two shadow bindings above are comparison.
|
|
116
117
|
// params = (projA, projB, _, _) — the depth-linearisation pair of the SHARED
|
|
@@ -144,7 +145,7 @@ struct MirrorVP { viewProj: mat4x4f, params: vec4f, };
|
|
|
144
145
|
struct MirrorClip { plane: vec4f, on: f32, _p0: f32, _p1: f32, _p2: f32, };
|
|
145
146
|
@group(0) @binding(14) var<uniform> clip: MirrorClip;
|
|
146
147
|
${WORLD_AMBIENT_WGSL}
|
|
147
|
-
${lightsApi(0, 6)}
|
|
148
|
+
${lightsApi(0, 6, "1u")}
|
|
148
149
|
|
|
149
150
|
struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };
|
|
150
151
|
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {
|
|
@@ -178,21 +179,6 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
|
|
|
178
179
|
return out;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
// Outside a cascade's frustum there IS no shadow information — the clamped
|
|
182
|
-
// border samples compare against unrelated depths and read "shadowed",
|
|
183
|
-
// darkening the whole far plane with a visible band at the frustum edge
|
|
184
|
-
// (masked by the opaque surface normally, glaring in green-screen mode where
|
|
185
|
-
// only the shadow-catcher term renders). So each cascade fades out near its
|
|
186
|
-
// border — the near one INTO the far one's answer, the far one into lit.
|
|
187
|
-
let l0 = lightVP.viewProj[0] * vec4f(i.worldPos, 1.0);
|
|
188
|
-
let ndc = l0.xyz / max(l0.w, 1e-6);
|
|
189
|
-
let inZ = select(0.0, 1.0, ndc.z > 0.0 && ndc.z < 1.0);
|
|
190
|
-
let frustum = (1.0 - smoothstep(0.88, 0.96, abs(ndc.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc.y))) * inZ;
|
|
191
|
-
let l1 = lightVP.viewProj[1] * vec4f(i.worldPos, 1.0);
|
|
192
|
-
let ndc1 = l1.xyz / max(l1.w, 1e-6);
|
|
193
|
-
let inZ1 = select(0.0, 1.0, ndc1.z > 0.0 && ndc1.z < 1.0);
|
|
194
|
-
let frustum1 = (1.0 - smoothstep(0.88, 0.96, abs(ndc1.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc1.y))) * inZ1;
|
|
195
|
-
|
|
196
182
|
// THE cost of this shader, measured: a full-screen ground ran 25 shadow
|
|
197
183
|
// comparisons per pixel, and each one is hardware-bilinear, so ~100 depth
|
|
198
184
|
// samples per pixel. Gutting the rest of the shader changed nothing; cutting
|
|
@@ -201,11 +187,9 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
|
|
|
201
187
|
// 3x3 at two-texel spacing covers the same +/-2 texel penumbra the 5x5 at
|
|
202
188
|
// one-texel spacing did — that kernel was oversampled, its taps overlapping
|
|
203
189
|
// almost entirely once the comparison sampler's own 2x2 filter is counted.
|
|
204
|
-
// Nine taps instead of twenty-five for the same blur radius
|
|
190
|
+
// Nine taps instead of twenty-five for the same blur radius, and one
|
|
191
|
+
// cascade's worth per pixel: the first that holds it.
|
|
205
192
|
//
|
|
206
|
-
// The far cascade's taps run ONLY where the near one is fading or absent
|
|
207
|
-
// (frustum < 1), so a pixel in the near core costs exactly what it did with
|
|
208
|
-
// one cascade — and that core is where the camera usually looks.
|
|
209
193
|
// Can anything cast a shadow ONTO this point at all?
|
|
210
194
|
//
|
|
211
195
|
// The ground is vastly larger than what stands on it, and the answer for most
|
|
@@ -261,33 +245,26 @@ ${
|
|
|
261
245
|
let rot = ign * 6.28318530718;`
|
|
262
246
|
: ""
|
|
263
247
|
}
|
|
264
|
-
// The
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
let
|
|
276
|
-
let
|
|
277
|
-
let
|
|
278
|
-
|
|
279
|
-
${pcfWgsl("shadowMapFar", "suv1_c", `${1 / SHADOW_CASCADES[SHADOW_CASCADES.length - 1].mapSize}`, "compareZ1", "acc1", " ", soft)}
|
|
280
|
-
vis = mix(1.0, acc1, frustum1);
|
|
281
|
-
}
|
|
282
|
-
if (frustum > 0.0) {
|
|
283
|
-
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
284
|
-
let suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));
|
|
248
|
+
// The first cascade whose box holds the point, as the materials pick it.
|
|
249
|
+
// Outside a cascade's box there IS no shadow information — the clamped
|
|
250
|
+
// border samples compare against unrelated depths and read "shadowed",
|
|
251
|
+
// darkening the far floor with a band at the edge — so the outermost one
|
|
252
|
+
// fades into lit near its border. Inner cascades hand over to the next.
|
|
253
|
+
let n = ${SHADOW_CASCADES.length}u;
|
|
254
|
+
for (var ci = 0u; ci < n; ci++) {
|
|
255
|
+
let lc = lightVP.viewProj[ci] * vec4f(i.worldPos, 1.0);
|
|
256
|
+
let ndc = lc.xyz / max(lc.w, 1e-6);
|
|
257
|
+
if (!(all(abs(ndc.xy) < vec2f(${SHADOW_MARGIN})) && ndc.z > 0.0 && ndc.z < 1.0)) { continue; }
|
|
258
|
+
let frustum = select(1.0, (1.0 - smoothstep(0.88, 0.96, abs(ndc.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc.y))), ci == n - 1u);
|
|
259
|
+
let tile = vec2f(f32(ci % 2u), f32(ci / 2u)) * ${SHADOW_CASCADES[0].mapSize / SHADOW_ATLAS_SIZE};
|
|
260
|
+
let tileLo = tile + vec2f(${0.5 / SHADOW_ATLAS_SIZE});
|
|
261
|
+
let tileHi = tile + vec2f(${(SHADOW_CASCADES[0].mapSize - 0.5) / SHADOW_ATLAS_SIZE});
|
|
262
|
+
let suv = tile + vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5) * ${SHADOW_CASCADES[0].mapSize / SHADOW_ATLAS_SIZE};
|
|
285
263
|
let compareZ = ndc.z - 0.0035;
|
|
286
264
|
var acc = 0.0;
|
|
287
|
-
${pcfWgsl("
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
vis = mix(vis, acc, frustum);
|
|
265
|
+
${pcfWgsl("shadowAtlas", "suv", `${1 / SHADOW_ATLAS_SIZE}`, "compareZ", "acc", " ", soft)}
|
|
266
|
+
vis = mix(1.0, acc, frustum);
|
|
267
|
+
break;
|
|
291
268
|
}
|
|
292
269
|
}
|
|
293
270
|
|
|
@@ -102,6 +102,15 @@ type ParticleSource = {
|
|
|
102
102
|
* where every stacked fragment is otherwise shaded in full; the prepass is
|
|
103
103
|
* the shape of that rejection, done by hand and cheaply. */
|
|
104
104
|
cover: boolean
|
|
105
|
+
/** The effect defines `particleOrient(p: Particle, id: u32) -> mat3x3f`: the
|
|
106
|
+
* quad lies along its first two columns (right, up) instead of facing the
|
|
107
|
+
* camera. A card that belongs to a surface — a raindrop's glow on a window,
|
|
108
|
+
* a sheet of water standing in its fall — is flat on that surface and turns
|
|
109
|
+
* with it, not with the eye: Unity's Local and World render alignments. The
|
|
110
|
+
* particle's rotation still turns the corners within that plane, and a
|
|
111
|
+
* stretched particle stretches along its velocity projected into it. `id`
|
|
112
|
+
* is the particle's slot, so the axes can come from its emitter. */
|
|
113
|
+
orient?: boolean
|
|
105
114
|
/** The effect defines `particleCount() -> u32`: how many of the pool are
|
|
106
115
|
* live this frame, as a function of its dials. The step is dispatched and
|
|
107
116
|
* the quads drawn for that many slots and no more, through indirect
|
|
@@ -121,7 +130,7 @@ type ParticleSource = {
|
|
|
121
130
|
textures?: number
|
|
122
131
|
}
|
|
123
132
|
|
|
124
|
-
/** Where the shading stage's
|
|
133
|
+
/** Where the shading stage's four scene-light bindings start — see scene-light-api.ts. */
|
|
125
134
|
export const PARTICLE_LIGHT_BINDING = 10
|
|
126
135
|
/** The effect's named points (`#points`), in both stages — see points-api.ts. */
|
|
127
136
|
export const PARTICLE_POINTS_BINDING = 15
|
|
@@ -196,8 +205,9 @@ fn rzCamPos() -> vec3f { return cam.camPos; }
|
|
|
196
205
|
/** Does the source define the particle contract? All three are required
|
|
197
206
|
* together; `cover` is optional and only means anything to a cutout, and
|
|
198
207
|
* `count` is optional and bounds the frame's work — see particleCount. */
|
|
199
|
-
export function particleEntryPoints(wgsl: string): { init: boolean; step: boolean; shade: boolean; cover: boolean; count: boolean } {
|
|
208
|
+
export function particleEntryPoints(wgsl: string): { init: boolean; step: boolean; shade: boolean; cover: boolean; count: boolean; orient: boolean } {
|
|
200
209
|
return {
|
|
210
|
+
orient: /\bfn\s+particleOrient\s*\(/.test(wgsl),
|
|
201
211
|
init: /\bfn\s+particleInit\s*\(/.test(wgsl),
|
|
202
212
|
step: /\bfn\s+particleStep\s*\(/.test(wgsl),
|
|
203
213
|
shade: /\bfn\s+particleShade\s*\(/.test(wgsl),
|
|
@@ -372,9 +382,13 @@ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut
|
|
|
372
382
|
out.clip = vec4f(0.0, 0.0, -2.0, 1.0);
|
|
373
383
|
return out;
|
|
374
384
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
385
|
+
var right = vec3f(cam.view[0][0], cam.view[1][0], cam.view[2][0]);
|
|
386
|
+
var up = vec3f(cam.view[0][1], cam.view[1][1], cam.view[2][1]);
|
|
387
|
+
${src.orient ? ` // the author's own plane (particleOrient): a card on a surface, not facing the eye
|
|
388
|
+
let o = particleOrient(p, ii);
|
|
389
|
+
right = o[0];
|
|
390
|
+
up = o[1];
|
|
391
|
+
` : ""} let s = sin(p.rot);
|
|
378
392
|
let k = cos(p.rot);
|
|
379
393
|
var r = vec2f(c.x * k - c.y * s, c.x * s + c.y * k);
|
|
380
394
|
// Stretched along the direction of travel ON SCREEN — which is not the world
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SHADOW_CASCADES } from "../shadow-cascades"
|
|
1
|
+
import { SHADOW_ATLAS_SIZE, SHADOW_CASCADES } from "../shadow-cascades"
|
|
2
2
|
// The scene's own light, as an effect author sees it: the sun's shadow and the
|
|
3
3
|
// world's ambient.
|
|
4
4
|
//
|
|
@@ -51,31 +51,61 @@ fn rzWorldAmbientAvg() -> vec3f {
|
|
|
51
51
|
`
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
54
|
+
* The sun's shadow atlas, as every reader looks it up: `<p>Locate(q)` finds
|
|
55
|
+
* world point q in the first cascade whose box holds it and answers
|
|
56
|
+
* (atlas uv, depth, cascade) — cascade -1 outside them all — and `<p>Taps(a)`
|
|
57
|
+
* is the 3×3 PCF at that place.
|
|
55
58
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
59
|
+
* ONE implementation for the materials, the ground and effects, so a blade of
|
|
60
|
+
* grass and her own skin go dark at the same edge. `vp` names the cascades'
|
|
61
|
+
* view-projection array in the module that includes it.
|
|
62
|
+
*
|
|
63
|
+
* The box test keeps a margin (SHADOW_MARGIN of the tile's half-width) so the
|
|
64
|
+
* whole filter stays inside the tile: a tap that crossed into the neighbouring
|
|
65
|
+
* tile would compare against another cascade's depths. The sampler is linear,
|
|
66
|
+
* so each tap is itself a 2×2 compare. Unrolled — Safari's Metal backend
|
|
67
|
+
* doesn't unroll nested shadow loops reliably.
|
|
60
68
|
*/
|
|
61
|
-
export const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
export const SHADOW_MARGIN = 0.98
|
|
70
|
+
|
|
71
|
+
export const sunShadowWgsl = (p: string, map: string, sampler: string, vp: string) => {
|
|
72
|
+
const n = SHADOW_CASCADES.length
|
|
73
|
+
const tiles = SHADOW_CASCADES.map((c) => `vec2f(${c.origin[0] / SHADOW_ATLAS_SIZE}, ${c.origin[1] / SHADOW_ATLAS_SIZE})`).join(", ")
|
|
74
|
+
const scale = SHADOW_CASCADES[0].mapSize / SHADOW_ATLAS_SIZE
|
|
75
|
+
return /* wgsl */ `
|
|
76
|
+
const ${p}Tiles = array<vec2f, ${n}>(${tiles});
|
|
77
|
+
|
|
78
|
+
fn ${p}Locate(q: vec3f) -> vec4f {
|
|
79
|
+
for (var i = 0u; i < ${n}u; i++) {
|
|
80
|
+
let c = ${vp}[i] * vec4f(q, 1.0);
|
|
81
|
+
let ndc = c.xyz / max(c.w, 1e-6);
|
|
82
|
+
if (all(abs(ndc.xy) < vec2f(${SHADOW_MARGIN})) && ndc.z > 0.0 && ndc.z < 1.0) {
|
|
83
|
+
let uv = ${p}Tiles[i] + vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5) * ${scale};
|
|
84
|
+
return vec4f(uv, ndc.z, f32(i));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return vec4f(0.0, 0.0, 0.0, -1.0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn ${p}Taps(a: vec4f) -> f32 {
|
|
91
|
+
let cmpZ = a.z - 0.001;
|
|
92
|
+
let ts = ${1 / SHADOW_ATLAS_SIZE};
|
|
93
|
+
let s00 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f(-ts, -ts), cmpZ);
|
|
94
|
+
let s10 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f(0.0, -ts), cmpZ);
|
|
95
|
+
let s20 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f( ts, -ts), cmpZ);
|
|
96
|
+
let s01 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f(-ts, 0.0), cmpZ);
|
|
97
|
+
let s11 = textureSampleCompareLevel(${map}, ${sampler}, a.xy, cmpZ);
|
|
98
|
+
let s21 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f( ts, 0.0), cmpZ);
|
|
99
|
+
let s02 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f(-ts, ts), cmpZ);
|
|
100
|
+
let s12 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f(0.0, ts), cmpZ);
|
|
101
|
+
let s22 = textureSampleCompareLevel(${map}, ${sampler}, a.xy + vec2f( ts, ts), cmpZ);
|
|
74
102
|
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
103
|
+
}
|
|
75
104
|
`
|
|
105
|
+
}
|
|
76
106
|
|
|
77
107
|
/** Bindings the real accessors take, from `binding` upward. */
|
|
78
|
-
export const SCENE_LIGHT_API_BINDINGS =
|
|
108
|
+
export const SCENE_LIGHT_API_BINDINGS = 4
|
|
79
109
|
|
|
80
110
|
/**
|
|
81
111
|
* `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
|
|
@@ -84,12 +114,11 @@ export const SCENE_LIGHT_API_BINDINGS = 5
|
|
|
84
114
|
* `rzWorldAmbient(n)`: the world's light arriving at a surface facing `n` —
|
|
85
115
|
* the materials' own ambient, so an effect's shade can match hers.
|
|
86
116
|
*
|
|
87
|
-
* With `on`, declares
|
|
117
|
+
* With `on`, declares four bindings from `binding`:
|
|
88
118
|
* +0 the light uniform (ambient, the sun — direction, and in .w the shadow
|
|
89
119
|
* switch — and the HDRI's irradiance)
|
|
90
120
|
* +1 the cascade view-projections, then the caster sphere
|
|
91
|
-
* +2 the
|
|
92
|
-
* +4 the comparison sampler
|
|
121
|
+
* +2 the sun's shadow atlas +3 the comparison sampler
|
|
93
122
|
*
|
|
94
123
|
* It takes a POINT and no normal. The materials' sampleShadow also rejects a
|
|
95
124
|
* surface facing away from the sun, which is right for skin and wrong for a
|
|
@@ -114,17 +143,9 @@ struct _RzSceneLightU { ambientColor: vec4f, lights: array<_RzSceneLight, 4>, sh
|
|
|
114
143
|
struct _RzShadowBlock { viewProj: array<mat4x4f, ${n}>, casters: vec4f, }
|
|
115
144
|
@group(${group}) @binding(${binding}) var<uniform> _rzLight: _RzSceneLightU;
|
|
116
145
|
@group(${group}) @binding(${binding + 1}) var<uniform> _rzShadowVP: _RzShadowBlock;
|
|
117
|
-
@group(${group}) @binding(${binding + 2}) var
|
|
118
|
-
@group(${group}) @binding(${binding + 3}) var
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
fn _rzShadowNearTaps(ndc: vec3f) -> f32 {
|
|
122
|
-
${pcf9("_rzShadowNear", `1.0 / ${SHADOW_CASCADES[0].mapSize}.0`, "_rzShadowCmp")}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
fn _rzShadowFarTaps(ndc: vec3f) -> f32 {
|
|
126
|
-
${pcf9("_rzShadowFar", `1.0 / ${SHADOW_CASCADES[n - 1].mapSize}.0`, "_rzShadowCmp")}
|
|
127
|
-
}
|
|
146
|
+
@group(${group}) @binding(${binding + 2}) var _rzShadowAtlas: texture_depth_2d;
|
|
147
|
+
@group(${group}) @binding(${binding + 3}) var _rzShadowCmp: sampler_comparison;
|
|
148
|
+
${sunShadowWgsl("_rzSun", "_rzShadowAtlas", "_rzShadowCmp", "_rzShadowVP.viewProj")}
|
|
128
149
|
|
|
129
150
|
fn rzShadow(p: vec3f) -> f32 {
|
|
130
151
|
// The scene's one shadow switch, on the sun — see sampleShadow.
|
|
@@ -142,18 +163,10 @@ fn rzShadow(p: vec3f) -> f32 {
|
|
|
142
163
|
let perp = length(toCaster - toSun * along);
|
|
143
164
|
if (along <= -cs.w || perp > cs.w) { return 1.0; }
|
|
144
165
|
}
|
|
145
|
-
let
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return mix(1.0, _rzShadowNearTaps(n0), castAmt);
|
|
150
|
-
}
|
|
151
|
-
let c1 = _rzShadowVP.viewProj[${n - 1}] * vec4f(q, 1.0);
|
|
152
|
-
let n1 = c1.xyz / max(c1.w, 1e-6);
|
|
153
|
-
if (all(abs(n1.xy) < vec2f(0.98)) && n1.z > 0.0 && n1.z < 1.0) {
|
|
154
|
-
return mix(1.0, _rzShadowFarTaps(n1), castAmt);
|
|
155
|
-
}
|
|
156
|
-
return 1.0;
|
|
166
|
+
let a = _rzSunLocate(p + toSun * 0.08);
|
|
167
|
+
// Outside every cascade there is no occlusion information: lit.
|
|
168
|
+
if (a.w < 0.0) { return 1.0; }
|
|
169
|
+
return mix(1.0, _rzSunTaps(a), castAmt);
|
|
157
170
|
}
|
|
158
171
|
${worldAmbientWgsl("_rzLight")}`
|
|
159
172
|
}
|