ff-render 0.18.1

GPU compositing pipeline for real-time preview (wgpu-based)
Documentation
// Hue / saturation / lightness adjustment. Converts the stored RGB to HSL,
// applies a hue rotation, a saturation multiplier, and a lightness offset, then
// converts back. Operates on the stored (non-linearised) values, matching the
// other colour nodes.

struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) uv: vec2<f32>,
}

@vertex
fn vs_main(@builtin(vertex_index) vertex_idx: u32) -> VertexOutput {
    var positions = array<vec2<f32>, 6>(
        vec2<f32>(-1.0,  1.0), vec2<f32>( 1.0,  1.0), vec2<f32>(-1.0, -1.0),
        vec2<f32>(-1.0, -1.0), vec2<f32>( 1.0,  1.0), vec2<f32>( 1.0, -1.0),
    );
    var uvs = array<vec2<f32>, 6>(
        vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 0.0), vec2<f32>(0.0, 1.0),
        vec2<f32>(0.0, 1.0), vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 1.0),
    );
    var out: VertexOutput;
    out.position = vec4<f32>(positions[vertex_idx], 0.0, 1.0);
    out.uv = uvs[vertex_idx];
    return out;
}

@group(0) @binding(0) var input_tex: texture_2d<f32>;
@group(0) @binding(1) var<uniform> u: HslUniforms;

// Matches Rust HslUniforms (field order/size kept in sync).
struct HslUniforms {
    hue_shift:  f32,   // degrees, -180..180
    saturation: f32,   // multiplier (0 = grey, 1 = unchanged)
    lightness:  f32,   // additive offset, -1..1
    _pad:       f32,
}

fn rgb_to_hsl(c: vec3<f32>) -> vec3<f32> {
    let mx = max(c.r, max(c.g, c.b));
    let mn = min(c.r, min(c.g, c.b));
    let l = (mx + mn) * 0.5;
    let d = mx - mn;
    if (d < 1e-6) {
        return vec3<f32>(0.0, 0.0, l);
    }
    let s = d / (1.0 - abs(2.0 * l - 1.0));
    var h = 0.0;
    if (mx == c.r) {
        h = (c.g - c.b) / d;
        h = h - 6.0 * floor(h / 6.0);   // modulo 6
    } else if (mx == c.g) {
        h = (c.b - c.r) / d + 2.0;
    } else {
        h = (c.r - c.g) / d + 4.0;
    }
    return vec3<f32>(h / 6.0, s, l);   // h in [0,1)
}

fn hue_to_rgb(p: f32, q: f32, t_in: f32) -> f32 {
    var t = t_in - floor(t_in);   // wrap to [0,1)
    if (t < 1.0 / 6.0) { return p + (q - p) * 6.0 * t; }
    if (t < 0.5)       { return q; }
    if (t < 2.0 / 3.0) { return p + (q - p) * (2.0 / 3.0 - t) * 6.0; }
    return p;
}

fn hsl_to_rgb(hsl: vec3<f32>) -> vec3<f32> {
    let h = hsl.x;
    let s = hsl.y;
    let l = hsl.z;
    if (s < 1e-6) {
        return vec3<f32>(l, l, l);
    }
    var q = l + s - l * s;
    if (l < 0.5) {
        q = l * (1.0 + s);
    }
    let p = 2.0 * l - q;
    return vec3<f32>(
        hue_to_rgb(p, q, h + 1.0 / 3.0),
        hue_to_rgb(p, q, h),
        hue_to_rgb(p, q, h - 1.0 / 3.0),
    );
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let color = textureLoad(input_tex, vec2<i32>(in.position.xy), 0);
    var hsl = rgb_to_hsl(color.rgb);
    hsl.x = hsl.x + u.hue_shift / 360.0;
    hsl.y = clamp(hsl.y * u.saturation, 0.0, 1.0);
    hsl.z = clamp(hsl.z + u.lightness, 0.0, 1.0);
    let rgb = hsl_to_rgb(hsl);
    return vec4<f32>(rgb, color.a);
}