scena 1.0.0

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

use crate::diagnostics::PrepareError;
use crate::geometry::{GeometryDesc, GeometryTopology, Primitive, Vertex};
use crate::material::{
    AlphaMode, Color, DEFAULT_EDGE_ANGLE_THRESHOLD_DEGREES, MaterialDesc, MaterialKind,
};
use crate::scene::{NodeKey, Vec3};

use super::super::RasterTarget;

pub(super) fn append_wireframe_primitives(
    node: NodeKey,
    geometry: &GeometryDesc,
    material: &MaterialDesc,
    target: RasterTarget,
    primitives: &mut Vec<Primitive>,
) -> Result<(), PrepareError> {
    let (color, width_px) = technical_stroke_material(node, material)?;
    let vertices = geometry.vertices();
    for triangle in geometry.indices().chunks_exact(3) {
        for (start, end) in triangle_edges(triangle) {
            append_line_segment(
                vertices[start as usize].position,
                vertices[end as usize].position,
                color,
                width_px,
                target,
                primitives,
            );
        }
    }
    Ok(())
}

pub(super) fn append_edge_primitives(
    node: NodeKey,
    geometry: &GeometryDesc,
    material: &MaterialDesc,
    target: RasterTarget,
    primitives: &mut Vec<Primitive>,
) -> Result<(), PrepareError> {
    let (color, width_px) = technical_stroke_material(node, material)?;
    let threshold = material
        .edge_angle_threshold_degrees()
        .unwrap_or(DEFAULT_EDGE_ANGLE_THRESHOLD_DEGREES);
    let mut edges: BTreeMap<(u32, u32), EdgeCandidate> = BTreeMap::new();
    for triangle in geometry.indices().chunks_exact(3) {
        let normal = triangle_normal(geometry, triangle);
        for (start, end) in triangle_edges(triangle) {
            let key = ordered_edge_key(start, end);
            edges
                .entry(key)
                .and_modify(|edge| edge.add_face(normal))
                .or_insert_with(|| EdgeCandidate::new(start, end, normal));
        }
    }

    let vertices = geometry.vertices();
    for edge in edges.values() {
        if edge.is_visible(threshold) {
            append_line_segment(
                vertices[edge.start as usize].position,
                vertices[edge.end as usize].position,
                color,
                width_px,
                target,
                primitives,
            );
        }
    }
    Ok(())
}

pub(super) fn append_line_primitives(
    node: NodeKey,
    geometry: &GeometryDesc,
    material: &MaterialDesc,
    target: RasterTarget,
    primitives: &mut Vec<Primitive>,
) -> Result<(), PrepareError> {
    let (color, width_px) = line_material(node, material)?;
    let vertices = geometry.vertices();
    for segment in geometry.indices().chunks_exact(2) {
        append_line_segment(
            vertices[segment[0] as usize].position,
            vertices[segment[1] as usize].position,
            color,
            width_px,
            target,
            primitives,
        );
    }
    Ok(())
}

fn technical_stroke_material(
    node: NodeKey,
    material: &MaterialDesc,
) -> Result<(Color, f32), PrepareError> {
    if !matches!(
        material.kind(),
        MaterialKind::Line | MaterialKind::Wireframe | MaterialKind::Edge
    ) {
        return Err(PrepareError::UnsupportedMaterialKind {
            node,
            kind: material.kind(),
        });
    }

    let mut color = material.base_color();
    match material.alpha_mode() {
        AlphaMode::Opaque => color.a = 1.0,
        AlphaMode::Mask { .. } | AlphaMode::Blend => {
            return Err(PrepareError::UnsupportedAlphaMode {
                node,
                alpha_mode: material.alpha_mode(),
            });
        }
    }
    Ok((color, material.stroke_width_px().unwrap_or(1.0)))
}

fn line_material(node: NodeKey, material: &MaterialDesc) -> Result<(Color, f32), PrepareError> {
    match material.kind() {
        MaterialKind::Line => {}
        MaterialKind::Unlit | MaterialKind::PbrMetallicRoughness => {
            return Err(PrepareError::UnsupportedGeometryTopology {
                node,
                topology: GeometryTopology::Lines,
            });
        }
        MaterialKind::Wireframe | MaterialKind::Edge => {
            return Err(PrepareError::UnsupportedMaterialKind {
                node,
                kind: material.kind(),
            });
        }
    }

    technical_stroke_material(node, material)
}

