scena 1.7.1

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use std::collections::BTreeSet;

use crate::material::Color;
use crate::scene::{NodeKey, Scene, Vec3};

use super::{PreparedSceneState, prepare};

pub(super) fn assign_original_vertex_offsets(
    primitives: Vec<prepare::PreparedPrimitive>,
) -> Vec<prepare::PreparedPrimitive> {
    let mut next_vertex_offset = 0_u32;
    primitives
        .into_iter()
        .map(|primitive| {
            let updated = primitive.with_original_vertex_offset(next_vertex_offset);
            if updated.gpu_triangle_path() {
                next_vertex_offset = next_vertex_offset.saturating_add(3);
            }
            updated
        })
        .collect()
}

pub(super) fn next_gpu_vertex_offset(primitives: &[prepare::PreparedPrimitive]) -> u32 {
    primitives
        .iter()
        .filter(|primitive| primitive.gpu_triangle_path())
        .count()
        .saturating_mul(3) as u32
}

pub(super) fn assign_original_stroke_indices(
    strokes: Vec<prepare::PreparedStrokeSegment>,
) -> Vec<prepare::PreparedStrokeSegment> {
    strokes
        .into_iter()
        .enumerate()
        .map(|(index, stroke)| stroke.with_original_segment_index(index as u32))
        .collect()
}

pub(super) fn assign_original_instance_vertex_offsets(
    mut instances: Vec<prepare::PreparedInstanceSet>,
    mut next_vertex_offset: u32,
) -> Vec<prepare::PreparedInstanceSet> {
    for set in &mut instances {
        for primitive in set.primitives_mut() {
            let updated = primitive
                .clone()
                .with_original_vertex_offset(next_vertex_offset);
            *primitive = updated;
            next_vertex_offset = next_vertex_offset.saturating_add(3);
        }
    }
    instances
}

pub(super) fn filter_retained_primitives_for_scene(
    scene: &Scene,
    retained: &[prepare::PreparedPrimitive],
) -> Option<Vec<prepare::PreparedPrimitive>> {
    let mut visible = Vec::with_capacity(retained.len());
    for primitive in retained {
        let mut primitive = primitive.clone();
        if let Some(node) = primitive.source_node() {
            scene.node(node)?;
            if !scene.visible_for_active_camera(node) {
                continue;
            }
            primitive.set_tint(prepare::draw_uniform_tint(scene.node_tint(node).ok()?));
        }
        visible.push(primitive);
    }
    Some(visible)
}

pub(super) fn filter_retained_instances_for_scene(
    scene: &Scene,
    retained: &[prepare::PreparedInstanceSet],
) -> Option<Vec<prepare::PreparedInstanceSet>> {
    let mut visible_sets = Vec::with_capacity(retained.len());
    for set in retained {
        scene.node(set.source_node())?;
        if !scene.visible_for_active_camera(set.source_node()) {
            continue;
        }
        let live = scene.instance_set(set.source_set())?;
        let node_tint = scene.node_tint(set.source_node()).ok()?;
        let records = live
            .instances()
            .filter(|instance| instance.visible())
            .map(|instance| {
                prepare::PreparedInstanceRecord::new(
                    instance.id(),
                    prepare::transforms::world_from_model_matrix(instance.transform(), Vec3::ZERO),
                    prepare::transforms::normal_from_model_matrix(instance.transform()),
                    prepare::draw_uniform_tint(multiply_tint(node_tint, instance.tint())),
                )
            })
            .collect::<Vec<_>>();
        let mut set = set.clone();
        set.set_instances(records);
        visible_sets.push(set);
    }
    Some(visible_sets)
}

pub(super) fn filter_retained_strokes_for_scene(
    scene: &Scene,
    retained: &[prepare::PreparedStrokeSegment],
) -> Option<Vec<prepare::PreparedStrokeSegment>> {
    let mut visible = Vec::with_capacity(retained.len());
    for stroke in retained {
        let mut stroke = stroke.clone();
        if let Some(node) = stroke.source_node() {
            scene.node(node)?;
            if !scene.visible_for_active_camera(node) {
                continue;
            }
            stroke.set_tint(prepare::draw_uniform_tint(scene.node_tint(node).ok()?));
        }
        visible.push(stroke);
    }
    Some(visible)
}

