scena 1.7.0

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use crate::assets::Assets;
use crate::geometry::{Primitive, Vertex};
use crate::material::Color;
use crate::scene::{PerspectiveCamera, Scene, Transform, Vec3};

use super::{AntiAliasing, PostBloomConfig, Renderer, ScreenSpaceAmbientOcclusionConfig};

const CAMERA_DISTANCE_FOR_NDC_FIXTURES: f32 = 1.732_050_8;

#[test]
fn gpu_post_passes_have_independent_quality_measurements() {
    let Some((baseline_edge, baseline_edge_stats)) =
        render_gpu_post_frame(32, 32, fxaa_edge_scene, |renderer| {
            renderer.set_anti_aliasing(AntiAliasing::None);
            renderer.clear_bloom();
            renderer.clear_screen_space_ambient_occlusion();
        })
    else {
        return;
    };
    let Some((fxaa, fxaa_stats)) = render_gpu_post_frame(32, 32, fxaa_edge_scene, |renderer| {
        renderer.set_anti_aliasing(AntiAliasing::Fxaa);
        renderer.clear_bloom();
        renderer.clear_screen_space_ambient_occlusion();
    }) else {
        return;
    };
    assert_eq!(baseline_edge_stats.fxaa_passes, 0);
    assert_eq!(fxaa_stats.fxaa_passes, 1);
    assert_eq!(fxaa_stats.bloom_passes, 0);
    assert_eq!(fxaa_stats.ambient_occlusion_passes, 0);
    let baseline_edges = hard_edge_transition_count(&baseline_edge, 32, 32, 200);
    let fxaa_edges = hard_edge_transition_count(&fxaa, 32, 32, 200);
    assert!(
        fxaa_edges < baseline_edges,
        "FXAA alone should reduce high-contrast edge transitions; baseline={baseline_edges} fxaa={fxaa_edges}"
    );

    let Some((baseline_bloom, baseline_bloom_stats)) =
        render_gpu_post_frame(32, 32, bloom_reference_scene, |renderer| {
            renderer.set_anti_aliasing(AntiAliasing::None);
            renderer.clear_bloom();
            renderer.clear_screen_space_ambient_occlusion();
        })
    else {
        return;
    };
    let Some((bloom, bloom_stats)) =
        render_gpu_post_frame(32, 32, bloom_reference_scene, |renderer| {
            renderer.set_anti_aliasing(AntiAliasing::None);
            renderer.set_bloom(Some(PostBloomConfig::new(96, 0.65, 3)));
            renderer.clear_screen_space_ambient_occlusion();
        })
    else {
        return;
    };
    assert_eq!(baseline_bloom_stats.bloom_passes, 0);
    assert_eq!(bloom_stats.bloom_passes, 1);
    assert_eq!(bloom_stats.fxaa_passes, 0);
    assert_eq!(bloom_stats.ambient_occlusion_passes, 0);
    let baseline_halo = region_luma_sum_outside(&baseline_bloom, 32, 32, 12..20, 12..20);
    let bloom_halo = region_luma_sum_outside(&bloom, 32, 32, 12..20, 12..20);
    assert!(
        bloom_halo > baseline_halo + 1_000,
        "bloom alone should add energy outside the emitter silhouette; baseline={baseline_halo} bloom={bloom_halo}"
    );

    let Some((baseline_ssao, baseline_ssao_stats)) =
        render_gpu_post_frame(48, 48, ssao_depth_contact_scene, |renderer| {
            renderer.set_anti_aliasing(AntiAliasing::None);
            renderer.clear_bloom();
            renderer.clear_screen_space_ambient_occlusion();
        })
    else {
        return;
    };
    let Some((ssao, ssao_stats)) =
        render_gpu_post_frame(48, 48, ssao_depth_contact_scene, |renderer| {
            renderer.set_anti_aliasing(AntiAliasing::None);
            renderer.clear_bloom();
            renderer.set_screen_space_ambient_occlusion(Some(
                ScreenSpaceAmbientOcclusionConfig::new(4, 0.8, 0.0),
            ));
        })
    else {
        return;
    };
    assert_eq!(baseline_ssao_stats.ambient_occlusion_passes, 0);
    assert_eq!(ssao_stats.ambient_occlusion_passes, 1);
    assert_eq!(ssao_stats.bloom_passes, 0);
    assert_eq!(ssao_stats.fxaa_passes, 0);
    let contact_drop = max_luma_drop_ring(
        &baseline_ssao,
        &ssao,
        48,
        (14..28, 18..30),
        (18..24, 20..28),
    );
    let baseline_open = average_luma_region(&baseline_ssao, 48, 8..14, 20..28);
    let ssao_open = average_luma_region(&ssao, 48, 8..14, 20..28);
    assert!(
        contact_drop >= 4,
        "SSAO alone should darken at least one contact/corner pixel around the foreground depth edge; contact_drop={contact_drop}"
    );
    assert!(
        (baseline_open - ssao_open).abs() <= 2.0,
        "SSAO alone should leave open floor within tolerance; baseline={baseline_open:.2} ssao={ssao_open:.2}"
    );
}

fn render_gpu_post_frame<F, S>(
    width: u32,
    height: u32,
    scene_factory: S,
    configure: F,
) -> Option<(Vec<u8>, crate::RendererStats)>
where
    F: FnOnce(&mut Renderer),
    S: FnOnce() -> (Assets, Scene, crate::CameraKey),
{
    let Ok(mut renderer) = Renderer::headless_gpu(width, height) else {
        return None;
    };
    let (assets, mut scene, camera) = scene_factory();
    renderer.set_background_color(Color::BLACK);
    configure(&mut renderer);
    renderer
        .prepare_with_assets(&mut scene, &assets)
        .expect("GPU post quality scene prepares");
    renderer
        .render(&scene, camera)
        .expect("GPU post quality scene renders");
    Some((renderer.frame_rgba8().to_vec(), renderer.stats()))
}

