#![allow(dead_code)]
use std::f32::consts::PI;
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct AoConfig {
pub radius: f32,
pub sample_count: u32,
pub intensity: f32,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct AoRenderer {
pub config: AoConfig,
pub pass_name: String,
}
#[allow(dead_code)]
pub fn new_ao_renderer(config: AoConfig) -> AoRenderer {
AoRenderer {
config,
pass_name: "ao_pass".to_owned(),
}
}
#[allow(dead_code)]
pub fn default_ao_config() -> AoConfig {
AoConfig {
radius: 0.5,
sample_count: 16,
intensity: 1.0,
}
}
fn van_der_corput_base2(mut bits: u32) -> f32 {
bits = bits.rotate_right(16);
bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1);
bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2);
bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4);
bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8);
(bits as f64 / 4_294_967_296.0_f64) as f32
}
fn hammersley_2d(i: u32, n: u32) -> (f32, f32) {
let u = i as f32 / n as f32;
let v = van_der_corput_base2(i);
(u, v)
}
fn hemisphere_sample_cosine(u: f32, v: f32) -> [f32; 3] {
let theta = (1.0_f32 - u).max(0.0).sqrt().acos();
let phi = 2.0 * PI * v;
let sin_t = theta.sin();
[sin_t * phi.cos(), sin_t * phi.sin(), theta.cos()]
}
fn orthonormal_basis(n: [f32; 3]) -> ([f32; 3], [f32; 3]) {
let [nx, ny, nz] = n;
let sign = if nz >= 0.0 { 1.0_f32 } else { -1.0_f32 };
let a = -1.0 / (sign + nz);
let b = nx * ny * a;
let tangent = [1.0 + sign * nx * nx * a, sign * b, -sign * nx];
let bitangent = [b, sign + ny * ny * a, -ny];
(tangent, bitangent)
}
#[allow(dead_code)]
pub fn compute_ao_at_vertex(config: &AoConfig, normal: [f32; 3], _position: [f32; 3]) -> f32 {
let len_sq = normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2];
if len_sq < 1e-12 {
return 0.0;
}
let inv_len = len_sq.sqrt().recip();
let n = [normal[0] * inv_len, normal[1] * inv_len, normal[2] * inv_len];
let (tangent, bitangent) = orthonormal_basis(n);
let sample_count = config.sample_count.max(1);
let mut occlusion_sum = 0.0_f32;
for i in 0..sample_count {
let (u, v) = hammersley_2d(i, sample_count);
let local = hemisphere_sample_cosine(u, v);
let d = [
local[0] * tangent[0] + local[1] * bitangent[0] + local[2] * n[0],
local[0] * tangent[1] + local[1] * bitangent[1] + local[2] * n[1],
local[0] * tangent[2] + local[1] * bitangent[2] + local[2] * n[2],
];
let cos_theta = n[0] * d[0] + n[1] * d[1] + n[2] * d[2];
occlusion_sum += (1.0 - cos_theta).max(0.0);
}
let mean_occlusion = occlusion_sum / sample_count as f32;
(1.0 - mean_occlusion * config.intensity).clamp(0.0, 1.0)
}
#[allow(dead_code)]
pub fn ao_radius(config: &AoConfig) -> f32 {
config.radius
}
#[allow(dead_code)]
pub fn ao_sample_count(config: &AoConfig) -> u32 {
config.sample_count
}
#[allow(dead_code)]
pub fn ao_to_texture(values: &[f32], width: u32, height: u32) -> Vec<u8> {
let total = (width * height) as usize;
let mut buf = Vec::with_capacity(total);
for i in 0..total {
let v = if i < values.len() { values[i] } else { 1.0 };
buf.push((v.clamp(0.0, 1.0) * 255.0) as u8);
}
buf
}
#[allow(dead_code)]
pub fn ao_intensity(config: &AoConfig) -> f32 {
config.intensity
}
#[allow(dead_code)]
pub fn ao_pass_name(renderer: &AoRenderer) -> &str {
&renderer.pass_name
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_ao_config() {
let c = default_ao_config();
assert_eq!(c.sample_count, 16);
assert!((c.radius - 0.5).abs() < 1e-6);
}
#[test]
fn test_new_ao_renderer() {
let r = new_ao_renderer(default_ao_config());
assert_eq!(r.pass_name, "ao_pass");
}
#[test]
fn test_compute_ao_at_vertex_up() {
let c = default_ao_config();
let ao = compute_ao_at_vertex(&c, [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]);
assert!((0.0..=1.0).contains(&ao));
}
#[test]
fn test_compute_ao_at_vertex_zero() {
let c = default_ao_config();
let ao = compute_ao_at_vertex(&c, [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
assert!((ao - 0.0).abs() < 1e-6);
}
#[test]
fn test_ao_radius() {
let c = default_ao_config();
assert!((ao_radius(&c) - 0.5).abs() < 1e-6);
}
#[test]
fn test_ao_sample_count() {
let c = default_ao_config();
assert_eq!(ao_sample_count(&c), 16);
}
#[test]
fn test_ao_to_texture() {
let vals = vec![0.0, 0.5, 1.0, 0.25];
let tex = ao_to_texture(&vals, 2, 2);
assert_eq!(tex.len(), 4);
assert_eq!(tex[0], 0);
assert_eq!(tex[2], 255);
}
#[test]
fn test_ao_to_texture_pad() {
let vals = vec![0.5];
let tex = ao_to_texture(&vals, 2, 2);
assert_eq!(tex.len(), 4);
assert_eq!(tex[1], 255); }
#[test]
fn test_ao_intensity() {
let c = default_ao_config();
assert!((ao_intensity(&c) - 1.0).abs() < 1e-6);
}
#[test]
fn test_ao_pass_name() {
let r = new_ao_renderer(default_ao_config());
assert_eq!(ao_pass_name(&r), "ao_pass");
}
#[test]
fn test_ao_intensity_scales_result() {
let pos = [0.5_f32, 1.0, 0.5];
let nor = [0.0_f32, 1.0, 0.0];
let config_base = AoConfig {
radius: 0.5,
sample_count: 16,
intensity: 0.5,
};
let config_doubled = AoConfig {
radius: 0.5,
sample_count: 16,
intensity: 1.0,
};
let ao_base = compute_ao_at_vertex(&config_base, nor, pos);
let ao_doubled = compute_ao_at_vertex(&config_doubled, nor, pos);
assert!((0.0..=1.0).contains(&ao_base));
assert!((0.0..=1.0).contains(&ao_doubled));
let occ_base = 1.0 - ao_base;
let occ_doubled = 1.0 - ao_doubled;
if occ_base > 1e-6 {
let ratio = occ_doubled / occ_base;
assert!(
(ratio - 2.0).abs() < 0.1,
"expected ratio ~2.0, got {ratio}"
);
}
}
#[test]
fn test_hammersley_deterministic() {
let (u1, v1) = hammersley_2d(0, 16);
let (u2, v2) = hammersley_2d(0, 16);
assert_eq!(u1, u2);
assert_eq!(v1, v2);
}
#[test]
fn test_van_der_corput_range() {
for i in 0..256_u32 {
let v = van_der_corput_base2(i);
assert!((0.0..1.0).contains(&v), "vdc({i}) = {v} out of range");
}
}
}