scena 1.0.2

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use crate::geometry::{Primitive, Vertex};
use crate::material::Color;
use crate::scene::{ClippingPlane, Vec3};

use super::RasterTarget;
use super::camera::CameraProjection;
use super::output::OutputTransform;

pub(super) struct CpuFrame<'frame> {
    target: RasterTarget,
    output: OutputTransform,
    linear_frame: &'frame mut [Color],
    depth_frame: &'frame mut [f32],
    frame: &'frame mut [u8],
}

impl<'frame> CpuFrame<'frame> {
    pub(super) const fn new(
        target: RasterTarget,
        output: OutputTransform,
        linear_frame: &'frame mut [Color],
        depth_frame: &'frame mut [f32],
        frame: &'frame mut [u8],
    ) -> Self {
        Self {
            target,
            output,
            linear_frame,
            depth_frame,
            frame,
        }
    }
}

pub(super) fn clear_cpu(cpu_frame: &mut CpuFrame<'_>, color: Color) {
    let rgba = cpu_frame.output.encode_clear_rgba8(color);
    for ((linear, depth), pixel) in cpu_frame
        .linear_frame
        .iter_mut()
        .zip(cpu_frame.depth_frame.iter_mut())
        .zip(cpu_frame.frame.chunks_exact_mut(4))
    {
        *linear = color;
        *depth = f32::INFINITY;
        pixel.copy_from_slice(&rgba);
    }
    debug_assert_eq!(cpu_frame.linear_frame.len(), cpu_frame.target.pixel_len());
    debug_assert_eq!(cpu_frame.depth_frame.len(), cpu_frame.target.pixel_len());
}

pub(super) fn draw_primitive_cpu(
    cpu_frame: &mut CpuFrame<'_>,
    primitive: &Primitive,
    clipping_planes: &[ClippingPlane],
    camera: &CameraProjection,
) {
    let [a, b, c] = primitive.vertices();
    let Some(a) = ScreenVertex::from_vertex(*a, cpu_frame.target, camera) else {
        return;
    };
    let Some(b) = ScreenVertex::from_vertex(*b, cpu_frame.target, camera) else {
        return;
    };
    let Some(c) = ScreenVertex::from_vertex(*c, cpu_frame.target, camera) else {
        return;
    };

    let min_x = a.x.min(b.x).min(c.x).floor().max(0.0) as u32;
    let max_x =
        a.x.max(b.x)
            .max(c.x)
            .ceil()
            .min(cpu_frame.target.width as f32 - 1.0) as u32;
    let min_y = a.y.min(b.y).min(c.y).floor().max(0.0) as u32;
    let max_y =
        a.y.max(b.y)
            .max(c.y)
            .ceil()
            .min(cpu_frame.target.height as f32 - 1.0) as u32;

    let area = edge(a, b, c.x, c.y);
    if area.abs() <= f32::EPSILON {
        return;
    }

    for y in min_y..=max_y {
        for x in min_x..=max_x {
            let px = x as f32 + 0.5;
            let py = y as f32 + 0.5;
            let w0 = edge(b, c, px, py) / area;
            let w1 = edge(c, a, px, py) / area;
            let w2 = edge(a, b, px, py) / area;
            if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 {
                continue;
            }
            let position = mix_position(a.position, b.position, c.position, w0, w1, w2);
            if is_clipped(position, clipping_planes) {
                continue;
            }
            let color = mix_color(a, b, c, w0, w1, w2);
            let depth = mix_depth(a.depth, b.depth, c.depth, w0, w1, w2);
            write_pixel(cpu_frame, x, y, color, depth);
        }
    }
}

#[derive(Debug, Clone, Copy)]
struct ScreenVertex {
    x: f32,
    y: f32,
    depth: f32,
    inv_depth: f32,
    position: Vec3,
    color: Color,
}

impl ScreenVertex {
    fn from_vertex(
        vertex: Vertex,
        target: RasterTarget,
        camera: &CameraProjection,
    ) -> Option<Self> {
        let projected = camera.project(vertex.position)?;
        let width = target.width.saturating_sub(1) as f32;
        let height = target.height.saturating_sub(1) as f32;
        Some(Self {
            x: (projected.ndc_x * 0.5 + 0.5) * width,
            y: (1.0 - (projected.ndc_y * 0.5 + 0.5)) * height,
            depth: projected.depth,
            inv_depth: projected.depth.recip(),
            position: vertex.position,
            color: vertex.color,
        })
    }
}

