mirage-engine 0.1.0

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! What overlapping translucent and additive draws leave over one another.

use super::*;

/// The linear value an 8-bit sRGB-encoded channel byte holds, matching
/// the transfer function the target's own sRGB texture format applies.
fn srgb_to_linear(byte: u8) -> f32 {
    let encoded = f32::from(byte) / 255.0;
    if encoded <= 0.04045 {
        encoded / 12.92
    } else {
        ((encoded + 0.055) / 1.055).powf(2.4)
    }
}

/// The 8-bit sRGB-encoded byte a linear value reads back as.
fn linear_to_srgb(linear: f32) -> u8 {
    let linear = linear.clamp(0.0, 1.0);
    let encoded = if linear <= 0.003_130_8 {
        linear * 12.92
    } else {
        1.055 * linear.powf(1.0 / 2.4) - 0.055
    };
    (encoded * 255.0).round() as u8
}

/// One square at the origin, drawn flat in `color`.
fn pane(color: Color) -> Panes {
    Panes::new(vec![(0.0, Material::color(color))])
}

#[test]
fn overlapping_translucent_draws_composite_back_to_front_from_either_order() {
    let far = (-0.5, Material::color(Color::rgba(1.0, 0.0, 0.0, 0.5)));
    let near = (0.5, Material::color(Color::rgba(0.0, 0.0, 1.0, 0.5)));
    let read = |panes| rendered(raw("headless transparency"), panes);

    let Some(as_drawn) = read(Panes::new(vec![far, near])) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let Some(reversed) = read(Panes::new(vec![near, far])) else {
        return;
    };

    assert_eq!(
        middle(&as_drawn),
        middle(&reversed),
        "the pass sorts what the frame did not"
    );

    // The target's corner, a margin outside both squares, reads as the
    // frame's own background.
    let background = pixel(&as_drawn, 0, 0);
    let over = |channel: usize, far_channel: f32, near_channel: f32| {
        let bg = srgb_to_linear(background[channel]);
        let past_far = far_channel * 0.5 + bg * 0.5;
        linear_to_srgb(near_channel * 0.5 + past_far * 0.5)
    };
    let composite = [
        over(0, 1.0, 0.0),
        over(1, 0.0, 0.0),
        over(2, 0.0, 1.0),
        u8::MAX,
    ];
    let blended = middle(&as_drawn);

    assert!(
        blended
            .iter()
            .zip(composite)
            .all(|(&channel, expected)| channel.abs_diff(expected) <= 1),
        "half the near square over half the far one over the frame's own \
         background: {blended:?} against {composite:?}"
    );
}

#[test]
fn overlapping_additive_draws_leave_the_same_sum_from_either_order() {
    let far = (-0.5, Material::color(Color::rgb(0.25, 0.0, 0.0)).additive());
    let near = (0.5, Material::color(Color::rgb(0.0, 0.0, 0.5)).additive());
    let read = |panes| rendered(raw("headless additive"), panes);

    let Some(as_drawn) = read(Panes::new(vec![far, near])) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let Some(reversed) = read(Panes::new(vec![near, far])) else {
        return;
    };

    assert_eq!(
        middle(&as_drawn),
        middle(&reversed),
        "addition takes them in any order"
    );

    // The target's corner, a margin outside both squares, reads as the
    // frame's own background.
    let background = pixel(&as_drawn, 0, 0);
    let summed =
        |channel: usize, added: f32| linear_to_srgb(srgb_to_linear(background[channel]) + added);
    let expected = [summed(0, 0.25), summed(1, 0.0), summed(2, 0.5), u8::MAX];
    let blended = middle(&as_drawn);

    assert!(
        blended
            .iter()
            .zip(expected)
            .all(|(&channel, expected)| channel.abs_diff(expected) <= 1),
        "and leaves the two of them added onto the frame's own background: \
         {blended:?} against {expected:?}"
    );
}

#[test]
fn an_additive_draw_never_reads_back_darker_than_what_it_covers() {
    let ground = (-0.5, Material::color(Color::rgb(0.5, 0.25, 0.125)));
    let read = |panes| Some(middle(&rendered(raw("headless additive"), panes)?));
    let over = |color| {
        read(Panes::new(vec![
            ground,
            (0.5, Material::color(color).additive()),
        ]))
    };

    let Some(alone) = read(Panes::new(vec![ground])) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let Some(under_black) = over(Color::BLACK) else {
        return;
    };
    let Some(under_light) = over(Color::rgba(1.0, 1.0, 1.0, 0.25)) else {
        return;
    };
    let Some(scaled) = read(pane(Color::rgb(0.75, 0.5, 0.375))) else {
        return;
    };

    assert_eq!(under_black, alone, "a draw with no light in it adds none");
    assert!(
        under_light
            .iter()
            .zip(alone)
            .all(|(over, under)| *over >= under),
        "and one with light in it only ever adds: {under_light:?} over {alone:?}"
    );
    assert_eq!(under_light, scaled, "as much of it as the alpha asked for");
}