mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 11:07:54 -06:00
987 lines
33 KiB
WebGPU Shading Language
987 lines
33 KiB
WebGPU Shading Language
// Compositing and effect kernels (rgba32float output). Parameter layouts are documented next to
|
||
// each entry point and mirrored by the Rust dispatch code (src/ops.rs, src/effects.rs).
|
||
|
||
@group(0) @binding(3) var out: texture_storage_2d<rgba32float, write>;
|
||
|
||
fn out_dims() -> vec2<i32> {
|
||
return vec2<i32>(textureDimensions(out));
|
||
}
|
||
|
||
// ---------------------------------------------------------------- compositing
|
||
|
||
// Warp `src` by the inverse matrix (output pixel → source pixel) and blend over `aux`
|
||
// (raster::composite_warp / accumulate_warp).
|
||
// u[0] = (mode, sampling, seed, flags: 1 accumulate, 2 clear outside bounds)
|
||
// f[0..3] = inverse rows; f[3] = (opacity, weight); f[4] = bounds (x0, y0, x1, y1)
|
||
@compute @workgroup_size(16, 16)
|
||
fn warp_blend(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let x = i32(gid.x);
|
||
let y = i32(gid.y);
|
||
if (x >= dims.x || y >= dims.y) {
|
||
return;
|
||
}
|
||
let mode = P.u[0].x;
|
||
let sampling = P.u[0].y;
|
||
let seed = P.u[0].z;
|
||
let flags = P.u[0].w;
|
||
let b = P.f[4];
|
||
var d = textureLoad(aux, vec2<i32>(x, y), 0);
|
||
let fx = f32(x) + 0.5;
|
||
let fy = f32(y) + 0.5;
|
||
if (fx < b.x || fy < b.y || fx > b.z || fy > b.w) {
|
||
if ((flags & 2u) != 0u) {
|
||
d = vec4<f32>(0.0);
|
||
}
|
||
store_q(vec2<i32>(x, y), d, P.f[3].z);
|
||
return;
|
||
}
|
||
let r0 = P.f[0];
|
||
let r1 = P.f[1];
|
||
let r2 = P.f[2];
|
||
var px = r0.x * fx + r0.y * fy + r0.z;
|
||
var py = r1.x * fx + r1.y * fy + r1.z;
|
||
let w = r2.x * fx + r2.y * fy + r2.z;
|
||
if (w != 1.0 && w != 0.0) {
|
||
px = px / w;
|
||
py = py / w;
|
||
}
|
||
let sz = vec2<f32>(textureDimensions(src));
|
||
let stencil = is_stencil(mode);
|
||
if (px < -1.0 || py < -1.0 || px > sz.x + 1.0 || py > sz.y + 1.0) {
|
||
if (stencil && (flags & 1u) == 0u) {
|
||
d = blend_pixel(mode, d, vec4<f32>(0.0), 0.5);
|
||
}
|
||
store_q(vec2<i32>(x, y), d, P.f[3].z);
|
||
return;
|
||
}
|
||
var s = sample(src, sampling, px, py);
|
||
if ((flags & 1u) != 0u) {
|
||
if (s.w > 0.0) {
|
||
d = d + s * P.f[3].y;
|
||
}
|
||
store_q(vec2<i32>(x, y), d, P.f[3].z);
|
||
return;
|
||
}
|
||
if (s.w <= 0.0 && !stencil) {
|
||
store_q(vec2<i32>(x, y), d, P.f[3].z);
|
||
return;
|
||
}
|
||
let op = P.f[3].x;
|
||
if (op < 1.0) {
|
||
s = s * op;
|
||
}
|
||
var n = 0.5;
|
||
if (mode == 1u || mode == 2u) {
|
||
n = hash_noise(u32(x), u32(y), seed);
|
||
}
|
||
store_q(vec2<i32>(x, y), blend_pixel(mode, d, s, n), P.f[3].z);
|
||
}
|
||
|
||
// Blend `src` (same size) onto `aux` (Image::blend_from).
|
||
// u[0] = (mode, _, seed); f[0].x = opacity
|
||
@compute @workgroup_size(16, 16)
|
||
fn blend_full(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
let mode = P.u[0].x;
|
||
let d = textureLoad(aux, p, 0);
|
||
var s = textureLoad(src, p, 0);
|
||
if (s.w <= 0.0 && !is_stencil(mode)) {
|
||
store_q(p, d, P.f[0].y);
|
||
return;
|
||
}
|
||
s = s * P.f[0].x;
|
||
var n = 0.5;
|
||
if (mode == 1u || mode == 2u) {
|
||
n = hash_noise(gid.x, gid.y, P.u[0].z);
|
||
}
|
||
store_q(p, blend_pixel(mode, d, s, n), P.f[0].y);
|
||
}
|
||
|
||
// Track matte: `src` × matte factor of `aux`. u[0].x = 0 alpha, 1 alpha inverted, 2 luma,
|
||
// 3 luma inverted.
|
||
@compute @workgroup_size(16, 16)
|
||
fn matte(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
let q = textureLoad(aux, p, 0);
|
||
var k: f32;
|
||
switch P.u[0].x {
|
||
case 0u: { k = q.w; }
|
||
case 1u: { k = 1.0 - q.w; }
|
||
case 2u: { k = luminance(q.xyz); }
|
||
default: { k = 1.0 - luminance(q.xyz); }
|
||
}
|
||
k = clamp(k, 0.0, 1.0);
|
||
textureStore(out, p, textureLoad(src, p, 0) * k);
|
||
}
|
||
|
||
// Preserve Transparency: `src` × alpha of `aux`.
|
||
@compute @workgroup_size(16, 16)
|
||
fn preserve(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
textureStore(out, p, textureLoad(src, p, 0) * textureLoad(aux, p, 0).w);
|
||
}
|
||
|
||
// Knockout: `src` × (1 − alpha of `aux`).
|
||
@compute @workgroup_size(16, 16)
|
||
fn knockout(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
let keep = 1.0 - clamp(textureLoad(aux, p, 0).w, 0.0, 1.0);
|
||
textureStore(out, p, textureLoad(src, p, 0) * keep);
|
||
}
|
||
|
||
// Styled layer finish: canvas `src` moves toward `aux` by the opacity, per enabled channel.
|
||
// u[0].xyz = R/G/B switches; f[0].x = opacity.
|
||
@compute @workgroup_size(16, 16)
|
||
fn channel_mix(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
let c = textureLoad(src, p, 0);
|
||
let t = textureLoad(aux, p, 0);
|
||
let op = P.f[0].x;
|
||
var v = t;
|
||
if (P.u[0].x == 0u) {
|
||
v.x = c.x;
|
||
}
|
||
if (P.u[0].y == 0u) {
|
||
v.y = c.y;
|
||
}
|
||
if (P.u[0].z == 0u) {
|
||
v.z = c.z;
|
||
}
|
||
textureStore(out, p, c + (v - c) * op);
|
||
}
|
||
|
||
// 8/16 bpc: clamp premultiplied pixels to 0..1 (colour ≤ alpha) and round. f[0].x = levels.
|
||
@compute @workgroup_size(16, 16)
|
||
fn quantize(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
textureStore(out, p, quantize_px(textureLoad(src, p, 0), P.f[0].x));
|
||
}
|
||
|
||
fn quantize_px(s: vec4<f32>, levels: f32) -> vec4<f32> {
|
||
let inv = 1.0 / levels;
|
||
let a = round_away(clamp(s.w, 0.0, 1.0) * levels) * inv;
|
||
var o = vec4<f32>(0.0, 0.0, 0.0, a);
|
||
o.x = round_away(clamp(s.x, 0.0, a) * levels) * inv;
|
||
o.y = round_away(clamp(s.y, 0.0, a) * levels) * inv;
|
||
o.z = round_away(clamp(s.z, 0.0, a) * levels) * inv;
|
||
return o;
|
||
}
|
||
|
||
// Store a compositing result, clamped and quantised to `levels` when > 0 (8/16 bpc: the comp
|
||
// after a layer, fused into the layer's composite).
|
||
fn store_q(p: vec2<i32>, v: vec4<f32>, levels: f32) {
|
||
if (levels > 0.0) {
|
||
textureStore(out, p, quantize_px(v, levels));
|
||
} else {
|
||
textureStore(out, p, v);
|
||
}
|
||
}
|
||
|
||
// Colour conversion on straight colour (render::color::convert).
|
||
// u[0] = (decode curve, has matrix, encode curve); f[0..3] = matrix rows.
|
||
@compute @workgroup_size(16, 16)
|
||
fn convert(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
let s = textureLoad(src, p, 0);
|
||
let a = s.w;
|
||
if (a <= 0.0) {
|
||
textureStore(out, p, s);
|
||
return;
|
||
}
|
||
var c = s.xyz / a;
|
||
let dc = P.u[0].x;
|
||
if (dc != 0u) {
|
||
c = vec3<f32>(curve_decode(dc, c.x), curve_decode(dc, c.y), curve_decode(dc, c.z));
|
||
}
|
||
if (P.u[0].y != 0u) {
|
||
let m0 = P.f[0].xyz;
|
||
let m1 = P.f[1].xyz;
|
||
let m2 = P.f[2].xyz;
|
||
c = vec3<f32>(m0.x * c.x + m0.y * c.y + m0.z * c.z, m1.x * c.x + m1.y * c.y + m1.z * c.z, m2.x * c.x + m2.y * c.y + m2.z * c.z);
|
||
}
|
||
let ec = P.u[0].z;
|
||
if (ec != 0u) {
|
||
c = vec3<f32>(curve_encode(ec, c.x), curve_encode(ec, c.y), curve_encode(ec, c.z));
|
||
}
|
||
textureStore(out, p, vec4<f32>(c * a, a));
|
||
}
|
||
|
||
// Box-filter halving for minification (raster::warp::half).
|
||
@compute @workgroup_size(16, 16)
|
||
fn half(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
let sx = 2 * p.x;
|
||
let sy = 2 * p.y;
|
||
var acc = vec4<f32>(0.0);
|
||
acc += tex_get_clamped(src, sx, sy);
|
||
acc += tex_get_clamped(src, sx + 1, sy);
|
||
acc += tex_get_clamped(src, sx, sy + 1);
|
||
acc += tex_get_clamped(src, sx + 1, sy + 1);
|
||
textureStore(out, p, acc * 0.25);
|
||
}
|
||
|
||
// ---------------------------------------------------------------- blurs (raster::blur)
|
||
|
||
fn line_get(horizontal: bool, line: i32, i: i32, n: i32, repeat: bool) -> vec4<f32> {
|
||
var j = i;
|
||
if (i < 0 || i >= n) {
|
||
if (!repeat) {
|
||
return vec4<f32>(0.0);
|
||
}
|
||
j = clamp(i, 0, n - 1);
|
||
}
|
||
if (horizontal) {
|
||
return textureLoad(src, vec2<i32>(j, line), 0);
|
||
}
|
||
return textureLoad(src, vec2<i32>(line, j), 0);
|
||
}
|
||
|
||
// One box pass of radius r along rows (horizontal) or columns, as running sums over blocks of
|
||
// `block` pixels (each block restarts its sum, as the CPU's vertical pass does).
|
||
fn box_pass(gid: vec3<u32>, horizontal: bool) {
|
||
let dims = out_dims();
|
||
let r = i32(P.u[0].x);
|
||
let repeat = P.u[0].y != 0u;
|
||
let block = i32(P.u[0].z);
|
||
var lines = dims.x;
|
||
var n = dims.y;
|
||
if (horizontal) {
|
||
lines = dims.y;
|
||
n = dims.x;
|
||
}
|
||
let line = i32(gid.x);
|
||
let b0 = i32(gid.y) * block;
|
||
if (line >= lines || b0 >= n) {
|
||
return;
|
||
}
|
||
let norm = 1.0 / f32(2 * r + 1);
|
||
var acc = vec4<f32>(0.0);
|
||
for (var i = b0 - r; i <= b0 + r; i++) {
|
||
acc += line_get(horizontal, line, i, n, repeat);
|
||
}
|
||
let b1 = min(b0 + block, n);
|
||
for (var i = b0; i < b1; i++) {
|
||
if (horizontal) {
|
||
textureStore(out, vec2<i32>(i, line), acc * norm);
|
||
} else {
|
||
textureStore(out, vec2<i32>(line, i), acc * norm);
|
||
}
|
||
let add = line_get(horizontal, line, i + r + 1, n, repeat);
|
||
let sub = line_get(horizontal, line, i - r, n, repeat);
|
||
acc += add - sub;
|
||
}
|
||
}
|
||
|
||
// u[0] = (radius, repeat edges, block); dispatched over (line, block).
|
||
@compute @workgroup_size(64, 1)
|
||
fn box_h(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
box_pass(gid, true);
|
||
}
|
||
|
||
@compute @workgroup_size(64, 1)
|
||
fn box_v(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
box_pass(gid, false);
|
||
}
|
||
|
||
// Directional blur (raster::directional_blur). f[0] = (dx, dy, length, samples).
|
||
@compute @workgroup_size(16, 16)
|
||
fn directional(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
let dx = P.f[0].x;
|
||
let dy = P.f[0].y;
|
||
let len = P.f[0].z;
|
||
let n = i32(P.f[0].w);
|
||
var acc = vec4<f32>(0.0);
|
||
for (var i = 0; i < n; i++) {
|
||
let t = (f32(i) / f32(n - 1) - 0.5) * len;
|
||
acc += sample_bilinear(src, f32(p.x) + 0.5 + dx * t, f32(p.y) + 0.5 + dy * t);
|
||
}
|
||
textureStore(out, p, acc / f32(n));
|
||
}
|
||
|
||
// ---------------------------------------------------------------- Glow / Drop Shadow
|
||
|
||
// Glow's A & B gradient position (effects::misc::glow_ab_t). f[0].yzw = loops, phase, midpoint;
|
||
// u[0].z = colour looping.
|
||
fn glow_ab_t(l: f32) -> f32 {
|
||
let u = clamp(l, 0.0, 1.0) * max(P.f[0].y, 0.0) + P.f[0].z;
|
||
var t = u - floor(u);
|
||
if (u > 0.0 && t <= 0.0) {
|
||
t = 1.0;
|
||
}
|
||
let tri = 1.0 - abs(2.0 * t - 1.0);
|
||
let lp = P.u[0].z;
|
||
if (lp == 1u) {
|
||
t = 1.0 - t;
|
||
} else if (lp == 3u) {
|
||
t = 1.0 - tri;
|
||
} else if (lp != 0u) {
|
||
t = tri;
|
||
}
|
||
let m = clamp(P.f[0].w, 0.01, 0.99);
|
||
return pow(max(t, 0.0), log(0.5) / log(m));
|
||
}
|
||
|
||
// Glow bright pass. u[0].x = A & B colours; u[0].y = based on alpha; f[0].x = threshold;
|
||
// f[1] = colour A; f[2] = colour B.
|
||
@compute @workgroup_size(16, 16)
|
||
fn glow_bright(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
let px = textureLoad(src, p, 0);
|
||
let a = px.w;
|
||
if (a <= 0.0) {
|
||
textureStore(out, p, vec4<f32>(0.0));
|
||
return;
|
||
}
|
||
let thr = P.f[0].x;
|
||
var l = luminance(px.xyz / a);
|
||
if (P.u[0].y != 0u) {
|
||
l = a;
|
||
}
|
||
let k = clamp((l - thr) / max(1.0 - thr, 1e-3), 0.0, 1.0);
|
||
if (P.u[0].x != 0u) {
|
||
let t = glow_ab_t(l);
|
||
let ca = P.f[1].xyz;
|
||
let cb = P.f[2].xyz;
|
||
let c = ca + (cb - ca) * t;
|
||
textureStore(out, p, vec4<f32>(c * k * a, k * a));
|
||
} else {
|
||
textureStore(out, p, px * k);
|
||
}
|
||
}
|
||
|
||
// Glow composite: original `src` with the blurred glow `aux`. u[0].x = 0 on top, 1 behind,
|
||
// 2 none (glow only); f[0].x = intensity.
|
||
@compute @workgroup_size(16, 16)
|
||
fn glow_combine(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
var o = textureLoad(src, p, 0);
|
||
let g = textureLoad(aux, p, 0) * P.f[0].x;
|
||
if (P.u[0].x == 2u) {
|
||
o = g;
|
||
} else if (P.u[0].x == 1u) {
|
||
let k = 1.0 - o.w;
|
||
o = o + g * k;
|
||
} else {
|
||
o = vec4<f32>(o.xyz + g.xyz, min(o.w + g.w * (1.0 - o.w), 1.0));
|
||
}
|
||
o.w = clamp(o.w, 0.0, 1.0);
|
||
textureStore(out, p, o);
|
||
}
|
||
|
||
// Drop shadow silhouette. f[0] = shadow colour; f[1] = (opacity, dx, dy).
|
||
@compute @workgroup_size(16, 16)
|
||
fn shadow_make(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
let c = P.f[0];
|
||
let a = sample_bilinear(src, f32(p.x) + 0.5 - P.f[1].y, f32(p.y) + 0.5 - P.f[1].z).w * P.f[1].x * c.w;
|
||
textureStore(out, p, vec4<f32>(c.xyz * a, a));
|
||
}
|
||
|
||
// The layer over its shadow: `aux` (original) over `src` (shadow).
|
||
@compute @workgroup_size(16, 16)
|
||
fn shadow_combine(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
let s = textureLoad(src, p, 0);
|
||
let o = textureLoad(aux, p, 0);
|
||
textureStore(out, p, o + s * (1.0 - o.w));
|
||
}
|
||
|
||
// ---------------------------------------------------------------- per-pixel effects
|
||
|
||
fn mix3(a: vec3<f32>, b: vec3<f32>, t: f32) -> vec3<f32> {
|
||
return a + (b - a) * t;
|
||
}
|
||
|
||
// Curves: data = [n, xs.., ys.., ms.., lut (1025)] per channel at offsets f[c].
|
||
fn curve_exact(base: u32, x: f32) -> f32 {
|
||
let n = u32(data[base]);
|
||
let xs = base + 1u;
|
||
let ys = xs + n;
|
||
let ms = ys + n;
|
||
if (x <= data[xs]) {
|
||
return data[ys] + (x - data[xs]) * data[ms];
|
||
}
|
||
if (x >= data[xs + n - 1u]) {
|
||
return data[ys + n - 1u] + (x - data[xs + n - 1u]) * data[ms + n - 1u];
|
||
}
|
||
// partition_point(|v| v <= x) - 1, clamped to n - 2.
|
||
var k = 0u;
|
||
for (var i = 0u; i < n; i++) {
|
||
if (data[xs + i] <= x) {
|
||
k = i + 1u;
|
||
}
|
||
}
|
||
k = min(max(k, 1u) - 1u, n - 2u);
|
||
let h = data[xs + k + 1u] - data[xs + k];
|
||
let t = (x - data[xs + k]) / h;
|
||
let t2 = t * t;
|
||
let t3 = t * t * t;
|
||
let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
|
||
let h10 = t3 - 2.0 * t2 + t;
|
||
let h01 = -2.0 * t3 + 3.0 * t2;
|
||
let h11 = t3 - t2;
|
||
return h00 * data[ys + k] + h10 * h * data[ms + k] + h01 * data[ys + k + 1u] + h11 * h * data[ms + k + 1u];
|
||
}
|
||
|
||
fn curve_eval(base_f: f32, x: f32) -> f32 {
|
||
if (base_f < 0.0) {
|
||
return x;
|
||
}
|
||
let base = u32(base_f);
|
||
if (!(x >= 0.0 && x <= 1.0)) {
|
||
return curve_exact(base, x);
|
||
}
|
||
let n = u32(data[base]);
|
||
let lut = base + 1u + 3u * n;
|
||
let f = x * 1024.0;
|
||
let i = min(u32(f), 1023u);
|
||
let t = f - f32(i);
|
||
return data[lut + i] + (data[lut + i + 1u] - data[lut + i]) * t;
|
||
}
|
||
|
||
// Fractal Noise lattice (effects::generate::lattice / value_noise).
|
||
fn lattice(ix: i32, iy: i32, iz: i32, seed: u32) -> f32 {
|
||
var h = (bitcast<u32>(ix) * 0x27d4eb2du) ^ (bitcast<u32>(iy) * 0x165667b1u) ^ (bitcast<u32>(iz) * 0x9e3779b9u) ^ (seed * 0x85ebca6bu);
|
||
h = h ^ (h >> 15u);
|
||
h = h * 0x2c1b3c6du;
|
||
h = h ^ (h >> 12u);
|
||
h = h * 0x297a2d39u;
|
||
h = h ^ (h >> 15u);
|
||
return f32(h & 0xffffu) / 65535.0;
|
||
}
|
||
|
||
fn fade(t: f32) -> f32 {
|
||
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
|
||
}
|
||
|
||
fn value_noise(x: f32, y: f32, z: f32, seed: u32) -> f32 {
|
||
let x0 = floor(x);
|
||
let y0 = floor(y);
|
||
let z0 = floor(z);
|
||
let fx = fade(x - x0);
|
||
let fy = fade(y - y0);
|
||
let fz = fade(z - z0);
|
||
let ix = i32(x0);
|
||
let iy = i32(y0);
|
||
let iz = i32(z0);
|
||
let c000 = lattice(ix, iy, iz, seed);
|
||
let c100 = lattice(ix + 1, iy, iz, seed);
|
||
let c010 = lattice(ix, iy + 1, iz, seed);
|
||
let c110 = lattice(ix + 1, iy + 1, iz, seed);
|
||
let c001 = lattice(ix, iy, iz + 1, seed);
|
||
let c101 = lattice(ix + 1, iy, iz + 1, seed);
|
||
let c011 = lattice(ix, iy + 1, iz + 1, seed);
|
||
let c111 = lattice(ix + 1, iy + 1, iz + 1, seed);
|
||
let l00 = c000 + (c100 - c000) * fx;
|
||
let l01 = c010 + (c110 - c010) * fx;
|
||
let a = l00 + (l01 - l00) * fy;
|
||
let l10 = c001 + (c101 - c001) * fx;
|
||
let l11 = c011 + (c111 - c011) * fx;
|
||
let b = l10 + (l11 - l10) * fy;
|
||
return a + (b - a) * fz;
|
||
}
|
||
|
||
// generate::put: blend a generated straight colour (alpha ga) over the pixel.
|
||
fn put(px: vec4<f32>, g: vec3<f32>, ga: f32, blend_orig: f32) -> vec4<f32> {
|
||
let g2 = vec4<f32>(g * ga, ga);
|
||
let k = 1.0 - blend_orig;
|
||
return px * blend_orig + g2 * k;
|
||
}
|
||
|
||
// Straight-colour map helper: skips transparent pixels (Image::map_straight).
|
||
fn straight(px: vec4<f32>) -> vec3<f32> {
|
||
return px.xyz / px.w;
|
||
}
|
||
|
||
// u[0].x = op:
|
||
// 1 Tint f[0] = black, f[1] = white, f[2].x = amount
|
||
// 2 Bright/Contrast f[0] = (brightness, k)
|
||
// 3 Hue/Saturation u[0].y = colorize; f[0] = (hue, sat, light); f[1] = (ch, cs, cl)
|
||
// 4 Levels f[0] = (ib, iw, g, ob); f[1] = (ow); u[0].y = clip
|
||
// 5 Exposure f[0] = (e, offset, gamma)
|
||
// 6 Invert u[0].y = channel; f[0].x = blend
|
||
// 7 Fill f[0] = colour; f[1] = (opacity); u[0].y = invert
|
||
// 8 Curves f[0] = data offsets (rgb, red, green, blue), f[1].x = alpha (−1 = none)
|
||
// 9 Gradient Ramp f[0] = (sx, sy, ex, ey); f[1] = start colour; f[2] = end colour;
|
||
// f[3] = (len2, scatter, blend); u[0] = (_, radial, seed)
|
||
// 10 Fractal Noise f[0] = (contrast, brightness, scale x, frac); f[1] = (ox, oy, sr, cr);
|
||
// f[2] = (evo, blend, opacity, scale y); f[3] = (sub influence, 1 / sub scaling);
|
||
// u[0] = (_, kind, n_oct, seed); u[1] = (invert, overflow, normal blend)
|
||
@compute @workgroup_size(16, 16)
|
||
fn pointwise(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
var px = textureLoad(src, p, 0);
|
||
let a = px.w;
|
||
let op = P.u[0].x;
|
||
switch op {
|
||
case 1u: {
|
||
if (a > 0.0) {
|
||
let c = straight(px);
|
||
let l = clamp(luminance(c), 0.0, 1.0);
|
||
let blk = P.f[0].xyz;
|
||
let wht = P.f[1].xyz;
|
||
let t = blk + (wht - blk) * l;
|
||
px = vec4<f32>(mix3(c, t, P.f[2].x) * a, a);
|
||
}
|
||
}
|
||
case 2u: {
|
||
if (a > 0.0) {
|
||
let c = straight(px);
|
||
let br = P.f[0].x;
|
||
let k = P.f[0].y;
|
||
let ct = P.f[0].z;
|
||
if (P.u[0].y == 1u) {
|
||
px = vec4<f32>(max((c + br - 0.5) * k + 0.5, vec3<f32>(0.0)) * a, a);
|
||
} else if (P.u[0].y == 2u) {
|
||
// Brightness & Contrast tone curves (effects crate bc_modern).
|
||
var o = c;
|
||
let pb = exp2(-br);
|
||
let g = exp2(2.0 * ct);
|
||
for (var i = 0; i < 3; i++) {
|
||
if (c[i] < 1.0) {
|
||
let v = powz(max(c[i], 0.0), pb);
|
||
if (v < 0.5) {
|
||
o[i] = 0.5 * powz(2.0 * v, g);
|
||
} else {
|
||
o[i] = 1.0 - 0.5 * powz(max(2.0 - 2.0 * v, 0.0), g);
|
||
}
|
||
}
|
||
}
|
||
px = vec4<f32>(o * a, a);
|
||
}
|
||
}
|
||
}
|
||
case 3u: {
|
||
if (a > 0.0) {
|
||
let c = straight(px);
|
||
let hsl = rgb_to_hsl(c);
|
||
var h = hsl.x;
|
||
var s = hsl.y;
|
||
var l = hsl.z;
|
||
if (P.u[0].y != 0u) {
|
||
let cl = P.f[1].z;
|
||
var k = l;
|
||
if (cl > 0.0) {
|
||
k = 1.0 - l;
|
||
}
|
||
h = P.f[1].x;
|
||
s = P.f[1].y;
|
||
l = clamp(l + cl * k, 0.0, 1.0);
|
||
} else {
|
||
let hue = P.f[0].x;
|
||
let sat = P.f[0].y;
|
||
let light = P.f[0].z;
|
||
var s2: f32;
|
||
if (sat >= 0.0) {
|
||
s2 = s + (1.0 - s) * sat * min(s, 1.0);
|
||
} else {
|
||
s2 = s * (1.0 + sat);
|
||
}
|
||
var l2: f32;
|
||
if (light >= 0.0) {
|
||
l2 = l + (1.0 - l) * light;
|
||
} else {
|
||
l2 = l * (1.0 + light);
|
||
}
|
||
h = fract_euclid(h + hue);
|
||
s = clamp(s2, 0.0, 1.0);
|
||
l = l2;
|
||
}
|
||
px = vec4<f32>(hsl_to_rgb(h, s, l) * a, a);
|
||
}
|
||
}
|
||
case 4u: {
|
||
if (a > 0.0) {
|
||
let c = straight(px);
|
||
let ib = P.f[0].x;
|
||
let iw = P.f[0].y;
|
||
let g = P.f[0].z;
|
||
let ob = P.f[0].w;
|
||
let ow = P.f[1].x;
|
||
var o = vec3<f32>(0.0);
|
||
for (var i = 0; i < 3; i++) {
|
||
var t = (c[i] - ib) / max(iw - ib, 1e-6);
|
||
if (P.u[0].y != 0u) {
|
||
t = max(t, 0.0);
|
||
}
|
||
if (P.u[0].z != 0u) {
|
||
t = min(t, 1.0);
|
||
}
|
||
t = powz(max(t, 0.0), 1.0 / g);
|
||
o[i] = ob + (ow - ob) * t;
|
||
}
|
||
px = vec4<f32>(o * a, a);
|
||
}
|
||
}
|
||
case 5u: {
|
||
if (a > 0.0) {
|
||
let c = straight(px);
|
||
var o = vec3<f32>(0.0);
|
||
for (var i = 0; i < 3; i++) {
|
||
// Per-channel gain, offset, gamma; negative values are mirrored.
|
||
if (P.u[0].y != 0u) {
|
||
let x = c[i] * P.f[0][i] + P.f[1][i];
|
||
o[i] = sign(x) * powz(abs(x), 1.0 / P.f[2][i]);
|
||
} else {
|
||
let lin = srgb_to_linear(max(c[i], 0.0));
|
||
let x = lin * P.f[0][i] + P.f[1][i];
|
||
o[i] = sign(x) * linear_to_srgb(powz(abs(x), 1.0 / P.f[2][i]));
|
||
}
|
||
}
|
||
px = vec4<f32>(o * a, a);
|
||
}
|
||
}
|
||
case 6u: {
|
||
let ch = P.u[0].y;
|
||
let blend = P.f[0].x;
|
||
if (ch == 4u) {
|
||
let na = 1.0 - a;
|
||
var k = 0.0;
|
||
if (a > 0.0) {
|
||
k = na / a;
|
||
}
|
||
let inv = vec4<f32>(px.xyz * k, na);
|
||
px = px + (inv - px) * blend;
|
||
} else if (a > 0.0) {
|
||
let s = straight(px);
|
||
var o = s;
|
||
switch ch {
|
||
case 1u: { o.x = 1.0 - s.x; }
|
||
case 2u: { o.y = 1.0 - s.y; }
|
||
case 3u: { o.z = 1.0 - s.z; }
|
||
default: { o = 1.0 - s; }
|
||
}
|
||
px = vec4<f32>((s + (o - s) * blend) * a, a);
|
||
}
|
||
}
|
||
case 7u: {
|
||
let c = P.f[0];
|
||
let invert = P.u[0].y != 0u;
|
||
let opacity = P.f[1].x;
|
||
var fa = px.w;
|
||
if (invert) {
|
||
fa = 1.0 - px.w;
|
||
}
|
||
let f = c.xyz * fa;
|
||
let rgb = px.xyz + (f - px.xyz) * opacity;
|
||
var alpha = px.w;
|
||
if (invert) {
|
||
alpha = px.w + (fa - px.w) * opacity;
|
||
}
|
||
px = vec4<f32>(rgb, alpha);
|
||
}
|
||
case 8u: {
|
||
// effects::color2::map_ca: transparent pixels only change through the alpha curve.
|
||
if (a <= 0.0 && px.x == 0.0 && px.y == 0.0 && px.z == 0.0) {
|
||
let na = curve_eval(P.f[1].x, 0.0);
|
||
if (na > 0.0) {
|
||
px = vec4<f32>(0.0, 0.0, 0.0, clamp(na, 0.0, 1.0));
|
||
}
|
||
} else {
|
||
var c = vec3<f32>(0.0);
|
||
var aa = max(a, 0.0);
|
||
if (a > 1e-6) {
|
||
c = px.xyz / a;
|
||
aa = a;
|
||
}
|
||
var o = vec3<f32>(0.0);
|
||
for (var i = 0; i < 3; i++) {
|
||
o[i] = curve_eval(P.f[0][i + 1], curve_eval(P.f[0].x, c[i]));
|
||
}
|
||
let na = clamp(curve_eval(P.f[1].x, aa), 0.0, 1.0);
|
||
px = vec4<f32>(o * na, na);
|
||
}
|
||
}
|
||
case 9u: {
|
||
let s = P.f[0].xy;
|
||
let e = P.f[0].zw;
|
||
let d = e - s;
|
||
let v = vec2<f32>(f32(p.x) + 0.5, f32(p.y) + 0.5) - s;
|
||
let len2 = P.f[3].x;
|
||
var t: f32;
|
||
if (P.u[0].y != 0u) {
|
||
t = sqrt((v.x * v.x + v.y * v.y) / len2);
|
||
} else {
|
||
t = (v.x * d.x + v.y * d.y) / len2;
|
||
}
|
||
let scatter = P.f[3].y;
|
||
if (scatter > 0.0) {
|
||
t += (hash_noise(gid.x, gid.y, P.u[0].z) - 0.5) * scatter;
|
||
}
|
||
t = clamp(t, 0.0, 1.0);
|
||
let c0 = P.f[1].xyz;
|
||
let c1 = P.f[2].xyz;
|
||
px = put(px, c0 + (c1 - c0) * t, 1.0, P.f[3].z);
|
||
}
|
||
case 10u: {
|
||
let contrast = P.f[0].x;
|
||
let brightness = P.f[0].y;
|
||
let scale = P.f[0].z;
|
||
let scale_y = P.f[2].w;
|
||
let infl = P.f[3].x;
|
||
let fmul = P.f[3].y;
|
||
let frac = P.f[0].w;
|
||
let ox = P.f[1].x;
|
||
let oy = P.f[1].y;
|
||
let sr = P.f[1].z;
|
||
let cr = P.f[1].w;
|
||
let evo = P.f[2].x;
|
||
let kind = P.u[0].y;
|
||
let n_oct = P.u[0].z;
|
||
let seed = P.u[0].w;
|
||
let dx = f32(p.x) + 0.5 - ox;
|
||
let dy = f32(p.y) + 0.5 - oy;
|
||
let u = (dx * cr + dy * sr) / scale;
|
||
let v = (-dx * sr + dy * cr) / scale_y;
|
||
var sum = 0.0;
|
||
var amp = 1.0;
|
||
var norm = 0.0;
|
||
var f = 1.0;
|
||
for (var o = 0u; o < n_oct; o++) {
|
||
var w = 1.0;
|
||
if (o + 1u == n_oct && frac > 0.0) {
|
||
w = frac;
|
||
}
|
||
var n = value_noise(u * f, v * f, evo + f32(o) * 7.31, seed + o);
|
||
let s = n * 2.0 - 1.0;
|
||
if (kind == 1u) {
|
||
n = 1.0 - s * s;
|
||
} else if (kind == 2u) {
|
||
n = 1.0 - abs(s);
|
||
}
|
||
sum += n * amp * w;
|
||
norm += amp * w;
|
||
amp *= infl;
|
||
f *= fmul;
|
||
}
|
||
var val = sum / max(norm, 1e-6);
|
||
val = (val - 0.5) * contrast + 0.5 + brightness;
|
||
// Overflow: Clip, Soft Clamp, Wrap Back, Allow HDR Results.
|
||
let ov = P.u[1].y;
|
||
if (ov == 0u) {
|
||
val = clamp(val, 0.0, 1.0);
|
||
} else if (ov == 1u) {
|
||
val = clamp(0.5 + 0.5 * tanh(2.0 * (val - 0.5)) / tanh(1.0), 0.0, 1.0);
|
||
} else if (ov == 2u) {
|
||
let t = val - 2.0 * floor(val / 2.0);
|
||
val = select(t, 2.0 - t, t > 1.0);
|
||
}
|
||
if (P.u[1].x != 0u) {
|
||
val = 1.0 - val;
|
||
}
|
||
let op = P.f[2].z;
|
||
if (P.u[1].z == 1u) {
|
||
// Normal: the noise over the layer at Opacity, then Blend With Original.
|
||
let over = vec4<f32>(vec3<f32>(val) * op, op) + px * (1.0 - op);
|
||
px = px * P.f[2].y + over * (1.0 - P.f[2].y);
|
||
} else {
|
||
px = put(px, vec3<f32>(val), op, P.f[2].y);
|
||
}
|
||
}
|
||
default: {}
|
||
}
|
||
textureStore(out, p, px);
|
||
}
|
||
|
||
// Adjustment layer finish (Renderer::draw_adjustment): canvas `src` moves toward the adjusted
|
||
// comp `aux` by the footprint's alpha × opacity. The footprint is `data` (RGBA f32 rows of
|
||
// u[0].x pixels); f[0].x = opacity.
|
||
@compute @workgroup_size(16, 16)
|
||
fn adjust_mix(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
let c = textureLoad(src, p, 0);
|
||
let a = textureLoad(aux, p, 0);
|
||
let k = data[(gid.y * P.u[0].x + gid.x) * 4u + 3u] * P.f[0].x;
|
||
textureStore(out, p, c + (a - c) * k);
|
||
}
|
||
|
||
// ---------------------------------------------------------------- Classic 3D bokeh depth of field
|
||
// (render::three_d::bokeh: boost_highlights, row prefix sums, span gathers per blur level
|
||
// blended by each pixel's radius.)
|
||
|
||
// Highlight boost: f[0] = (gain, threshold, saturation).
|
||
@compute @workgroup_size(16, 16)
|
||
fn bokeh_boost(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
var c = textureLoad(src, p, 0);
|
||
let gain = P.f[0].x;
|
||
let thr = clamp(P.f[0].y, 0.0, 1.0);
|
||
let satk = P.f[0].z;
|
||
if (c.w > 0.0 && gain > 0.0) {
|
||
let a = c.w;
|
||
let rgb = c.xyz / a;
|
||
let l = 0.2126 * rgb.x + 0.7152 * rgb.y + 0.0722 * rgb.z;
|
||
if (!(l < thr || (thr >= 1.0 && l < 1.0))) {
|
||
var over = 1.0;
|
||
if (thr < 1.0) {
|
||
over = clamp((l - thr) / (1.0 - thr), 0.0, 1.0);
|
||
}
|
||
let k = 1.0 + gain * 4.0 * max(over, 0.25);
|
||
let sat = 1.0 + satk;
|
||
let o = max(vec3<f32>(l) + (rgb - vec3<f32>(l)) * sat, vec3<f32>(0.0)) * k;
|
||
c = vec4<f32>(o * a, a);
|
||
}
|
||
}
|
||
textureStore(out, p, c);
|
||
}
|
||
|
||
// Row prefix sums: out is (w + 1) × h, out[x] = sum of src[0..x]. One invocation per row.
|
||
@compute @workgroup_size(64, 1)
|
||
fn bokeh_prefix(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = vec2<i32>(textureDimensions(src));
|
||
let y = i32(gid.x);
|
||
if (y >= dims.y) {
|
||
return;
|
||
}
|
||
var acc = vec4<f32>(0.0);
|
||
textureStore(out, vec2<i32>(0, y), acc);
|
||
for (var x = 0; x < dims.x; x = x + 1) {
|
||
acc = acc + textureLoad(src, vec2<i32>(x, y), 0);
|
||
textureStore(out, vec2<i32>(x + 1, y), acc);
|
||
}
|
||
}
|
||
|
||
fn bokeh_prefix_at(y: i32, t: f32) -> vec4<f32> {
|
||
let n = i32(textureDimensions(src).x) - 1;
|
||
if (t <= 0.0) {
|
||
return vec4<f32>(0.0);
|
||
}
|
||
if (t >= f32(n)) {
|
||
return textureLoad(src, vec2<i32>(n, y), 0);
|
||
}
|
||
let i = i32(floor(t));
|
||
let f = t - f32(i);
|
||
let a = textureLoad(src, vec2<i32>(i, y), 0);
|
||
let b = textureLoad(src, vec2<i32>(i + 1, y), 0);
|
||
return a + (b - a) * f;
|
||
}
|
||
|
||
// One blur level added into the accumulator `aux`: src = prefix sums; data = the radius map
|
||
// (w·h) then spans (dy, x0, x1, weight). u[0] = (span offset, span count, level, levels),
|
||
// u[1].x = 1 for the first level (no accumulator yet); f[0] = (1 / norm, lo, hi).
|
||
@compute @workgroup_size(16, 16)
|
||
fn bokeh_gather(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
var acc = vec4<f32>(0.0);
|
||
if (P.u[1].x == 0u) {
|
||
acc = textureLoad(aux, p, 0);
|
||
}
|
||
let r = data[u32(p.y) * u32(dims.x) + u32(p.x)];
|
||
let k = P.u[0].z;
|
||
let n = P.u[0].w;
|
||
var wgt = 1.0;
|
||
if (n > 1u) {
|
||
let lo = P.f[0].y;
|
||
let hi = P.f[0].z;
|
||
let f = clamp((r - lo) / (hi - lo) * f32(n - 1u), 0.0, f32(n - 1u));
|
||
let kk = min(u32(floor(f)), n - 2u);
|
||
let t = f - f32(kk);
|
||
if (k == kk) {
|
||
wgt = 1.0 - t;
|
||
} else if (k == kk + 1u) {
|
||
wgt = t;
|
||
} else {
|
||
wgt = 0.0;
|
||
}
|
||
}
|
||
if (wgt > 0.0) {
|
||
var g = vec4<f32>(0.0);
|
||
let off = P.u[0].x;
|
||
for (var i = 0u; i < P.u[0].y; i = i + 1u) {
|
||
let s = off + i * 4u;
|
||
let sy = p.y + i32(data[s]);
|
||
if (sy < 0 || sy >= dims.y) {
|
||
continue;
|
||
}
|
||
let hi = bokeh_prefix_at(sy, f32(p.x) + data[s + 2u] + 0.5);
|
||
let lo = bokeh_prefix_at(sy, f32(p.x) + data[s + 1u] + 0.5);
|
||
g = g + (hi - lo) * data[s + 3u];
|
||
}
|
||
acc = acc + g * P.f[0].x * wgt;
|
||
}
|
||
textureStore(out, p, acc);
|
||
}
|
||
|
||
// ---------------------------------------------------------------- pool
|
||
|
||
// Set every pixel to f[0] (clearing reused working textures; NaN poison under test).
|
||
@compute @workgroup_size(16, 16)
|
||
fn fill(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||
let dims = out_dims();
|
||
let p = vec2<i32>(gid.xy);
|
||
if (p.x >= dims.x || p.y >= dims.y) {
|
||
return;
|
||
}
|
||
textureStore(out, p, P.f[0]);
|
||
}
|