scena 1.7.2

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

use super::math::{normalize_or, rotate_vec3, subtract_vec3};

pub(super) const AREA_LIGHT_SAMPLE_COUNT: usize = 16;
const FRAC_1_SQRT_2: f32 = std::f32::consts::FRAC_1_SQRT_2;

const RECT_SAMPLE_OFFSETS: [(f32, f32); AREA_LIGHT_SAMPLE_COUNT] = [
    (-0.75, -0.75),
    (-0.25, -0.75),
    (0.25, -0.75),
    (0.75, -0.75),
    (-0.75, -0.25),
    (-0.25, -0.25),
    (0.25, -0.25),
    (0.75, -0.25),
    (-0.75, 0.25),
    (-0.25, 0.25),
    (0.25, 0.25),
    (0.75, 0.25),
    (-0.75, 0.75),
    (-0.25, 0.75),
    (0.25, 0.75),
    (0.75, 0.75),
];

const DISC_SAMPLE_OFFSETS: [(f32, f32); AREA_LIGHT_SAMPLE_COUNT] = [
    (0.35, 0.0),
    (0.247487, 0.247487),
    (0.0, 0.35),
    (-0.247487, 0.247487),
    (-0.35, 0.0),
    (-0.247487, -0.247487),
    (0.0, -0.35),
    (0.247487, -0.247487),
    (0.69291, 0.287013),
    (0.287013, 0.69291),
    (-0.287013, 0.69291),
    (-0.69291, 0.287013),
    (-0.69291, -0.287013),
    (-0.287013, -0.69291),
    (0.287013, -0.69291),
    (0.69291, -0.287013),
];

const SPHERE_SAMPLE_OFFSETS: [(f32, f32, f32); AREA_LIGHT_SAMPLE_COUNT] = [
    (0.577350, 0.577350, 0.577350),
    (-0.577350, 0.577350, 0.577350),
    (0.577350, -0.577350, 0.577350),
    (-0.577350, -0.577350, 0.577350),
    (0.577350, 0.577350, -0.577350),
    (-0.577350, 0.577350, -0.577350),
    (0.577350, -0.577350, -0.577350),
    (-0.577350, -0.577350, -0.577350),
    (1.0, 0.0, 0.0),
    (-1.0, 0.0, 0.0),
    (0.0, 1.0, 0.0),
    (0.0, -1.0, 0.0),
    (0.0, 0.0, 1.0),
    (0.0, 0.0, -1.0),
    (0.0, FRAC_1_SQRT_2, FRAC_1_SQRT_2),
    (0.0, -FRAC_1_SQRT_2, -FRAC_1_SQRT_2),
];

#[derive(Clone, Copy)]
pub(super) struct PreparedAreaLight {
    pub(super) color: Color,
    pub(super) position: Vec3,
    pub(super) axis_x: Vec3,
    pub(super) axis_y: Vec3,
    pub(super) luminous_flux_lumens: f32,
    pub(super) range: Option<f32>,
    pub(super) shape: AreaLightShape,
}

pub(super) fn prepared_area_light(
    light: AreaLight,
    transform: Transform,
    origin_shift: Vec3,
) -> Option<PreparedAreaLight> {
    let luminous_flux_lumens = light.luminous_flux_lumens();
    if !luminous_flux_lumens.is_finite() || luminous_flux_lumens <= 0.0 {
        return None;
    }
    let shape = light.shape();
    let right = rotate_vec3(transform.rotation, Vec3::X);
    let up = rotate_vec3(transform.rotation, Vec3::Y);
    let (axis_x, axis_y) = area_light_axes(shape, right, up);
    Some(PreparedAreaLight {
        color: light.color(),
        position: subtract_vec3(transform.translation, origin_shift),
        axis_x,
        axis_y,
        luminous_flux_lumens,
        range: light.range(),
        shape,
    })
}

fn area_light_axes(shape: AreaLightShape, right: Vec3, up: Vec3) -> (Vec3, Vec3) {
    match shape {
        AreaLightShape::Rect { width, height } => (
            right * (width.max(0.001) * 0.5),
            up * (height.max(0.001) * 0.5),
        ),
        AreaLightShape::Disc { radius } => {
            let r = radius.max(0.001);
            (right * r, up * r)
        }
        AreaLightShape::Sphere { radius } => {
            let r = radius.max(0.001);
            (right * r, up * r)
        }
    }
}

pub(super) fn area_light_shape_code(shape: AreaLightShape) -> f32 {
    match shape {
        AreaLightShape::Rect { .. } => 0.0,
        AreaLightShape::Disc { .. } => 1.0,
        AreaLightShape::Sphere { .. } => 2.0,
    }
}

pub(super) fn area_light_sample_positions(
    light: PreparedAreaLight,
) -> [Vec3; AREA_LIGHT_SAMPLE_COUNT] {
    let mut positions = [light.position; AREA_LIGHT_SAMPLE_COUNT];
    match light.shape {
        AreaLightShape::Rect { .. } => {
            for (index, (x, y)) in RECT_SAMPLE_OFFSETS.iter().copied().enumerate() {
                positions[index] = light.position + light.axis_x * x + light.axis_y * y;
            }
        }
        AreaLightShape::Disc { .. } => {
            for (index, (x, y)) in DISC_SAMPLE_OFFSETS.iter().copied().enumerate() {
                positions[index] = light.position + light.axis_x * x + light.axis_y * y;
            }
        }
        AreaLightShape::Sphere { .. } => {
            let radius = light.axis_x.length().max(light.axis_y.length()).max(0.001);
            let forward = normalize_or(light.axis_x.cross(light.axis_y), Vec3::Z) * radius;
            for (index, (x, y, z)) in SPHERE_SAMPLE_OFFSETS.iter().copied().enumerate() {
                positions[index] =
                    light.position + light.axis_x * x + light.axis_y * y + forward * z;
            }
        }
    }
    positions
}