scena 1.7.2

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use super::*;
use crate::assets::Assets;
use crate::diagnostics::Backend;
use crate::geometry::GeometryDesc;
use crate::material::{Color, MaterialDesc};
use crate::render::prepare::cpu_bake::baked_shadow_visibility;
use crate::scene::Transform;

#[test]
fn backend_shaded_materials_skip_cpu_shadow_visibility_bake() {
    let scene = Scene::new();
    let lights = PreparedLights::from_scene(&scene, Vec3::ZERO);
    let position = Vec3::new(0.0, 0.0, 0.0);

    assert_eq!(baked_shadow_visibility(position, &lights, &[], true), 1.0);
    assert_eq!(baked_shadow_visibility(position, &lights, &[], false), 1.0);
}

#[test]
fn asset_mesh_primitives_keep_model_draw_transform_for_gpu_templates() {
    let assets = Assets::new();
    let geometry = assets.create_geometry(GeometryDesc::box_xyz(1.0, 1.0, 1.0));
    let material =
        assets.create_material(MaterialDesc::pbr_metallic_roughness(Color::WHITE, 0.0, 0.8));
    let mut scene = Scene::new();
    scene
        .mesh(geometry, material)
        .transform(Transform::at(Vec3::new(2.0, 0.0, 0.0)))
        .add()
        .expect("mesh inserts");
    let material_slots = collect_backend_material_slots(&scene, Some(&assets));
    let material_handles = material_slots
        .iter()
        .map(|slot| slot.handle)
        .collect::<Vec<_>>();

    let prepared = collect_prepared_primitives(
        RasterTarget {
            width: 64,
            height: 64,
            backend: Backend::HeadlessGpu,
        },
        1.0,
        &scene,
        Some(&assets),
        None,
        &[],
        &material_handles,
        PreparedEnvironmentLighting::default(),
    )
    .expect("scene prepares");

    assert!(
        prepared
            .primitives
            .iter()
            .any(|primitive| primitive.world_from_model()[12] == 2.0),
        "asset-backed GPU primitives must keep the model draw matrix so transform-only frames can update uniforms without rebuilding vertex bytes"
    );
}

#[test]
fn blended_material_primitives_skip_gpu_depth_prepass() {
    let assets = Assets::new();
    let geometry = assets.create_geometry(GeometryDesc::box_xyz(1.0, 1.0, 1.0));
    let material = assets.create_material(MaterialDesc::clear_glass(Color::CYAN));
    let mut scene = Scene::new();
    scene.mesh(geometry, material).add().expect("mesh inserts");
    let material_slots = collect_backend_material_slots(&scene, Some(&assets));
    let material_handles = material_slots
        .iter()
        .map(|slot| slot.handle)
        .collect::<Vec<_>>();

    let prepared = collect_prepared_primitives(
        RasterTarget {
            width: 64,
            height: 64,
            backend: Backend::WebGl2,
        },
        1.0,
        &scene,
        Some(&assets),
        None,
        &[],
        &material_handles,
        PreparedEnvironmentLighting::default(),
    )
    .expect("scene prepares");

    assert!(
        prepared
            .primitives
            .iter()
            .any(|primitive| !primitive.depth_prepass_eligible()),
        "alpha-blended material primitives must not write the GPU depth pre-pass; \
         otherwise glass occludes the scene it is supposed to transmit or blend over"
    );
    assert!(
        prepared
            .primitives
            .iter()
            .all(|primitive| !primitive.depth_prepass_eligible()),
        "all primitives in this one-glass-mesh scene should be transparent pass primitives"
    );
}

#[test]
fn opaque_node_tint_is_retained_per_primitive_not_baked_into_vertex_colors() {
    let assets = Assets::new();
    let geometry = assets.create_geometry(GeometryDesc::box_xyz(1.0, 1.0, 1.0));
    let material = assets.create_material(MaterialDesc::unlit(Color::WHITE));
    let mut scene = Scene::new();
    let mesh = scene
        .mesh(geometry, material)
        .transform(Transform::at(Vec3::new(-1.5, 0.0, 0.0)))
        .add()
        .expect("mesh inserts");
    let instance_set = scene
        .add_instance_set(
            scene.root(),
            geometry,
            material,
            Transform::at(Vec3::new(1.5, 0.0, 0.0)),
        )
        .expect("instance set inserts");
    scene
        .push_instance(instance_set, Transform::IDENTITY)
        .expect("instance inserts");
    let (instance_node, _, _) = scene
        .instance_set_nodes()
        .next()
        .expect("instance-set node is visible");

    scene
        .set_node_tint(mesh, Some(Color::from_linear_rgba(1.0, 0.0, 0.0, 1.0)))
        .expect("mesh tint sets");
    scene
        .set_node_tint(
            instance_node,
            Some(Color::from_linear_rgba(0.0, 0.0, 1.0, 1.0)),
        )
        .expect("instance-set tint sets");

    let prepared = collect_prepared_primitives(
        RasterTarget {
            width: 64,
            height: 64,
            backend: Backend::Headless,
        },
        1.0,
        &scene,
        Some(&assets),
        None,
        &[],
        &[],
        PreparedEnvironmentLighting::default(),
    )
    .expect("scene prepares");

    assert!(
        prepared.primitives.iter().any(|primitive| {
            primitive.tint() == Color::from_linear_rgba(1.0, 0.0, 0.0, 1.0)
                && primitive
                    .vertices()
                    .iter()
                    .all(|vertex| vertex.color == Color::WHITE)
        }),
        "mesh tint should be retained as draw tint without rewriting vertex colors"
    );
    assert!(
        prepared.primitives.iter().any(|primitive| {
            primitive.tint() == Color::from_linear_rgba(0.0, 0.0, 1.0, 1.0)
                && primitive
                    .vertices()
                    .iter()
                    .all(|vertex| vertex.color == Color::WHITE)
        }),
        "instance-set tint should be retained as draw tint without rewriting vertex colors"
    );
}

