use glam::Vec3;
#[cfg(feature = "gpu")]
use crate::render::Color;
pub(crate) const KEY_LIGHT: Vec3 = Vec3::new(0.35, -0.45, 0.82);
pub(crate) const AMBIENT_INTENSITY: f32 = 0.35;
pub(crate) const DIFFUSE_INTENSITY: f32 = 0.65;
pub(crate) fn srgb_to_linear(channel: f32) -> f32 {
if channel <= 0.04045 {
channel / 12.92
} else {
((channel + 0.055) / 1.055).powf(2.4)
}
}
pub(crate) fn linear_to_srgb(channel: f32) -> f32 {
if channel <= 0.003_130_8 {
channel * 12.92
} else {
1.055 * channel.powf(1.0 / 2.4) - 0.055
}
}
#[cfg(feature = "gpu")]
pub(crate) fn linear_color(color: Color) -> [f32; 4] {
[
srgb_to_linear(f32::from(color.r) / 255.0),
srgb_to_linear(f32::from(color.g) / 255.0),
srgb_to_linear(f32::from(color.b) / 255.0),
f32::from(color.a) / 255.0,
]
}
pub(crate) fn aspect_corrected_normal(normal: Vec3, axis_aspect: Vec3) -> Vec3 {
let corrected = normal / axis_aspect;
if corrected.is_finite() && corrected.length_squared() > f32::EPSILON {
corrected
} else {
Vec3::Z
}
}
fn normalized_or_z(normal: Vec3) -> Vec3 {
if normal.is_finite() && normal.length_squared() > f32::EPSILON {
normal.normalize()
} else {
Vec3::Z
}
}
pub(crate) fn lambert_intensity(normal: Vec3, two_sided: bool) -> f32 {
let normal = normalized_or_z(normal);
let diffuse = normal.dot(KEY_LIGHT.normalize());
let diffuse = if two_sided {
diffuse.abs()
} else {
diffuse.max(0.0)
};
(AMBIENT_INTENSITY + DIFFUSE_INTENSITY * diffuse).clamp(0.0, 1.0)
}
pub(crate) fn scale_srgb_channel(channel: u8, intensity: f32) -> u8 {
let linear = srgb_to_linear(f32::from(channel) / 255.0) * intensity.clamp(0.0, 1.0);
(linear_to_srgb(linear.clamp(0.0, 1.0)) * 255.0).round() as u8
}
pub(crate) fn unpremultiply_encoded(pixel: [u32; 4], sample_count: u32) -> [u8; 4] {
let [red, green, blue, alpha] = pixel;
if alpha == 0 {
return [0, 0, 0, 0];
}
[
divide_out(red, alpha),
divide_out(green, alpha),
divide_out(blue, alpha),
(alpha / sample_count.max(1)).min(255) as u8,
]
}
fn divide_out(weighted: u32, alpha: u32) -> u8 {
((weighted + alpha / 2) / alpha).min(255) as u8
}
#[cfg(feature = "gpu")]
pub(crate) fn unpremultiply_linear_srgb_bytes(pixels: &mut [u8]) {
for pixel in pixels.chunks_exact_mut(4) {
let alpha = f32::from(pixel[3]) / 255.0;
if alpha <= 0.0 {
pixel[0] = 0;
pixel[1] = 0;
pixel[2] = 0;
continue;
}
for channel in &mut pixel[..3] {
let linear = srgb_to_linear(f32::from(*channel) / 255.0) / alpha;
*channel = (linear_to_srgb(linear.clamp(0.0, 1.0)) * 255.0).round() as u8;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn srgb_transfer_round_trips_every_byte() {
for byte in 0..=255_u8 {
let value = f32::from(byte) / 255.0;
let round_tripped = (linear_to_srgb(srgb_to_linear(value)) * 255.0).round() as u8;
assert_eq!(round_tripped, byte);
}
}
#[test]
fn linear_shading_is_brighter_than_the_old_srgb_scaling() {
let scaled = scale_srgb_channel(255, 0.5);
assert!(
scaled > 180,
"linear half-light encodes near 188, got {scaled}"
);
assert!(scaled < 195);
}
#[test]
fn aspect_correction_matches_the_analytic_normal_of_the_scaled_surface() {
let aspect = Vec3::new(1.0, 1.0, 0.2);
let corrected = aspect_corrected_normal(Vec3::new(-0.5, 0.0, 1.0), aspect).normalize();
let expected = Vec3::new(-0.1, 0.0, 1.0).normalize();
assert!(
(corrected - expected).length() < 1.0e-6,
"{corrected:?} != {expected:?}"
);
let plateau = lambert_intensity(aspect_corrected_normal(Vec3::Z, aspect), false);
assert!((plateau - lambert_intensity(Vec3::Z, false)).abs() < 1.0e-6);
let flattened = lambert_intensity(
aspect_corrected_normal(Vec3::new(-0.5, 0.0, 1.0), aspect),
false,
);
let unflattened = lambert_intensity(Vec3::new(-0.5, 0.0, 1.0), false);
assert_ne!(flattened, unflattened);
assert!((flattened - lambert_intensity(expected, false)).abs() < 1.0e-6);
}
#[test]
fn uniform_aspect_leaves_normals_alone() {
let normal = Vec3::new(0.3, -0.4, 0.86).normalize();
let corrected = aspect_corrected_normal(normal, Vec3::ONE);
assert!((corrected - normal).length() < 1.0e-6);
}
#[test]
fn half_coverage_resolves_to_the_source_colour_with_half_alpha() {
let accumulated = [200 * 255 * 2, 100 * 255 * 2, 50 * 255 * 2, 255 * 2];
assert_eq!(unpremultiply_encoded(accumulated, 4), [200, 100, 50, 127]);
}
#[test]
fn zero_coverage_resolves_to_fully_transparent_black() {
assert_eq!(unpremultiply_encoded([0, 0, 0, 0], 4), [0, 0, 0, 0]);
}
}