scena 1.0.0

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use crate::assets::{Assets, GeometryHandle, MaterialHandle};
use crate::geometry::GeometryTopology;

use super::super::transforms::{compose_transform, rotate_vec3};
use super::super::{Camera, CameraKey, InstanceId, NodeKey, Scene, Transform, Vec3};
use super::{
    SceneCameraFrustumInspection, SceneDrawInspection, SceneMaterialInspection,
    SceneNormalInspection,
};

impl Scene {
    pub(super) fn inspect_draw_list<F>(
        &self,
        assets: Option<&Assets<F>>,
    ) -> Vec<SceneDrawInspection> {
        let Some(assets) = assets else {
            return Vec::new();
        };
        let mut draw_list = Vec::new();
        for (node, mesh, node_transform) in self.mesh_nodes() {
            append_draw_entry(
                &mut draw_list,
                assets,
                DrawEntryInput {
                    node,
                    instance: None,
                    geometry: mesh.geometry(),
                    material: mesh.material(),
                    world_transform: self.world_transform(node).unwrap_or(node_transform),
                },
            );
        }
        for (node, instance_set, node_transform) in self.instance_set_nodes() {
            let node_world = self.world_transform(node).unwrap_or(node_transform);
            for instance in instance_set.instances() {
                append_draw_entry(
                    &mut draw_list,
                    assets,
                    DrawEntryInput {
                        node,
                        instance: Some(instance.id()),
                        geometry: instance_set.geometry(),
                        material: instance_set.material(),
                        world_transform: compose_transform(node_world, instance.transform()),
                    },
                );
            }
        }
        draw_list
    }

    pub(super) fn inspect_normal_overlays<F>(
        &self,
        assets: Option<&Assets<F>>,
    ) -> Vec<SceneNormalInspection> {
        let Some(assets) = assets else {
            return Vec::new();
        };
        let mut overlays = Vec::new();
        for (node, mesh, node_transform) in self.mesh_nodes() {
            append_normal_overlay(
                &mut overlays,
                assets,
                NormalOverlayInput {
                    node,
                    instance: None,
                    geometry: mesh.geometry(),
                    world_transform: self.world_transform(node).unwrap_or(node_transform),
                },
            );
        }
        for (node, instance_set, node_transform) in self.instance_set_nodes() {
            let node_world = self.world_transform(node).unwrap_or(node_transform);
            for instance in instance_set.instances() {
                append_normal_overlay(
                    &mut overlays,
                    assets,
                    NormalOverlayInput {
                        node,
                        instance: Some(instance.id()),
                        geometry: instance_set.geometry(),
                        world_transform: compose_transform(node_world, instance.transform()),
                    },
                );
            }
        }
        overlays
    }

    pub(super) fn inspect_camera_frustums(&self) -> Vec<SceneCameraFrustumInspection> {
        self.cameras
            .iter()
            .filter_map(|(camera, desc)| {
                let node = self.camera_node(camera)?;
                let world_transform = self.world_transform(node)?;
                Some(camera_frustum(camera, node, desc.clone(), world_transform))
            })
            .collect()
    }
}

#[derive(Debug, Clone, Copy)]
struct NormalOverlayInput {
    node: NodeKey,
    instance: Option<InstanceId>,
    geometry: GeometryHandle,
    world_transform: Transform,
}

const NORMAL_DEBUG_LENGTH: f32 = 0.1;

fn append_normal_overlay<F>(
    overlays: &mut Vec<SceneNormalInspection>,
    assets: &Assets<F>,
    input: NormalOverlayInput,
) {
    let Some(geometry) = assets.geometry(input.geometry) else {
        return;
    };
    let segments = geometry
        .vertices()
        .iter()
        .map(|vertex| {
            let start = transform_point(input.world_transform, vertex.position);
            let end = transform_point(
                input.world_transform,
                Vec3::new(
                    vertex.position.x + vertex.normal.x * NORMAL_DEBUG_LENGTH,
                    vertex.position.y + vertex.normal.y * NORMAL_DEBUG_LENGTH,
                    vertex.position.z + vertex.normal.z * NORMAL_DEBUG_LENGTH,
                ),
            );
            [start, end]
        })
        .collect();
    overlays.push(SceneNormalInspection {
        node: input.node,
        instance: input.instance,
        geometry: input.geometry,
        length: NORMAL_DEBUG_LENGTH,
        segments,
    });
}

