scena 1.7.2

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use crate::assets::{Assets, GeometryHandle, MaterialHandle, TextureHandle};
use crate::diagnostics::PrepareError;
use crate::material::MaterialKind;
use crate::scene::{Scene, Vec3};

use super::lighting::PreparedLights;
use super::materials::validate_material_texture_handles;
use super::primitives::append_geometry_primitives;
use super::shadows;
use super::transforms;
use super::types::{
    DeformationInputs, GeometryPrimitiveSource, PreparedInstanceRecord, PreparedInstanceSet,
    PreparedStrokeSegment, PrimitiveBakeParams, PrimitiveSinks,
};
use super::{PreparedEnvironmentLighting, draw_uniform_tint};
use crate::render::{RasterTarget, camera::CameraProjection};

const MIN_AUTO_INSTANCE_GROUP_SIZE: usize = 4;

#[derive(Debug, Clone, Copy)]
struct AutoInstanceCandidate {
    node: crate::scene::NodeKey,
    transform: crate::scene::Transform,
}

#[derive(Debug)]
struct AutoInstanceGroup {
    geometry: GeometryHandle,
    material: MaterialHandle,
    candidates: Vec<AutoInstanceCandidate>,
}

#[allow(clippy::too_many_arguments)]
pub(super) fn append_auto_instanced_mesh_groups<F>(
    target: RasterTarget,
    screen_space_scale: f32,
    scene: &Scene,
    assets: Option<&Assets<F>>,
    origin_shift: Vec3,
    lights: &PreparedLights,
    shadow_occluders: &[shadows::ShadowOccluder],
    camera_projection: Option<&CameraProjection>,
    backend_sampled_base_color_textures: &[TextureHandle],
    backend_material_slots: &[crate::assets::MaterialHandle],
    environment_lighting: PreparedEnvironmentLighting,
    instances: &mut Vec<PreparedInstanceSet>,
    strokes: &mut Vec<PreparedStrokeSegment>,
) -> Result<Vec<crate::scene::NodeKey>, PrepareError> {
    let Some(assets) = assets else {
        return Ok(Vec::new());
    };
    let mut groups: Vec<AutoInstanceGroup> = Vec::new();
    for (node, mesh, transform) in scene.mesh_nodes() {
        if scene.mesh_lods(node).is_some() {
            continue;
        }
        if scene.morph_weights(node).is_some() || scene.skin_matrices(node).is_some() {
            continue;
        }
        let tint = scene.node_tint(node).unwrap_or(None);
        if tint.is_some_and(|tint| tint.a < 1.0) {
            continue;
        }
        let candidate = AutoInstanceCandidate { node, transform };
        if let Some(group) = groups
            .iter_mut()
            .find(|group| group.geometry == mesh.geometry() && group.material == mesh.material())
        {
            group.candidates.push(candidate);
        } else {
            groups.push(AutoInstanceGroup {
                geometry: mesh.geometry(),
                material: mesh.material(),
                candidates: vec![candidate],
            });
        }
    }

    let mut auto_instanced_nodes = Vec::new();
    for group in groups
        .into_iter()
        .filter(|group| group.candidates.len() >= MIN_AUTO_INSTANCE_GROUP_SIZE)
    {
        let source_node = group.candidates[0].node;
        let geometry = assets
            .geometry(group.geometry)
            .ok_or(PrepareError::GeometryNotFound {
                node: source_node,
                geometry: group.geometry,
            })?;
        let material = assets
            .material(group.material)
            .ok_or(PrepareError::MaterialNotFound {
                node: source_node,
                material: group.material,
            })?;
        if matches!(
            material.kind(),
            MaterialKind::Line | MaterialKind::Wireframe | MaterialKind::Edge
        ) {
            continue;
        }
        validate_material_texture_handles(source_node, group.material, &material, assets)?;

        let mut retained = Vec::new();
        let mut instance_strokes = Vec::new();
        let mut transparent = Vec::new();
        append_geometry_primitives(
            GeometryPrimitiveSource {
                node: source_node,
                material_handle: group.material,
                geometry: &geometry,
                material: &material,
                assets,
                tint: None,
            },
            DeformationInputs::default(),
            PrimitiveBakeParams {
                target,
                screen_space_scale,
                transform: crate::scene::Transform::IDENTITY,
                origin_shift,
                lights,
                shadow_occluders,
                camera_projection,
                backend_sampled_base_color_textures,
                backend_material_slots,
                environment_lighting: environment_lighting.clone(),
            },
            PrimitiveSinks {
                primitives: &mut retained,
                strokes: &mut instance_strokes,
                transparent_primitives: &mut transparent,
            },
        )?;
        retained.extend(
            transparent
                .into_iter()
                .map(|transparent| transparent.primitive),
        );
        if retained.is_empty() {
            continue;
        }

        let records = group
            .candidates
            .iter()
            .map(|candidate| {
                PreparedInstanceRecord::auto_batched(
                    transforms::world_from_model_matrix(candidate.transform, Vec3::ZERO),
                    transforms::normal_from_model_matrix(candidate.transform),
                    draw_uniform_tint(scene.node_tint(candidate.node).unwrap_or(None)),
                )
            })
            .collect::<Vec<_>>();
        strokes.extend(instance_strokes);
        auto_instanced_nodes.extend(group.candidates.iter().map(|candidate| candidate.node));
        instances.push(PreparedInstanceSet::new_auto_batched(
            source_node,
            retained,
            records,
        ));
    }
    Ok(auto_instanced_nodes)
}