reze-engine 0.39.0 → 0.41.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/README.md +8 -5
- package/dist/engine.d.ts +96 -39
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +246 -85
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +10 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/model.d.ts +0 -5
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +0 -36
- 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 +21 -0
- package/dist/shaders/passes/composite.d.ts +27 -12
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +191 -43
- package/package.json +1 -1
- package/src/engine.ts +286 -93
- package/src/graph/registry.ts +11 -0
- package/src/index.ts +4 -3
- package/src/model.ts +0 -36
- package/src/shaders/materials/nodes.ts +21 -0
- package/src/shaders/passes/composite.ts +218 -59
- package/dist/particles.d.ts +0 -43
- package/dist/particles.d.ts.map +0 -1
- package/dist/particles.js +0 -183
- package/dist/shaders/passes/particles.d.ts +0 -2
- package/dist/shaders/passes/particles.d.ts.map +0 -1
- package/dist/shaders/passes/particles.js +0 -99
- package/src/particles.ts +0 -232
- package/src/shaders/passes/particles.ts +0 -100
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
// Bloom tint/intensity applied at combine (EEVEE treats them as combine-stage params, not prefilter).
|
|
3
3
|
//
|
|
4
4
|
// The shader is a TEMPLATE: buildCompositeShader() emits either the base pass or
|
|
5
|
-
// a variant with
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// a variant with user WGSL injected at one or both effect MOUNTS (setEffect).
|
|
6
|
+
// The two mounts are the same idea on either side of the scene:
|
|
7
|
+
//
|
|
8
|
+
// background(...) under the scene — a sibling of the 360 equirect (mode 2),
|
|
9
|
+
// reusing the same per-pixel view-ray reconstruction.
|
|
10
|
+
// foreground(...) over the finished frame, handed the scene's depth in metres
|
|
11
|
+
// so it can be occluded by whatever it passes behind.
|
|
12
|
+
//
|
|
13
|
+
// Both composite in display space, so neither affects lighting, bloom, or
|
|
14
|
+
// tonemapping, and both are captured by offline export like any background.
|
|
9
15
|
const COMPOSITE_HEAD = /* wgsl */ `
|
|
10
16
|
// Pipeline-override constant: the engine creates two composite pipelines, one
|
|
11
17
|
// with APPLY_GAMMA=false (gamma=1 fast path) and one with APPLY_GAMMA=true.
|
|
@@ -17,7 +23,7 @@ override APPLY_GAMMA: bool = true;
|
|
|
17
23
|
@group(0) @binding(0) var hdrTex: texture_2d<f32>;
|
|
18
24
|
@group(0) @binding(1) var bloomTex: texture_2d<f32>; // bloomUpTexture mip 0 (full pyramid top)
|
|
19
25
|
@group(0) @binding(2) var bloomSamp: sampler;
|
|
20
|
-
@group(0) @binding(3) var<uniform> viewU: array<vec4<f32>,
|
|
26
|
+
@group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 11>;
|
|
21
27
|
// Aux mask/alpha texture. .r = bloom mask (unused here; bloom blit uses it).
|
|
22
28
|
// .g = accumulated canvas alpha (what hdr.a carried before the HDR format
|
|
23
29
|
// became rg11b10ufloat). We unpremultiply HDR by this alpha for tonemap, then
|
|
@@ -36,18 +42,63 @@ override APPLY_GAMMA: bool = true;
|
|
|
36
42
|
// viewU[2] = (background.rgb, mode) — display-space sRGB, composited UNDER the
|
|
37
43
|
// scene post-tonemap. BASE-layer mode: 0 transparent (DOM shows),
|
|
38
44
|
// 1 solid color, 2 = 360 equirect skybox sampled by view ray. A user
|
|
39
|
-
// WGSL effect is a separate LAYER over the base
|
|
45
|
+
// WGSL effect is a separate LAYER over the base — no mode of its own,
|
|
46
|
+
// and no on/off uniform either: the pipeline is REBUILT per effect, so
|
|
47
|
+
// the compiled variant IS the flag.
|
|
40
48
|
// viewU[3] = (camera right, tanHalfFov·aspect); viewU[4] = (camera up, tanHalfFov);
|
|
41
49
|
// viewU[5] = (camera forward, _) — refreshed per frame while skybox/effect active.
|
|
42
|
-
// viewU[6] = (time seconds,
|
|
50
|
+
// viewU[6] = (time seconds, _, canvas width, canvas height).
|
|
43
51
|
// viewU[7] = (grade offset.rgb, contrast); viewU[8] = (grade power.rgb, saturation);
|
|
44
52
|
// viewU[9] = (grade slope.rgb, grade on/off) — see grade() below.
|
|
53
|
+
// viewU[10] = (camera world position, _) — refreshed with the basis above.
|
|
45
54
|
// invGamma = 1/gamma precomputed on CPU — avoids a per-pixel divide.
|
|
46
55
|
@group(0) @binding(6) var bgEquirect: texture_2d<f32>;
|
|
56
|
+
// The scene pass's own MSAA depth buffer, bound depth-only. NOT an extra
|
|
57
|
+
// render target: with neither depth of field nor a foreground effect active the
|
|
58
|
+
// scene pass discards depth (TBDR tile memory never spills) and this binding is
|
|
59
|
+
// never read. Either feature makes the pass store it instead, and both read
|
|
60
|
+
// sample 0 — the DoF gather, and linearDepth() for the depth handed to
|
|
61
|
+
// foreground().
|
|
62
|
+
@group(0) @binding(8) var depthTex: texture_depth_multisampled_2d;
|
|
63
|
+
// dofU[0] = (enabled, focusDistance, focusRange, aperture)
|
|
64
|
+
// dofU[1] = (maxBlurRadiusPx, bladeCount, sampleCount, anamorphicRatio)
|
|
65
|
+
// dofU[2] = (projA, projB, _, _) — z-buffer → camera-space depth inversion,
|
|
66
|
+
// viewZ = projB / (z - projA), rebuilt per frame because near/far
|
|
67
|
+
// track the camera radius. Cleared depth (1.0) inverts to the far
|
|
68
|
+
// plane, so empty sky reads as maximally defocused background.
|
|
69
|
+
@group(0) @binding(9) var<uniform> dofU: array<vec4<f32>, 3>;
|
|
47
70
|
|
|
48
71
|
// Must match FILMIC_LUT_WIDTH in engine.ts (bakeFilmicLut).
|
|
49
72
|
const FILMIC_LUT_W: f32 = 256.0;
|
|
50
73
|
|
|
74
|
+
fn linearDepth(coord: vec2<i32>) -> f32 {
|
|
75
|
+
let z = textureLoad(depthTex, coord, 0);
|
|
76
|
+
// projA > 1 for every valid z in [0,1], so the divisor never crosses zero.
|
|
77
|
+
return clamp(dofU[2].y / (z - dofU[2].x), 0.05, 100000.0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Signed circle of confusion in device pixels — negative in front of the
|
|
81
|
+
* focus band, positive behind it, zero inside it. */
|
|
82
|
+
fn circleOfConfusion(depth: f32) -> f32 {
|
|
83
|
+
let focus = max(dofU[0].y, 0.05);
|
|
84
|
+
let halfRange = max(dofU[0].z * 0.5, 0.01);
|
|
85
|
+
let delta = depth - focus;
|
|
86
|
+
let outside = max(abs(delta) - halfRange, 0.0);
|
|
87
|
+
let radius = min(outside / max(depth, 0.05) * dofU[0].w * dofU[1].x, dofU[1].x);
|
|
88
|
+
return select(-radius, radius, delta >= 0.0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Premultiplied scene color (HDR + bloom) at an arbitrary pixel, for the
|
|
92
|
+
* bokeh gather. Explicit-LOD sampling only — legal in non-uniform flow. */
|
|
93
|
+
fn sceneSample(coord: vec2<i32>, fullSzI: vec2<i32>, fullSz: vec2f) -> vec4f {
|
|
94
|
+
let p = clamp(coord, vec2<i32>(0), fullSzI - vec2<i32>(1));
|
|
95
|
+
let sAlpha = textureLoad(maskTex, p, 0).g;
|
|
96
|
+
let sHdr = textureLoad(hdrTex, p, 0).rgb / max(sAlpha, 1e-6);
|
|
97
|
+
let sUv = (vec2f(p) + vec2f(0.5)) / fullSz;
|
|
98
|
+
let sBloom = textureSampleLevel(bloomTex, bloomSamp, sUv, 0.0).rgb * viewU[1].xyz * viewU[1].w;
|
|
99
|
+
return vec4f((sHdr + sBloom) * sAlpha, sAlpha);
|
|
100
|
+
}
|
|
101
|
+
|
|
51
102
|
fn filmic(x: f32) -> f32 {
|
|
52
103
|
// Reference checkpoints (Blender 3.6 Filmic MHC, sobotka/filmic-blender
|
|
53
104
|
// look_medium-high-contrast.spi1d): linear 0.18 → ~0.395, linear 1.0 → ~0.83.
|
|
@@ -59,9 +110,26 @@ fn filmic(x: f32) -> f32 {
|
|
|
59
110
|
return textureSampleLevel(filmicLut, bloomSamp, vec2f(u, 0.5), 0.0).r;
|
|
60
111
|
}
|
|
61
112
|
|
|
62
|
-
/** Canvas size in pixels — for user
|
|
113
|
+
/** Canvas size in pixels — for user effects (aspect correction). */
|
|
63
114
|
fn bgResolution() -> vec2f { return viewU[6].zw; }
|
|
64
115
|
|
|
116
|
+
/** The camera's world position. */
|
|
117
|
+
fn bgCameraPos() -> vec3f { return viewU[10].xyz; }
|
|
118
|
+
|
|
119
|
+
/** Where in the WORLD the scene drew this pixel — the depth handed to
|
|
120
|
+
* foreground() turned into a place. Without it an effect can only think in
|
|
121
|
+
* distances from the lens, which is no use to anything that belongs somewhere:
|
|
122
|
+
* fog lying on the ground has to know where the ground is.
|
|
123
|
+
*
|
|
124
|
+
* depth measures along the VIEW AXIS, not along the ray, so it is divided by
|
|
125
|
+
* the ray's projection onto camera-forward before being walked out. At the far
|
|
126
|
+
* plane (nothing drawn) this lands a very long way off, which is what a sky
|
|
127
|
+
* should do to anything reading it. */
|
|
128
|
+
fn bgWorldPos(ray: vec3f, depth: f32) -> vec3f {
|
|
129
|
+
let axis = max(dot(normalize(ray), viewU[5].xyz), 1e-4);
|
|
130
|
+
return bgCameraPos() + normalize(ray) * (depth / axis);
|
|
131
|
+
}
|
|
132
|
+
|
|
65
133
|
/** Color grading, applied to the tonemapped SCENE (not the background — see the
|
|
66
134
|
* call site). The core is ASC CDL, the film-industry interchange standard:
|
|
67
135
|
*
|
|
@@ -101,7 +169,59 @@ const COMPOSITE_BODY = /* wgsl */ `
|
|
|
101
169
|
let intensity = viewU[1].w;
|
|
102
170
|
let bloom = textureSampleLevel(bloomTex, bloomSamp, bloomUv, 0.0).rgb * tint * intensity;
|
|
103
171
|
let combined = straight + bloom;
|
|
104
|
-
|
|
172
|
+
|
|
173
|
+
// ── Depth of field ──
|
|
174
|
+
// Single-pass golden-angle gather over a polygonal (bladed) disk, in
|
|
175
|
+
// premultiplied HDR before tonemap. Near-field taps that see focused
|
|
176
|
+
// background are heavily down-weighted so a sharp subject doesn't bleed into
|
|
177
|
+
// a blurred foreground; the reverse leak (background bokeh over the subject
|
|
178
|
+
// edge) is damped less — that soft halo is what real lenses do. Scene layer
|
|
179
|
+
// only: the composited background (solid / 360 / effect) stays sharp, which
|
|
180
|
+
// is invisible while it sits at infinity behind a far-blurred stage.
|
|
181
|
+
var scenePm = vec4f(combined * alpha, alpha);
|
|
182
|
+
if (dofU[0].x > 0.5) {
|
|
183
|
+
let centerDepth = linearDepth(coord);
|
|
184
|
+
let centerCoc = circleOfConfusion(centerDepth);
|
|
185
|
+
let radius = abs(centerCoc);
|
|
186
|
+
if (radius > 0.35) {
|
|
187
|
+
let fullSzI = vec2<i32>(fullSz);
|
|
188
|
+
let sampleCount = clamp(dofU[1].z, 6.0, 24.0);
|
|
189
|
+
let blades = clamp(dofU[1].y, 3.0, 12.0);
|
|
190
|
+
let sector = 6.28318530718 / blades;
|
|
191
|
+
var accum = scenePm;
|
|
192
|
+
var weightSum = 1.0;
|
|
193
|
+
for (var i = 0u; i < 24u; i++) {
|
|
194
|
+
if (f32(i) >= sampleCount) { break; }
|
|
195
|
+
let fi = f32(i) + 0.5;
|
|
196
|
+
let ring = sqrt(fi / sampleCount);
|
|
197
|
+
let angle = fi * 2.39996323;
|
|
198
|
+
let localAngle = (fract((angle + 3.14159265359) / sector) - 0.5) * sector;
|
|
199
|
+
let polygonRadius = cos(3.14159265359 / blades) / max(cos(localAngle), 0.01);
|
|
200
|
+
var disk = vec2f(cos(angle), sin(angle)) * ring * polygonRadius;
|
|
201
|
+
disk.x *= max(dofU[1].w, 0.25);
|
|
202
|
+
let sp = coord + vec2<i32>(round(disk * radius));
|
|
203
|
+
let cp = clamp(sp, vec2<i32>(0), fullSzI - vec2<i32>(1));
|
|
204
|
+
let sampleDepth = linearDepth(cp);
|
|
205
|
+
let sampleCoc = circleOfConfusion(sampleDepth);
|
|
206
|
+
var w = 1.0;
|
|
207
|
+
if (centerCoc < 0.0 && sampleDepth > centerDepth + dofU[0].z) {
|
|
208
|
+
w *= 0.08;
|
|
209
|
+
} else if (centerCoc > 0.0 && sampleDepth > centerDepth + dofU[0].z * 2.0) {
|
|
210
|
+
w *= 0.35;
|
|
211
|
+
}
|
|
212
|
+
// A tap only contributes where its own blur circle reaches this pixel.
|
|
213
|
+
let sampleRadius = abs(sampleCoc);
|
|
214
|
+
w *= mix(0.2, 1.0, smoothstep(ring * radius - 1.0, ring * radius + 1.0, sampleRadius));
|
|
215
|
+
accum += sceneSample(cp, fullSzI, fullSz) * w;
|
|
216
|
+
weightSum += w;
|
|
217
|
+
}
|
|
218
|
+
scenePm = mix(scenePm, accum / max(weightSum, 1e-5), smoothstep(0.35, 1.75, radius));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
let sceneAlpha = scenePm.a;
|
|
222
|
+
let sceneStraight = scenePm.rgb / max(sceneAlpha, 1e-6);
|
|
223
|
+
|
|
224
|
+
let exposed = sceneStraight * exp2(viewU[0].x);
|
|
105
225
|
let tm = vec3f(filmic(exposed.r), filmic(exposed.g), filmic(exposed.b));
|
|
106
226
|
var disp = max(tm, vec3f(0.0));
|
|
107
227
|
// Grade the SCENE only, before the display gamma. Deliberately not applied to
|
|
@@ -120,64 +240,92 @@ const COMPOSITE_BODY = /* wgsl */ `
|
|
|
120
240
|
let bg = viewU[2];
|
|
121
241
|
var bgA = select(0.0, 1.0, bg.w > 0.5);
|
|
122
242
|
var bgPm = bg.rgb * bgA; // premultiplied accumulator
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
243
|
+
// This pixel's world-space view ray, rebuilt from the camera basis — what the
|
|
244
|
+
// equirect samples by, and what both effect mounts navigate by. The dome sits
|
|
245
|
+
// at infinity (no parallax): PhotoDome-style, display-only. Hoisted out of the
|
|
246
|
+
// branch below because the foreground mount is past the end of it; it is pure
|
|
247
|
+
// arithmetic on uniforms, which every backend sinks into whatever reads it.
|
|
248
|
+
let ndc = vec2f(fragCoord.x / fullSz.x * 2.0 - 1.0, 1.0 - fragCoord.y / fullSz.y * 2.0);
|
|
249
|
+
let dir = normalize(viewU[5].xyz + ndc.x * viewU[3].w * viewU[3].xyz + ndc.y * viewU[4].w * viewU[4].xyz);
|
|
250
|
+
if (BACKGROUND_COND) {
|
|
130
251
|
if (bg.w > 1.5) {
|
|
131
252
|
// LH world (+Z forward): longitude = atan2(x, z), Babylon-PhotoDome convention.
|
|
132
253
|
let su = 0.5 + atan2(dir.x, dir.z) * 0.15915494309; // 1/(2π)
|
|
133
254
|
let sv = 0.5 - asin(clamp(dir.y, -1.0, 1.0)) * 0.31830988618; // 1/π
|
|
134
255
|
bgPm = textureSampleLevel(bgEquirect, bloomSamp, vec2f(su, sv), 0.0).rgb;
|
|
135
256
|
}
|
|
136
|
-
|
|
257
|
+
BACKGROUND_CALL
|
|
137
258
|
}
|
|
138
|
-
|
|
259
|
+
// The frame, premultiplied: scene over background. A var, not the return
|
|
260
|
+
// expression, because the foreground mount composites onto it.
|
|
261
|
+
var outRgb = disp * sceneAlpha + bgPm * (1.0 - sceneAlpha);
|
|
262
|
+
var outA = sceneAlpha + bgA * (1.0 - sceneAlpha);
|
|
263
|
+
FOREGROUND_CALL
|
|
264
|
+
return vec4f(outRgb, outA);
|
|
139
265
|
}
|
|
140
266
|
`;
|
|
141
|
-
// Base variant: no effect installed, the flag is never set — the ray block only
|
|
142
|
-
// runs for the equirect, and there is nothing to add. (`dir` may go unused when
|
|
143
|
-
// this compiles with mode<2 shaders; WGSL is fine with an unused let.)
|
|
144
|
-
const NO_EFFECT_CALL = `_ = dir;`;
|
|
145
267
|
// uv flipped to bottom-left origin (shadertoy convention); clamped so a stray
|
|
146
268
|
// effect can't push negatives/NaN into the premultiplied composite. Standard
|
|
147
|
-
// OVER onto the base layer.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
269
|
+
// OVER onto the base layer. No `if` around it: the pipeline is rebuilt per
|
|
270
|
+
// effect, so this text only exists in variants whose WGSL defines background().
|
|
271
|
+
const BACKGROUND_CALL = /* wgsl */ `
|
|
272
|
+
let bgUv = vec2f(fragCoord.x / fullSz.x, 1.0 - fragCoord.y / fullSz.y);
|
|
273
|
+
let bgFx = clamp(background(dir, bgUv, viewU[6].x), vec4f(0.0), vec4f(1.0));
|
|
274
|
+
bgPm = bgFx.rgb * bgFx.a + bgPm * (1.0 - bgFx.a);
|
|
275
|
+
bgA = bgFx.a + bgA * (1.0 - bgFx.a);
|
|
276
|
+
`;
|
|
277
|
+
// Same OVER, one layer later — onto the finished frame rather than onto the
|
|
278
|
+
// base. Ungated by design: a foreground runs at every pixel, including the ones
|
|
279
|
+
// the model covers, because covering them is the point.
|
|
280
|
+
const FOREGROUND_CALL = /* wgsl */ `
|
|
281
|
+
let fgUv = vec2f(fragCoord.x / fullSz.x, 1.0 - fragCoord.y / fullSz.y);
|
|
282
|
+
// The scene's own depth, so the effect can tell what is in front of it: a
|
|
283
|
+
// petal compares its distance against this and lets the model take the pixel,
|
|
284
|
+
// and fog's alpha is nothing but a function of it. Pixels the scene never drew
|
|
285
|
+
// read the far plane, so distance fog closes over the backdrop too.
|
|
286
|
+
let fgFx = clamp(foreground(dir, fgUv, viewU[6].x, linearDepth(coord)), vec4f(0.0), vec4f(1.0));
|
|
287
|
+
outRgb = fgFx.rgb * fgFx.a + outRgb * (1.0 - fgFx.a);
|
|
288
|
+
outA = fgFx.a + outA * (1.0 - fgFx.a);
|
|
155
289
|
`;
|
|
156
290
|
// Derivative builtins are illegal in non-uniform control flow (WGSL uniformity
|
|
157
291
|
// analysis rejects the pipeline), so the coverage gate below can only wrap
|
|
158
292
|
// effect code that doesn't use them. Checked textually at build time.
|
|
159
293
|
const USES_DERIVATIVES = /\b(?:fwidth|dpdx|dpdy)(?:Fine|Coarse)?\s*\(/;
|
|
160
|
-
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
294
|
+
/** The condition on the background block (equirect sample + background effect).
|
|
295
|
+
*
|
|
296
|
+
* Two jobs. It skips the block behind pixels the model fully covers — the
|
|
297
|
+
* composite multiplies the result by (1 - alpha) = 0 there anyway, and on a
|
|
298
|
+
* full-screen effect that's a third or more of the frame (the cost Safari feels
|
|
299
|
+
* most). And with no background effect compiled in, it also skips the block
|
|
300
|
+
* entirely unless the equirect needs it.
|
|
301
|
+
*
|
|
302
|
+
* The equirect uses explicit-LOD sampling, which is always legal in non-uniform
|
|
303
|
+
* flow; only derivative-using effects must keep uniform control flow and forgo
|
|
304
|
+
* the coverage half. The test is textual over the whole file, so a foreground
|
|
305
|
+
* that uses fwidth costs the background its gate — conservative, and only ever
|
|
306
|
+
* in the direction of correctness. (The foreground mount itself sits in uniform
|
|
307
|
+
* flow, so derivatives are always legal there.) */
|
|
308
|
+
function backgroundCondition(effect) {
|
|
309
|
+
// sceneAlpha, not alpha: the bokeh gather spreads coverage, so a pixel the
|
|
310
|
+
// sharp scene fully covered can end up needing background behind its blur.
|
|
311
|
+
const coverage = "sceneAlpha < 0.999";
|
|
312
|
+
if (!effect?.hasBackground)
|
|
313
|
+
return `bg.w > 1.5 && ${coverage}`;
|
|
314
|
+
return USES_DERIVATIVES.test(effect.wgsl) ? "true" : coverage;
|
|
169
315
|
}
|
|
170
316
|
export function buildCompositeShader(effect) {
|
|
317
|
+
const body = COMPOSITE_BODY.replace("BACKGROUND_COND", backgroundCondition(effect))
|
|
318
|
+
.replace("BACKGROUND_CALL", effect?.hasBackground ? BACKGROUND_CALL.trim() : "")
|
|
319
|
+
.replace("FOREGROUND_CALL", effect?.hasForeground ? FOREGROUND_CALL.trim() : "");
|
|
171
320
|
if (!effect)
|
|
172
|
-
return
|
|
173
|
-
COMPOSITE_BODY.replace("BG_EFFECT_CALL", NO_EFFECT_CALL).replace("COVERAGE_GATE", coverageGate(null)));
|
|
321
|
+
return COMPOSITE_HEAD + body;
|
|
174
322
|
return (COMPOSITE_HEAD +
|
|
175
|
-
"\n// ── user
|
|
323
|
+
"\n// ── user effect (setEffect) ──\n" +
|
|
176
324
|
effect.paramsDecl +
|
|
177
325
|
"\n" +
|
|
178
326
|
effect.wgsl +
|
|
179
327
|
"\n" +
|
|
180
|
-
|
|
328
|
+
body);
|
|
181
329
|
}
|
|
182
330
|
/** Kept for compatibility with existing imports (the base, no-effect shader). */
|
|
183
331
|
export const COMPOSITE_SHADER_WGSL = buildCompositeShader(null);
|