Files
effectcraft/crates/gpu/src/shaders/fx_lut.wgsl
T
bflatasticandClaude Opus 5.5 492fa6de0a Effects: OCIO custom configs read Blender's v2 transforms and warn (#410)
Cause: the config reader didn't know ColorSpaceTransform or
ExponentWithLinearTransform and passed colours through them silently.
Blender's Linear Rec.709 -> sRGB therefore stayed in its E-white XYZ
reference space (the blue cast) without the sRGB curve. Display
colorspaces, view transforms, v2 views, looks, AllocationTransform,
BuiltinTransform and .spi1d / .spimtx files were missing too, and
nothing told the user.

Fix (from the OCIO v2 documentation and the CLF "moncurve", not OCIO's
source): ExponentWithLinear, ColorSpace, Look, DisplayView, Allocation
and Builtin transforms (identity, ACES AP0/AP1 matrices, ACEScct /
ACEScc, the sRGB / P3 / Rec.1886 / gamma / PQ displays). RangeTransform
now clamps and takes one-sided ranges. Display colorspaces meet the
scene reference through the default view transform. Added v2 views,
view looks, the looks section and .spi1d / .spi3d / .spimtx files. A
conversion is built once as a flat list of steps (Config::path /
display_path) for both the CPU effect and the GPU colour program (new
moncurve and clamp ops and the ACEScc curve; a program over the
kernel's 255 ops renders on the CPU). Named-transform resolution is
limited to 16 levels and 4096 steps, and YAML nesting to 64 levels, so
hostile or looping configs stop. Transforms that are still unsupported
(Grading Primary / Tone, ACES output, HLG…), unreadable configs and
unknown spaces are now reported: a warning under the effect in Effect
Controls, and the new effect.warning query.

Tests: ocio_config::blender_like_config_converts_linear_rec709_to_srgb
(CST, display colorspaces, v1 / v2 views, allocation, looks, builtin,
range, loops), moncurve_matches_srgb_and_inverts, builtin_styles,
yaml_nesting_is_bounded, ocio::spi_files_parse,
tests_effects::ocio_effect_warns_about_unsupported_config_transforms,
gpu tests_fx_lut::ocio_custom_config_v2 (GPU = CPU).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:57:12 +07:00

583 lines
18 KiB
WebGPU Shading Language
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ---------------------------------------------------------------- colour management (fx_lut.rs)
//
// A colour program in `data`: opcodes with their arguments, ending in 0, then LUT tables
// (layout in fx_lut.rs::push_lut). Every step mirrors effects::ocio / effects::utility.
fn fxl_d3(i: u32) -> vec3<f32> {
return vec3<f32>(data[i], data[i + 1u], data[i + 2u]);
}
// ---- LUTs (utility::Lut)
fn fxl_lut_norm(o: u32, c: vec3<f32>) -> vec3<f32> {
let dmin = fxl_d3(o + 2u);
let dmax = fxl_d3(o + 5u);
return clamp((c - dmin) / max(dmax - dmin, vec3<f32>(1e-9)), vec3<f32>(0.0), vec3<f32>(1.0));
}
fn fxl_lut_3d(base: u32, n: u32, r: u32, g: u32, b: u32) -> vec3<f32> {
return fxl_d3(base + (r + g * n + b * n * n) * 3u);
}
// Lut::apply_interp: 0 nearest, 1 trilinear, otherwise tetrahedral (1D tables linear unless 0).
fn fxl_lut_apply(o: u32, c0: vec3<f32>, interp: u32) -> vec3<f32> {
let n1 = u32(data[o]);
let n3 = u32(data[o + 1u]);
let b1 = o + 8u;
let b3 = b1 + n1 * 3u;
var c = c0;
if (n1 > 1u) {
let t = fxl_lut_norm(o, c);
for (var i = 0u; i < 3u; i++) {
let x = t[i] * f32(n1 - 1u);
if (interp == 0u) {
c[i] = data[b1 + min(u32(round_away(x)), n1 - 1u) * 3u + i];
} else {
let i0 = min(u32(floor(x)), n1 - 2u);
let f = x - f32(i0);
let a = data[b1 + i0 * 3u + i];
let b = data[b1 + (i0 + 1u) * 3u + i];
c[i] = a + (b - a) * f;
}
}
}
if (n3 > 1u) {
let t = fxl_lut_norm(o, c);
let s = f32(n3 - 1u);
if (interp == 0u) {
let ir = min(u32(round_away(t.x * s)), n3 - 1u);
let ig = min(u32(round_away(t.y * s)), n3 - 1u);
let ib = min(u32(round_away(t.z * s)), n3 - 1u);
return fxl_lut_3d(b3, n3, ir, ig, ib);
}
let v = t * s;
let r = min(u32(floor(v.x)), n3 - 2u);
let g = min(u32(floor(v.y)), n3 - 2u);
let b = min(u32(floor(v.z)), n3 - 2u);
let fr = v.x - f32(r);
let fg = v.y - f32(g);
let fb = v.z - f32(b);
if (interp == 1u) {
let c00 = mix_exact(fxl_lut_3d(b3, n3, r, g, b), fxl_lut_3d(b3, n3, r + 1u, g, b), fr);
let c10 = mix_exact(fxl_lut_3d(b3, n3, r, g + 1u, b), fxl_lut_3d(b3, n3, r + 1u, g + 1u, b), fr);
let c01 = mix_exact(fxl_lut_3d(b3, n3, r, g, b + 1u), fxl_lut_3d(b3, n3, r + 1u, g, b + 1u), fr);
let c11 = mix_exact(fxl_lut_3d(b3, n3, r, g + 1u, b + 1u), fxl_lut_3d(b3, n3, r + 1u, g + 1u, b + 1u), fr);
return mix_exact(mix_exact(c00, c10, fg), mix_exact(c01, c11, fg), fb);
}
let c000 = fxl_lut_3d(b3, n3, r, g, b);
let c111 = fxl_lut_3d(b3, n3, r + 1u, g + 1u, b + 1u);
var w1: f32;
var w2: f32;
var w3: f32;
var p1: vec3<f32>;
var p2: vec3<f32>;
if (fr > fg) {
if (fg > fb) {
w1 = fr;
w2 = fg;
w3 = fb;
p1 = fxl_lut_3d(b3, n3, r + 1u, g, b);
p2 = fxl_lut_3d(b3, n3, r + 1u, g + 1u, b);
} else if (fr > fb) {
w1 = fr;
w2 = fb;
w3 = fg;
p1 = fxl_lut_3d(b3, n3, r + 1u, g, b);
p2 = fxl_lut_3d(b3, n3, r + 1u, g, b + 1u);
} else {
w1 = fb;
w2 = fr;
w3 = fg;
p1 = fxl_lut_3d(b3, n3, r, g, b + 1u);
p2 = fxl_lut_3d(b3, n3, r + 1u, g, b + 1u);
}
} else if (fb > fg) {
w1 = fb;
w2 = fg;
w3 = fr;
p1 = fxl_lut_3d(b3, n3, r, g, b + 1u);
p2 = fxl_lut_3d(b3, n3, r, g + 1u, b + 1u);
} else if (fb > fr) {
w1 = fg;
w2 = fb;
w3 = fr;
p1 = fxl_lut_3d(b3, n3, r, g + 1u, b);
p2 = fxl_lut_3d(b3, n3, r, g + 1u, b + 1u);
} else {
w1 = fg;
w2 = fr;
w3 = fb;
p1 = fxl_lut_3d(b3, n3, r, g + 1u, b);
p2 = fxl_lut_3d(b3, n3, r + 1u, g + 1u, b);
}
return c000 + w1 * (p1 - c000) + w2 * (p2 - p1) + w3 * (c111 - p2);
}
return c;
}
// a + (b - a) t, per component (the CPU's lerp, not WGSL's mix).
fn mix_exact(a: vec3<f32>, b: vec3<f32>, t: f32) -> vec3<f32> {
return a + (b - a) * t;
}
// ocio::shape over `n` (x, y) points at `at` (`flip`: the curve with x and y swapped).
fn fxl_shape(at: u32, n: u32, x: f32, flip: bool) -> f32 {
if (n < 2u) {
return x;
}
let kx = select(0u, 1u, flip);
let ky = 1u - kx;
// partition_point(|p| p.x < x).
var lo = 0u;
var hi = n;
while (lo < hi) {
let mid = (lo + hi) / 2u;
if (data[at + mid * 2u + kx] < x) {
lo = mid + 1u;
} else {
hi = mid;
}
}
let i = clamp(lo, 1u, n - 1u);
let ax = data[at + (i - 1u) * 2u + kx];
let ay = data[at + (i - 1u) * 2u + ky];
let bx = data[at + i * 2u + kx];
let by = data[at + i * 2u + ky];
let t = clamp((x - ax) / max(bx - ax, 1e-12), 0.0, 1.0);
return ay + (by - ay) * t;
}
// Shaper channel k's points (three counts, then the channels' (x, y) pairs).
fn fxl_shaper_at(s: u32, k: u32) -> vec2<u32> {
var at = s + 3u;
for (var j = 0u; j < k; j++) {
at += u32(data[s + j]) * 2u;
}
return vec2<u32>(at, u32(data[s + k]));
}
fn fxl_file_fwd(o: u32, sh: i32, c0: vec3<f32>, interp: u32) -> vec3<f32> {
var c = c0;
if (sh >= 0) {
for (var k = 0u; k < 3u; k++) {
let a = fxl_shaper_at(u32(sh), k);
c[k] = fxl_shape(a.x, a.y, c[k], false);
}
}
return fxl_lut_apply(o, c, interp);
}
// The inverse of a 1D table channel (ocio::FileXform::apply: points (table, domain)).
fn fxl_inv_1d(o: u32, i: u32, y: f32) -> f32 {
let n = u32(data[o]);
let dmin = data[o + 2u + i];
let dmax = data[o + 5u + i];
let b1 = o + 8u;
// partition_point over the table's values.
var lo = 0u;
var hi = n;
while (lo < hi) {
let mid = (lo + hi) / 2u;
if (data[b1 + mid * 3u + i] < y) {
lo = mid + 1u;
} else {
hi = mid;
}
}
let k = clamp(lo, 1u, n - 1u);
let ax = data[b1 + (k - 1u) * 3u + i];
let bx = data[b1 + k * 3u + i];
let ay = dmin + (dmax - dmin) * f32(k - 1u) / f32(n - 1u);
let by = dmin + (dmax - dmin) * f32(k) / f32(n - 1u);
let t = clamp((y - ax) / max(bx - ax, 1e-12), 0.0, 1.0);
return ay + (by - ay) * t;
}
fn fxl_file(o: u32, sh: i32, c: vec3<f32>, interp: u32, inverse: bool) -> vec3<f32> {
if (!inverse) {
return fxl_file_fwd(o, sh, c, interp);
}
let n1 = u32(data[o]);
let n3 = u32(data[o + 1u]);
if (n3 == 0u) {
var x = c;
if (n1 > 1u) {
x = vec3<f32>(fxl_inv_1d(o, 0u, c.x), fxl_inv_1d(o, 1u, c.y), fxl_inv_1d(o, 2u, c.z));
}
if (sh >= 0) {
for (var k = 0u; k < 3u; k++) {
let a = fxl_shaper_at(u32(sh), k);
x[k] = fxl_shape(a.x, a.y, x[k], true);
}
}
return x;
}
// Damped fixed point from the target, as the CPU.
var x = c;
for (var it = 0; it < 40; it++) {
let y = fxl_file_fwd(o, sh, x, interp);
let e = c - y;
if (all(abs(e) < vec3<f32>(1e-6))) {
break;
}
x = x + e;
}
return x;
}
// ---- transfer functions (ocio::Tf)
const FXL_PQ_M1: f32 = 0.1593017578125;
const FXL_PQ_M2: f32 = 78.84375;
const FXL_PQ_C1: f32 = 0.8359375;
const FXL_PQ_C2: f32 = 18.8515625;
const FXL_PQ_C3: f32 = 18.6875;
fn fxl_copysign(m: f32, v: f32) -> f32 {
return select(abs(m), -abs(m), v < 0.0 || (v == 0.0 && bitcast<u32>(v) != 0u));
}
fn fxl_decode(tf: u32, g: f32, v: f32) -> f32 {
switch (tf) {
case 1u: {
let a = abs(v);
var l: f32;
if (a <= 0.04045) {
l = a / 12.92;
} else {
l = pow((a + 0.055) / 1.055, 2.4);
}
return fxl_copysign(l, v);
}
case 2u: {
return fxl_copysign(powz(abs(v), g), v);
}
case 3u: {
if (v <= 0.155251141552511) {
return (v - 0.0729055341958355) / 10.5402377416545;
}
if (v < (log2(65504.0) + 9.72) / 17.52) {
return exp2(v * 17.52 - 9.72);
}
return 65504.0;
}
case 4u: {
let e = powz(max(v, 0.0), 1.0 / FXL_PQ_M2);
let l = powz(max(e - FXL_PQ_C1, 0.0) / (FXL_PQ_C2 - FXL_PQ_C3 * e), 1.0 / FXL_PQ_M1);
return l * 100.0;
}
case 5u: {
// ACEScc.
if (v < (-15.0 + 9.72) / 17.52) {
return (exp2(v * 17.52 - 9.72) - exp2(-16.0)) * 2.0;
}
if (v < (log2(65504.0) + 9.72) / 17.52) {
return exp2(v * 17.52 - 9.72);
}
return 65504.0;
}
default: {
return v;
}
}
}
fn fxl_encode(tf: u32, g: f32, v: f32) -> f32 {
switch (tf) {
case 1u: {
let a = abs(v);
var e: f32;
if (a <= 0.0031308) {
e = a * 12.92;
} else {
e = 1.055 * pow(a, 1.0 / 2.4) - 0.055;
}
return fxl_copysign(e, v);
}
case 2u: {
return fxl_copysign(powz(abs(v), 1.0 / g), v);
}
case 3u: {
if (v <= 0.0078125) {
return 10.5402377416545 * v + 0.0729055341958355;
}
return (log2(v) + 9.72) / 17.52;
}
case 4u: {
let y = powz(max(v / 100.0, 0.0), FXL_PQ_M1);
return powz((FXL_PQ_C1 + FXL_PQ_C2 * y) / (1.0 + FXL_PQ_C3 * y), FXL_PQ_M2);
}
case 5u: {
// ACEScc.
if (v <= 0.0) {
return (-16.0 + 9.72) / 17.52;
}
if (v < exp2(-15.0)) {
return (log2(exp2(-16.0) + v * 0.5) + 9.72) / 17.52;
}
return (log2(v) + 9.72) / 17.52;
}
default: {
return v;
}
}
}
// ---- ASC CDL (ocio::Cdl), args at `a`: slope (3), offset (3), power (3), sat, clamp, inverse.
const FXL_LUMA: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);
fn fxl_cdl(a: u32, c0: vec3<f32>) -> vec3<f32> {
let slope = fxl_d3(a);
let offset = fxl_d3(a + 3u);
let power = fxl_d3(a + 6u);
let sat = data[a + 9u];
let clampo = data[a + 10u] != 0.0;
let zero = vec3<f32>(0.0);
let one = vec3<f32>(1.0);
if (data[a + 11u] == 0.0) {
var o = vec3<f32>(0.0);
for (var i = 0u; i < 3u; i++) {
let v = c0[i] * slope[i] + offset[i];
if (clampo) {
o[i] = powz(clamp(v, 0.0, 1.0), power[i]);
} else if (v >= 0.0) {
o[i] = powz(v, power[i]);
} else {
o[i] = v;
}
}
let l = o.x * FXL_LUMA.x + o.y * FXL_LUMA.y + o.z * FXL_LUMA.z;
o = l + sat * (o - l);
if (clampo) {
o = clamp(o, zero, one);
}
return o;
}
var c = c0;
if (clampo) {
c = clamp(c, zero, one);
}
let l = c.x * FXL_LUMA.x + c.y * FXL_LUMA.y + c.z * FXL_LUMA.z;
var s = sat;
if (abs(s) < 1e-9) {
s = 1e-9;
}
let o = l + (c - l) / s;
var r = vec3<f32>(0.0);
for (var i = 0u; i < 3u; i++) {
var v = o[i];
if (clampo) {
v = clamp(v, 0.0, 1.0);
}
let pw = max(power[i], 1e-9);
var u = v;
if (v >= 0.0) {
u = powz(v, 1.0 / pw);
}
var sl = slope[i];
if (abs(sl) < 1e-9) {
sl = 1e-9;
}
r[i] = (u - offset[i]) / sl;
}
if (clampo) {
r = clamp(r, zero, one);
}
return r;
}
fn fxl_tonemap(v: f32, inverse: bool) -> f32 {
if (!inverse) {
if (v <= 0.0) {
return v;
}
return v * (1.0 + v / 256.0) / (1.0 + v);
}
if (v <= 0.0) {
return v;
}
let a = 1.0 / 256.0;
let b = 1.0 - v;
let c = -v;
return (-b + sqrt(b * b - 4.0 * a * c)) / (2.0 * a);
}
// ExponentWithLinearTransform (ocio_config::moncurve): forward decodes; a gamma of at most 1 or
// a non-positive offset is a plain power curve.
fn fxl_moncurve(v0: f32, g: f32, o: f32, inv: bool, mirror: bool) -> f32 {
var v = v0;
var sign = 1.0;
if (mirror && v < 0.0) {
v = -v;
sign = -1.0;
}
if (g <= 1.0 || o <= 0.0) {
var p = g;
if (inv) {
p = 1.0 / max(g, 1e-9);
}
return sign * powz(max(v, 0.0), p);
}
let xb = o / (g - 1.0);
let yb = powz((xb + o) / (1.0 + o), g);
let slope = yb / xb;
if (!inv) {
if (v >= xb) {
return sign * powz((v + o) / (1.0 + o), g);
}
return sign * v * slope;
}
if (v >= yb) {
return sign * ((1.0 + o) * powz(v, 1.0 / g) - o);
}
return sign * v / slope;
}
// Run the colour program at data[0..] on a straight colour.
fn fxl_run(c0: vec3<f32>) -> vec3<f32> {
var c = c0;
var pc = 0u;
for (var guard = 0; guard < 256; guard++) {
let op = u32(data[pc]);
switch (op) {
case 1u, 2u: {
let tf = u32(data[pc + 1u]);
let g = data[pc + 2u];
for (var i = 0u; i < 3u; i++) {
if (op == 1u) {
c[i] = fxl_decode(tf, g, c[i]);
} else {
c[i] = fxl_encode(tf, g, c[i]);
}
}
pc += 3u;
}
case 3u: {
let r0 = fxl_d3(pc + 1u);
let r1 = fxl_d3(pc + 4u);
let r2 = fxl_d3(pc + 7u);
c = vec3<f32>(r0.x * c.x + r0.y * c.y + r0.z * c.z, r1.x * c.x + r1.y * c.y + r1.z * c.z, r2.x * c.x + r2.y * c.y + r2.z * c.z);
pc += 10u;
}
case 4u: {
c = fxl_cdl(pc + 1u, c);
pc += 13u;
}
case 5u: {
let inv = data[pc + 1u] != 0.0;
c = vec3<f32>(fxl_tonemap(c.x, inv), fxl_tonemap(c.y, inv), fxl_tonemap(c.z, inv));
pc += 2u;
}
case 6u: {
let luma = fxl_d3(pc + 1u);
let l = c.x * luma.x + c.y * luma.y + c.z * luma.z;
if (l <= 0.0) {
c = max(c, vec3<f32>(0.0));
} else {
let mn = min(min(c.x, c.y), c.z);
if (mn < 0.0) {
let t = clamp(l / (l - mn), 0.0, 1.0);
c = l + (c - l) * t;
}
}
pc += 4u;
}
case 7u: {
c = max(c, vec3<f32>(0.0));
pc += 1u;
}
case 8u: {
let o = u32(data[pc + 1u]);
let interp = u32(data[pc + 2u]);
let inv = data[pc + 3u] != 0.0;
let sh = i32(data[pc + 4u]);
c = fxl_file(o, sh, c, interp, inv);
pc += 5u;
}
case 9u: {
// Affine: matrix × (c + pre) + post (a config's MatrixTransform, RangeTransform).
let r0 = fxl_d3(pc + 1u);
let r1 = fxl_d3(pc + 4u);
let r2 = fxl_d3(pc + 7u);
let d = c + fxl_d3(pc + 10u);
c = vec3<f32>(r0.x * d.x + r0.y * d.y + r0.z * d.z, r1.x * d.x + r1.y * d.y + r1.z * d.z, r2.x * d.x + r2.y * d.y + r2.z * d.z)
+ fxl_d3(pc + 13u);
pc += 16u;
}
case 10u: {
// ExponentTransform: max(c, 0) ^ p.
let e = fxl_d3(pc + 1u);
c = vec3<f32>(powz(max(c.x, 0.0), e.x), powz(max(c.y, 0.0), e.y), powz(max(c.z, 0.0), e.z));
pc += 4u;
}
case 11u: {
// Log / LogAffineTransform: base, log slope, log offset, lin slope, lin offset,
// inverse, ln of f64's smallest normal (the CPU's floor).
let base = data[pc + 1u];
let ls = fxl_d3(pc + 2u);
let lo = fxl_d3(pc + 5u);
let lis = fxl_d3(pc + 8u);
let lio = fxl_d3(pc + 11u);
let lnb = log(base);
if (data[pc + 14u] != 0.0) {
for (var i = 0u; i < 3u; i++) {
c[i] = (exp((c[i] - lo[i]) / ls[i] * lnb) - lio[i]) / lis[i];
}
} else {
for (var i = 0u; i < 3u; i++) {
let v = lis[i] * c[i] + lio[i];
var ln = data[pc + 15u];
if (v >= 1.1754944e-38) {
ln = log(v);
}
c[i] = ls[i] * (ln / lnb) + lo[i];
}
}
pc += 16u;
}
case 12u: {
// ExponentWithLinearTransform: gamma (3), offset (3), inverse, mirror.
let g = fxl_d3(pc + 1u);
let o = fxl_d3(pc + 4u);
let inv = data[pc + 7u] != 0.0;
let mirror = data[pc + 8u] != 0.0;
for (var i = 0u; i < 3u; i++) {
c[i] = fxl_moncurve(c[i], g[i], o[i], inv, mirror);
}
pc += 9u;
}
case 13u: {
// Clamp (a clamping RangeTransform): low (3), high (3).
c = min(max(c, fxl_d3(pc + 1u)), fxl_d3(pc + 4u));
pc += 7u;
}
default: {
return c;
}
}
}
return c;
}
// u[0].x = 0: OCIO pixels (unpremul); 1: colour ÷ alpha (Image::map_straight).
@compute @workgroup_size(16, 16)
fn fxl_point(@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, px);
return;
}
var c = px.xyz / a;
if (P.u[0].x == 0u && a <= 1e-6) {
c = vec3<f32>(0.0);
}
textureStore(out, p, vec4<f32>(fxl_run(c) * a, a));
}