Skip to main content

pfx_post/
wgsl.rs

1macro_rules! shader {
2    (normal, $body:literal) => {
3        shader!(
4            r#"
5@group(0) @binding(4) var normal_tex: texture_2d<f32>;
6
7fn normal_of(x: i32, y: i32) -> vec3<f32> {
8    let dim = vec2<i32>(min(textureDimensions(normal_tex), post.extent.xy));
9    if (dim.x <= 0 || dim.y <= 0) { return vec3<f32>(0.0, 0.0, 1.0); }
10    let c = clamp(vec2<i32>(x, y), vec2<i32>(0), dim - vec2<i32>(1));
11    return textureLoad(normal_tex, c, 0).xyz;
12}
13"#,
14            $body
15        )
16    };
17    (depth, $body:literal) => {
18        shader!(
19            r#"
20@group(0) @binding(3) var<storage, read> depth_buf: array<f32>;
21
22fn depth_of(x: i32, y: i32) -> f32 {
23    let dim = vec2<i32>(src_dim());
24    if (dim.x <= 0 || dim.y <= 0) { return 0.0; }
25    let c = clamp(vec2<i32>(x, y), vec2<i32>(0), dim - vec2<i32>(1));
26    return depth_buf[u32(c.y) * u32(dim.x) + u32(c.x)];
27}
28"#,
29            $body
30        )
31    };
32    (geometry, $body:literal) => {
33        shader!(
34            r#"
35@group(0) @binding(3) var<storage, read> depth_buf: array<f32>;
36@group(0) @binding(4) var normal_tex: texture_2d<f32>;
37
38fn depth_of(x: i32, y: i32) -> f32 {
39    let dim = vec2<i32>(src_dim());
40    if (dim.x <= 0 || dim.y <= 0) { return 0.0; }
41    let c = clamp(vec2<i32>(x, y), vec2<i32>(0), dim - vec2<i32>(1));
42    return depth_buf[u32(c.y) * u32(dim.x) + u32(c.x)];
43}
44
45fn normal_of(x: i32, y: i32) -> vec3<f32> {
46    let dim = vec2<i32>(min(textureDimensions(normal_tex), post.extent.xy));
47    if (dim.x <= 0 || dim.y <= 0) { return vec3<f32>(0.0, 0.0, 1.0); }
48    let c = clamp(vec2<i32>(x, y), vec2<i32>(0), dim - vec2<i32>(1));
49    return textureLoad(normal_tex, c, 0).xyz;
50}
51"#,
52            $body
53        )
54    };
55    (bloomtex, $body:expr) => {
56        shader!(bloom_tex!(), $body)
57    };
58    (grade, $name:literal) => {
59        shader!(
60            grade_ops!(),
61            concat!(
62                "    store(p, grade_",
63                $name,
64                "(src, p, post.p0, post.p1, post.p2, post.p3));\n"
65            )
66        )
67    };
68    ($extra:expr, $body:expr) => {
69        concat!(
70            post_head!(),
71            $extra,
72            post_common!(),
73            post_main!(),
74            $body,
75            "\n}\n"
76        )
77    };
78}
79
80macro_rules! post_head {
81    () => {
82        r#"
83struct Post {
84    size: vec2<u32>,
85    frame: u32,
86    seed: u32,
87    p0: vec4<f32>,
88    p1: vec4<f32>,
89    p2: vec4<f32>,
90    p3: vec4<f32>,
91    region: vec4<u32>,
92    extent: vec4<u32>,
93}
94
95@group(0) @binding(0) var<uniform> post: Post;
96@group(0) @binding(1) var src_tex: texture_2d<f32>;
97@group(0) @binding(2) var dst_tex: texture_storage_2d<rgba16float, write>;
98"#
99    };
100}
101
102macro_rules! bloom_tex {
103    () => {
104        r#"
105@group(0) @binding(3) var bloom_tex: texture_2d<f32>;
106
107fn bloom_dim() -> vec2<u32> {
108    return min(textureDimensions(bloom_tex), post.extent.zw);
109}
110
111fn load_bloom(p: vec2<i32>) -> vec4<f32> {
112    let dim = vec2<i32>(bloom_dim());
113    if (dim.x <= 0 || dim.y <= 0) { return vec4<f32>(0.0); }
114    let c = clamp(p, vec2<i32>(0), dim - vec2<i32>(1));
115    return textureLoad(bloom_tex, c, 0);
116}
117
118fn sample_bloom(x: f32, y: f32) -> vec4<f32> {
119    let dim = vec2<f32>(bloom_dim());
120    if (dim.x < 1.0 || dim.y < 1.0) { return vec4<f32>(0.0); }
121    let xc = clamp(x, 0.0, dim.x - 1.0);
122    let yc = clamp(y, 0.0, dim.y - 1.0);
123    let x0 = i32(floor(xc));
124    let y0 = i32(floor(yc));
125    let tx = xc - floor(xc);
126    let ty = yc - floor(yc);
127    let a = load_bloom(vec2<i32>(x0, y0));
128    let b = load_bloom(vec2<i32>(x0 + 1, y0));
129    let c = load_bloom(vec2<i32>(x0, y0 + 1));
130    let d = load_bloom(vec2<i32>(x0 + 1, y0 + 1));
131    return mix(mix(a, b, tx), mix(c, d, tx), ty);
132}
133"#
134    };
135}
136
137macro_rules! post_common {
138    () => {
139        r#"
140fn src_dim() -> vec2<u32> {
141    return min(textureDimensions(src_tex), post.extent.xy);
142}
143
144fn load_src(p: vec2<i32>) -> vec4<f32> {
145    let dim = vec2<i32>(src_dim());
146    if (dim.x <= 0 || dim.y <= 0) { return vec4<f32>(0.0); }
147    let c = clamp(p, vec2<i32>(0), dim - vec2<i32>(1));
148    return textureLoad(src_tex, c, 0);
149}
150
151fn sample_src(x: f32, y: f32) -> vec4<f32> {
152    let dim = vec2<f32>(src_dim());
153    if (dim.x < 1.0 || dim.y < 1.0) { return vec4<f32>(0.0); }
154    let xc = clamp(x, 0.0, dim.x - 1.0);
155    let yc = clamp(y, 0.0, dim.y - 1.0);
156    let x0 = i32(floor(xc));
157    let y0 = i32(floor(yc));
158    let tx = xc - floor(xc);
159    let ty = yc - floor(yc);
160    let a = load_src(vec2<i32>(x0, y0));
161    let b = load_src(vec2<i32>(x0 + 1, y0));
162    let c = load_src(vec2<i32>(x0, y0 + 1));
163    let d = load_src(vec2<i32>(x0 + 1, y0 + 1));
164    return mix(mix(a, b, tx), mix(c, d, tx), ty);
165}
166
167fn store(p: vec2<u32>, c: vec4<f32>) {
168    textureStore(dst_tex, p, c);
169}
170
171fn luma(c: vec3<f32>) -> f32 {
172    return dot(c, vec3<f32>(0.2126, 0.7152, 0.0722));
173}
174
175fn luma_at(x: i32, y: i32) -> f32 {
176    return luma(load_src(vec2<i32>(x, y)).rgb);
177}
178
179fn sobel(x: i32, y: i32) -> f32 {
180    let gx = -luma_at(x - 1, y - 1) + luma_at(x + 1, y - 1) - 2.0 * luma_at(x - 1, y) + 2.0 * luma_at(x + 1, y) - luma_at(x - 1, y + 1) + luma_at(x + 1, y + 1);
181    let gy = -luma_at(x - 1, y - 1) - 2.0 * luma_at(x, y - 1) - luma_at(x + 1, y - 1) + luma_at(x - 1, y + 1) + 2.0 * luma_at(x, y + 1) + luma_at(x + 1, y + 1);
182    return sqrt(gx * gx + gy * gy);
183}
184
185fn normalize3(v: vec3<f32>) -> vec3<f32> {
186    let len = length(v);
187    if (len < 1e-8) { return vec3<f32>(0.0, 0.0, 1.0); }
188    return v / len;
189}
190
191fn safe_pow(a: f32, b: f32) -> f32 {
192    if (a <= 0.0) { return 0.0; }
193    return pow(a, b);
194}
195
196fn edge_step(edge0: f32, edge1: f32, x: f32) -> f32 {
197    let span = edge1 - edge0;
198    if (abs(span) < 1e-8) {
199        if (x < edge0) { return 0.0; }
200        return 1.0;
201    }
202    let t = clamp((x - edge0) / span, 0.0, 1.0);
203    return t * t * (3.0 - 2.0 * t);
204}
205
206fn grain_hash(x: u32, y: u32, seed: u32) -> f32 {
207    var h = x * 0x8da6b343u ^ y * 0xd8163841u ^ seed * 0xcb1ab31fu;
208    h = h ^ (h >> 15u);
209    h = h * 0x2c1b3c6du;
210    h = h ^ (h >> 12u);
211    return f32(h >> 8u) / 16777216.0;
212}
213
214fn mixed_seed() -> u32 {
215    return post.seed ^ (post.frame * 0x9e3779b9u);
216}
217
218fn hash21(x: f32, y: f32) -> f32 {
219    var qx = fract(x * 123.34);
220    var qy = fract(y * 456.21);
221    let d = qx * (qx + 45.32) + qy * (qy + 45.32);
222    qx = qx + d;
223    qy = qy + d;
224    return fract(qx * qy);
225}
226
227fn grain_hash_i(x: i32, y: i32, seed: u32) -> f32 {
228    return grain_hash(u32(x), u32(y), seed);
229}
230
231fn value_noise(x: f32, y: f32, seed: u32) -> f32 {
232    let x0 = floor(x);
233    let y0 = floor(y);
234    let tx = x - x0;
235    let ty = y - y0;
236    let sx = tx * tx * (3.0 - 2.0 * tx);
237    let sy = ty * ty * (3.0 - 2.0 * ty);
238    let xi = i32(x0);
239    let yi = i32(y0);
240    let a = grain_hash_i(xi, yi, seed);
241    let b = grain_hash_i(xi + 1, yi, seed);
242    let c = grain_hash_i(xi, yi + 1, seed);
243    let d = grain_hash_i(xi + 1, yi + 1, seed);
244    let ab = a + (b - a) * sx;
245    let cd = c + (d - c) * sx;
246    return ab + (cd - ab) * sy;
247}
248
249fn bayer(x: u32, y: u32) -> f32 {
250    var v = 0u;
251    var xx = x & 7u;
252    var yy = y & 7u;
253    var half = 4u;
254    var mul = 1u;
255    for (var i = 0u; i < 3u; i = i + 1u) {
256        let cx = xx >= half;
257        let cy = yy >= half;
258        if (xx >= half) { xx = xx - half; }
259        if (yy >= half) { yy = yy - half; }
260        var add = 0u;
261        if (cx && !cy) { add = 2u; }
262        else if (!cx && cy) { add = 3u; }
263        else if (cx && cy) { add = 1u; }
264        v = v + add * mul;
265        mul = mul * 4u;
266        half = half / 2u;
267    }
268    return (f32(v) + 0.5) / 64.0;
269}
270
271fn saturate3(c: vec3<f32>, amount: f32) -> vec3<f32> {
272    let l = luma(c);
273    return max(vec3<f32>(l) + (c - vec3<f32>(l)) * amount, vec3<f32>(0.0));
274}
275
276fn quantize(v: f32, levels: f32) -> f32 {
277    let t = clamp(v, 0.0, 1.0);
278    let q = min(floor(t * levels), levels - 1.0);
279    return q / (levels - 1.0);
280}
281
282fn keep_rgb(rgb: vec3<f32>, threshold: f32, knee: f32, knee_width: f32, soft: f32) -> vec3<f32> {
283    let l = max(rgb.r, max(rgb.g, rgb.b));
284    var hard = 0.0;
285    if (l > threshold) { hard = max(l - threshold, 0.0) / max(l, 1e-4); }
286    var knee_t = 0.0;
287    if (knee_width > 1e-6) { knee_t = clamp((l - knee) / knee_width, 0.0, 1.0); }
288    return rgb * (hard + knee_t * knee_t * soft);
289}
290
291fn aces_channel(x: f32) -> f32 {
292    let a = x * (x * 2.51 + 0.03);
293    let b = x * (x * 2.43 + 0.59) + 0.14;
294    if (abs(b) < 1e-8) { return 0.0; }
295    return clamp(a / b, 0.0, 1.0);
296}
297
298fn agx_contrast(x: f32) -> f32 {
299    let x2 = x * x;
300    let x4 = x2 * x2;
301    return 15.5 * x4 * x2 - 40.14 * x4 * x + 31.96 * x4 - 6.868 * x2 * x + 0.4298 * x2 + 0.1191 * x - 0.00232;
302}
303
304fn agx(c: vec3<f32>) -> vec3<f32> {
305    let r0 = vec3<f32>(0.842479062253094, 0.0784335999999992, 0.0792237451477643);
306    let r1 = vec3<f32>(0.0423282422610123, 0.878468636469772, 0.0791661274605434);
307    let r2 = vec3<f32>(0.0423756549057051, 0.0784336, 0.879142973793104);
308    let v = vec3<f32>(dot(r0, c), dot(r1, c), dot(r2, c));
309    let min_ev = -12.47393;
310    let max_ev = 4.026069;
311    let span = max_ev - min_ev;
312    let t0 = clamp((log2(max(v.r, 1e-10)) - min_ev) / span, 0.0, 1.0);
313    let t1 = clamp((log2(max(v.g, 1e-10)) - min_ev) / span, 0.0, 1.0);
314    let t2 = clamp((log2(max(v.b, 1e-10)) - min_ev) / span, 0.0, 1.0);
315    return clamp(vec3<f32>(agx_contrast(t0), agx_contrast(t1), agx_contrast(t2)), vec3<f32>(0.0), vec3<f32>(1.0));
316}
317
318fn neutral_map(c: vec3<f32>, start: f32, toe: f32, desat: f32, clamp_hi: bool) -> vec3<f32> {
319    let low = min(c.r, min(c.g, c.b));
320    var offset = toe;
321    if (low < 0.08) { offset = low - 6.25 * low * low; }
322    var v = c - vec3<f32>(offset);
323    let peak = max(v.r, max(v.g, v.b));
324    if (peak >= start && peak > 1e-8) {
325        let d = 1.0 - start;
326        let denom = peak + d - start;
327        var mapped = start;
328        if (abs(denom) >= 1e-8) { mapped = 1.0 - d * d / denom; }
329        v = v * (mapped / peak);
330        let g = 1.0 - 1.0 / (desat * (peak - mapped) + 1.0);
331        v = mix(v, vec3<f32>(mapped), g);
332    }
333    if (clamp_hi) { v = clamp(v, vec3<f32>(0.0), vec3<f32>(1.0)); }
334    return v;
335}
336
337fn band_shade(light: f32, bands: f32, softness: f32, threshold: f32, shadow: f32) -> f32 {
338    let bands_f = max(bands, 1.0);
339    let light_c = clamp(light, 0.0, 1.0);
340    let x = min(light_c * bands_f, bands_f - 1e-4);
341    var w = 0.0;
342    if (softness > 0.0) { w = edge_step(0.5 - softness * 0.5, 0.5 + softness * 0.5, fract(x)); }
343    let idx = floor(x) + w;
344    var q = light_c;
345    if (bands_f > 1.0) { q = clamp(idx / (bands_f - 1.0), 0.0, 1.0); }
346    var gate = 1.0;
347    if (softness <= 1e-5) {
348        if (light_c < threshold) { gate = 0.0; }
349    } else {
350        gate = edge_step(threshold - softness, threshold + softness, light_c);
351    }
352    return q * (shadow + (1.0 - shadow) * gate);
353}
354
355fn palette(i: u32) -> vec3<f32> {
356    if (i == 0u) { return vec3<f32>(0.0, 0.0, 0.0); }
357    if (i == 1u) { return vec3<f32>(0.0122865, 0.0241576, 0.0865005); }
358    if (i == 2u) { return vec3<f32>(0.208637, 0.0185002, 0.0865005); }
359    if (i == 3u) { return vec3<f32>(0.0, 0.242281, 0.0822827); }
360    if (i == 4u) { return vec3<f32>(0.40724, 0.0843762, 0.0368895); }
361    if (i == 5u) { return vec3<f32>(0.114435, 0.0953075, 0.0781874); }
362    if (i == 6u) { return vec3<f32>(0.539479, 0.545724, 0.571125); }
363    if (i == 7u) { return vec3<f32>(1.0, 0.879622, 0.806952); }
364    if (i == 8u) { return vec3<f32>(1.0, 0.0, 0.0742136); }
365    if (i == 9u) { return vec3<f32>(1.0, 0.366253, 0.0); }
366    if (i == 10u) { return vec3<f32>(1.0, 0.838799, 0.0202886); }
367    if (i == 11u) { return vec3<f32>(0.0, 0.775822, 0.0368895); }
368    if (i == 12u) { return vec3<f32>(0.0221739, 0.417885, 1.0); }
369    if (i == 13u) { return vec3<f32>(0.226966, 0.181164, 0.332452); }
370    if (i == 14u) { return vec3<f32>(1.0, 0.184475, 0.391572); }
371    return vec3<f32>(1.0, 0.603827, 0.401978);
372}
373
374fn nearest_palette(c: vec3<f32>) -> vec3<f32> {
375    var best = palette(0u);
376    var best_d = 1e30;
377    for (var i = 0u; i < 16u; i = i + 1u) {
378        let swatch = palette(i);
379        let d = dot(c - swatch, c - swatch);
380        if (d < best_d) { best_d = d; best = swatch; }
381    }
382    return best;
383}
384
385fn encode_channel(c: f32) -> f32 {
386    if (c <= 0.0031308) { return c * 12.92; }
387    return 1.055 * safe_pow(max(c, 0.0), 1.0 / 2.4) - 0.055;
388}
389
390fn gaussian(p: vec2<u32>, dirx: i32, diry: i32, radius: u32, spread: f32) -> vec4<f32> {
391    var acc = vec4<f32>(0.0);
392    var total = 0.0;
393    var s = spread;
394    if (s <= 1e-4) { s = 1.0; }
395    let r = i32(min(radius, 64u));
396    for (var k = -r; k <= r; k = k + 1) {
397        let w = exp(-f32(k * k) / s);
398        acc = acc + load_src(vec2<i32>(i32(p.x) + k * dirx, i32(p.y) + k * diry)) * w;
399        total = total + w;
400    }
401    return acc / max(total, 1e-8);
402}
403
404fn ring_radius(ring: u32) -> f32 {
405    if (ring == 0u) { return post.p1.x; }
406    if (ring == 1u) { return post.p1.y; }
407    if (ring == 2u) { return post.p1.z; }
408    return post.p1.w;
409}
410
411fn ring_gain(ring: u32) -> f32 {
412    if (ring == 0u) { return post.p2.x; }
413    if (ring == 1u) { return post.p2.y; }
414    if (ring == 2u) { return post.p2.z; }
415    return post.p2.w;
416}
417
418fn ring_channel(c: f32, threshold: f32, clamp_hi: f32) -> f32 {
419    var v = max(c - threshold, 0.0);
420    if (clamp_hi > 0.0) { v = min(v, clamp_hi); }
421    return v;
422}
423"#
424    };
425}
426
427macro_rules! post_main {
428    () => {
429        concat!(
430            "\n@compute @workgroup_size(8, 8)\nfn main(@builtin(global_invocation_id) id: vec3<u32>) {\n",
431            "    let dst_dim = textureDimensions(dst_tex);\n",
432            "    let scissor = post.region.z != 0u;\n",
433            "    let p = select(id.xy, id.xy + post.region.xy, scissor);\n",
434            "    if (scissor && (p.x >= post.region.z || p.y >= post.region.w)) { return; }\n",
435            "    if (p.x >= dst_dim.x || p.y >= dst_dim.y) { return; }\n",
436            "    if (post.size.x != 0u && (p.x >= post.size.x || p.y >= post.size.y)) { return; }\n",
437            "    let src = load_src(vec2<i32>(p));\n"
438        )
439    };
440}
441
442macro_rules! grade_ops {
443    () => {
444        r#"
445fn as_half(c: vec4<f32>) -> vec4<f32> {
446    return vec4<f32>(unpack2x16float(pack2x16float(c.xy)), unpack2x16float(pack2x16float(c.zw)));
447}
448
449fn grade_exposure(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
450    return vec4<f32>(src.rgb * q0.x, src.a);
451}
452
453fn grade_black(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
454    let denom = 1.0 - q0.x;
455    if (abs(denom) < 1e-6) { return vec4<f32>(0.0, 0.0, 0.0, src.a); }
456    return vec4<f32>(max((src.rgb - vec3<f32>(q0.x)) / denom, vec3<f32>(0.0)), src.a);
457}
458
459fn grade_saturation(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
460    let l = dot(src.rgb, vec3<f32>(q0.y, q0.z, q0.w));
461    return vec4<f32>(max(vec3<f32>(l) + (src.rgb - vec3<f32>(l)) * q0.x, vec3<f32>(0.0)), src.a);
462}
463
464fn grade_contrast(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
465    if (q0.z > 0.5) {
466        return vec4<f32>(clamp(vec3<f32>(
467            safe_pow(max(src.r, 0.0), q0.x),
468            safe_pow(max(src.g, 0.0), q0.x),
469            safe_pow(max(src.b, 0.0), q0.x)
470        ) * q0.y, vec3<f32>(0.0), vec3<f32>(1.0)), src.a);
471    }
472    return vec4<f32>((src.rgb - vec3<f32>(q0.y)) * q0.x + vec3<f32>(q0.y), src.a);
473}
474
475fn grade_warmth(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
476    if (q0.y > 0.5) {
477        let l = clamp(luma(src.rgb), 0.0, 1.0);
478        let t = edge_step(q0.z, q0.w, l);
479        let warm = mix(q1.xy, q1.zw, t);
480        let scale = mix(vec2<f32>(1.0), warm, q0.x);
481        return vec4<f32>(max(vec3<f32>(src.r * scale.x, src.g, src.b * scale.y), vec3<f32>(0.0)), src.a);
482    }
483    return vec4<f32>(src.r * (1.0 + q0.x), src.g, src.b * (1.0 - q0.x), src.a);
484}
485
486fn grade_tone(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
487    if (q0.x < 0.5) {
488        return vec4<f32>(aces_channel(src.r * q0.y), aces_channel(src.g * q0.y), aces_channel(src.b * q0.y), src.a);
489    }
490    if (q0.x < 1.5) {
491        return vec4<f32>(neutral_map(src.rgb, q0.y, q0.z, q0.w, q1.x > 0.5), src.a);
492    }
493    return vec4<f32>(agx(src.rgb), src.a);
494}
495
496fn grade_vignette(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
497    if (abs(q0.x) < 1e-8) { return src; }
498    let dim = max(vec2<f32>(src_dim()), vec2<f32>(1.0));
499    let st = (vec2<f32>(p) + vec2<f32>(0.5)) / dim;
500    var factor = 1.0;
501    if (q0.y > 0.5) {
502        let d = vec2<f32>(st.x - 0.5, (st.y - 0.5) * q0.w);
503        factor = 1.0 - q0.x * edge_step(q1.y, q1.z, length(d));
504    } else {
505        let uv = st * 2.0 - vec2<f32>(1.0);
506        let r = length(vec2<f32>(uv.x, uv.y * q0.w)) / max(q1.x, 1e-4);
507        factor = 1.0 - q0.x * safe_pow(max(r, 0.0), q0.z);
508    }
509    return vec4<f32>(src.rgb * factor, src.a);
510}
511
512fn grade_grain(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
513    let mixed = mixed_seed();
514    let n = grain_hash(p.x, p.y, mixed ^ 7u) + grain_hash(p.x, p.y, mixed ^ 19u) - 1.0;
515    let add = n * q0.x * (1.0 - q0.y * luma(src.rgb));
516    var out = src.rgb + vec3<f32>(add);
517    if (q0.z > 0.5) { out = max(out, vec3<f32>(0.0)); }
518    return vec4<f32>(out, src.a);
519}
520
521fn grade_dither(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
522    if (abs(q0.x) < 1e-8) { return src; }
523    let h = hash21(f32(p.x) + f32(post.seed % 1024u), f32(p.y) + f32(post.frame % 1024u));
524    let d = (h - 0.5) * q0.x / 255.0;
525    return vec4<f32>(src.rgb + vec3<f32>(d), src.a);
526}
527
528fn grade_encode(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
529    return vec4<f32>(encode_channel(src.r), encode_channel(src.g), encode_channel(src.b), src.a);
530}
531
532fn grade_bw(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
533    let l = dot(src.rgb, vec3<f32>(q0.x, q0.y, q0.z));
534    return vec4<f32>(l, l, l, src.a);
535}
536
537fn grade_noir(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
538    let l = luma(src.rgb);
539    var out = vec3<f32>(l);
540    if (q0.z > 0.5) {
541        let keep = vec3<f32>(q1.x, q1.y, q1.z);
542        let kdir = keep - vec3<f32>(luma(keep));
543        let chroma = src.rgb - vec3<f32>(l);
544        let kn = length(kdir);
545        let cn = length(chroma);
546        if (kn > 1e-4 && cn > 1e-4) {
547            let gate = edge_step(q0.w, 1.0, dot(chroma, kdir) / (kn * cn));
548            out = mix(vec3<f32>(l), src.rgb, gate);
549        }
550    }
551    out = (out - vec3<f32>(0.5)) * q0.x + vec3<f32>(0.5);
552    if (q0.y < 0.999) {
553        out = max((out - vec3<f32>(q0.y)) / max(1.0 - q0.y, 1e-4), vec3<f32>(0.0));
554    }
555    return vec4<f32>(max(out, vec3<f32>(0.0)), src.a);
556}
557
558fn grade_sepia(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
559    let tint = vec3<f32>(
560        dot(src.rgb, vec3<f32>(0.393, 0.769, 0.189)),
561        dot(src.rgb, vec3<f32>(0.349, 0.686, 0.168)),
562        dot(src.rgb, vec3<f32>(0.272, 0.534, 0.131))
563    );
564    return vec4<f32>(mix(src.rgb, tint, clamp(q0.x, 0.0, 1.0)), src.a);
565}
566
567fn grade_posterize(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
568    let levels = max(q0.x, 2.0);
569    return vec4<f32>(quantize(src.r, levels), quantize(src.g, levels), quantize(src.b, levels), src.a);
570}
571
572fn grade_flicker(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
573    let h = grain_hash(post.frame, 0u, post.seed ^ 0xF11CE001u);
574    let scale = 1.0 + (h - 0.5) * q0.x;
575    return vec4<f32>(src.rgb * scale, src.a);
576}
577
578fn grade_scanlines(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
579    if (abs(q0.x) < 1e-8) { return src; }
580    let period = max(u32(q0.y), 2u);
581    var out = src.rgb;
582    if (p.y % period == period - 1u) { out = src.rgb * max(1.0 - q0.x, 0.0); }
583    return vec4<f32>(out, src.a);
584}
585
586fn grade_aperture(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
587    if (abs(q0.x) < 1e-8) { return src; }
588    let s = clamp(q0.x, 0.0, 1.0);
589    var out = src.rgb;
590    let column = p.x % 3u;
591    if (column == 0u) { out = vec3<f32>(src.r, src.g * (1.0 - s), src.b * (1.0 - s)); }
592    else if (column == 1u) { out = vec3<f32>(src.r * (1.0 - s), src.g, src.b * (1.0 - s)); }
593    else { out = vec3<f32>(src.r * (1.0 - s), src.g * (1.0 - s), src.b); }
594    return vec4<f32>(out, src.a);
595}
596
597fn grade_duotone(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
598    let l = clamp(luma(src.rgb), 0.0, 1.0);
599    let shadow = vec3<f32>(q0.x, q0.y, q0.z);
600    let highlight = vec3<f32>(q1.x, q1.y, q1.z);
601    return vec4<f32>(mix(shadow, highlight, l), src.a);
602}
603
604fn grade_gradient_map(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {
605    let l = clamp(luma(src.rgb), 0.0, 1.0);
606    var lower = q0;
607    var upper = q3;
608    if (q0.w <= l) { lower = q0; }
609    if (q1.w <= l) { lower = q1; }
610    if (q2.w <= l) { lower = q2; }
611    if (q3.w <= l) { lower = q3; }
612    if (q0.w >= l) { upper = q0; }
613    else if (q1.w >= l) { upper = q1; }
614    else if (q2.w >= l) { upper = q2; }
615    else { upper = q3; }
616    let span = upper.w - lower.w;
617    var t = 0.0;
618    if (abs(span) >= 1e-6) { t = clamp((l - lower.w) / span, 0.0, 1.0); }
619    return vec4<f32>(mix(lower.xyz, upper.xyz, t), src.a);
620}
621"#
622    };
623}
624
625pub const HEAD: &str = post_head!();
626pub const BLOOM_TEX: &str = bloom_tex!();
627pub const COMMON: &str = post_common!();
628pub const MAIN: &str = post_main!();
629pub const GRADE_OPS: &str = grade_ops!();
630
631pub const EXPOSURE: &str = shader!(grade, "exposure");
632
633pub const BLACK: &str = shader!(grade, "black");
634
635pub const SATURATION: &str = shader!(grade, "saturation");
636
637pub const CONTRAST: &str = shader!(grade, "contrast");
638
639pub const WARMTH: &str = shader!(grade, "warmth");
640
641pub const LUT: &str = shader!(
642    r#"
643@group(0) @binding(3) var<storage, read> lut_buf: array<f32>;
644
645fn lut_at(i: u32) -> vec3<f32> {
646    let b = i * 3u;
647    return vec3<f32>(lut_buf[b], lut_buf[b + 1u], lut_buf[b + 2u]);
648}
649"#,
650    r#"
651    let n = u32(post.p0.x);
652    if (n < 2u) { store(p, src); return; }
653    let scale = f32(n - 1u);
654    let xr = clamp(src.r, 0.0, 1.0) * scale;
655    let yg = clamp(src.g, 0.0, 1.0) * scale;
656    let zb = clamp(src.b, 0.0, 1.0) * scale;
657    let x0 = min(u32(floor(xr)), n - 1u);
658    let y0 = min(u32(floor(yg)), n - 1u);
659    let z0 = min(u32(floor(zb)), n - 1u);
660    let x1 = min(x0 + 1u, n - 1u);
661    let y1 = min(y0 + 1u, n - 1u);
662    let z1 = min(z0 + 1u, n - 1u);
663    let tx = xr - f32(x0);
664    let ty = yg - f32(y0);
665    let tz = zb - f32(z0);
666    let c00 = mix(lut_at(x0 + n * (y0 + n * z0)), lut_at(x1 + n * (y0 + n * z0)), tx);
667    let c10 = mix(lut_at(x0 + n * (y1 + n * z0)), lut_at(x1 + n * (y1 + n * z0)), tx);
668    let c01 = mix(lut_at(x0 + n * (y0 + n * z1)), lut_at(x1 + n * (y0 + n * z1)), tx);
669    let c11 = mix(lut_at(x0 + n * (y1 + n * z1)), lut_at(x1 + n * (y1 + n * z1)), tx);
670    let c0 = mix(c00, c10, ty);
671    let c1 = mix(c01, c11, ty);
672    store(p, vec4<f32>(mix(c0, c1, tz), src.a));
673"#
674);
675
676pub const TONE: &str = shader!(grade, "tone");
677
678pub const VIGNETTE: &str = shader!(grade, "vignette");
679
680pub const GRAIN: &str = shader!(grade, "grain");
681
682pub const DITHER: &str = shader!(grade, "dither");
683
684pub const ENCODE: &str = shader!(grade, "encode");
685
686pub const CEL: &str = shader!(
687    normal,
688    r#"
689    let l = max(luma(src.rgb), 0.0);
690    var light = clamp(l, 0.0, 1.0);
691    var from_normal = false;
692    if (post.p1.w > 0.5) {
693        light = clamp(dot(normalize3(normal_of(i32(p.x), i32(p.y))), normalize3(post.p2.xyz)), 0.0, 1.0);
694        from_normal = true;
695    }
696    let shade = max(band_shade(light, post.p0.x, post.p0.w, post.p0.z, post.p0.y), select(0.18, 0.55, from_normal));
697    let gray = src.r == src.g && src.g == src.b;
698    var out = vec3<f32>(shade);
699    if (from_normal) {
700        out = src.rgb * shade;
701    } else if (!gray && l > 1e-5) {
702        out = src.rgb * (shade / l);
703    }
704    if (post.p1.x > 0.0) {
705        let rough = max(post.p1.y, 0.04);
706        let denom = max(1.0 - post.p1.z, 1e-3);
707        let t = clamp((light - post.p1.z) / denom, 0.0, 1.0);
708        let spec = safe_pow(t, 1.0 / rough) * post.p1.x;
709        out = out + vec3<f32>(spec);
710    }
711    if (from_normal && light < 0.55) {
712        let cool = (0.55 - light) * 0.11;
713        out = out * vec3<f32>(1.0 - cool, 1.0, 1.0) + vec3<f32>(0.0, cool * 0.35, cool);
714    }
715    out = saturate3(out, 1.12);
716    store(p, vec4<f32>(out, src.a));
717"#
718);
719
720pub const OUTLINE: &str = shader!(
721    geometry,
722    r#"
723    if (post.p1.w < 0.5) { store(p, src); return; }
724    let radius = clamp(post.p0.x, 1.0, 8.0);
725    let rad = select(i32(ceil(radius)), 1, radius <= 1.4142135);
726    let has_depth = post.p2.x > 0.5;
727    let has_normal = post.p2.y > 0.5;
728    var edge = 0.0;
729    if (!has_depth && !has_normal) {
730        edge = max(edge, clamp(sobel(i32(p.x), i32(p.y)), 0.0, 1.0));
731    }
732    var center_depth = 0.0;
733    var center_normal = vec3<f32>(0.0, 0.0, 1.0);
734    if (has_depth) { center_depth = depth_of(i32(p.x), i32(p.y)); }
735    if (has_normal) { center_normal = normalize3(normal_of(i32(p.x), i32(p.y))); }
736    if (has_depth || has_normal || post.p2.z > 0.5) {
737        for (var dy = -rad; dy <= rad; dy = dy + 1) {
738            for (var dx = -rad; dx <= rad; dx = dx + 1) {
739                if (dx == 0 && dy == 0) { continue; }
740                let dist = sqrt(f32(dx * dx + dy * dy));
741                if (dist > radius) { continue; }
742                if (post.p2.z > 0.5) {
743                    let diff = abs(src.rgb - load_src(vec2<i32>(i32(p.x) + dx, i32(p.y) + dy)).rgb);
744                    edge = max(edge, edge_step(0.08, 0.22, max(diff.r, max(diff.g, diff.b))));
745                }
746                if (has_depth) {
747                    let gap = abs(center_depth - depth_of(i32(p.x) + dx, i32(p.y) + dy));
748                    if (gap > post.p0.z) {
749                        edge = max(edge, clamp((gap - post.p0.z) / max(post.p0.z, 1e-4), 0.0, 1.0));
750                    }
751                }
752                if (has_normal) {
753                    let nn = normalize3(normal_of(i32(p.x) + dx, i32(p.y) + dy));
754                    let dots = clamp(dot(center_normal, nn), -1.0, 1.0);
755                    if (dots < post.p0.w) {
756                        edge = max(edge, clamp((post.p0.w - dots) / max(1.0 + post.p0.w, 0.1), 0.0, 1.0));
757                    }
758                }
759            }
760        }
761    }
762    let a = clamp(edge * post.p0.y, 0.0, 1.0);
763    var ink = post.p1.xyz;
764    if (post.p2.z > 0.5) {
765        ink = max(src.rgb * 0.15, vec3<f32>(0.035));
766    }
767    store(p, vec4<f32>(mix(src.rgb, ink, a), src.a));
768"#
769);
770
771pub const CAVITY: &str = shader!(
772    normal,
773    r#"
774    if (post.p0.z < 0.5 || abs(post.p0.x) < 1e-8) { store(p, src); return; }
775    let d = i32(floor(max(post.p0.y, 1.0) + 0.5));
776    let n = normalize3(normal_of(i32(p.x), i32(p.y)));
777    var bend = 0.0;
778    bend = bend + max(1.0 - dot(n, normalize3(normal_of(i32(p.x) + d, i32(p.y)))), 0.0);
779    bend = bend + max(1.0 - dot(n, normalize3(normal_of(i32(p.x) - d, i32(p.y)))), 0.0);
780    bend = bend + max(1.0 - dot(n, normalize3(normal_of(i32(p.x), i32(p.y) + d))), 0.0);
781    bend = bend + max(1.0 - dot(n, normalize3(normal_of(i32(p.x), i32(p.y) - d))), 0.0);
782    let k = max(1.0 - post.p0.x * clamp(bend * 0.25, 0.0, 1.0), 0.0);
783    store(p, vec4<f32>(src.rgb * k, src.a));
784"#
785);
786
787pub const RIM: &str = shader!(
788    normal,
789    r#"
790    if (post.p1.y < 0.5 || abs(post.p0.x) < 1e-8) { store(p, src); return; }
791    let power = clamp(1.0 / max(post.p0.y, 0.05), 0.05, 32.0);
792    let n = normalize3(normal_of(i32(p.x), i32(p.y)));
793    let fres = safe_pow(clamp(1.0 - clamp(abs(n.z), 0.0, 1.0), 0.0, 1.0), power);
794    let color = vec3<f32>(post.p0.z, post.p0.w, post.p1.x);
795    store(p, vec4<f32>(src.rgb + color * fres * post.p0.x, src.a));
796"#
797);
798
799pub const FUSED_CAVITY_RIM: &str = shader!(
800    normal,
801    r#"
802    if (post.p1.w < 0.5) { store(p, src); return; }
803    let x = i32(p.x);
804    let y = i32(p.y);
805    let n = normalize3(normal_of(x, y));
806    let d = i32(floor(max(post.p0.y, 1.0) + 0.5));
807    var bend = 0.0;
808    bend += max(1.0 - dot(n, normalize3(normal_of(x + d, y))), 0.0);
809    bend += max(1.0 - dot(n, normalize3(normal_of(x - d, y))), 0.0);
810    bend += max(1.0 - dot(n, normalize3(normal_of(x, y + d))), 0.0);
811    bend += max(1.0 - dot(n, normalize3(normal_of(x, y - d))), 0.0);
812    let shade = max(1.0 - post.p0.x * clamp(bend * 0.25, 0.0, 1.0), 0.0);
813    let power = clamp(1.0 / max(post.p0.w, 0.05), 0.05, 32.0);
814    let fres = safe_pow(clamp(1.0 - clamp(abs(n.z), 0.0, 1.0), 0.0, 1.0), power);
815    let rim = vec3<f32>(post.p1.x, post.p1.y, post.p1.z) * fres * post.p0.z;
816    store(p, vec4<f32>(src.rgb * shade + rim, src.a));
817"#
818);
819
820pub const BW: &str = shader!(grade, "bw");
821
822pub const NOIR: &str = shader!(grade, "noir");
823
824pub const SEPIA: &str = shader!(grade, "sepia");
825
826pub const POSTERIZE: &str = shader!(grade, "posterize");
827
828pub const NEON: &str = shader!(
829    "",
830    r#"
831    let center = saturate3(src.rgb, post.p0.x);
832    let radius_full = u32(max(post.p0.z, 0.0));
833    let radius = min(radius_full, 8u);
834    if (radius == 0u || abs(post.p0.y) < 1e-8) { store(p, vec4<f32>(center, src.a)); return; }
835    let spread = f32(max(radius_full, 1u)) * 6.0;
836    var acc = vec3<f32>(0.0);
837    var weight = 0.0;
838    for (var dy = -8; dy <= 8; dy = dy + 1) {
839        for (var dx = -8; dx <= 8; dx = dx + 1) {
840            if (u32(abs(dx)) > radius || u32(abs(dy)) > radius) { continue; }
841            let e = clamp(sobel(i32(p.x) + dx, i32(p.y) + dy), 0.0, 4.0);
842            let s = saturate3(load_src(vec2<i32>(i32(p.x) + dx, i32(p.y) + dy)).rgb, post.p0.x);
843            let w = exp(-f32(dx * dx + dy * dy) / spread);
844            acc = acc + s * e * w;
845            weight = weight + w;
846        }
847    }
848    var glow = vec3<f32>(0.0);
849    if (weight > 1e-8) { glow = acc / weight; }
850    store(p, vec4<f32>(center + glow * post.p0.y, src.a));
851"#
852);
853
854pub const NEON_EDGE: &str = shader!(
855    "",
856    r#"
857    let x = i32(p.x * 2u + 1u);
858    let y = i32(p.y * 2u + 1u);
859    let e = clamp(sobel(x, y), 0.0, 4.0);
860    let c = saturate3(load_src(vec2<i32>(x, y)).rgb, post.p0.x);
861    store(p, vec4<f32>(c * e, 1.0));
862"#
863);
864
865pub const NEON_ADD: &str = shader!(
866    bloomtex,
867    r#"
868    let full = max(vec2<f32>(src_dim()), vec2<f32>(1.0));
869    let small = vec2<f32>(bloom_dim());
870    let x = (f32(p.x) + 0.5) / full.x * small.x - 0.5;
871    let y = (f32(p.y) + 0.5) / full.y * small.y - 0.5;
872    let glow = sample_bloom(x, y).rgb;
873    let center = saturate3(src.rgb, post.p0.x);
874    store(p, vec4<f32>(center + glow * post.p0.y, src.a));
875"#
876);
877
878pub const FLICKER: &str = shader!(grade, "flicker");
879
880pub const WEAVE: &str = shader!(
881    "",
882    r#"
883    let hx = grain_hash(post.frame, 1u, post.seed ^ 0x11A00001u);
884    let hy = grain_hash(post.frame, 2u, post.seed ^ 0x22B00002u);
885    let dx = (hx - 0.5) * 2.0 * post.p0.x;
886    let dy = (hy - 0.5) * 4.0 * post.p0.x;
887    store(p, sample_src(f32(p.x) - dx, f32(p.y) - dy));
888"#
889);
890
891pub const DUST: &str = shader!(
892    "",
893    r#"
894    let mixed = mixed_seed() ^ 0xD0570003u;
895    let h = grain_hash(p.x, p.y, mixed);
896    var out = src.rgb;
897    if (post.p0.x > 0.0 && h < post.p0.x) {
898        let a = 0.55 + 0.45 * grain_hash(p.x, p.y, mixed ^ 0x51u);
899        out = out + vec3<f32>(a);
900    }
901    store(p, vec4<f32>(out, src.a));
902"#
903);
904
905pub const SCRATCHES: &str = shader!(
906    "",
907    r#"
908    let count = u32(max(post.p0.x, 0.0));
909    let dim = src_dim();
910    var out = src.rgb;
911    if (post.p0.y != 0.0 && count > 0u && dim.x > 0u) {
912        for (var i = 0u; i < count; i = i + 1u) {
913            let x0 = grain_hash(i, post.frame, post.seed ^ 0x5C8A0005u) * f32(dim.x);
914            let wobble = (grain_hash(i, p.y / 8u, post.seed ^ 0x5C8A0004u) - 0.5) * 3.0;
915            let dist = abs(f32(p.x) - x0 - wobble);
916            if (dist < 0.8) {
917                let gap = grain_hash(i * 97u + p.x, p.y, post.seed ^ post.frame);
918                if (gap > 0.4) {
919                    out = out + vec3<f32>(post.p0.y * (1.0 - dist / 0.8));
920                }
921            }
922        }
923    }
924    store(p, vec4<f32>(out, src.a));
925"#
926);
927
928pub const ABERRATION: &str = shader!(
929    "",
930    r#"
931    if (abs(post.p0.x) < 1e-6) { store(p, src); return; }
932    let dim = src_dim();
933    let cx = f32(dim.x - 1u) * 0.5;
934    let cy = f32(dim.y - 1u) * 0.5;
935    let dx = f32(p.x) - cx;
936    let dy = f32(p.y) - cy;
937    let len = sqrt(dx * dx + dy * dy);
938    var ux = 0.0;
939    var uy = 0.0;
940    if (len >= 1e-4) { ux = dx / len; uy = dy / len; }
941    let r = sample_src(f32(p.x) + ux * post.p0.x, f32(p.y) + uy * post.p0.x);
942    let b = sample_src(f32(p.x) - ux * post.p0.x, f32(p.y) - uy * post.p0.x);
943    store(p, vec4<f32>(r.r, src.g, b.b, src.a));
944"#
945);
946
947pub const DISTORTION: &str = shader!(
948    "",
949    r#"
950    if (abs(post.p0.x) < 1e-8) { store(p, src); return; }
951    let dim = max(vec2<f32>(src_dim()), vec2<f32>(1.0));
952    let nx = (f32(p.x) + 0.5) / dim.x * 2.0 - 1.0;
953    let ny = (f32(p.y) + 0.5) / dim.y * 2.0 - 1.0;
954    let r2 = nx * nx + ny * ny;
955    let f = 1.0 + post.p0.x * r2;
956    let sx = (nx * f * 0.5 + 0.5) * dim.x - 0.5;
957    let sy = (ny * f * 0.5 + 0.5) * dim.y - 0.5;
958    store(p, sample_src(sx, sy));
959"#
960);
961
962pub const SCANLINES: &str = shader!(grade, "scanlines");
963
964pub const APERTURE: &str = shader!(grade, "aperture");
965
966pub const ONE_BIT: &str = shader!(
967    "",
968    r#"
969    let v = select(0.0, 1.0, clamp(luma(src.rgb), 0.0, 1.0) >= bayer(p.x, p.y));
970    store(p, vec4<f32>(v, v, v, src.a));
971"#
972);
973
974pub const ONE_BIT_BLUE: &str = shader!(
975    r#"
976@group(0) @binding(3) var<storage, read> blue_buf: array<u32>;
977
978fn blue_at(x: u32, y: u32) -> f32 {
979    let n = 64u;
980    return f32(blue_buf[(y % n) * n + (x % n)] & 255u) / 255.0;
981}
982"#,
983    r#"
984    let v = select(0.0, 1.0, clamp(luma(src.rgb), 0.0, 1.0) >= blue_at(p.x, p.y));
985    store(p, vec4<f32>(v, v, v, src.a));
986"#
987);
988
989pub const HALFTONE: &str = shader!(
990    "",
991    r#"
992    let rad = radians(post.p0.y);
993    let c = cos(rad);
994    let s = sin(rad);
995    let cell = max(post.p0.x, 1.0);
996    let fx = f32(p.x) * c + f32(p.y) * s;
997    let fy = -f32(p.x) * s + f32(p.y) * c;
998    let lx = fract(fx / cell) - 0.5;
999    let ly = fract(fy / cell) - 0.5;
1000    let dist = sqrt(lx * lx + ly * ly);
1001    let radius = sqrt(1.0 - clamp(luma(src.rgb), 0.0, 1.0)) * 0.5;
1002    var out = src.rgb;
1003    if (dist < radius) { out = vec3<f32>(post.p0.z, post.p0.w, post.p1.x); }
1004    store(p, vec4<f32>(out, src.a));
1005"#
1006);
1007
1008pub const COMIC: &str = shader!(
1009    geometry,
1010    r#"
1011    let light = clamp(luma(src.rgb), 0.0, 1.0);
1012    let peak = max(src.r, max(src.g, src.b));
1013    var base = vec3<f32>(0.97, 0.93, 0.78);
1014    if (peak - min(src.r, min(src.g, src.b)) >= 0.12) {
1015        if (src.r >= src.g && src.r >= src.b) {
1016            if (src.g > src.r * 0.75) { base = vec3<f32>(0.98, 0.77, 0.12); }
1017            else { base = vec3<f32>(0.91, 0.12, 0.16); }
1018        } else if (src.g >= src.b) { base = vec3<f32>(0.11, 0.68, 0.39); }
1019        else { base = vec3<f32>(0.12, 0.38, 0.82); }
1020    }
1021    let angle = radians(15.0);
1022    let s = sin(angle);
1023    let c = cos(angle);
1024    let fx = f32(p.x) * c + f32(p.y) * s;
1025    let fy = -f32(p.x) * s + f32(p.y) * c;
1026    let cell = max(post.p0.x, 2.0) + 2.0;
1027    let dx = fract(fx / cell) - 0.5;
1028    let dy = fract(fy / cell) - 0.5;
1029    let radius = sqrt(1.0 - light) * 0.34;
1030    let dist = sqrt(dx * dx + dy * dy);
1031    let dot_ink = 1.0 - edge_step(radius - 0.5 / cell, radius + 0.5 / cell, dist);
1032    let x = i32(p.x);
1033    let y = i32(p.y);
1034    var ink = edge_step(0.12, 0.30, sobel(x, y)) * 0.65;
1035    for (var k = 0; k < 4; k = k + 1) {
1036        var dx = 0;
1037        var dy = 0;
1038        if (k == 0) { dx = -2; }
1039        if (k == 1) { dx = 2; }
1040        if (k == 2) { dy = -2; }
1041        if (k == 3) { dy = 2; }
1042        let diff = abs(src.rgb - load_src(vec2<i32>(x + dx, y + dy)).rgb);
1043        ink = max(ink, edge_step(0.08, 0.22, max(diff.r, max(diff.g, diff.b))));
1044    }
1045    if (post.p0.z > 0.5) {
1046        let z = depth_of(x, y);
1047        ink = max(ink, edge_step(0.002, 0.012, abs(z - depth_of(x - 2, y))));
1048        ink = max(ink, edge_step(0.002, 0.012, abs(z - depth_of(x + 2, y))));
1049        ink = max(ink, edge_step(0.002, 0.012, abs(z - depth_of(x, y - 2))));
1050        ink = max(ink, edge_step(0.002, 0.012, abs(z - depth_of(x, y + 2))));
1051    }
1052    if (post.p0.w > 0.5) {
1053        let n = normalize3(normal_of(x, y));
1054        ink = max(ink, edge_step(0.3, 0.6, 1.0 - dot(n, normalize3(normal_of(x - 1, y)))) * 0.55);
1055        ink = max(ink, edge_step(0.3, 0.6, 1.0 - dot(n, normalize3(normal_of(x + 1, y)))) * 0.55);
1056        ink = max(ink, edge_step(0.3, 0.6, 1.0 - dot(n, normalize3(normal_of(x, y - 1)))) * 0.55);
1057        ink = max(ink, edge_step(0.3, 0.6, 1.0 - dot(n, normalize3(normal_of(x, y + 1)))) * 0.55);
1058    }
1059    let covered = max(ink, dot_ink * 0.55) * clamp(post.p0.y, 0.0, 1.0);
1060    store(p, vec4<f32>(mix(base, vec3<f32>(0.004, 0.003, 0.008), covered), src.a));
1061"#
1062);
1063
1064pub const RUBBER_HOSE: &str = shader!(
1065    geometry,
1066    r#"
1067    let x = i32(p.x);
1068    let y = i32(p.y);
1069    let mixed = mixed_seed();
1070    let wobble = grain_hash(p.x / 32u, p.y / 32u, mixed);
1071    var shift = 0;
1072    if (wobble < 0.33) { shift = -1; }
1073    else if (wobble > 0.66) { shift = 1; }
1074    let radius = 5 + select(0, 1, grain_hash(p.x / 48u, p.y / 48u, mixed ^ 0x12ab9921u) > 0.5);
1075    var edge = 0.0;
1076    if (post.p0.x > 0.5) {
1077        let z = depth_of(x + shift, y);
1078        edge = max(edge, edge_step(0.002, 0.012, abs(z - depth_of(x + shift + radius, y))));
1079        edge = max(edge, edge_step(0.002, 0.012, abs(z - depth_of(x + shift - radius, y))));
1080        edge = max(edge, edge_step(0.002, 0.012, abs(z - depth_of(x + shift, y + radius))));
1081        edge = max(edge, edge_step(0.002, 0.012, abs(z - depth_of(x + shift, y - radius))));
1082    }
1083    if (post.p0.y > 0.5) {
1084        let n = normalize3(normal_of(x + shift, y));
1085        edge = max(edge, edge_step(0.3, 0.65, 1.0 - dot(n, normalize3(normal_of(x + shift + 2, y)))));
1086        edge = max(edge, edge_step(0.3, 0.65, 1.0 - dot(n, normalize3(normal_of(x + shift - 2, y)))));
1087        edge = max(edge, edge_step(0.3, 0.65, 1.0 - dot(n, normalize3(normal_of(x + shift, y + 2)))));
1088        edge = max(edge, edge_step(0.3, 0.65, 1.0 - dot(n, normalize3(normal_of(x + shift, y - 2)))));
1089    }
1090    edge = max(edge, edge_step(0.12, 0.32, sobel(x + shift, y)) * 0.7);
1091    let center = load_src(vec2<i32>(x + shift, y)).rgb;
1092    let dx0 = abs(center - load_src(vec2<i32>(x + shift + radius, y)).rgb);
1093    let dx1 = abs(center - load_src(vec2<i32>(x + shift - radius, y)).rgb);
1094    let dy0 = abs(center - load_src(vec2<i32>(x + shift, y + radius)).rgb);
1095    let dy1 = abs(center - load_src(vec2<i32>(x + shift, y - radius)).rgb);
1096    edge = max(edge, edge_step(0.08, 0.22, max(dx0.r, max(dx0.g, dx0.b))));
1097    edge = max(edge, edge_step(0.08, 0.22, max(dx1.r, max(dx1.g, dx1.b))));
1098    edge = max(edge, edge_step(0.08, 0.22, max(dy0.r, max(dy0.g, dy0.b))));
1099    edge = max(edge, edge_step(0.08, 0.22, max(dy1.r, max(dy1.g, dy1.b))));
1100    let l = clamp(luma(src.rgb), 0.0, 1.0);
1101    var dark = l < 0.18;
1102    if (post.p0.y > 0.5) {
1103        let n = normalize3(normal_of(x, y));
1104        dark = dark || (n.z > 0.1 && dot(n, normalize3(vec3<f32>(-0.45, 0.65, 0.61))) < 0.1);
1105    }
1106    dark = dark && (post.p0.x < 0.5 || depth_of(x, y) < 0.999);
1107    let fill = select(0.77, 0.91, l > 0.56);
1108    let grain = (grain_hash(p.x, p.y, mixed) - 0.5) * 0.07;
1109    let flicker = (grain_hash(0u, 0u, mixed ^ 0x55aa10efu) - 0.5) * 0.08;
1110    let u = (f32(p.x) / max(f32(post.size.x), 1.0) - 0.5) * 2.0;
1111    let v = (f32(p.y) / max(f32(post.size.y), 1.0) - 0.5) * 2.0;
1112    let vignette = 1.0 - 0.25 * min(u * u + v * v, 1.0);
1113    let dust = grain_hash(p.x / 3u, p.y / 3u, mixed ^ 0x29837781u) < 0.0008;
1114    let light = clamp((fill + grain + flicker) * vignette, 0.0, 1.0);
1115    let ink = select(light, 0.003, edge > 0.5 || dust || dark);
1116    store(p, vec4<f32>(ink * vec3<f32>(1.0, 0.91, 0.73), src.a));
1117"#
1118);
1119
1120pub const MODERN_COMIC: &str = shader!(
1121    geometry,
1122    r#"
1123    let x = i32(p.x);
1124    let y = i32(p.y);
1125    let left_color = load_src(vec2<i32>(x - 1, y)).rgb;
1126    let right_color = load_src(vec2<i32>(x + 1, y)).rgb;
1127    let up_color = load_src(vec2<i32>(x, y - 1)).rgb;
1128    let down_color = load_src(vec2<i32>(x, y + 1)).rgb;
1129    let filtered = (src.rgb * 4.0 + left_color + right_color + up_color + down_color) * 0.125;
1130    let light = clamp(luma(filtered), 0.0, 1.0);
1131    let left = luma(left_color);
1132    let right = luma(right_color);
1133    let up = luma(up_color);
1134    let down = luma(down_color);
1135    let gradient = max(abs(left - right), abs(up - down)) * 0.5;
1136    let steps = clamp(post.p0.x, 3.0, 5.0);
1137    let scaled = light * steps;
1138    let base = min(floor(scaled), steps - 1.0);
1139    let aa = clamp(gradient * steps * 0.5, 0.002, 0.18);
1140    let blend = edge_step(1.0 - aa, 1.0, scaled - base);
1141    let low = (base + 0.5) / steps;
1142    var high = (base + 1.5) / steps;
1143    if (base >= steps - 2.0) { high = 1.0; }
1144    var tone = mix(low, high, blend);
1145    if (base >= steps - 1.0 || (light >= post.p1.y && base >= steps - 2.0)) { tone = 1.0; }
1146    let scale = tone / max(light, 0.025);
1147    let black_aa = max(gradient * 0.5, 0.001);
1148    let black_gate = edge_step(post.p0.y - black_aa, post.p0.y + black_aa, light);
1149    var out = clamp(saturate3(clamp(filtered * scale, vec3<f32>(0.0), vec3<f32>(1.0)), post.p1.x) * black_gate, vec3<f32>(0.0), vec3<f32>(1.0));
1150    let surround = (clamp(luma_at(x - 2, y), 0.0, 1.0) + clamp(luma_at(x + 2, y), 0.0, 1.0) + clamp(luma_at(x, y - 2), 0.0, 1.0) + clamp(luma_at(x, y + 2), 0.0, 1.0)) * 0.25;
1151    let detail = max(surround - luma(src.rgb), gradient * 0.35);
1152    let feature = edge_step(0.12, 0.26, detail) * clamp(post.p0.w, 0.0, 1.0);
1153    var silhouette = 0.0;
1154    var crease = 0.0;
1155    var distance = 1;
1156    if (post.p1.z > 0.5) { distance = select(2, 3, depth_of(x, y) < 0.55); }
1157    for (var k = 0; k < 4; k = k + 1) {
1158        var dx = 0;
1159        var dy = 0;
1160        if (k == 0) { dx = distance; }
1161        if (k == 1) { dx = -distance; }
1162        if (k == 2) { dy = distance; }
1163        if (k == 3) { dy = -distance; }
1164        if (post.p1.z > 0.5) {
1165            let diff = abs(src.rgb - load_src(vec2<i32>(x + dx, y + dy)).rgb);
1166            silhouette = max(silhouette, edge_step(0.10, 0.28, max(diff.r, max(diff.g, diff.b))));
1167            let gap = abs(depth_of(x, y) - depth_of(x + dx, y + dy));
1168            silhouette = max(silhouette, edge_step(0.002, 0.015, gap));
1169        }
1170        if (post.p1.w > 0.5) {
1171            let a = normalize3(normal_of(x, y));
1172            let b = normalize3(normal_of(x + sign(dx), y + sign(dy)));
1173            crease = max(crease, edge_step(0.16, 0.48, 1.0 - dot(a, b)));
1174        }
1175    }
1176    let weight = clamp(post.p0.z, 0.0, 1.0);
1177    let ink = clamp(max(max(silhouette * weight, crease * weight * 0.78), feature), 0.0, 1.0);
1178    out = mix(out, vec3<f32>(0.005, 0.004, 0.008), ink);
1179    store(p, vec4<f32>(out, src.a));
1180"#
1181);
1182
1183pub const WATERCOLOR: &str = shader!(
1184    "",
1185    r#"
1186    let scale = max(post.p0.z, 1.0);
1187    let e = clamp(sobel(i32(p.x), i32(p.y)), 0.0, 1.0);
1188    let dark = max(1.0 - post.p0.x * e, 0.0);
1189    let n = value_noise(f32(p.x) / scale, f32(p.y) / scale, mixed_seed() ^ 0xA9E10006u);
1190    let grain = 1.0 + (n - 0.5) * post.p0.y;
1191    store(p, vec4<f32>(max(src.rgb * dark * grain, vec3<f32>(0.0)), src.a));
1192"#
1193);
1194
1195pub const PAPER_GRAIN: &str = shader!(
1196    "",
1197    r#"
1198    let scale = max(post.p0.y, 1.0);
1199    let n = value_noise(f32(p.x) / scale, f32(p.y) / scale, mixed_seed() ^ 0xA9E10006u);
1200    let grain = 1.0 + (n - 0.5) * post.p0.x;
1201    store(p, vec4<f32>(max(src.rgb * grain, vec3<f32>(0.0)), src.a));
1202"#
1203);
1204
1205pub const PIXEL: &str = shader!(
1206    "",
1207    r#"
1208    let dim = src_dim();
1209    let size = max(u32(post.p0.x), 1u);
1210    let x0 = (p.x / size) * size;
1211    let y0 = (p.y / size) * size;
1212    let x1 = min(x0 + size, dim.x);
1213    let y1 = min(y0 + size, dim.y);
1214    var acc = vec3<f32>(0.0);
1215    var n = 0.0;
1216    for (var y = y0; y < y1; y = y + 1u) {
1217        for (var x = x0; x < x1; x = x + 1u) {
1218            acc = acc + load_src(vec2<i32>(i32(x), i32(y))).rgb;
1219            n = n + 1.0;
1220        }
1221    }
1222    var mean = acc / max(n, 1.0);
1223    if (post.p0.z > 0.5) {
1224        mean = nearest_palette(mean);
1225    } else if (post.p0.y >= 2.0) {
1226        mean = vec3<f32>(quantize(mean.r, post.p0.y), quantize(mean.g, post.p0.y), quantize(mean.b, post.p0.y));
1227    }
1228    store(p, vec4<f32>(mean, src.a));
1229"#
1230);
1231
1232pub const DUOTONE: &str = shader!(grade, "duotone");
1233
1234pub const GRADIENT: &str = shader!(grade, "gradient_map");
1235
1236pub const KUWAHARA: &str = shader!(
1237    "",
1238    r#"
1239    let r = i32(min(u32(max(post.p0.x, 0.0)), 8u));
1240    if (r <= 0) { store(p, src); return; }
1241    var best = vec3<f32>(0.0);
1242    var best_var = 1e30;
1243    for (var q = 0; q < 4; q = q + 1) {
1244        var x0 = -r;
1245        var x1 = 0;
1246        var y0 = -r;
1247        var y1 = 0;
1248        if (q == 1 || q == 3) { x0 = 0; x1 = r; }
1249        if (q == 2 || q == 3) { y0 = 0; y1 = r; }
1250        var sum = vec3<f32>(0.0);
1251        var sq = 0.0;
1252        var n = 0.0;
1253        for (var dy = y0; dy <= y1; dy = dy + 1) {
1254            for (var dx = x0; dx <= x1; dx = dx + 1) {
1255                let s = load_src(vec2<i32>(i32(p.x) + dx, i32(p.y) + dy));
1256                sum = sum + s.rgb;
1257                let l = luma(s.rgb);
1258                sq = sq + l * l;
1259                n = n + 1.0;
1260            }
1261        }
1262        let mean = sum / n;
1263        let ml = luma(mean);
1264        let var_ = max(sq / n - ml * ml, 0.0);
1265        if (var_ < best_var) { best_var = var_; best = mean; }
1266    }
1267    store(p, vec4<f32>(best, src.a));
1268"#
1269);
1270
1271pub const TILT_SHIFT: &str = shader!(
1272    depth,
1273    r#"
1274    if (post.p0.w < 0.5 || post.p0.z < 0.35 || abs(post.p0.y) < 1e-6) { store(p, src); return; }
1275    let z = depth_of(i32(p.x), i32(p.y));
1276    let coc = clamp(abs(z - post.p0.x) / post.p0.y, 0.0, 1.0) * post.p0.z;
1277    if (coc < 0.35) { store(p, src); return; }
1278    var acc = src;
1279    var weight = 1.0;
1280    for (var k = 0u; k < 8u; k = k + 1u) {
1281        let a = f32(k) * 6.283185307179586 / 8.0;
1282        let d = coc * sqrt((f32(k) + 0.5) / 8.0);
1283        acc = acc + sample_src(f32(p.x) + cos(a) * d, f32(p.y) + sin(a) * d);
1284        weight = weight + 1.0;
1285    }
1286    store(p, acc / weight);
1287"#
1288);
1289
1290pub const BLOOM_DOWN: &str = shader!(
1291    "",
1292    r#"
1293    let factor_f = max(post.p0.x, 1.0);
1294    let factor = max(u32(factor_f), 1u);
1295    var acc = vec3<f32>(0.0);
1296    if (post.p1.y > 0.5) {
1297        let base_x = f32(p.x) * factor_f + factor_f * 0.5 - 0.5;
1298        let base_y = f32(p.y) * factor_f + factor_f * 0.5 - 0.5;
1299        let s00 = sample_src(base_x - 1.0, base_y - 1.0);
1300        let s10 = sample_src(base_x + 1.0, base_y - 1.0);
1301        let s01 = sample_src(base_x - 1.0, base_y + 1.0);
1302        let s11 = sample_src(base_x + 1.0, base_y + 1.0);
1303        acc = (s00.rgb + s10.rgb + s01.rgb + s11.rgb) * 0.25;
1304    } else {
1305        let span = min(factor, 64u);
1306        let base_x = i32(p.x) * i32(factor);
1307        let base_y = i32(p.y) * i32(factor);
1308        var n = 0.0;
1309        for (var dy = 0u; dy < span; dy = dy + 1u) {
1310            for (var dx = 0u; dx < span; dx = dx + 1u) {
1311                acc = acc + load_src(vec2<i32>(base_x + i32(dx), base_y + i32(dy))).rgb;
1312                n = n + 1.0;
1313            }
1314        }
1315        acc = acc / max(n, 1.0);
1316    }
1317    let kept = keep_rgb(acc * post.p1.z, post.p0.y, post.p0.z, post.p0.w, post.p1.x);
1318    store(p, vec4<f32>(kept, 1.0));
1319"#
1320);
1321
1322pub const BLOOM_DOWN_BOX2: &str = shader!(
1323    "",
1324    r#"
1325    let base = vec2<i32>(p) * 2;
1326    let acc = (
1327        load_src(base).rgb
1328        + load_src(base + vec2<i32>(1, 0)).rgb
1329        + load_src(base + vec2<i32>(0, 1)).rgb
1330        + load_src(base + vec2<i32>(1, 1)).rgb
1331    ) * 0.25;
1332    let kept = keep_rgb(acc * post.p1.z, post.p0.y, post.p0.z, post.p0.w, post.p1.x);
1333    store(p, vec4<f32>(kept, 1.0));
1334"#
1335);
1336
1337pub const BLOOM_BLUR: &str = shader!(
1338    "",
1339    r#"
1340    let radius = u32(max(post.p0.z, 0.0));
1341    store(p, gaussian(p, i32(post.p0.x), i32(post.p0.y), radius, post.p0.w));
1342"#
1343);
1344
1345pub fn bloom_blur_linear(radius: u32, spread: f32) -> String {
1346    let (center, taps) = crate::bloom::linear_taps(radius, spread);
1347    let list = |part: fn(&(f64, f64)) -> f64| {
1348        if taps.is_empty() {
1349            "0.0".to_string()
1350        } else {
1351            taps.iter()
1352                .map(|tap| format!("{:.10}", part(tap)))
1353                .collect::<Vec<_>>()
1354                .join(", ")
1355        }
1356    };
1357    let body = format!(
1358        "    let weights = array<f32, {slots}>({weights});\n    let offsets = array<f32, {slots}>({offsets});\n    let size = vec2<f32>(textureDimensions(src_tex));\n    let edge = select(vec2<f32>(1e30), vec2<f32>(src_dim()) - vec2<f32>(0.5), src_dim() < textureDimensions(src_tex));\n    let center = vec2<f32>(p) + vec2<f32>(0.5);\n    let direction = vec2<f32>(post.p0.x, post.p0.y);\n    var acc = load_src(vec2<i32>(p)) * {center:.10};\n    for (var k = 0u; k < {count}u; k = k + 1u) {{\n        let offset = direction * offsets[k];\n        let left = textureSampleLevel(src_tex, blur_sampler, min(center - offset, edge) / size, 0.0);\n        let right = textureSampleLevel(src_tex, blur_sampler, min(center + offset, edge) / size, 0.0);\n        acc = acc + (left + right) * weights[k];\n    }}\n    store(p, acc);\n",
1359        slots = taps.len().max(1),
1360        weights = list(|tap| tap.0),
1361        offsets = list(|tap| tap.1),
1362        count = taps.len(),
1363    );
1364    format!(
1365        "{}{}{}{}{}\n}}\n",
1366        post_head!(),
1367        "@group(0) @binding(3) var blur_sampler: sampler;\n",
1368        post_common!(),
1369        post_main!(),
1370        body
1371    )
1372}
1373
1374pub const BLOOM_ADD: &str = shader!(
1375    bloomtex,
1376    r#"
1377    if (abs(post.p0.x) < 1e-8) { store(p, src); return; }
1378    let full = max(vec2<f32>(src_dim()), vec2<f32>(1.0));
1379    let small = vec2<f32>(bloom_dim());
1380    let u = (f32(p.x) + 0.5) / full.x;
1381    let v = (f32(p.y) + 0.5) / full.y;
1382    let s = sample_bloom(u * small.x - 0.5, v * small.y - 0.5);
1383    store(p, vec4<f32>(src.rgb + s.rgb * post.p0.x, src.a));
1384"#
1385);
1386
1387macro_rules! bloom_linear {
1388    () => {
1389        r#"
1390@group(0) @binding(3) var bloom_tex: texture_2d<f32>;
1391@group(0) @binding(4) var bloom_sampler: sampler;
1392
1393fn bloom_uv(p: vec2<u32>) -> vec2<f32> {
1394    let size = textureDimensions(bloom_tex);
1395    let small = min(size, post.extent.zw);
1396    let ratio = select(vec2<f32>(small) / vec2<f32>(size), vec2<f32>(1.0), small == size);
1397    let edge = select(vec2<f32>(1e30), (vec2<f32>(small) - vec2<f32>(0.5)) / vec2<f32>(size), small < size);
1398    return min((vec2<f32>(p) + vec2<f32>(0.5)) / vec2<f32>(src_dim()) * ratio, edge);
1399}
1400"#
1401    };
1402}
1403
1404pub const BLOOM_LINEAR: &str = bloom_linear!();
1405
1406pub const BLOOM_ADD_LINEAR: &str = shader!(
1407    bloom_linear!(),
1408    r#"
1409    let uv = bloom_uv(p);
1410    let bloom = textureSampleLevel(bloom_tex, bloom_sampler, uv, 0.0);
1411    store(p, vec4<f32>(src.rgb + bloom.rgb * post.p0.x, src.a));
1412"#
1413);
1414
1415pub const BLOOM_RINGS: &str = shader!(
1416    "",
1417    r#"
1418    var acc = vec3<f32>(0.0);
1419    var weight = 0.0;
1420    for (var ring = 0u; ring < 4u; ring = ring + 1u) {
1421        let radius = ring_radius(ring) * post.p0.w;
1422        let gain = ring_gain(ring);
1423        for (var i = 0u; i < 20u; i = i + 1u) {
1424            let a = f32(i) * 6.283185307179586 / 20.0 + f32(ring) * 0.37;
1425            let s = sample_src(f32(p.x) + cos(a) * radius, f32(p.y) + sin(a) * radius);
1426            acc = acc + s.rgb * gain;
1427        }
1428        weight = weight + 20.0 * gain;
1429    }
1430    if (weight > 1e-8) {
1431        store(p, vec4<f32>(acc / weight, 1.0));
1432    } else {
1433        store(p, src);
1434    }
1435"#
1436);
1437
1438pub const BLOOM_RING_DOWN: &str = shader!(
1439    "",
1440    r#"
1441    let factor = max(post.p0.x, 1.0);
1442    let x = f32(p.x) * factor + factor * 0.5 - 0.5;
1443    let y = f32(p.y) * factor + factor * 0.5 - 0.5;
1444    let c = (sample_src(x - 1.0, y - 1.0).rgb + sample_src(x + 1.0, y - 1.0).rgb + sample_src(x - 1.0, y + 1.0).rgb + sample_src(x + 1.0, y + 1.0).rgb) * 0.25;
1445    var v = max(c * post.p0.w - vec3<f32>(post.p0.y), vec3<f32>(0.0));
1446    if (post.p0.z > 0.0) { v = min(v, vec3<f32>(post.p0.z)); }
1447    store(p, vec4<f32>(v, 1.0));
1448"#
1449);
1450
1451pub const HALATION_HOT: &str = shader!(
1452    "",
1453    r#"
1454    let factor = post.p0.y;
1455    let x = f32(p.x) * factor + factor * 0.5 - 0.5;
1456    let y = f32(p.y) * factor + factor * 0.5 - 0.5;
1457    let c = (sample_src(x - 1.0, y - 1.0).rgb + sample_src(x + 1.0, y - 1.0).rgb + sample_src(x - 1.0, y + 1.0).rgb + sample_src(x + 1.0, y + 1.0).rgb) * 0.25;
1458    let h = max(luma(c) - post.p0.x, 0.0);
1459    store(p, vec4<f32>(h, h * 0.25, h * 0.05, src.a));
1460"#
1461);
1462
1463pub const HALATION_ADD: &str = shader!(
1464    bloomtex,
1465    r#"
1466    if (abs(post.p0.x) < 1e-8) { store(p, src); return; }
1467    let full = max(vec2<f32>(src_dim()), vec2<f32>(1.0));
1468    let small = vec2<f32>(bloom_dim());
1469    let u = (f32(p.x) + 0.5) / full.x;
1470    let v = (f32(p.y) + 0.5) / full.y;
1471    let s = sample_bloom(u * small.x - 0.5, v * small.y - 0.5);
1472    store(p, vec4<f32>(src.rgb + s.rgb * post.p0.x, src.a));
1473"#
1474);