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.
@@ -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
  // Ribbons along a bone's recorded path, drawn as geometry.
14
15
  //
15
16
  // This is the effect the whole geometry path was built for. As a fullscreen
@@ -301,7 +302,7 @@ fn rzTurnRadius(a: vec3f, b: vec3f, c: vec3f) -> f32 {
301
302
  gridReadApi(0, 8, 9, src.gridSize) +
302
303
  // Stubbed: this module cannot read an attachment the scene pass writes — see
303
304
  // id-api.ts. The author's whole file compiles here, so the names must exist.
304
- idApi(false, 0, 0) + castDistanceStub() +
305
+ idApi(false, 0, 0) + castDistanceStub() + sceneLightApi(false, 0, 0) +
305
306
  "\n// ── user effect ──\n" +
306
307
  src.wgsl +
307
308
  /* wgsl */ `
@@ -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
+ }