molgfx-render 0.3.4

The render graph, passes and the engine that drives a frame.
Documentation
use super::{
    AntiAliasingStyle, BackdropStyle, DepthOfField, DisplayTransform, EffectLayer,
    IllustrationStyle, LightingEnvironment, MotionBlur, PresentationEffect, RenderProfile,
};
use crate::FocusTarget;
use crate::{DisplayGamut, TransferFunction};

fn approximately(left: f32, right: f32) -> bool {
    (left - right).abs() <= f32::EPSILON
}

#[test]
fn inspection_profile_resolves_to_neutral_presentation() {
    assert_eq!(
        RenderProfile::inspection().resolve().illustration(),
        IllustrationStyle::default()
    );
}

#[test]
fn illustrative_profile_resolves_the_publication_treatment() {
    assert_eq!(
        RenderProfile::illustrative().resolve().illustration(),
        IllustrationStyle::publication()
    );
}

#[test]
fn cinematic_profile_resolves_a_bounded_physical_lens() {
    let Some(lens) = RenderProfile::cinematic().resolve().depth_of_field() else {
        panic!("cinematic profile includes a lens")
    };
    assert_eq!(lens, DepthOfField::cinematic());
    assert_eq!(
        RenderProfile::cinematic().resolve().motion_blur(),
        Some(MotionBlur::cinematic())
    );
    let expected = [24.0, 50.0 / (8.0 * 36.0), 14.0, 7.0];
    assert!(
        lens.packed(24.0)
            .into_iter()
            .zip(expected)
            .all(|(left, right)| approximately(left, right))
    );
}

#[test]
fn cinematic_profile_does_not_select_a_backdrop_as_biological_context() {
    let resolved = RenderProfile::cinematic().resolve();
    assert_eq!(resolved.backdrop(), BackdropStyle::default());
    assert_eq!(resolved.display(), DisplayTransform::cinematic());
    assert!(
        resolved
            .packed_presentation(false)
            .into_iter()
            .flatten()
            .all(f32::is_finite)
    );
}

#[test]
fn backdrop_and_display_are_independently_composable() {
    let custom = BackdropStyle {
        top: molgfx_math::Rgba8::opaque(12, 24, 36),
        bottom: molgfx_math::Rgba8::opaque(40, 52, 64),
        glow_color: molgfx_math::Rgba8::opaque(80, 90, 100),
        glow_strength: 0.0,
    };
    let resolved = RenderProfile::inspection()
        .with_effect(PresentationEffect::Backdrop(custom))
        .resolve();
    assert_eq!(resolved.backdrop(), custom);
    assert_eq!(resolved.display(), DisplayTransform::default());
}

#[test]
fn transparent_backdrops_pack_zero_coverage_without_changing_display_state() {
    let packed = crate::engine::backdrop::pack(
        BackdropStyle::transparent(),
        DisplayTransform::default(),
        false,
        [0.0; 4],
    );
    assert_eq!(packed[4][0].to_bits(), 0.0f32.to_bits());
    assert_eq!(packed[4][1].to_bits(), 0.0f32.to_bits());
    assert_eq!(packed[1][3].to_bits(), 1.0f32.to_bits());
}

#[test]
fn managed_display_state_packs_gamut_transfer_and_peak_luminance() {
    let display = DisplayTransform {
        gamut: DisplayGamut::Rec2020,
        transfer: TransferFunction::Pq,
        peak_luminance_nits: 1_000.0,
        ..DisplayTransform::default()
    };
    let packed = crate::engine::backdrop::pack(BackdropStyle::default(), display, false, [0.0; 4]);
    assert!(approximately(packed[4][3], 2.0 + 2.0 * 4.0));
    assert!(approximately(packed[5][3], 1_000.0));
}

