ff-render 0.14.3

GPU compositing pipeline for real-time preview (wgpu-based)
Documentation
// Blend mode compositing — 18 Photoshop-compatible modes.
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;
}

// ── Bindings ──────────────────────────────────────────────────────────────────

@group(0) @binding(0) var tex_base:    texture_2d<f32>;
@group(0) @binding(1) var tex_overlay: texture_2d<f32>;
@group(0) @binding(2) var tex_sampler: sampler;
@group(0) @binding(3) var<uniform> u: BlendUniforms;

struct BlendUniforms {
    // Mode codes: Normal=0 Multiply=1 Screen=2 Overlay=3 SoftLight=4 HardLight=5
    //             ColorDodge=6 ColorBurn=7 Difference=8 Exclusion=9 Add=10 Subtract=11
    //             Darken=12 Lighten=13 Hue=14 Saturation=15 Color=16 Luminosity=17
    mode:    u32,
    opacity: f32,
    _pad0:   f32,
    _pad1:   f32,
}

// ── HSL helpers ───────────────────────────────────────────────────────────────

fn hue_to_rgb(p: f32, q: f32, t_in: f32) -> f32 {
    var t = t_in;
    if t < 0.0 { t += 1.0; }
    if t > 1.0 { t -= 1.0; }
    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 rgb_to_hsl(rgb: vec3<f32>) -> vec3<f32> {
    let max_c = max(max(rgb.r, rgb.g), rgb.b);
    let min_c = min(min(rgb.r, rgb.g), rgb.b);
    let l = (max_c + min_c) * 0.5;
    if max_c == min_c {
        return vec3<f32>(0.0, 0.0, l);
    }
    let delta = max_c - min_c;
    let s = select(delta / (2.0 - max_c - min_c), delta / (max_c + min_c), l < 0.5);
    var h: f32;
    if max_c == rgb.r {
        h = (rgb.g - rgb.b) / delta + select(6.0, 0.0, rgb.g >= rgb.b);
    } else if max_c == rgb.g {
        h = (rgb.b - rgb.r) / delta + 2.0;
    } else {
        h = (rgb.r - rgb.g) / delta + 4.0;
    }
    return vec3<f32>(h / 6.0, s, l);
}

fn hsl_to_rgb(hsl: vec3<f32>) -> vec3<f32> {
    if hsl.y == 0.0 {
        return vec3<f32>(hsl.z);
    }
    let q = select(hsl.z + hsl.y - hsl.z * hsl.y, hsl.z * (1.0 + hsl.y), hsl.z < 0.5);
    let p = 2.0 * hsl.z - q;
    return vec3<f32>(
        hue_to_rgb(p, q, hsl.x + 1.0 / 3.0),
        hue_to_rgb(p, q, hsl.x),
        hue_to_rgb(p, q, hsl.x - 1.0 / 3.0),
    );
}

// ── Per-channel blend helpers ─────────────────────────────────────────────────

fn overlay_ch(b: f32, o: f32) -> f32 {
    return select(1.0 - 2.0 * (1.0 - b) * (1.0 - o), 2.0 * b * o, b < 0.5);
}

fn soft_light_d(b: f32) -> f32 {
    return select(sqrt(b), ((16.0 * b - 12.0) * b + 4.0) * b, b <= 0.25);
}

fn soft_light_ch(b: f32, o: f32) -> f32 {
    return select(
        b + (2.0 * o - 1.0) * (soft_light_d(b) - b),
        b - (1.0 - 2.0 * o) * b * (1.0 - b),
        o <= 0.5,
    );
}

// ── Fragment ──────────────────────────────────────────────────────────────────

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let base    = textureSample(tex_base,    tex_sampler, in.uv);
    let overlay = textureSample(tex_overlay, tex_sampler, in.uv);

    var blend_rgb: vec3<f32>;
    switch u.mode {
        // Normal
        case 0u  { blend_rgb = overlay.rgb; }
        // Multiply
        case 1u  { blend_rgb = base.rgb * overlay.rgb; }
        // Screen
        case 2u  { blend_rgb = 1.0 - (1.0 - base.rgb) * (1.0 - overlay.rgb); }
        // Overlay
        case 3u  {
            blend_rgb = vec3<f32>(
                overlay_ch(base.r, overlay.r),
                overlay_ch(base.g, overlay.g),
                overlay_ch(base.b, overlay.b),
            );
        }
        // SoftLight
        case 4u  {
            blend_rgb = vec3<f32>(
                soft_light_ch(base.r, overlay.r),
                soft_light_ch(base.g, overlay.g),
                soft_light_ch(base.b, overlay.b),
            );
        }
        // HardLight (swap base/overlay in Overlay formula)
        case 5u  {
            blend_rgb = vec3<f32>(
                overlay_ch(overlay.r, base.r),
                overlay_ch(overlay.g, base.g),
                overlay_ch(overlay.b, base.b),
            );
        }
        // ColorDodge
        case 6u  {
            blend_rgb = clamp(base.rgb / (1.0 - overlay.rgb + 0.0001), vec3<f32>(0.0), vec3<f32>(1.0));
        }
        // ColorBurn
        case 7u  {
            blend_rgb = clamp(1.0 - (1.0 - base.rgb) / (overlay.rgb + 0.0001), vec3<f32>(0.0), vec3<f32>(1.0));
        }
        // Difference
        case 8u  { blend_rgb = abs(base.rgb - overlay.rgb); }
        // Exclusion
        case 9u  { blend_rgb = base.rgb + overlay.rgb - 2.0 * base.rgb * overlay.rgb; }
        // Add
        case 10u { blend_rgb = clamp(base.rgb + overlay.rgb, vec3<f32>(0.0), vec3<f32>(1.0)); }
        // Subtract
        case 11u { blend_rgb = clamp(base.rgb - overlay.rgb, vec3<f32>(0.0), vec3<f32>(1.0)); }
        // Darken
        case 12u { blend_rgb = min(base.rgb, overlay.rgb); }
        // Lighten
        case 13u { blend_rgb = max(base.rgb, overlay.rgb); }
        // Hue: overlay hue + base saturation + base lightness
        case 14u {
            let base_hsl    = rgb_to_hsl(base.rgb);
            let overlay_hsl = rgb_to_hsl(overlay.rgb);
            blend_rgb = hsl_to_rgb(vec3<f32>(overlay_hsl.x, base_hsl.y, base_hsl.z));
        }
        // Saturation: base hue + overlay saturation + base lightness
        case 15u {
            let base_hsl    = rgb_to_hsl(base.rgb);
            let overlay_hsl = rgb_to_hsl(overlay.rgb);
            blend_rgb = hsl_to_rgb(vec3<f32>(base_hsl.x, overlay_hsl.y, base_hsl.z));
        }
        // Color: overlay hue + overlay saturation + base lightness
        case 16u {
            let base_hsl    = rgb_to_hsl(base.rgb);
            let overlay_hsl = rgb_to_hsl(overlay.rgb);
            blend_rgb = hsl_to_rgb(vec3<f32>(overlay_hsl.x, overlay_hsl.y, base_hsl.z));
        }
        // Luminosity: base hue + base saturation + overlay lightness
        case 17u {
            let base_hsl    = rgb_to_hsl(base.rgb);
            let overlay_hsl = rgb_to_hsl(overlay.rgb);
            blend_rgb = hsl_to_rgb(vec3<f32>(base_hsl.x, base_hsl.y, overlay_hsl.z));
        }
        default  { blend_rgb = overlay.rgb; }
    }

    // Apply opacity: modulate blend result against base using overlay.a * opacity.
    let effective_alpha = overlay.a * u.opacity;
    let out_rgb = mix(base.rgb, blend_rgb, effective_alpha);
    return vec4<f32>(clamp(out_rgb, vec3<f32>(0.0), vec3<f32>(1.0)), base.a);
}