#[derive(Debug, Clone, Copy)]
struct DrawEntryInput {
    node: NodeKey,
    instance: Option<InstanceId>,
    geometry: GeometryHandle,
    material: MaterialHandle,
    world_transform: Transform,
}

fn append_draw_entry<F>(
    draw_list: &mut Vec<SceneDrawInspection>,
    assets: &Assets<F>,
    input: DrawEntryInput,
) {
    let Some(geometry) = assets.geometry(input.geometry) else {
        return;
    };
    let material_preview = assets
        .material(input.material)
        .map(|material| SceneMaterialInspection::new(input.material, material, assets));
    draw_list.push(SceneDrawInspection {
        node: input.node,
        instance: input.instance,
        geometry: input.geometry,
        material: input.material,
        material_preview,
        topology: geometry.topology(),
        primitive_count: primitive_count(geometry.topology(), geometry.indices().len()),
        vertex_count: geometry.vertices().len(),
        index_count: geometry.indices().len(),
        local_bounds: geometry.bounds(),
        world_transform: input.world_transform,
        visible: true,
    });
}

const fn primitive_count(topology: GeometryTopology, index_count: usize) -> usize {
    match topology {
        GeometryTopology::Triangles => index_count / 3,
        GeometryTopology::Lines => index_count / 2,
    }
}

fn camera_frustum(
    camera: CameraKey,
    node: NodeKey,
    desc: Camera,
    world_transform: Transform,
) -> SceneCameraFrustumInspection {
    let (near, far, local_corners) = match desc {
        Camera::Perspective(camera_desc) => {
            let near = camera_desc.near;
            let far = camera_desc.far;
            let aspect = positive_or(camera_desc.aspect, 1.0);
            let half_fov_tan = (camera_desc.vertical_fov.radians() * 0.5).tan();
            let near_half_y = near * half_fov_tan;
            let near_half_x = near_half_y * aspect;
            let far_half_y = far * half_fov_tan;
            let far_half_x = far_half_y * aspect;
            (
                near,
                far,
                frustum_corners_from_extents(
                    -near_half_x,
                    near_half_x,
                    -near_half_y,
                    near_half_y,
                    -near,
                    -far_half_x,
                    far_half_x,
                    -far_half_y,
                    far_half_y,
                    -far,
                ),
            )
        }
        Camera::Orthographic(camera_desc) => (
            camera_desc.near,
            camera_desc.far,
            frustum_corners_from_extents(
                camera_desc.left,
                camera_desc.right,
                camera_desc.bottom,
                camera_desc.top,
                -camera_desc.near,
                camera_desc.left,
                camera_desc.right,
                camera_desc.bottom,
                camera_desc.top,
                -camera_desc.far,
            ),
        ),
    };
    SceneCameraFrustumInspection {
        camera,
        node,
        near,
        far,
        corners: local_corners.map(|corner| transform_point(world_transform, corner)),
    }
}

#[allow(clippy::too_many_arguments)]
const fn frustum_corners_from_extents(
    near_left: f32,
    near_right: f32,
    near_bottom: f32,
    near_top: f32,
    near_z: f32,
    far_left: f32,
    far_right: f32,
    far_bottom: f32,
    far_top: f32,
    far_z: f32,
) -> [Vec3; 8] {
    [
        Vec3::new(near_left, near_bottom, near_z),
        Vec3::new(near_right, near_bottom, near_z),
        Vec3::new(near_right, near_top, near_z),
        Vec3::new(near_left, near_top, near_z),
        Vec3::new(far_left, far_bottom, far_z),
        Vec3::new(far_right, far_bottom, far_z),
        Vec3::new(far_right, far_top, far_z),
        Vec3::new(far_left, far_top, far_z),
    ]
}

fn transform_point(transform: Transform, point: Vec3) -> Vec3 {
    let scaled = Vec3::new(
        point.x * transform.scale.x,
        point.y * transform.scale.y,
        point.z * transform.scale.z,
    );
    let rotated = rotate_vec3(transform.rotation, scaled);
    Vec3::new(
        transform.translation.x + rotated.x,
        transform.translation.y + rotated.y,
        transform.translation.z + rotated.z,
    )
}

const fn positive_or(value: f32, fallback: f32) -> f32 {
    if value.is_finite() && value > 0.0 {
        value
    } else {
        fallback
    }
}