fn edge(a: ScreenVertex, b: ScreenVertex, x: f32, y: f32) -> f32 {
    (x - a.x) * (b.y - a.y) - (y - a.y) * (b.x - a.x)
}

fn mix_color(
    a: ScreenVertex,
    b: ScreenVertex,
    c: ScreenVertex,
    w0: f32,
    w1: f32,
    w2: f32,
) -> Color {
    let iw0 = w0 * a.inv_depth;
    let iw1 = w1 * b.inv_depth;
    let iw2 = w2 * c.inv_depth;
    let inv_sum = iw0 + iw1 + iw2;
    if inv_sum.abs() <= f32::EPSILON || !inv_sum.is_finite() {
        return mix_color_affine(a.color, b.color, c.color, w0, w1, w2);
    }
    let w0 = iw0 / inv_sum;
    let w1 = iw1 / inv_sum;
    let w2 = iw2 / inv_sum;
    mix_color_affine(a.color, b.color, c.color, w0, w1, w2)
}

fn mix_color_affine(a: Color, b: Color, c: Color, w0: f32, w1: f32, w2: f32) -> Color {
    Color::from_linear_rgba(
        a.r * w0 + b.r * w1 + c.r * w2,
        a.g * w0 + b.g * w1 + c.g * w2,
        a.b * w0 + b.b * w1 + c.b * w2,
        a.a * w0 + b.a * w1 + c.a * w2,
    )
}

fn mix_position(a: Vec3, b: Vec3, c: Vec3, w0: f32, w1: f32, w2: f32) -> Vec3 {
    Vec3::new(
        a.x * w0 + b.x * w1 + c.x * w2,
        a.y * w0 + b.y * w1 + c.y * w2,
        a.z * w0 + b.z * w1 + c.z * w2,
    )
}

fn mix_depth(a: f32, b: f32, c: f32, w0: f32, w1: f32, w2: f32) -> f32 {
    a * w0 + b * w1 + c * w2
}

fn is_clipped(position: Vec3, clipping_planes: &[ClippingPlane]) -> bool {
    clipping_planes
        .iter()
        .any(|plane| !plane.contains(position))
}

fn write_pixel(cpu_frame: &mut CpuFrame<'_>, x: u32, y: u32, color: Color, depth: f32) {
    if !depth.is_finite() {
        return;
    }
    let pixel_index = cpu_frame.target.pixel_index(x, y);
    if depth > cpu_frame.depth_frame[pixel_index] + f32::EPSILON {
        return;
    }
    let blended = blend_source_over(color, cpu_frame.linear_frame[pixel_index]);
    cpu_frame.linear_frame[pixel_index] = blended;
    if clamp_alpha_or(color.a, 1.0) >= 1.0 - f32::EPSILON {
        cpu_frame.depth_frame[pixel_index] = depth;
    }

    let byte_index = pixel_index * 4;
    cpu_frame.frame[byte_index..byte_index + 4]
        .copy_from_slice(&cpu_frame.output.encode_rgba8(blended));
}

fn blend_source_over(source: Color, destination: Color) -> Color {
    let source_alpha = clamp_alpha_or(source.a, 1.0);
    let destination_alpha = clamp_alpha_or(destination.a, 1.0);
    if source_alpha == 1.0 {
        return Color::from_linear_rgba(source.r, source.g, source.b, 1.0);
    }
    if source_alpha <= 0.0 {
        return destination;
    }

    let inverse_source_alpha = 1.0 - source_alpha;
    let output_alpha = source_alpha + destination_alpha * inverse_source_alpha;
    let premultiplied_r =
        source.r * source_alpha + destination.r * destination_alpha * inverse_source_alpha;
    let premultiplied_g =
        source.g * source_alpha + destination.g * destination_alpha * inverse_source_alpha;
    let premultiplied_b =
        source.b * source_alpha + destination.b * destination_alpha * inverse_source_alpha;

    if output_alpha <= f32::EPSILON {
        Color::from_linear_rgba(0.0, 0.0, 0.0, 0.0)
    } else {
        Color::from_linear_rgba(
            premultiplied_r / output_alpha,
            premultiplied_g / output_alpha,
            premultiplied_b / output_alpha,
            output_alpha,
        )
    }
}

fn clamp_alpha_or(value: f32, fallback: f32) -> f32 {
    if value.is_finite() {
        value.clamp(0.0, 1.0)
    } else {
        fallback
    }
}