use glam::Vec3;
#[inline]
#[must_use]
#[allow(
clippy::disallowed_methods,
reason = "rendering helper: the result is a colour or shading weight and never feeds a distance, bound or mesh"
)]
pub fn vignette(uv_x: f32, uv_y: f32, radius: f32, softness: f32) -> f32 {
let dx = uv_x - 0.5;
let dy = uv_y - 0.5;
let d = dx.hypot(dy);
let inner = radius.max(0.0);
let outer = (inner + softness.max(0.0)).max(inner + 1e-6);
if d <= inner {
1.0
} else if d >= outer {
0.0
} else {
let t = ((d - inner) / (outer - inner)).clamp(0.0, 1.0);
(t * t) * (-(2.0f32 * -t + 3.0)) + 1.0
}
}
#[inline]
#[must_use]
pub fn bloom_toon(color: Vec3, threshold: f32, intensity: f32) -> Vec3 {
let max_ch = color.x.max(color.y).max(color.z);
if max_ch > threshold {
color * intensity
} else {
Vec3::ZERO
}
}
#[inline]
#[must_use]
pub fn chromatic_offsets(uv_x: f32, uv_y: f32, strength: f32) -> [(f32, f32); 3] {
let dx = uv_x - 0.5;
let dy = uv_y - 0.5;
[
(uv_x + dx * strength, uv_y + dy * strength),
(uv_x, uv_y),
(uv_x - dx * strength, uv_y - dy * strength),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vignette_center_is_full() {
assert!((vignette(0.5, 0.5, 0.3, 0.1) - 1.0).abs() < 1e-6);
}
#[test]
fn vignette_corner_is_zero() {
assert!(vignette(0.0, 0.0, 0.3, 0.1).abs() < 1e-6);
}
#[test]
fn vignette_bounded_across_uv() {
for i in 0..=10 {
for j in 0..=10 {
let uvx = i as f32 / 10.0;
let uvy = j as f32 / 10.0;
let out = vignette(uvx, uvy, 0.3, 0.2);
assert!((0.0..=1.0).contains(&out), "out of range: {out}");
}
}
}
#[test]
fn bloom_below_threshold_is_zero() {
let out = bloom_toon(Vec3::new(0.4, 0.4, 0.4), 0.5, 1.0);
assert_eq!(out, Vec3::ZERO);
}
#[test]
fn bloom_above_threshold_scales_color() {
let color = Vec3::new(0.9, 0.9, 0.9);
let out = bloom_toon(color, 0.5, 0.5);
assert!((out - color * 0.5).length() < 1e-6);
}
#[test]
fn bloom_partial_channel_above_threshold() {
let color = Vec3::new(0.9, 0.1, 0.1);
let out = bloom_toon(color, 0.5, 1.0);
assert!((out - color).length() < 1e-6);
}
#[test]
fn chromatic_offsets_center_unchanged() {
let offs = chromatic_offsets(0.5, 0.5, 0.02);
for (u, v) in &offs {
assert!((*u - 0.5).abs() < 1e-6 && (*v - 0.5).abs() < 1e-6);
}
}
#[test]
fn chromatic_offsets_green_matches_input() {
let offs = chromatic_offsets(0.7, 0.3, 0.05);
let green = offs[1];
assert!((green.0 - 0.7).abs() < 1e-6 && (green.1 - 0.3).abs() < 1e-6);
}
#[test]
fn chromatic_offsets_red_and_blue_symmetric_around_input() {
let (uvx, uvy) = (0.7_f32, 0.3_f32);
let strength = 0.05_f32;
let offs = chromatic_offsets(uvx, uvy, strength);
let red = offs[0];
let blue = offs[2];
let mid_x = f32::midpoint(red.0, blue.0);
let mid_y = f32::midpoint(red.1, blue.1);
assert!((mid_x - uvx).abs() < 1e-6 && (mid_y - uvy).abs() < 1e-6);
}
}