scena 1.7.2

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

use super::super::super::area_ltc;
use super::super::pbr_contract::{PbrMaterial, inverse_square_range_attenuation};
use super::area::PreparedAreaLight;
use super::math::{normalize_or, scale_color, subtract_vec3};

pub(super) fn ltc_area_light_specular_contribution(
    light: PreparedAreaLight,
    position: Vec3,
    normal: Vec3,
    view: Vec3,
    material: PbrMaterial,
    shadow_factor: f32,
) -> Vec3 {
    let shadow_factor = shadow_factor.clamp(0.0, 1.0);
    if shadow_factor <= f32::EPSILON {
        return Vec3::ZERO;
    }
    let polygon = ltc_area_light_polygon(light, position, normal);
    let probe = area_ltc::evaluate_specular_polygon(
        polygon,
        position,
        normal,
        view,
        material.roughness,
        material.f0(),
    );
    if probe.irradiance <= f32::EPSILON {
        return Vec3::ZERO;
    }

    let to_light = subtract_vec3(light.position, position);
    let radiance = scale_color(
        light.color,
        light.luminous_flux_lumens / (4.0 * std::f32::consts::PI)
            * inverse_square_range_attenuation(to_light, light.range),
    );
    scale_vec3(
        multiply_vec3(probe.fresnel_scale, radiance),
        probe.irradiance * shadow_factor,
    )
}

fn ltc_area_light_polygon(light: PreparedAreaLight, position: Vec3, normal: Vec3) -> [Vec3; 4] {
    match light.shape {
        AreaLightShape::Rect { .. } => [
            light.position - light.axis_x - light.axis_y,
            light.position + light.axis_x - light.axis_y,
            light.position + light.axis_x + light.axis_y,
            light.position - light.axis_x + light.axis_y,
        ],
        AreaLightShape::Disc { .. } => [
            light.position - light.axis_x,
            light.position - light.axis_y,
            light.position + light.axis_x,
            light.position + light.axis_y,
        ],
        AreaLightShape::Sphere { .. } => {
            let radius = light.axis_x.length().max(light.axis_y.length()).max(0.001);
            let to_surface = normalize_or(subtract_vec3(position, light.position), -normal);
            let tangent = normalize_or(cross_vec3(normal, to_surface), Vec3::X) * radius;
            let bitangent = normalize_or(cross_vec3(to_surface, tangent), Vec3::Z) * radius;
            [
                light.position - tangent - bitangent,
                light.position + tangent - bitangent,
                light.position + tangent + bitangent,
                light.position - tangent + bitangent,
            ]
        }
    }
}

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

fn scale_vec3(value: Vec3, scale: f32) -> Vec3 {
    Vec3::new(value.x * scale, value.y * scale, value.z * scale)
}

fn cross_vec3(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,
    )
}

#[cfg(test)]
mod tests {
    use crate::material::Color;
    use crate::scene::AreaLightShape;

    use super::*;

    #[test]
    fn area_ltc_specular_is_width_and_shape_sensitive() {
        let material = PbrMaterial::new(Vec3::new(0.82, 0.78, 0.72), 1.0, 0.34);
        let position = Vec3::ZERO;
        let normal = Vec3::Y;
        let view = normalize_or(Vec3::new(0.0, 0.8, 1.6), Vec3::Y);
        let base = PreparedAreaLight {
            color: Color::from_linear_rgb(1.0, 0.96, 0.9),
            position: Vec3::new(0.0, 1.35, 0.32),
            axis_x: Vec3::X * 0.05,
            axis_y: Vec3::Z * 0.05,
            luminous_flux_lumens: 900.0,
            range: None,
            shape: AreaLightShape::rect(0.1, 0.1),
        };
        let wide = PreparedAreaLight {
            axis_x: Vec3::X * 0.9,
            axis_y: Vec3::Z * 0.45,
            shape: AreaLightShape::rect(1.8, 0.9),
            ..base
        };
        let disc = PreparedAreaLight {
            axis_x: Vec3::X * 0.55,
            axis_y: Vec3::Z * 0.55,
            shape: AreaLightShape::disc(0.55),
            ..base
        };
        let sphere = PreparedAreaLight {
            axis_x: Vec3::X * 0.42,
            axis_y: Vec3::Z * 0.42,
            shape: AreaLightShape::sphere(0.42),
            ..base
        };

        let narrow_specular =
            ltc_area_light_specular_contribution(base, position, normal, view, material, 1.0);
        let wide_specular =
            ltc_area_light_specular_contribution(wide, position, normal, view, material, 1.0);
        let disc_specular =
            ltc_area_light_specular_contribution(disc, position, normal, view, material, 1.0);
        let sphere_specular =
            ltc_area_light_specular_contribution(sphere, position, normal, view, material, 1.0);

        assert!(
            wide_specular.x > narrow_specular.x * 1.6,
            "wide rectangular LTC area light should produce a materially broader/brighter specular response than a tiny emitter; narrow={narrow_specular:?}, wide={wide_specular:?}"
        );
        assert!(
            disc_specular.x > 0.0 && sphere_specular.x > 0.0,
            "disc and sphere LTC area light paths must not be inert; disc={disc_specular:?}, sphere={sphere_specular:?}"
        );
    }

    #[test]
    fn area_ltc_specular_matches_selfshadow_reference_probe() {
        let material = PbrMaterial::new(Vec3::new(0.82, 0.78, 0.72), 1.0, 0.34);
        let position = Vec3::ZERO;
        let normal = Vec3::Y;
        let view = normalize_or(Vec3::new(0.0, 0.8, 1.6), Vec3::Y);
        let wide = PreparedAreaLight {
            color: Color::from_linear_rgb(1.0, 0.96, 0.9),
            position: Vec3::new(0.0, 1.35, 0.32),
            axis_x: Vec3::X * 0.9,
            axis_y: Vec3::Z * 0.45,
            luminous_flux_lumens: 900.0,
            range: None,
            shape: AreaLightShape::rect(1.8, 0.9),
        };

        let actual =
            ltc_area_light_specular_contribution(wide, position, normal, view, material, 1.0);
        let expected = Vec3::new(0.172_101_13, 0.157_660_25, 0.137_179_69);
        let delta = (actual - expected).abs();

        assert!(
            delta.max_element() <= 0.000_75,
            "LTC specular must match the selfshadow/ltc_code fitted-table probe; actual={actual:?}, expected={expected:?}, delta={delta:?}"
        );
    }
}