pub(super) fn retained_template_covers_visible_sources(
    scene: &Scene,
    retained: &[prepare::PreparedPrimitive],
    retained_strokes: &[prepare::PreparedStrokeSegment],
    retained_instances: &[prepare::PreparedInstanceSet],
) -> bool {
    let mut retained_sources = retained
        .iter()
        .filter_map(prepare::PreparedPrimitive::source_node)
        .collect::<BTreeSet<NodeKey>>();
    retained_sources.extend(
        retained_strokes
            .iter()
            .filter_map(prepare::PreparedStrokeSegment::source_node),
    );
    retained_sources.extend(
        retained_instances
            .iter()
            .map(prepare::PreparedInstanceSet::source_node),
    );

    scene
        .renderable_nodes()
        .all(|(node, _, _)| retained_sources.contains(&node))
        && scene
            .mesh_nodes()
            .all(|(node, _, _)| retained_sources.contains(&node))
        && scene
            .instance_set_nodes()
            .all(|(node, _, _)| retained_sources.contains(&node))
        && scene
            .label_nodes()
            .all(|(node, _, _, _)| retained_sources.contains(&node))
}

pub(super) fn prepared_instance_count(prepared: &PreparedSceneState) -> u64 {
    prepared
        .instances
        .iter()
        .map(|set| set.instances().len() as u64)
        .sum()
}

fn multiply_tint(node_tint: Option<Color>, instance_tint: Option<Color>) -> Option<Color> {
    match (node_tint, instance_tint) {
        (Some(left), Some(right)) => Some(Color::from_linear_rgba(
            left.r * right.r,
            left.g * right.g,
            left.b * right.b,
            left.a * right.a,
        )),
        (Some(tint), None) | (None, Some(tint)) => Some(tint),
        (None, None) => None,
    }
}

#[cfg(test)]
mod tests {
    use crate::assets::Assets;
    use crate::geometry::{GeometryDesc, Primitive};
    use crate::material::{Color, MaterialDesc};
    use crate::scene::{Scene, Transform};

    use super::*;

    #[test]
    fn gpu_vertex_offsets_skip_non_gpu_retained_primitives_before_instances() {
        let assets = Assets::new();
        let geometry = assets.create_geometry(GeometryDesc::box_xyz(0.1, 0.1, 0.1));
        let material = assets.create_material(MaterialDesc::unlit(Color::WHITE));
        let mut scene = Scene::new();
        let (node, set) = scene
            .add_instance_set_node(scene.root(), geometry, material, Transform::IDENTITY)
            .expect("instance set inserts");
        let instance = scene
            .push_instance(set, Transform::IDENTITY)
            .expect("instance inserts");

        let retained_primitives = assign_original_vertex_offsets(vec![
            prepared_triangle(true),
            prepared_triangle(false),
            prepared_triangle(true),
        ]);
        assert_eq!(retained_primitives[0].original_vertex_offset(), 0);
        assert_eq!(
            retained_primitives[1].original_vertex_offset(),
            3,
            "non-GPU retained primitives may share the next GPU offset because they are never encoded into the triangle vertex buffer",
        );
        assert_eq!(
            retained_primitives[2].original_vertex_offset(),
            3,
            "later GPU primitives must stay contiguous in the encoded GPU vertex buffer"
        );

        let instances = assign_original_instance_vertex_offsets(
            vec![prepare::PreparedInstanceSet::new(
                node,
                set,
                vec![prepared_triangle(true)],
                vec![prepare::PreparedInstanceRecord::new(
                    instance,
                    identity_matrix4(),
                    identity_matrix4(),
                    Color::WHITE,
                )],
            )],
            next_gpu_vertex_offset(&retained_primitives),
        );

        assert_eq!(
            instances[0].primitives()[0].original_vertex_offset(),
            6,
            "instance templates start immediately after encoded GPU triangle vertices, not after CPU-only retained primitives"
        );
    }

    fn prepared_triangle(gpu_triangle_path: bool) -> prepare::PreparedPrimitive {
        let primitive =
            prepare::PreparedPrimitive::new(Primitive::unlit_triangle(), None, Color::WHITE);
        if gpu_triangle_path {
            primitive
        } else {
            primitive.without_gpu_triangle_path()
        }
    }

    const fn identity_matrix4() -> [f32; 16] {
        [
            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
        ]
    }
}