// Classic 3D on the GPU: the per-pixel compositor of `render::three_d::compose` (draw_run). // // Every plane covering an output pixel is inverse-mapped through its homography (one per // motion-blur sub-sample), sampled from the plane atlas (`src`), shaded by the comp's lights // (Blinn-Phong, ray-cast shadows against the caster planes) and multiplied by its track matte; // the pixel's fragments are insertion-sorted far → near (coplanar fragments in stack order) and // blended over the canvas (`aux`) with each layer's mode and opacity. // // Bindings: group 0 as every kernel (P, src = atlas, aux = canvas, out, data = track mattes); // group 1 = planes, geometries, lights, caster indices. // P.u[0] = (planes, lights, casters, _) struct Plane { // Atlas rectangle (x, y, w, h) in texels. atlas: vec4, // Buffer offset (x, y), scale, bicubic (0 / 1). tex: vec4, // Output pixel bounds [x0, y0, x1, y1). bbox: vec4, // Layer-space bounds of the buffer. bounds: vec4, // World → layer (affine rows). winv0: vec4, winv1: vec4, winv2: vec4, // Ambient, diffuse, specular, Blinn-Phong exponent. mat0: vec4, // Metal, light transmission, opacity. mat1: vec4, // First geometry, geometry count, flags (1 draw, 2 preserve transparency, 4 accepts // lights), blend mode. ints: vec4, // Casts shadows, accepts shadows (0 off, 1 on, 2 only), layer id, stack order. ints2: vec4, // Track matte (index + 1; 0 = none), dissolve seed. ints3: vec4, } struct Geo { // Output pixel → homogeneous layer coordinates (rows). h0: vec4, h1: vec4, h2: vec4, // Camera depth = d.x·u + d.y·v + d.z. depth: vec4, // Layer → world (affine rows). w0: vec4, w1: vec4, w2: vec4, // Unit world normal. normal: vec4, // Camera eye (xyz) and orthographic flag (w). eye: vec4, // Camera forward (xyz). fwd: vec4, } struct Light { // Kind (0 ambient, 1 parallel, 2 point, 3 spot), falloff, casts shadows. ints: vec4, pos: vec4, dir: vec4, color: vec4, // Cone half-angle, feather, radius, falloff distance. cone: vec4, // Shadow darkness, shadow diffusion. shadow: vec4, } @group(1) @binding(0) var planes: array; @group(1) @binding(1) var geos: array; @group(1) @binding(2) var lights: array; @group(1) @binding(3) var casters: array; const NEAR3: f32 = 1.0; const MAX_FRAGS: u32 = 32u; fn atlas_get(i: u32, x: i32, y: i32) -> vec4 { let r = planes[i].atlas; if (x < 0 || y < 0 || x >= i32(r.z) || y >= i32(r.w)) { return vec4(0.0); } return textureLoad(src, vec2(i32(r.x) + x, i32(r.y) + y), 0); } // Image::sample_bilinear / sample_bicubic on a plane's buffer at layer coordinates (u, v). fn texel(i: u32, u: f32, v: f32) -> vec4 { let t = planes[i].tex; let x = u * t.z + t.x; let y = v * t.z + t.y; let fx = x - 0.5; let fy = y - 0.5; let x0 = floor(fx); let y0 = floor(fy); let xi = i32(x0); let yi = i32(y0); if (t.w > 0.5) { let wx = cubic_w(fx - x0); let wy = cubic_w(fy - y0); var o = vec4(0.0); for (var j = 0; j < 4; j++) { for (var k = 0; k < 4; k++) { o += atlas_get(i, xi - 1 + k, yi - 1 + j) * (wx[k] * wy[j]); } } return max(o, vec4(0.0)); } let tx = fx - x0; let ty = fy - y0; let a = atlas_get(i, xi, yi); let b = atlas_get(i, xi + 1, yi); let c = atlas_get(i, xi, yi + 1); let d = atlas_get(i, xi + 1, yi + 1); let top = a + (b - a) * tx; let bot = c + (d - c) * tx; return top + (bot - top) * ty; } fn affine(r0: vec4, r1: vec4, r2: vec4, p: vec3) -> vec3 { return vec3(dot(r0.xyz, p) + r0.w, dot(r1.xyz, p) + r1.w, dot(r2.xyz, p) + r2.w); } fn linear3(r0: vec4, r1: vec4, r2: vec4, v: vec3) -> vec3 { return vec3(dot(r0.xyz, v), dot(r1.xyz, v), dot(r2.xyz, v)); } // compose::unproject: (u, v, depth, ok). fn unproject(g: u32, sx: f32, sy: f32) -> vec4 { let gm = geos[g]; let ortho = gm.eye.w > 0.5; let qz = gm.h2.x * sx + gm.h2.y * sy + gm.h2.z; if ((!ortho && qz <= 0.0) || qz == 0.0) { return vec4(0.0); } let u = (gm.h0.x * sx + gm.h0.y * sy + gm.h0.z) / qz; let v = (gm.h1.x * sx + gm.h1.y * sy + gm.h1.z) / qz; let z = gm.depth.x * u + gm.depth.y * v + gm.depth.z; if (!ortho && z < NEAR3) { return vec4(0.0); } return vec4(u, v, z, 1.0); } // LightState::to_light: unit vector towards the light (xyz) and the distance (w; -1 = ∞). fn to_light(l: u32, p: vec3) -> vec4 { let li = lights[l]; if (li.ints.x == 1u) { return vec4(-li.dir.xyz, -1.0); } let d = li.pos.xyz - p; let len = length(d); if (len < 1e-9) { return vec4(0.0, 0.0, -1.0, 0.0); } return vec4(d / len, len); } fn smoothstep01(t: f32) -> f32 { return 1.0 - t * t * (3.0 - 2.0 * t); } // LightState::attenuation. fn attenuation(l: u32, p: vec3) -> f32 { let li = lights[l]; let tl = to_light(l, p); let dist = tl.w; let finite = dist >= 0.0; let radius = li.cone.z; let fd = li.cone.w; var k = 1.0; if (li.ints.y == 1u && finite) { if (dist <= radius) { k = 1.0; } else if (fd <= 0.0) { k = 0.0; } else { k = smoothstep01(clamp((dist - radius) / fd, 0.0, 1.0)); } } else if (li.ints.y == 2u && finite) { if (dist <= radius || (radius <= 0.0 && dist <= 1.0)) { k = 1.0; } else { let q = max(radius, 1.0) / dist; k = q * q; } } if (li.ints.x == 3u) { let cos_a = clamp(dot(-tl.xyz, li.dir.xyz), -1.0, 1.0); let a = acos_p(cos_a); let outer = li.cone.x; let inner = outer * (1.0 - li.cone.y); if (a <= inner) { } else if (a >= outer) { k = 0.0; } else { k *= smoothstep01((a - inner) / max(outer - inner, 1e-9)); } } return k; } // compose::occlusion: RGB transmittance of light `l` reaching `p` past caster `c`. fn occlusion(c: u32, p: vec3, l: u32) -> vec3 { let pl = planes[c]; let li = lights[l]; let a = affine(pl.winv0, pl.winv1, pl.winv2, p); var hit: vec3; var d_pc: f32; var d_cl: f32; if (li.ints.x == 1u) { // Towards the light, 1e6 away (in direction form: exact in f32). let dl = linear3(pl.winv0, pl.winv1, pl.winv2, -li.dir.xyz); if (dl.z == 0.0) { return vec3(1.0); } let s = -a.z / dl.z; if (s <= 1.0 || s >= 1.0e6) { return vec3(1.0); } hit = a + dl * s; d_pc = length(li.dir.xyz) * s; d_cl = 1000.0; } else { let b = affine(pl.winv0, pl.winv1, pl.winv2, li.pos.xyz); if (a.z == b.z || (a.z < 0.0) == (b.z < 0.0)) { return vec3(1.0); } let t = a.z / (a.z - b.z); if (t <= 1e-6 || t >= 1.0) { return vec3(1.0); } hit = a + (b - a) * t; let seg = length(li.pos.xyz - p); d_pc = seg * t; d_cl = max(seg * (1.0 - t), 1.0); } let r = li.shadow.y * min(d_pc / d_cl, 4.0); let bd = pl.bounds; if (hit.x < bd.x - r || hit.y < bd.y - r || hit.x > bd.z + r || hit.y > bd.w + r) { return vec3(1.0); } var px = vec4(0.0); if (r * pl.tex.z < 0.5) { px = texel(c, hit.x, hit.y); } else { for (var j = -1; j <= 1; j++) { for (var i = -1; i <= 1; i++) { px += texel(c, hit.x + f32(i) * r * 0.66, hit.y + f32(j) * r * 0.66) / 9.0; } } } let alpha = clamp(px.w * pl.mat1.z, 0.0, 1.0); if (alpha <= 0.0) { return vec3(1.0); } let tr = pl.mat1.y; let dark = li.shadow.x; var o = vec3(1.0); for (var k = 0; k < 3; k++) { var st = 0.0; if (px.w > 0.0) { st = clamp(px[k] / px.w, 0.0, 1.0); } let through = (1.0 - alpha) + alpha * tr * st; o[k] = 1.0 - dark * (1.0 - through); } return o; } fn shadow_of(it: u32, l: u32, p: vec3) -> vec3 { var tr = vec3(1.0); let n = P.u[0].z; let lid = planes[it].ints2.z; for (var k = 0u; k < n; k++) { let c = casters[k]; if (c == it || planes[c].ints2.z == lid) { continue; } tr *= occlusion(c, p, l); } return tr; } // compose::shade_texel (light::shade). fn shade_texel(it: u32, g: u32, u: f32, v: f32, t: vec4) -> vec4 { let pl = planes[it]; let n_lights = P.u[0].y; let accepts_shadows = pl.ints2.y; if (n_lights == 0u || (pl.ints.z & 4u) == 0u) { if (accepts_shadows == 2u) { return vec4(0.0); } return t; } let gm = geos[g]; let a = t.w; let rgb = t.xyz / a; let p = affine(gm.w0, gm.w1, gm.w2, vec3(u, v, 0.0)); var to_viewer: vec3; if (gm.eye.w > 0.5) { to_viewer = -gm.fwd.xyz; } else { to_viewer = normalize(gm.eye.xyz - p); } var n = gm.normal.xyz; if (dot(n, to_viewer) < 0.0) { n = -n; } var amb = vec3(0.0); var diff = vec3(0.0); var spec = vec3(0.0); var shadow_amt = 0.0; let ex = pl.mat0.w; let metal = pl.mat1.x; let transmission = pl.mat1.y; let specular = pl.mat0.z; for (var i = 0u; i < n_lights; i++) { let li = lights[i]; if (li.ints.x == 0u) { amb += li.color.xyz; continue; } let k = attenuation(i, p); if (k <= 0.0) { continue; } let lv = to_light(i, p).xyz; let ndl = dot(n, lv); let front = ndl > 0.0; var lambert = ndl; if (!front) { lambert = -ndl * transmission; } if (lambert <= 0.0) { continue; } var sh = vec3(1.0); if (li.ints.z != 0u && accepts_shadows != 0u) { sh = shadow_of(it, i, p); } shadow_amt = max(shadow_amt, 1.0 - (sh.x + sh.y + sh.z) / 3.0); diff += li.color.xyz * (lambert * k) * sh; if (front && specular > 0.0) { let h = normalize(lv + to_viewer); let s = powz(max(dot(n, h), 0.0), ex) * k; let tint = vec3(1.0) + (rgb - vec3(1.0)) * metal; spec += li.color.xyz * s * sh * tint; } } let lit = rgb * (pl.mat0.x * amb + pl.mat0.y * diff) + specular * spec; if (accepts_shadows == 2u) { return vec4(0.0, 0.0, 0.0, a * shadow_amt); } return vec4(lit * a, a); } // compose::before: a draws before b (farther; coplanar → lower stack order first). fn before(za: f32, oa: u32, zb: f32, ob: u32) -> bool { let tol = 1e-5 * max(max(abs(za), abs(zb)), 1.0); if (abs(za - zb) <= tol) { return oa < ob; } return za > zb; } @compute @workgroup_size(16, 16) fn classic3d(@builtin(global_invocation_id) gid: vec3) { let dims = out_dims(); let x = i32(gid.x); let y = i32(gid.y); if (x >= dims.x || y >= dims.y) { return; } let pix = vec2(x, y); var d = textureLoad(aux, pix, 0); let fx = f32(x); let fy = f32(y); let sx = fx + 0.5; let sy = fy + 0.5; var fz: array; var fo: array; var fi: array; var fp: array, MAX_FRAGS>; var nf = 0u; let n_planes = P.u[0].x; for (var i = 0u; i < n_planes; i++) { let pl = planes[i]; if ((pl.ints.z & 1u) == 0u) { continue; } if (fx < pl.bbox.x || fx >= pl.bbox.z || fy < pl.bbox.y || fy >= pl.bbox.w) { continue; } // compose::fragment: average of the motion-blur sub-samples. let g0 = pl.ints.x; let ng = pl.ints.y; var acc = vec4(0.0); var depth = 0.0; var hit = false; for (var k = 0u; k < ng; k++) { let q = unproject(g0 + k, sx, sy); if (q.w == 0.0) { continue; } let t = texel(i, q.x, q.y); if (t.w <= 1e-6) { continue; } if (!hit) { depth = q.z; hit = true; } acc += shade_texel(i, g0 + k, q.x, q.y, t); } if (!hit) { continue; } if (ng > 1u) { acc *= 1.0 / f32(ng); } let m = pl.ints3.x; if (m != 0u) { let f = data[(m - 1u) * u32(dims.x) * u32(dims.y) + u32(y) * u32(dims.x) + u32(x)]; acc *= f; } if (!(acc.w > 0.0 || acc.x != 0.0 || acc.y != 0.0 || acc.z != 0.0) || nf >= MAX_FRAGS) { continue; } // Insertion: far → near, coplanar in stack order (stable, as the CPU's insertion sort). let order = pl.ints2.w; var j = nf; while (j > 0u && before(depth, order, fz[j - 1u], fo[j - 1u])) { fz[j] = fz[j - 1u]; fo[j] = fo[j - 1u]; fi[j] = fi[j - 1u]; fp[j] = fp[j - 1u]; j--; } fz[j] = depth; fo[j] = order; fi[j] = i; fp[j] = acc; nf++; } for (var k = 0u; k < nf; k++) { let pl = planes[fi[k]]; var op = pl.mat1.z; if ((pl.ints.z & 2u) != 0u) { op *= d.w; } let s = fp[k] * op; let mode = pl.ints.w; var n = 0.5; if (mode == 1u || mode == 2u) { n = hash_noise(u32(x), u32(y), pl.ints3.y); } d = blend_pixel(mode, d, s, n); } textureStore(out, pix, d); }