kiss3d 0.45.0

Keep it simple, stupid, 2D and 3D graphics engine for Rust.
Documentation
import package::common::fullscreen_triangle_xy;
// Auto-exposure adaptation: smoothly move the current exposure toward the target
// implied by the metered average luminance (eye adaptation).
//
// Reads the 1x1 metered luminance and the previous frame's 1x1 exposure, and
// writes the new 1x1 exposure. The target exposure is `key / avg_luminance`,
// clamped to a [min, max] multiplier, approached exponentially over `dt`.

struct AdaptUniforms {
    dt: f32,
    speed: f32,
    min_exposure: f32,
    max_exposure: f32,
    key: f32,
    // Scalar padding (a vec3 here would force 16-byte alignment → 48-byte struct,
    // mismatching the tightly-packed 32-byte Rust `AdaptUniforms`).
    _pad0: f32,
    _pad1: f32,
    _pad2: f32,
};

@group(0) @binding(0) var t_meter: texture_2d<f32>;
@group(0) @binding(1) var t_prev: texture_2d<f32>;
@group(0) @binding(2) var s_point: sampler;
@group(0) @binding(3) var<uniform> u: AdaptUniforms;

struct VsOut {
    @builtin(position) pos: vec4<f32>,
};

@vertex
fn vs_main(@builtin(vertex_index) vid: u32) -> VsOut {
    var out: VsOut;
    out.pos = vec4<f32>(fullscreen_triangle_xy(vid), 0.0, 1.0);
    return out;
}

@fragment
fn fs_main(_in: VsOut) -> @location(0) vec4<f32> {
    let c = vec2<f32>(0.5, 0.5);
    let avg = textureSampleLevel(t_meter, s_point, c, 0.0).r;
    let prev = textureSampleLevel(t_prev, s_point, c, 0.0).r;

    var tgt = u.key / max(avg, 1e-4);
    tgt = clamp(tgt, u.min_exposure, u.max_exposure);

    // Exponential smoothing, frame-rate independent via dt.
    let t = clamp(1.0 - exp(-u.dt * u.speed), 0.0, 1.0);
    // First frame (prev == 0) snaps straight to the target.
    let blended = select(mix(prev, tgt, t), tgt, prev <= 0.0);
    return vec4<f32>(blended, 0.0, 0.0, 1.0);
}