#![allow(dead_code)]
use std::f32::consts::{FRAC_1_SQRT_2, PI};
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub struct AoBakerConfig {
pub sample_count: u32,
pub max_distance: f32,
pub falloff: f32,
pub bias: f32,
pub cosine_weighted: bool,
}
impl Default for AoBakerConfig {
fn default() -> Self {
Self {
sample_count: 64,
max_distance: 1.0,
falloff: 1.0,
bias: 0.001,
cosine_weighted: true,
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub struct AoSample {
pub vertex_index: usize,
pub occlusion: f32,
}
#[allow(dead_code)]
pub fn hammersley_hemisphere(i: u32, n: u32, cosine_weighted: bool) -> [f32; 3] {
if n == 0 {
return [0.0, 1.0, 0.0];
}
let xi1 = i as f32 / n as f32;
let xi2 = radical_inverse_base2(i);
let phi = 2.0 * PI * xi1;
let (cos_theta, sin_theta) = if cosine_weighted {
let cos_theta = xi2.sqrt();
let sin_theta = (1.0 - xi2).sqrt();
(cos_theta, sin_theta)
} else {
let cos_theta = xi2;
let sin_theta = (1.0 - xi2 * xi2).sqrt();
(cos_theta, sin_theta)
};
[sin_theta * phi.cos(), cos_theta, sin_theta * phi.sin()]
}
#[allow(dead_code)]
pub fn radical_inverse_base2(mut bits: u32) -> f32 {
bits = (bits << 16) | (bits >> 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 f32 / 0x1_0000_0000_u64 as f32
}
#[allow(dead_code)]
pub fn tangent_basis(normal: [f32; 3]) -> ([f32; 3], [f32; 3]) {
let up = if normal[1].abs() < 0.999 {
[0.0, 1.0, 0.0]
} else {
[1.0, 0.0, 0.0]
};
let tangent = cross(up, normal);
let tangent = normalize(tangent);
let bitangent = cross(normal, tangent);
(tangent, bitangent)
}
#[allow(dead_code)]
pub fn tangent_to_world(
dir: [f32; 3],
tangent: [f32; 3],
normal: [f32; 3],
bitangent: [f32; 3],
) -> [f32; 3] {
[
dir[0] * tangent[0] + dir[1] * normal[0] + dir[2] * bitangent[0],
dir[0] * tangent[1] + dir[1] * normal[1] + dir[2] * bitangent[1],
dir[0] * tangent[2] + dir[1] * normal[2] + dir[2] * bitangent[2],
]
}
#[allow(dead_code)]
pub fn ray_triangle_intersect(
origin: [f32; 3],
dir: [f32; 3],
v0: [f32; 3],
v1: [f32; 3],
v2: [f32; 3],
) -> Option<f32> {
let edge1 = sub(v1, v0);
let edge2 = sub(v2, v0);
let h = cross(dir, edge2);
let a = dot(edge1, h);
if a.abs() < 1e-7 {
return None;
}
let f = 1.0 / a;
let s = sub(origin, v0);
let u = f * dot(s, h);
if !(0.0..=1.0).contains(&u) {
return None;
}
let q = cross(s, edge1);
let v = f * dot(dir, q);
if v < 0.0 || u + v > 1.0 {
return None;
}
let t = f * dot(edge2, q);
if t > 1e-7 { Some(t) } else { None }
}
#[allow(dead_code)]
pub fn compute_vertex_ao(
position: [f32; 3],
normal: [f32; 3],
triangles: &[[usize; 3]],
positions: &[[f32; 3]],
config: &AoBakerConfig,
) -> f32 {
let (tangent, bitangent) = tangent_basis(normal);
let biased_pos = [
position[0] + normal[0] * config.bias,
position[1] + normal[1] * config.bias,
position[2] + normal[2] * config.bias,
];
let mut occluded = 0u32;
for i in 0..config.sample_count {
let ts_dir = hammersley_hemisphere(i, config.sample_count, config.cosine_weighted);
let ws_dir = tangent_to_world(ts_dir, tangent, normal, bitangent);
let mut hit = false;
for tri in triangles {
let v0 = positions[tri[0]];
let v1 = positions[tri[1]];
let v2 = positions[tri[2]];
if let Some(dist) = ray_triangle_intersect(biased_pos, ws_dir, v0, v1, v2) {
if dist < config.max_distance {
hit = true;
break;
}
}
}
if hit {
occluded += 1;
}
}
if config.sample_count == 0 {
return 0.0;
}
let ao = 1.0 - (occluded as f32 / config.sample_count as f32);
ao.powf(config.falloff)
}
#[allow(dead_code)]
pub fn bake_ao(
positions: &[[f32; 3]],
normals: &[[f32; 3]],
triangles: &[[usize; 3]],
config: &AoBakerConfig,
) -> Vec<AoSample> {
let mut results = Vec::with_capacity(positions.len());
for (i, (pos, nrm)) in positions.iter().zip(normals.iter()).enumerate() {
let ao = compute_vertex_ao(*pos, *nrm, triangles, positions, config);
results.push(AoSample {
vertex_index: i,
occlusion: ao,
});
}
results
}
fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn normalize(v: [f32; 3]) -> [f32; 3] {
let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
if len < 1e-9 { return [0.0, 1.0, 0.0]; }
[v[0] / len, v[1] / len, v[2] / len]
}
#[cfg(test)]
mod tests {
use super::*;
use std::f32::consts::PI;
#[test]
fn test_default_config() {
let c = AoBakerConfig::default();
assert_eq!(c.sample_count, 64);
assert!(c.max_distance > 0.0);
}
#[test]
fn test_radical_inverse_base2_zero() {
let v = radical_inverse_base2(0);
assert!(v.abs() < 1e-6);
}
#[test]
fn test_radical_inverse_base2_range() {
for i in 0..16 {
let v = radical_inverse_base2(i);
assert!((0.0..=1.0).contains(&v), "Out of range: {v}");
}
}
#[test]
fn test_hammersley_hemisphere_y_positive() {
for i in 0..8 {
let d = hammersley_hemisphere(i, 8, true);
assert!(d[1] >= 0.0, "y should be non-negative, got {}", d[1]);
}
}
#[test]
fn test_hammersley_zero_n() {
let d = hammersley_hemisphere(0, 0, true);
assert!((d[1] - 1.0).abs() < 1e-5);
}
#[test]
fn test_tangent_basis_up() {
let (t, b) = tangent_basis([0.0, 1.0, 0.0]);
let d = dot(t, b);
assert!(d.abs() < 1e-5, "Tangent and bitangent should be orthogonal");
}
#[test]
fn test_ray_triangle_hit() {
let origin = [0.0, 0.0, -1.0];
let dir = [0.0, 0.0, 1.0];
let v0 = [-1.0, -1.0, 0.0];
let v1 = [1.0, -1.0, 0.0];
let v2 = [0.0, 1.0, 0.0];
let hit = ray_triangle_intersect(origin, dir, v0, v1, v2);
assert!(hit.is_some());
assert!((hit.expect("should succeed") - 1.0).abs() < 1e-5);
}
#[test]
fn test_ray_triangle_miss() {
let origin = [10.0, 10.0, -1.0];
let dir = [0.0, 0.0, 1.0];
let v0 = [-1.0, -1.0, 0.0];
let v1 = [1.0, -1.0, 0.0];
let v2 = [0.0, 1.0, 0.0];
assert!(ray_triangle_intersect(origin, dir, v0, v1, v2).is_none());
}
#[test]
fn test_bake_ao_empty() {
let r = bake_ao(&[], &[], &[], &AoBakerConfig::default());
assert!(r.is_empty());
}
#[test]
fn test_compute_vertex_ao_no_triangles() {
let ao = compute_vertex_ao(
[0.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
&[],
&[],
&AoBakerConfig { sample_count: 8, ..Default::default() },
);
assert!((ao - 1.0).abs() < 1e-5, "No geometry means full AO = 1.0");
}
}