scena 1.7.2

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
// scena.color_contract.wgsl
//
// Shared display-transform contract for GPU/WebGPU/WebGL2 shaders. The Rust
// reference implementation lives in `src/render/color_contract.rs`; keep the
// constants and formulas byte-equivalent there and here.

const SCENA_PBR_NEUTRAL_F90: f32 = 0.04;
const SCENA_PBR_NEUTRAL_START_COMPRESSION: f32 = 0.8 - SCENA_PBR_NEUTRAL_F90;
const SCENA_PBR_NEUTRAL_DESATURATION: f32 = 0.15;

fn apply_tonemapper(color: vec3<f32>, color_management_mode: f32) -> vec3<f32> {
    if color_management_mode < 0.5 {
        return clamp(color, vec3<f32>(0.0), vec3<f32>(1.0));
    }
    if color_management_mode > 1.5 {
        return pbr_neutral_tonemap(color);
    }
    return aces_tonemap(color);
}

fn encode_post_target_rgb(color: vec3<f32>, output_transfer_mode: f32) -> vec3<f32> {
    if output_transfer_mode <= 0.5 {
        return color;
    }
    return linear_to_srgb(color);
}

fn linear_to_srgb(color: vec3<f32>) -> vec3<f32> {
    return vec3<f32>(
        linear_to_srgb_channel(color.r),
        linear_to_srgb_channel(color.g),
        linear_to_srgb_channel(color.b),
    );
}

fn linear_to_srgb_channel(channel: f32) -> f32 {
    let value = clamp(channel, 0.0, 1.0);
    if value <= 0.0031308 {
        return value * 12.92;
    }
    return 1.055 * pow(value, 1.0 / 2.4) - 0.055;
}

fn srgb_to_linear_channel(channel: f32) -> f32 {
    let value = clamp(channel, 0.0, 1.0);
    if value <= 0.04045 {
        return value / 12.92;
    }
    return pow((value + 0.055) / 1.055, 2.4);
}

fn pbr_neutral_tonemap(color_in: vec3<f32>) -> vec3<f32> {
    var color = max(color_in, vec3<f32>(0.0));
    let x = min(color.r, min(color.g, color.b));
    let offset = select(
        SCENA_PBR_NEUTRAL_F90,
        x - (x * x) / (4.0 * SCENA_PBR_NEUTRAL_F90),
        x < 2.0 * SCENA_PBR_NEUTRAL_F90,
    );
    color -= vec3<f32>(offset);
    let peak = max(color.r, max(color.g, color.b));
    if peak < SCENA_PBR_NEUTRAL_START_COMPRESSION {
        return color;
    }
    let compression_range = 1.0 - SCENA_PBR_NEUTRAL_START_COMPRESSION;
    let new_peak = 1.0 - compression_range * compression_range /
        (peak + compression_range - SCENA_PBR_NEUTRAL_START_COMPRESSION);
    color *= new_peak / peak;
    let desaturation_mix = 1.0 - 1.0 /
        (SCENA_PBR_NEUTRAL_DESATURATION * (peak - new_peak) + 1.0);
    return mix(color, new_peak * vec3<f32>(1.0), desaturation_mix);
}

fn aces_tonemap(color: vec3<f32>) -> vec3<f32> {
    let input = vec3<f32>(
        dot(vec3<f32>(0.59719, 0.35458, 0.04823), color),
        dot(vec3<f32>(0.076, 0.90834, 0.01566), color),
        dot(vec3<f32>(0.0284, 0.13383, 0.83777), color),
    );
    let fitted = vec3<f32>(
        rrt_and_odt_fit(input.r),
        rrt_and_odt_fit(input.g),
        rrt_and_odt_fit(input.b),
    );
    let output = vec3<f32>(
        dot(vec3<f32>(1.60475, -0.53108, -0.07367), fitted),
        dot(vec3<f32>(-0.10208, 1.10813, -0.00605), fitted),
        dot(vec3<f32>(-0.00327, -0.07276, 1.07602), fitted),
    );
    return clamp(output, vec3<f32>(0.0), vec3<f32>(1.0));
}

fn rrt_and_odt_fit(value: f32) -> f32 {
    let numerator = value * (value + 0.0245786) - 0.000090537;
    let denominator = value * (0.983729 * value + 0.432951) + 0.238081;
    return numerator / denominator;
}