#[test]
fn lighting_is_independent_from_backdrop_and_display() {
    let mut lighting = LightingEnvironment::documentary();
    lighting.key_strength = 2.25;
    let resolved = RenderProfile::inspection()
        .with_effect(PresentationEffect::Lighting(lighting))
        .resolve();
    assert!(approximately(
        resolved.lighting().key_strength,
        lighting.key_strength
    ));
    assert_eq!(resolved.lighting().key_color, lighting.key_color);
    assert!(resolved.lighting().key_direction.is_normalized());
    assert_eq!(resolved.backdrop(), BackdropStyle::default());
    assert_eq!(resolved.display(), DisplayTransform::default());
    assert!(
        resolved
            .packed_lighting()
            .into_iter()
            .flatten()
            .all(f32::is_finite)
    );
}

#[test]
fn malformed_lighting_resolves_to_finite_bounded_values() {
    let mut lighting = LightingEnvironment::neutral();
    lighting.key_direction = molgfx_math::Vec3::splat(f32::NAN);
    lighting.fill_direction = molgfx_math::Vec3::ZERO;
    lighting.diffuse_strength = f32::INFINITY;
    lighting.key_strength = -1.0;
    let resolved = RenderProfile::inspection()
        .with_effect(PresentationEffect::Lighting(lighting))
        .resolve()
        .lighting();
    assert!(resolved.key_direction.is_normalized());
    assert!(resolved.fill_direction.is_normalized());
    assert!(approximately(resolved.key_strength, 0.0));
    assert!(approximately(
        resolved.diffuse_strength,
        LightingEnvironment::neutral().diffuse_strength
    ));
}

#[test]
fn zero_weight_lens_layers_leave_graph_topology_neutral() {
    let profile = RenderProfile::inspection().with_layer(
        EffectLayer::new(PresentationEffect::DepthOfField(DepthOfField::cinematic()))
            .with_weight(0.0),
    );
    assert!(profile.resolve().depth_of_field().is_none());
}

#[test]
fn effect_layers_resolve_by_priority_and_blend_weight() {
    let strong = PresentationEffect::Illustration(IllustrationStyle {
        silhouette_strength: 1.0,
        cavity_strength: 0.0,
        depth_cue_strength: 0.0,
        posterize_levels: 0.0,
        motion_persistence: 0.0,
        outline_width: 0.0,
    });
    let soft = PresentationEffect::Illustration(IllustrationStyle {
        silhouette_strength: 0.2,
        cavity_strength: 0.4,
        depth_cue_strength: 0.6,
        posterize_levels: 0.0,
        motion_persistence: 0.0,
        outline_width: 0.0,
    });
    let resolved = RenderProfile::inspection()
        .with_layer(EffectLayer::new(soft).with_priority(20))
        .with_layer(EffectLayer::new(strong).with_priority(10))
        .with_layer(EffectLayer::new(strong).with_priority(30).with_weight(0.5))
        .resolve()
        .illustration();
    assert!(approximately(resolved.silhouette_strength, 0.6));
    assert!(approximately(resolved.cavity_strength, 0.2));
    assert!(approximately(resolved.depth_cue_strength, 0.3));
}

#[test]
fn malformed_layer_values_resolve_to_bounded_neutral_values() {
    let effect = PresentationEffect::Illustration(IllustrationStyle {
        silhouette_strength: f32::NAN,
        cavity_strength: -1.0,
        depth_cue_strength: 4.0,
        posterize_levels: 1.0,
        motion_persistence: -0.5,
        outline_width: -3.0,
    });
    let resolved = RenderProfile::inspection()
        .with_layer(EffectLayer::new(effect).with_weight(f32::INFINITY))
        .resolve()
        .illustration();
    assert_eq!(resolved, IllustrationStyle::default());
    assert_eq!(
        resolved.packed(f32::INFINITY).map(f32::to_bits),
        [0.0, 0.0, 0.0, 1.0].map(f32::to_bits)
    );
}