#[test]
fn hidden_instances_are_filtered_from_gpu_instance_records() {
    let assets = Assets::new();
    let geometry = assets.create_geometry(GeometryDesc::box_xyz(1.0, 1.0, 1.0));
    let material = assets.create_material(MaterialDesc::unlit(Color::WHITE));
    let mut scene = Scene::new();
    let instance_set = scene
        .add_instance_set(scene.root(), geometry, material, Transform::IDENTITY)
        .expect("instance set inserts");
    let visible = scene
        .push_instance(instance_set, Transform::at(Vec3::new(-1.0, 0.0, 0.0)))
        .expect("visible instance inserts");
    let hidden = scene
        .push_instance(instance_set, Transform::at(Vec3::new(1.0, 0.0, 0.0)))
        .expect("hidden instance inserts");
    scene
        .set_instance_visible(instance_set, hidden, false)
        .expect("hidden instance visibility sets");

    let prepared = collect_prepared_primitives(
        RasterTarget {
            width: 64,
            height: 64,
            backend: Backend::HeadlessGpu,
        },
        1.0,
        &scene,
        Some(&assets),
        None,
        &[],
        &[],
        PreparedEnvironmentLighting::default(),
    )
    .expect("scene prepares");

    assert_eq!(prepared.instances.len(), 1);
    let records = prepared.instances[0].instances();
    assert_eq!(
        records.len(),
        1,
        "hidden instances must not be retained for GPU instanced draws"
    );
    assert_eq!(records[0].source_instance(), Some(visible));
}

#[test]
fn repeated_mesh_nodes_auto_instance_on_gpu_prepare_path() {
    let assets = Assets::new();
    let geometry = assets.create_geometry(GeometryDesc::box_xyz(1.0, 1.0, 1.0));
    let material = assets.create_material(MaterialDesc::unlit(Color::WHITE));
    let mut scene = Scene::new();
    for x in [-1.5, -0.5, 0.5, 1.5] {
        scene
            .mesh(geometry, material)
            .transform(Transform::at(Vec3::new(x, 0.0, 0.0)))
            .add()
            .expect("mesh inserts");
    }

    let prepared = collect_prepared_primitives(
        RasterTarget {
            width: 64,
            height: 64,
            backend: Backend::HeadlessGpu,
        },
        1.0,
        &scene,
        Some(&assets),
        None,
        &[],
        &[],
        PreparedEnvironmentLighting::default(),
    )
    .expect("scene prepares");

    assert_eq!(
        prepared.instances.len(),
        1,
        "larger repeated ordinary mesh groups with the same geometry/material should auto-instance on the GPU path"
    );
    assert_eq!(
        prepared.instances[0].instances().len(),
        4,
        "the auto-instance batch must preserve every repeated node transform as an instance record"
    );
    assert!(
        prepared.primitives.is_empty(),
        "auto-instanced repeated mesh nodes should not also emit duplicate ordinary primitives"
    );

    let cpu_prepared = collect_prepared_primitives(
        RasterTarget {
            width: 64,
            height: 64,
            backend: Backend::Headless,
        },
        1.0,
        &scene,
        Some(&assets),
        None,
        &[],
        &[],
        PreparedEnvironmentLighting::default(),
    )
    .expect("cpu scene prepares");
    assert!(
        cpu_prepared.instances.is_empty(),
        "CPU reference rendering stays on ordinary primitives; auto-instancing is a GPU draw-call optimization"
    );
}

#[test]
fn screen_space_strokes_scale_to_full_frame_supersample_pixels() {
    let assets = Assets::new();
    let geometry = assets.create_geometry(GeometryDesc::line(
        Vec3::new(-0.5, 0.0, 0.0),
        Vec3::new(0.5, 0.0, 0.0),
    ));
    let material = assets.create_material(MaterialDesc::line(Color::WHITE, 2.0));
    let mut scene = Scene::new();
    scene.mesh(geometry, material).add().expect("line inserts");

    let prepared = collect_prepared_primitives(
        RasterTarget {
            width: 128,
            height: 128,
            backend: Backend::HeadlessGpu,
        },
        2.0,
        &scene,
        Some(&assets),
        None,
        &[],
        &[],
        PreparedEnvironmentLighting::default(),
    )
    .expect("line scene prepares");

    assert_eq!(prepared.strokes.len(), 1);
    assert_eq!(
        prepared.strokes[0].width_px(),
        4.0,
        "screen-space stroke widths are authored in final-frame pixels and must be expanded when drawing into a supersampled internal target"
    );
}