fn append_line_segment(
    start: Vec3,
    end: Vec3,
    color: Color,
    width_px: f32,
    target: RasterTarget,
    primitives: &mut Vec<Primitive>,
) {
    let start = ScreenPoint::from_vec3(start, target);
    let end = ScreenPoint::from_vec3(end, target);
    let delta_x = end.x - start.x;
    let delta_y = end.y - start.y;
    let length = (delta_x * delta_x + delta_y * delta_y).sqrt();
    if length <= f32::EPSILON {
        return;
    }

    let half_width = width_px * 0.5;
    let normal_x = -delta_y / length * half_width;
    let normal_y = delta_x / length * half_width;
    let a = start.offset(normal_x, normal_y).to_vec3(target);
    let b = end.offset(normal_x, normal_y).to_vec3(target);
    let c = end.offset(-normal_x, -normal_y).to_vec3(target);
    let d = start.offset(-normal_x, -normal_y).to_vec3(target);

    primitives.push(
        Primitive::triangle([
            Vertex { position: a, color },
            Vertex { position: b, color },
            Vertex { position: c, color },
        ])
        .without_depth_prepass(),
    );
    primitives.push(
        Primitive::triangle([
            Vertex { position: a, color },
            Vertex { position: c, color },
            Vertex { position: d, color },
        ])
        .without_depth_prepass(),
    );
}

fn triangle_edges(triangle: &[u32]) -> [(u32, u32); 3] {
    [
        (triangle[0], triangle[1]),
        (triangle[1], triangle[2]),
        (triangle[2], triangle[0]),
    ]
}

fn ordered_edge_key(start: u32, end: u32) -> (u32, u32) {
    if start <= end {
        (start, end)
    } else {
        (end, start)
    }
}

struct EdgeCandidate {
    start: u32,
    end: u32,
    first_normal: Vec3,
    second_normal: Option<Vec3>,
    face_count: u8,
}

impl EdgeCandidate {
    fn new(start: u32, end: u32, normal: Vec3) -> Self {
        Self {
            start,
            end,
            first_normal: normal,
            second_normal: None,
            face_count: 1,
        }
    }

    fn add_face(&mut self, normal: Vec3) {
        self.face_count = self.face_count.saturating_add(1);
        if self.second_normal.is_none() {
            self.second_normal = Some(normal);
        }
    }

    fn is_visible(&self, threshold_degrees: f32) -> bool {
        if self.face_count != 2 {
            return true;
        }
        let Some(second_normal) = self.second_normal else {
            return true;
        };
        angle_degrees(self.first_normal, second_normal) > threshold_degrees
    }
}

fn triangle_normal(geometry: &GeometryDesc, triangle: &[u32]) -> Vec3 {
    let vertices = geometry.vertices();
    let a = vertices[triangle[0] as usize].position;
    let b = vertices[triangle[1] as usize].position;
    let c = vertices[triangle[2] as usize].position;
    normalize(cross(sub(b, a), sub(c, a))).unwrap_or(vertices[triangle[0] as usize].normal)
}

fn angle_degrees(left: Vec3, right: Vec3) -> f32 {
    dot(left, right).clamp(-1.0, 1.0).acos().to_degrees()
}

fn sub(left: Vec3, right: Vec3) -> Vec3 {
    Vec3::new(left.x - right.x, left.y - right.y, left.z - right.z)
}

fn cross(left: Vec3, right: Vec3) -> Vec3 {
    Vec3::new(
        left.y * right.z - left.z * right.y,
        left.z * right.x - left.x * right.z,
        left.x * right.y - left.y * right.x,
    )
}

fn dot(left: Vec3, right: Vec3) -> f32 {
    left.x * right.x + left.y * right.y + left.z * right.z
}

fn normalize(value: Vec3) -> Option<Vec3> {
    let length = dot(value, value).sqrt();
    (length > f32::EPSILON).then(|| Vec3::new(value.x / length, value.y / length, value.z / length))
}

#[derive(Clone, Copy)]
struct ScreenPoint {
    x: f32,
    y: f32,
    z: f32,
}

impl ScreenPoint {
    fn from_vec3(position: Vec3, target: RasterTarget) -> Self {
        let width = target.width.saturating_sub(1) as f32;
        let height = target.height.saturating_sub(1) as f32;
        Self {
            x: (position.x * 0.5 + 0.5) * width,
            y: (1.0 - (position.y * 0.5 + 0.5)) * height,
            z: position.z,
        }
    }

    fn offset(self, x: f32, y: f32) -> Self {
        Self {
            x: self.x + x,
            y: self.y + y,
            z: self.z,
        }
    }

    fn to_vec3(self, target: RasterTarget) -> Vec3 {
        Vec3::new(
            screen_x_to_ndc(self.x, target),
            screen_y_to_ndc(self.y, target),
            self.z,
        )
    }
}

fn screen_x_to_ndc(x: f32, target: RasterTarget) -> f32 {
    if target.width <= 1 {
        0.0
    } else {
        (x / target.width.saturating_sub(1) as f32 - 0.5) * 2.0
    }
}

fn screen_y_to_ndc(y: f32, target: RasterTarget) -> f32 {
    if target.height <= 1 {
        0.0
    } else {
        ((1.0 - y / target.height.saturating_sub(1) as f32) - 0.5) * 2.0
    }
}