#[test]
fn cel_posterize_bands_snap_and_disable_below_two() {
    // Off by default, so ordinary shading is unchanged.
    assert_eq!(
        IllustrationStyle::default().npr_packed()[0].to_bits(),
        0.0f32.to_bits()
    );
    // A fractional count floors to whole bands.
    let cel = IllustrationStyle {
        posterize_levels: 4.7,
        ..IllustrationStyle::default()
    };
    assert_eq!(cel.npr_packed()[0].to_bits(), 4.0f32.to_bits());
    // Below two disables cel shading rather than snapping up.
    let low = IllustrationStyle {
        posterize_levels: 1.5,
        ..IllustrationStyle::default()
    };
    assert_eq!(low.npr_packed()[0].to_bits(), 0.0f32.to_bits());
    // Clamped above sixteen and immune to non-finite input.
    let high = IllustrationStyle {
        posterize_levels: 999.0,
        ..IllustrationStyle::default()
    };
    assert_eq!(high.npr_packed()[0].to_bits(), 16.0f32.to_bits());
    let bad = IllustrationStyle {
        posterize_levels: f32::NAN,
        ..IllustrationStyle::default()
    };
    assert_eq!(bad.npr_packed()[0].to_bits(), 0.0f32.to_bits());
    // Motion-trail persistence rides the same lane's y and clamps to [0, 1].
    let trails = IllustrationStyle {
        motion_persistence: 0.6,
        ..IllustrationStyle::default()
    };
    assert!((trails.npr_packed()[1] - 0.6).abs() < 1e-6);
    let over = IllustrationStyle {
        motion_persistence: 5.0,
        ..IllustrationStyle::default()
    };
    assert_eq!(over.npr_packed()[1].to_bits(), 1.0f32.to_bits());
    // Outline width rides z and clamps to [0, 8]; the default stays a 1px edge.
    assert_eq!(
        IllustrationStyle::default().npr_packed()[2].to_bits(),
        0.0f32.to_bits()
    );
    let thick = IllustrationStyle {
        outline_width: 12.0,
        ..IllustrationStyle::default()
    };
    assert_eq!(thick.npr_packed()[2].to_bits(), 8.0f32.to_bits());
}

#[test]
fn malformed_focus_targets_resolve_to_the_camera_target() {
    let mut lens = DepthOfField::cinematic();
    lens.focus = FocusTarget::Distance(f32::NAN);
    assert_eq!(lens.sanitize().focus, FocusTarget::CameraTarget);
    lens.focus = FocusTarget::WorldPoint(molgfx_math::Vec3::splat(f32::INFINITY));
    assert_eq!(lens.sanitize().focus, FocusTarget::CameraTarget);
}

#[test]
fn anti_aliasing_is_unset_until_a_layer_states_it() {
    assert_eq!(RenderProfile::inspection().resolve().antialias(), None);
    assert_eq!(RenderProfile::cinematic().resolve().antialias(), None);
}

#[test]
fn a_stated_anti_aliasing_layer_wins_over_the_tier_default() {
    let smoothed = RenderProfile::inspection().with_effect(PresentationEffect::AntiAliasing(
        AntiAliasingStyle::smoothed(),
    ));
    assert_eq!(
        smoothed.resolve().antialias(),
        Some(AntiAliasingStyle::smoothed())
    );

    let plain = RenderProfile::inspection()
        .with_effect(PresentationEffect::AntiAliasing(AntiAliasingStyle::none()));
    assert_eq!(plain.resolve().antialias(), Some(AntiAliasingStyle::none()));
}

#[test]
fn the_later_anti_aliasing_layer_in_priority_order_wins() {
    let profile = RenderProfile::inspection()
        .with_layer(EffectLayer::new(PresentationEffect::AntiAliasing(
            AntiAliasingStyle::none(),
        )))
        .with_layer(
            EffectLayer::new(PresentationEffect::AntiAliasing(
                AntiAliasingStyle::smoothed(),
            ))
            .with_priority(1),
        );
    assert_eq!(
        profile.resolve().antialias(),
        Some(AntiAliasingStyle::smoothed())
    );
}