scena 1.7.2

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

use super::RasterTarget;
use super::camera::CameraProjection;
use super::cpu::CpuFrame;
use super::physical_transmission::{PhysicalTransmissionInputs, physical_transmission_color};
use super::prepare::PreparedPrimitive;

pub(super) fn draw_physical_transmission_cpu(
    cpu_frame: &mut CpuFrame<'_>,
    primitive: &PreparedPrimitive,
    scene_color_frame: &[u8],
    clipping_planes: &[ClippingPlane],
    section_box: Option<SectionBox>,
    camera: &CameraProjection,
) {
    let Some(transmission) = primitive.material_transmission() else {
        return;
    };
    let [a, b, c] = primitive.vertices();
    let [attributes_a, attributes_b, attributes_c] = primitive.vertex_attributes();
    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(cpu_frame.row_start as f32) as u32;
    let max_y =
        a.y.max(b.y)
            .max(c.y)
            .ceil()
            .min(cpu_frame.row_end().saturating_sub(1) as f32) as u32;
    if min_y > max_y {
        return;
    }

    let area = edge(a, b, c.x, c.y);
    if area.abs() <= f32::EPSILON {
        return;
    }
    if !primitive.double_sided() && area < 0.0 {
        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, section_box) {
                continue;
            }
            let depth = mix_depth(a.depth, b.depth, c.depth, w0, w1, w2);
            if !depth.is_finite() {
                continue;
            }
            let Some(pixel_index) = cpu_frame.local_pixel_index(x, y) else {
                continue;
            };
            if depth > cpu_frame.depth_frame[pixel_index] + f32::EPSILON {
                continue;
            }

            let surface_color = multiply_color(mix_color(a, b, c, w0, w1, w2), primitive.tint());
            let surface_post = cpu_frame.output.post_color(surface_color);
            let normal =
                attributes_a.normal * w0 + attributes_b.normal * w1 + attributes_c.normal * w2;
            let view = camera.camera_position() - position;
            let target = cpu_frame.target;
            let transmitted = physical_transmission_color(
                transmission,
                PhysicalTransmissionInputs {
                    frag_coord: [px, py],
                    viewport: [target.width as f32, target.height as f32],
                    normal,
                    view,
                    tint: Vec3::new(surface_color.r, surface_color.g, surface_color.b),
                    surface_rgb: Vec3::new(surface_post.r, surface_post.g, surface_post.b),
                },
                |uv| sample_post_scene_color(scene_color_frame, target, uv),
            );
            let final_color =
                Color::from_linear_rgba(transmitted.x, transmitted.y, transmitted.z, 1.0);
            cpu_frame.linear_frame[pixel_index] = final_color;
            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_post_rgba8(final_color));
        }
    }
}

#[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.view_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 multiply_color(color: Color, tint: Color) -> Color {
    Color::from_linear_rgba(
        color.r * tint.r,
        color.g * tint.g,
        color.b * tint.b,
        color.a * tint.a,
    )
}

fn sample_post_scene_color(frame: &[u8], target: RasterTarget, uv: [f32; 2]) -> Vec3 {
    let x = uv[0].clamp(0.0, 1.0) * target.width.saturating_sub(1) as f32;
    let y = uv[1].clamp(0.0, 1.0) * target.height.saturating_sub(1) as f32;
    let x0 = x.floor() as u32;
    let y0 = y.floor() as u32;
    let x1 = x0.saturating_add(1).min(target.width.saturating_sub(1));
    let y1 = y0.saturating_add(1).min(target.height.saturating_sub(1));
    let tx = x - x0 as f32;
    let ty = y - y0 as f32;
    let c00 = sample_post_scene_texel(frame, target, x0, y0);
    let c10 = sample_post_scene_texel(frame, target, x1, y0);
    let c01 = sample_post_scene_texel(frame, target, x0, y1);
    let c11 = sample_post_scene_texel(frame, target, x1, y1);
    let top = c00 * (1.0 - tx) + c10 * tx;
    let bottom = c01 * (1.0 - tx) + c11 * tx;
    top * (1.0 - ty) + bottom * ty
}

fn sample_post_scene_texel(frame: &[u8], target: RasterTarget, x: u32, y: u32) -> Vec3 {
    let byte_index = target.pixel_index(x, y).saturating_mul(4);
    let Some(rgba) = frame.get(byte_index..byte_index + 4) else {
        return Vec3::ZERO;
    };
    Vec3::new(
        srgb_u8_to_linear(rgba[0]),
        srgb_u8_to_linear(rgba[1]),
        srgb_u8_to_linear(rgba[2]),
    )
}

fn srgb_u8_to_linear(value: u8) -> f32 {
    let value = f32::from(value) / 255.0;
    if value <= 0.04045 {
        value / 12.92
    } else {
        ((value + 0.055) / 1.055).powf(2.4)
    }
}

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],
    section_box: Option<SectionBox>,
) -> bool {
    clipping_planes
        .iter()
        .any(|plane| !plane.contains(position))
        || section_box.is_some_and(|section| section.clips(position))
}