#![allow(dead_code)]
use std::f32::consts::PI;
#[derive(Debug, Clone, Copy)]
pub struct AoBakeConfig {
pub ray_count: u32,
pub max_distance: f32,
pub bias: f32,
}
impl Default for AoBakeConfig {
fn default() -> Self {
Self {
ray_count: 64,
max_distance: 1.0,
bias: 0.001,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct AoVertex {
pub position: [f32; 3],
pub normal: [f32; 3],
pub ao: f32,
}
#[derive(Debug, Default, Clone)]
pub struct AoMesh {
pub vertices: Vec<AoVertex>,
pub indices: Vec<u32>,
pub config: AoBakeConfig,
}
impl AoMesh {
pub fn new(config: AoBakeConfig) -> Self {
Self {
config,
..Default::default()
}
}
pub fn vertex_count(&self) -> usize {
self.vertices.len()
}
pub fn average_ao(&self) -> f32 {
if self.vertices.is_empty() {
return 0.0;
}
let sum: f32 = self.vertices.iter().map(|v| v.ao).sum();
sum / self.vertices.len() as f32
}
pub fn fill_ao(&mut self, value: f32) {
let clamped = value.clamp(0.0, 1.0);
for v in self.vertices.iter_mut() {
v.ao = clamped;
}
}
pub fn smooth(&mut self, iterations: usize) {
if self.indices.is_empty() {
smooth_ao(&mut self.vertices);
} else {
let idx = self.indices.clone();
smooth_ao_laplacian(&mut self.vertices, &idx, iterations);
}
}
}
pub fn validate_ao_values(mesh: &AoMesh) -> bool {
mesh.vertices.iter().all(|v| (0.0..=1.0).contains(&v.ao))
}
pub fn hemisphere_samples(count: u32) -> Vec<[f32; 3]> {
let mut samples = Vec::with_capacity(count as usize);
for i in 0..count {
let theta = (i as f32 / count as f32) * PI * 0.5;
let phi = (i as f32 * 2.399_963) % (PI * 2.0);
samples.push([
theta.sin() * phi.cos(),
theta.cos(),
theta.sin() * phi.sin(),
]);
}
samples
}
pub fn boost_ao_contrast(vertices: &mut [AoVertex], power: f32) {
for v in vertices.iter_mut() {
v.ao = v.ao.powf(power).clamp(0.0, 1.0);
}
}
pub fn smooth_ao(vertices: &mut [AoVertex]) {
let n = vertices.len();
if n < 2 {
return;
}
let mut nn_sum = 0.0f32;
#[allow(clippy::needless_range_loop)]
for i in 0..n {
let pi = vertices[i].position;
let mut best = f32::INFINITY;
for j in 0..n {
if i == j {
continue;
}
let pj = vertices[j].position;
let dx = pi[0] - pj[0];
let dy = pi[1] - pj[1];
let dz = pi[2] - pj[2];
let d2 = dx * dx + dy * dy + dz * dz;
if d2 < best {
best = d2;
}
}
if best.is_finite() {
nn_sum += best.sqrt();
}
}
let mean_nn = nn_sum / n as f32;
let radius = (2.0 * mean_nn).max(1e-4);
let radius2 = radius * radius;
let mut smoothed = vec![0.0f32; n];
#[allow(clippy::needless_range_loop)]
for i in 0..n {
let pi = vertices[i].position;
let mut acc = 0.0f32;
let mut count = 0.0f32;
for j in 0..n {
if i == j {
continue;
}
let pj = vertices[j].position;
let dx = pi[0] - pj[0];
let dy = pi[1] - pj[1];
let dz = pi[2] - pj[2];
if dx * dx + dy * dy + dz * dz <= radius2 {
acc += vertices[j].ao;
count += 1.0;
}
}
smoothed[i] = if count > 0.0 {
0.5 * vertices[i].ao + 0.5 * (acc / count)
} else {
vertices[i].ao
};
}
for (v, &s) in vertices.iter_mut().zip(smoothed.iter()) {
v.ao = s.clamp(0.0, 1.0);
}
}
pub fn smooth_ao_laplacian(vertices: &mut [AoVertex], indices: &[u32], iterations: usize) {
let n = vertices.len();
if n == 0 || indices.len() < 3 {
return;
}
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for tri in indices.chunks_exact(3) {
let a = tri[0] as usize;
let b = tri[1] as usize;
let c = tri[2] as usize;
if a >= n || b >= n || c >= n {
continue;
}
for &(u, v) in &[(a, b), (b, c), (c, a)] {
if !adj[u].contains(&v) {
adj[u].push(v);
}
if !adj[v].contains(&u) {
adj[v].push(u);
}
}
}
for _ in 0..iterations {
let mut next = vec![0.0f32; n];
#[allow(clippy::needless_range_loop)]
for i in 0..n {
if adj[i].is_empty() {
next[i] = vertices[i].ao;
} else {
let sum: f32 = adj[i].iter().map(|&j| vertices[j].ao).sum();
let mean = sum / adj[i].len() as f32;
next[i] = 0.5 * vertices[i].ao + 0.5 * mean;
}
}
for (v, &s) in vertices.iter_mut().zip(next.iter()) {
v.ao = s.clamp(0.0, 1.0);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_mesh_with_ao(n: usize, ao: f32) -> AoMesh {
let mut mesh = AoMesh::new(AoBakeConfig::default());
for _ in 0..n {
mesh.vertices.push(AoVertex {
position: [0.0; 3],
normal: [0.0, 1.0, 0.0],
ao,
});
}
mesh
}
#[test]
fn test_new_ao_mesh_empty() {
assert_eq!(AoMesh::new(AoBakeConfig::default()).vertex_count(), 0);
}
#[test]
fn test_average_ao_empty() {
assert_eq!(AoMesh::new(AoBakeConfig::default()).average_ao(), 0.0);
}
#[test]
fn test_average_ao_uniform() {
let mesh = make_mesh_with_ao(4, 0.8);
assert!((mesh.average_ao() - 0.8).abs() < f32::EPSILON);
}
#[test]
fn test_validate_ao_valid() {
let mesh = make_mesh_with_ao(3, 0.5);
assert!(validate_ao_values(&mesh));
}
#[test]
fn test_fill_ao_clamps() {
let mut mesh = make_mesh_with_ao(2, 0.0);
mesh.fill_ao(5.0);
assert!(mesh.vertices.iter().all(|v| (0.0..=1.0).contains(&v.ao)));
}
#[test]
fn test_hemisphere_samples_count() {
assert_eq!(hemisphere_samples(16).len(), 16);
}
#[test]
fn test_boost_ao_contrast() {
let mut verts = vec![AoVertex {
position: [0.0; 3],
normal: [0.0, 1.0, 0.0],
ao: 0.5,
}];
boost_ao_contrast(&mut verts, 2.0);
assert!(verts[0].ao < 0.5);
}
#[test]
fn test_smooth_ao_changes_values() {
let mut verts = vec![
AoVertex {
position: [0.0; 3],
normal: [0.0, 1.0, 0.0],
ao: 0.0,
},
AoVertex {
position: [0.0; 3],
normal: [0.0, 1.0, 0.0],
ao: 1.0,
},
];
let before = verts[0].ao;
smooth_ao(&mut verts);
assert!((verts[0].ao - before).abs() > f32::EPSILON);
}
#[test]
fn test_smooth_ao_stays_in_range() {
let mut verts = vec![
AoVertex {
position: [0.0; 3],
normal: [0.0, 1.0, 0.0],
ao: 0.0,
},
AoVertex {
position: [0.0; 3],
normal: [0.0, 1.0, 0.0],
ao: 1.0,
},
];
smooth_ao(&mut verts);
assert!(verts.iter().all(|v| (0.0..=1.0).contains(&v.ao)));
}
#[test]
fn test_default_config_ray_count() {
assert_eq!(AoBakeConfig::default().ray_count, 64);
}
#[test]
fn test_smooth_ao_laplacian_diffuses_along_topology() {
let mut verts = vec![
AoVertex { position: [0.0, 0.0, 0.0], normal: [0.0, 1.0, 0.0], ao: 1.0 },
AoVertex { position: [1.0, 0.0, 0.0], normal: [0.0, 1.0, 0.0], ao: 0.0 },
AoVertex { position: [1.0, 1.0, 0.0], normal: [0.0, 1.0, 0.0], ao: 0.0 },
AoVertex { position: [0.0, 1.0, 0.0], normal: [0.0, 1.0, 0.0], ao: 0.0 },
];
let indices = [0u32, 1, 2, 0, 2, 3];
let before = verts[1].ao;
smooth_ao_laplacian(&mut verts, &indices, 1);
assert!(verts[1].ao > before, "neighbour of hot vertex should increase");
assert!(verts.iter().all(|v| (0.0..=1.0).contains(&v.ao)));
}
#[test]
fn test_ao_mesh_smooth_uses_indices() {
let mut mesh = AoMesh::new(AoBakeConfig::default());
mesh.vertices = vec![
AoVertex { position: [0.0, 0.0, 0.0], normal: [0.0, 1.0, 0.0], ao: 1.0 },
AoVertex { position: [1.0, 0.0, 0.0], normal: [0.0, 1.0, 0.0], ao: 0.0 },
AoVertex { position: [0.0, 1.0, 0.0], normal: [0.0, 1.0, 0.0], ao: 0.0 },
];
mesh.indices = vec![0, 1, 2];
mesh.smooth(2);
assert!(mesh.vertices.iter().all(|v| (0.0..=1.0).contains(&v.ao)));
assert!(mesh.vertices[1].ao > 0.0);
}
}