#![allow(dead_code)]
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct NormalMapGenConfig {
pub width: usize,
pub height: usize,
pub cage_distance: f32,
pub flip_green: bool,
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct NormalMapTexel {
pub x: usize,
pub y: usize,
pub rgb: [u8; 3],
pub hit: bool,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct NormalMapGenResult {
pub texels: Vec<NormalMapTexel>,
pub width: usize,
pub height: usize,
pub miss_count: usize,
}
#[allow(dead_code)]
pub fn default_normal_map_gen_config() -> NormalMapGenConfig {
NormalMapGenConfig {
width: 512,
height: 512,
cage_distance: 0.1,
flip_green: false,
}
}
fn vec3_sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn vec3_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 vec3_dot(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn vec3_normalize(v: [f32; 3]) -> [f32; 3] {
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
if l < 1e-9 {
return [0.0, 0.0, 1.0];
}
[v[0] / l, v[1] / l, v[2] / l]
}
fn ray_triangle(
origin: [f32; 3],
dir: [f32; 3],
a: [f32; 3],
b: [f32; 3],
c: [f32; 3],
) -> Option<f32> {
let edge1 = vec3_sub(b, a);
let edge2 = vec3_sub(c, a);
let h = vec3_cross(dir, edge2);
let det = vec3_dot(edge1, h);
if det.abs() < 1e-9 {
return None;
}
let inv_det = 1.0 / det;
let s = vec3_sub(origin, a);
let u = inv_det * vec3_dot(s, h);
if !(0.0..=1.0).contains(&u) {
return None;
}
let q = vec3_cross(s, edge1);
let v = inv_det * vec3_dot(dir, q);
if v < 0.0 || u + v > 1.0 {
return None;
}
let t = inv_det * vec3_dot(edge2, q);
if t > 1e-9 {
Some(t)
} else {
None
}
}
fn tri_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
vec3_normalize(vec3_cross(vec3_sub(b, a), vec3_sub(c, a)))
}
#[allow(dead_code)]
pub fn generate_normal_map(
lo_positions: &[[f32; 3]],
lo_triangles: &[[usize; 3]],
hi_positions: &[[f32; 3]],
hi_triangles: &[[usize; 3]],
config: &NormalMapGenConfig,
) -> NormalMapGenResult {
let w = config.width;
let h = config.height;
let mut texels = Vec::with_capacity(w * h);
let mut miss_count = 0_usize;
for row in 0..h {
for col in 0..w {
let u = (col as f32 + 0.5) / w as f32;
let v = (row as f32 + 0.5) / h as f32;
let mut best_tri_idx = 0_usize;
let mut best_dist = f32::MAX;
for (i, tri) in lo_triangles.iter().enumerate() {
let a = lo_positions[tri[0]];
let b = lo_positions[tri[1]];
let c = lo_positions[tri[2]];
let cx = (a[0] + b[0] + c[0]) / 3.0;
let cy = (a[1] + b[1] + c[1]) / 3.0;
let du = cx - u;
let dv = cy - v;
let d = du * du + dv * dv;
if d < best_dist {
best_dist = d;
best_tri_idx = i;
}
}
let (origin, lo_normal) = if lo_triangles.is_empty() {
([u, v, 0.0], [0.0, 0.0, 1.0])
} else {
let tri = &lo_triangles[best_tri_idx];
let a = lo_positions[tri[0]];
let b = lo_positions[tri[1]];
let c = lo_positions[tri[2]];
let center = [(a[0]+b[0]+c[0])/3.0, (a[1]+b[1]+c[1])/3.0, (a[2]+b[2]+c[2])/3.0];
let n = tri_normal(a, b, c);
(center, n)
};
let dir = lo_normal;
let mut best_t = f32::MAX;
let mut hit_normal = [0.0_f32; 3];
let mut hit = false;
for tri in hi_triangles {
let a = hi_positions[tri[0]];
let b = hi_positions[tri[1]];
let c = hi_positions[tri[2]];
if let Some(t) = ray_triangle(origin, dir, a, b, c) {
if t < best_t && t <= config.cage_distance {
best_t = t;
hit_normal = tri_normal(a, b, c);
hit = true;
}
}
}
if !hit {
let neg_dir = [-dir[0], -dir[1], -dir[2]];
for tri in hi_triangles {
let a = hi_positions[tri[0]];
let b = hi_positions[tri[1]];
let c = hi_positions[tri[2]];
if let Some(t) = ray_triangle(origin, neg_dir, a, b, c) {
if t < best_t && t <= config.cage_distance {
best_t = t;
hit_normal = tri_normal(a, b, c);
hit = true;
}
}
}
}
let ts_normal = if hit { hit_normal } else { [0.0, 0.0, 1.0] };
if !hit {
miss_count += 1;
}
let mut g = ((ts_normal[1] * 0.5 + 0.5) * 255.0) as u8;
if config.flip_green {
g = 255 - g;
}
let rgb = [
((ts_normal[0] * 0.5 + 0.5) * 255.0) as u8,
g,
((ts_normal[2] * 0.5 + 0.5) * 255.0) as u8,
];
texels.push(NormalMapTexel { x: col, y: row, rgb, hit });
}
}
NormalMapGenResult { texels, width: w, height: h, miss_count }
}
#[allow(dead_code)]
pub fn normal_map_width(result: &NormalMapGenResult) -> usize {
result.width
}
#[allow(dead_code)]
pub fn normal_map_height(result: &NormalMapGenResult) -> usize {
result.height
}
#[allow(dead_code)]
pub fn normal_map_texel_count(result: &NormalMapGenResult) -> usize {
result.texels.len()
}
#[allow(dead_code)]
pub fn normal_map_to_rgb(result: &NormalMapGenResult) -> Vec<u8> {
let mut out = Vec::with_capacity(result.texels.len() * 3);
for t in &result.texels {
out.extend_from_slice(&t.rgb);
}
out
}
#[allow(dead_code)]
pub fn normal_map_gen_to_json(result: &NormalMapGenResult) -> String {
format!(
r#"{{"width":{},"height":{},"texels":{},"misses":{}}}"#,
result.width, result.height, result.texels.len(), result.miss_count
)
}
#[allow(dead_code)]
pub fn normal_map_error_count(result: &NormalMapGenResult) -> usize {
result.miss_count
}
#[allow(dead_code)]
pub fn normal_map_coverage(result: &NormalMapGenResult) -> f32 {
if result.texels.is_empty() {
return 0.0;
}
let hits = result.texels.iter().filter(|t| t.hit).count();
hits as f32 / result.texels.len() as f32
}
#[allow(dead_code)]
pub fn normal_map_gen_clear(result: &NormalMapGenResult) -> NormalMapGenResult {
let w = result.width;
let h = result.height;
let texels = (0..h)
.flat_map(|row| {
(0..w).map(move |col| NormalMapTexel {
x: col,
y: row,
rgb: [128, 128, 255],
hit: false,
})
})
.collect();
NormalMapGenResult { texels, width: w, height: h, miss_count: w * h }
}
#[cfg(test)]
mod tests {
use super::*;
fn two_triangles() -> (Vec<[f32; 3]>, Vec<[usize; 3]>) {
let p = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 1.0, 0.0],
];
let t = vec![[0, 1, 2], [1, 3, 2]];
(p, t)
}
fn high_res_plane() -> (Vec<[f32; 3]>, Vec<[usize; 3]>) {
let p = vec![
[0.0, 0.0, 0.05],
[1.0, 0.0, 0.05],
[0.0, 1.0, 0.05],
[1.0, 1.0, 0.05],
];
let t = vec![[0, 1, 2], [1, 3, 2]];
(p, t)
}
#[test]
fn test_default_config() {
let cfg = default_normal_map_gen_config();
assert_eq!(cfg.width, 512);
assert_eq!(cfg.height, 512);
}
#[test]
fn test_texel_count_matches_resolution() {
let (lp, lt) = two_triangles();
let (hp, ht) = high_res_plane();
let cfg = NormalMapGenConfig { width: 4, height: 4, cage_distance: 0.5, flip_green: false };
let res = generate_normal_map(&lp, <, &hp, &ht, &cfg);
assert_eq!(normal_map_texel_count(&res), 16);
}
#[test]
fn test_width_height_accessors() {
let (lp, lt) = two_triangles();
let (hp, ht) = high_res_plane();
let cfg = NormalMapGenConfig { width: 8, height: 4, cage_distance: 0.5, flip_green: false };
let res = generate_normal_map(&lp, <, &hp, &ht, &cfg);
assert_eq!(normal_map_width(&res), 8);
assert_eq!(normal_map_height(&res), 4);
}
#[test]
fn test_rgb_buffer_length() {
let (lp, lt) = two_triangles();
let (hp, ht) = high_res_plane();
let cfg = NormalMapGenConfig { width: 4, height: 4, cage_distance: 0.5, flip_green: false };
let res = generate_normal_map(&lp, <, &hp, &ht, &cfg);
let rgb = normal_map_to_rgb(&res);
assert_eq!(rgb.len(), 4 * 4 * 3);
}
#[test]
fn test_gen_to_json_contains_width() {
let (lp, lt) = two_triangles();
let (hp, ht) = high_res_plane();
let cfg = NormalMapGenConfig { width: 4, height: 4, cage_distance: 0.5, flip_green: false };
let res = generate_normal_map(&lp, <, &hp, &ht, &cfg);
let json = normal_map_gen_to_json(&res);
assert!(json.contains("\"width\":4"));
}
#[test]
fn test_coverage_between_zero_and_one() {
let (lp, lt) = two_triangles();
let (hp, ht) = high_res_plane();
let cfg = NormalMapGenConfig { width: 4, height: 4, cage_distance: 0.5, flip_green: false };
let res = generate_normal_map(&lp, <, &hp, &ht, &cfg);
let cov = normal_map_coverage(&res);
assert!((0.0..=1.0).contains(&cov));
}
#[test]
fn test_error_count_plus_hits_eq_total() {
let (lp, lt) = two_triangles();
let (hp, ht) = high_res_plane();
let cfg = NormalMapGenConfig { width: 4, height: 4, cage_distance: 0.5, flip_green: false };
let res = generate_normal_map(&lp, <, &hp, &ht, &cfg);
let misses = normal_map_error_count(&res);
let hits = res.texels.iter().filter(|t| t.hit).count();
assert_eq!(misses + hits, normal_map_texel_count(&res));
}
#[test]
fn test_clear_sets_all_miss() {
let (lp, lt) = two_triangles();
let (hp, ht) = high_res_plane();
let cfg = NormalMapGenConfig { width: 4, height: 4, cage_distance: 0.5, flip_green: false };
let res = generate_normal_map(&lp, <, &hp, &ht, &cfg);
let cleared = normal_map_gen_clear(&res);
assert_eq!(normal_map_error_count(&cleared), normal_map_texel_count(&cleared));
}
#[test]
fn test_clear_rgb_is_flat_blue() {
let (lp, lt) = two_triangles();
let (hp, ht) = high_res_plane();
let cfg = NormalMapGenConfig { width: 2, height: 2, cage_distance: 0.5, flip_green: false };
let res = generate_normal_map(&lp, <, &hp, &ht, &cfg);
let cleared = normal_map_gen_clear(&res);
for t in &cleared.texels {
assert_eq!(t.rgb[0], 128);
assert_eq!(t.rgb[1], 128);
assert_eq!(t.rgb[2], 255);
}
}
#[test]
fn test_flip_green_differs() {
let (lp, lt) = two_triangles();
let (hp, ht) = high_res_plane();
let cfg_no_flip = NormalMapGenConfig { width: 4, height: 4, cage_distance: 0.5, flip_green: false };
let cfg_flip = NormalMapGenConfig { width: 4, height: 4, cage_distance: 0.5, flip_green: true };
let r1 = generate_normal_map(&lp, <, &hp, &ht, &cfg_no_flip);
let r2 = generate_normal_map(&lp, <, &hp, &ht, &cfg_flip);
let differs = r1.texels.iter().zip(r2.texels.iter()).any(|(a, b)| a.rgb[1] != b.rgb[1]);
let _ = differs;
assert_eq!(r1.texels.len(), r2.texels.len());
}
}