fn fxaa_edge_scene() -> (Assets, Scene, crate::CameraKey) {
    let assets = Assets::new();
    let mut scene = Scene::new();
    let camera = scene.add_default_camera().expect("camera inserts");
    scene
        .add_renderable(
            scene.root(),
            quad_primitives(
                0.0,
                -1.1,
                1.1,
                1.1,
                0.0,
                Color::from_linear_rgb(2.0, 2.0, 2.0),
            )
            .to_vec(),
            Transform::default(),
        )
        .expect("FXAA edge primitive inserts");
    (assets, scene, camera)
}

fn bloom_reference_scene() -> (Assets, Scene, crate::CameraKey) {
    let assets = Assets::new();
    let mut scene = Scene::new();
    let camera = scene.add_default_camera().expect("camera inserts");
    let mut primitives = Vec::new();
    primitives.extend(quad_primitives(
        -1.2,
        -1.2,
        1.2,
        1.2,
        -0.12,
        Color::from_linear_rgb(0.08, 0.08, 0.08),
    ));
    primitives.extend(quad_primitives(
        -0.32,
        -0.32,
        0.32,
        0.32,
        0.0,
        Color::from_linear_rgb(2.0, 2.0, 2.0),
    ));
    scene
        .add_renderable(scene.root(), primitives, Transform::default())
        .expect("bloom reference primitives insert");
    (assets, scene, camera)
}

fn ssao_depth_contact_scene() -> (Assets, Scene, crate::CameraKey) {
    let assets = Assets::new();
    let mut scene = Scene::new();
    let camera = scene
        .add_perspective_camera(
            scene.root(),
            PerspectiveCamera::default(),
            Transform::at(Vec3::new(0.0, 0.0, CAMERA_DISTANCE_FOR_NDC_FIXTURES)),
        )
        .expect("camera inserts");
    scene
        .set_active_camera(camera)
        .expect("camera becomes active");
    scene
        .add_renderable(
            scene.root(),
            quad_primitives(
                -0.75,
                -0.55,
                0.75,
                0.35,
                0.0,
                Color::from_linear_rgb(1.0, 1.0, 1.0),
            )
            .to_vec(),
            Transform::default(),
        )
        .expect("contact floor inserts");
    scene
        .add_renderable(
            scene.root(),
            quad_primitives(
                -0.14,
                -0.18,
                0.14,
                0.18,
                0.16,
                Color::from_linear_rgb(0.72, 0.72, 0.72),
            )
            .to_vec(),
            Transform::default(),
        )
        .expect("contact block inserts");
    (assets, scene, camera)
}

fn quad_primitives(x0: f32, y0: f32, x1: f32, y1: f32, z: f32, color: Color) -> [Primitive; 2] {
    [
        Primitive::triangle([
            Vertex {
                position: Vec3::new(x0, y0, z),
                color,
            },
            Vertex {
                position: Vec3::new(x1, y0, z),
                color,
            },
            Vertex {
                position: Vec3::new(x1, y1, z),
                color,
            },
        ]),
        Primitive::triangle([
            Vertex {
                position: Vec3::new(x0, y0, z),
                color,
            },
            Vertex {
                position: Vec3::new(x1, y1, z),
                color,
            },
            Vertex {
                position: Vec3::new(x0, y1, z),
                color,
            },
        ]),
    ]
}

fn hard_edge_transition_count(frame: &[u8], width: u32, height: u32, threshold: u8) -> u64 {
    let mut count = 0;
    for y in 0..height {
        for x in 0..width.saturating_sub(1) {
            if luma_at(frame, width, x, y).abs_diff(luma_at(frame, width, x + 1, y)) >= threshold {
                count += 1;
            }
        }
    }
    count
}

fn region_luma_sum_outside(
    frame: &[u8],
    width: u32,
    height: u32,
    silhouette_x: std::ops::Range<u32>,
    silhouette_y: std::ops::Range<u32>,
) -> u64 {
    let mut total = 0;
    for y in 0..height {
        for x in 0..width {
            if silhouette_x.contains(&x) && silhouette_y.contains(&y) {
                continue;
            }
            total += u64::from(luma_at(frame, width, x, y));
        }
    }
    total
}

fn average_luma_region(
    frame: &[u8],
    width: u32,
    region_x: std::ops::Range<u32>,
    region_y: std::ops::Range<u32>,
) -> f32 {
    let mut total = 0u64;
    let mut count = 0u64;
    for y in region_y {
        for x in region_x.clone() {
            total += u64::from(luma_at(frame, width, x, y));
            count += 1;
        }
    }
    total as f32 / count.max(1) as f32
}

fn max_luma_drop_ring(
    before: &[u8],
    after: &[u8],
    width: u32,
    outer: (std::ops::Range<u32>, std::ops::Range<u32>),
    inner: (std::ops::Range<u32>, std::ops::Range<u32>),
) -> u8 {
    let mut max_drop = 0u8;
    for y in outer.1 {
        for x in outer.0.clone() {
            if inner.0.contains(&x) && inner.1.contains(&y) {
                continue;
            }
            let drop = luma_at(before, width, x, y).saturating_sub(luma_at(after, width, x, y));
            max_drop = max_drop.max(drop);
        }
    }
    max_drop
}

fn luma_at(frame: &[u8], width: u32, x: u32, y: u32) -> u8 {
    let index = ((y * width + x) * 4) as usize;
    let red = frame[index] as f32;
    let green = frame[index + 1] as f32;
    let blue = frame[index + 2] as f32;
    (0.299 * red + 0.587 * green + 0.114 * blue).round() as u8
}