#![allow(dead_code)]
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct LocalFrameConfig {
pub right_handed: bool,
pub epsilon: f32,
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct VertexFrame {
pub tangent: [f32; 3],
pub bitangent: [f32; 3],
pub normal: [f32; 3],
pub handedness: f32,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct LocalFrameResult {
pub frames: Vec<VertexFrame>,
pub orthonormal: bool,
}
#[allow(dead_code)]
pub fn default_local_frame_config() -> LocalFrameConfig {
LocalFrameConfig {
right_handed: true,
epsilon: 1e-7,
}
}
fn v3_add(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn v3_sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn v3_scale(v: [f32; 3], s: f32) -> [f32; 3] {
[v[0] * s, v[1] * s, v[2] * s]
}
fn v3_dot(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn v3_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 v3_normalize(v: [f32; 3], eps: f32) -> [f32; 3] {
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
if l < eps {
[0.0, 0.0, 1.0]
} else {
[v[0] / l, v[1] / l, v[2] / l]
}
}
#[allow(dead_code)]
pub fn compute_local_frames(
positions: &[[f32; 3]],
uvs: &[[f32; 2]],
triangles: &[[usize; 3]],
config: &LocalFrameConfig,
) -> LocalFrameResult {
let n_verts = positions.len();
let eps = config.epsilon;
let mut tan1 = vec![[0.0_f32; 3]; n_verts];
let mut tan2 = vec![[0.0_f32; 3]; n_verts];
let mut norms = vec![[0.0_f32; 3]; n_verts];
for tri in triangles {
let i0 = tri[0];
let i1 = tri[1];
let i2 = tri[2];
let p0 = positions[i0];
let p1 = positions[i1];
let p2 = positions[i2];
let uv0 = uvs[i0];
let uv1 = uvs[i1];
let uv2 = uvs[i2];
let e1 = v3_sub(p1, p0);
let e2 = v3_sub(p2, p0);
let du1 = uv1[0] - uv0[0];
let dv1 = uv1[1] - uv0[1];
let du2 = uv2[0] - uv0[0];
let dv2 = uv2[1] - uv0[1];
let r_denom = du1 * dv2 - du2 * dv1;
let r = if r_denom.abs() > eps { 1.0 / r_denom } else { 0.0 };
let t = v3_scale(v3_sub(v3_scale(e1, dv2), v3_scale(e2, dv1)), r);
let b = v3_scale(v3_sub(v3_scale(e2, du1), v3_scale(e1, du2)), r);
let fn_ = v3_cross(e1, e2);
for &idx in &[i0, i1, i2] {
tan1[idx] = v3_add(tan1[idx], t);
tan2[idx] = v3_add(tan2[idx], b);
norms[idx] = v3_add(norms[idx], fn_);
}
}
let frames: Vec<VertexFrame> = (0..n_verts)
.map(|i| {
let n = v3_normalize(norms[i], eps);
let t = tan1[i];
let t_ortho = v3_normalize(
v3_sub(t, v3_scale(n, v3_dot(n, t))),
eps,
);
let cross = v3_cross(n, t_ortho);
let hand_sign = if v3_dot(cross, tan2[i]) < 0.0 { -1.0_f32 } else { 1.0_f32 };
let hand = if config.right_handed { hand_sign } else { -hand_sign };
let bt = v3_scale(v3_cross(n, t_ortho), hand);
VertexFrame {
tangent: t_ortho,
bitangent: bt,
normal: n,
handedness: hand,
}
})
.collect();
LocalFrameResult { frames, orthonormal: true }
}
#[allow(dead_code)]
pub fn local_frame_count(result: &LocalFrameResult) -> usize {
result.frames.len()
}
#[allow(dead_code)]
pub fn local_frame_tangent(result: &LocalFrameResult, index: usize) -> [f32; 3] {
result.frames[index].tangent
}
#[allow(dead_code)]
pub fn local_frame_bitangent(result: &LocalFrameResult, index: usize) -> [f32; 3] {
result.frames[index].bitangent
}
#[allow(dead_code)]
pub fn local_frame_normal(result: &LocalFrameResult, index: usize) -> [f32; 3] {
result.frames[index].normal
}
#[allow(dead_code)]
pub fn local_frames_to_json(result: &LocalFrameResult) -> String {
let frame_strs: Vec<String> = result
.frames
.iter()
.map(|f| {
format!(
r#"{{"t":[{:.4},{:.4},{:.4}],"b":[{:.4},{:.4},{:.4}],"n":[{:.4},{:.4},{:.4}],"h":{:.1}}}"#,
f.tangent[0], f.tangent[1], f.tangent[2],
f.bitangent[0], f.bitangent[1], f.bitangent[2],
f.normal[0], f.normal[1], f.normal[2],
f.handedness,
)
})
.collect();
format!(r#"{{"orthonormal":{},"frames":[{}]}}"#, result.orthonormal, frame_strs.join(","))
}
#[allow(dead_code)]
pub fn local_frame_handedness(result: &LocalFrameResult, index: usize) -> f32 {
result.frames[index].handedness
}
#[allow(dead_code)]
pub fn local_frame_orthonormalize(result: &LocalFrameResult, epsilon: f32) -> LocalFrameResult {
let frames = result
.frames
.iter()
.map(|f| {
let n = v3_normalize(f.normal, epsilon);
let t_raw = v3_sub(f.tangent, v3_scale(n, v3_dot(n, f.tangent)));
let t = v3_normalize(t_raw, epsilon);
let bt = v3_scale(v3_cross(n, t), f.handedness);
VertexFrame { tangent: t, bitangent: bt, normal: n, handedness: f.handedness }
})
.collect();
LocalFrameResult { frames, orthonormal: true }
}
#[allow(dead_code)]
pub fn local_frames_valid(result: &LocalFrameResult, tolerance: f32) -> bool {
result.frames.iter().all(|f| {
let tn = (f.tangent[0] * f.tangent[0]
+ f.tangent[1] * f.tangent[1]
+ f.tangent[2] * f.tangent[2])
.sqrt();
let nn = (f.normal[0] * f.normal[0]
+ f.normal[1] * f.normal[1]
+ f.normal[2] * f.normal[2])
.sqrt();
(tn - 1.0).abs() <= tolerance && (nn - 1.0).abs() <= tolerance
})
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::type_complexity)]
fn simple_quad() -> (Vec<[f32; 3]>, Vec<[f32; 2]>, Vec<[usize; 3]>) {
let positions = vec![
[0.0_f32, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
];
let uvs = vec![
[0.0_f32, 0.0],
[1.0, 0.0],
[1.0, 1.0],
[0.0, 1.0],
];
let triangles = vec![[0, 1, 2], [0, 2, 3]];
(positions, uvs, triangles)
}
#[test]
fn test_default_config() {
let cfg = default_local_frame_config();
assert!(cfg.right_handed);
assert!(cfg.epsilon > 0.0);
}
#[test]
fn test_frame_count_matches_vertex_count() {
let (p, uv, t) = simple_quad();
let cfg = default_local_frame_config();
let res = compute_local_frames(&p, &uv, &t, &cfg);
assert_eq!(local_frame_count(&res), p.len());
}
#[test]
fn test_frames_valid_after_compute() {
let (p, uv, t) = simple_quad();
let cfg = default_local_frame_config();
let res = compute_local_frames(&p, &uv, &t, &cfg);
assert!(local_frames_valid(&res, 1e-4));
}
#[test]
fn test_normal_points_up() {
let (p, uv, t) = simple_quad();
let cfg = default_local_frame_config();
let res = compute_local_frames(&p, &uv, &t, &cfg);
for i in 0..local_frame_count(&res) {
let n = local_frame_normal(&res, i);
assert!(n[2].abs() > 0.9, "normal z={}", n[2]);
}
}
#[test]
fn test_tangent_unit_length() {
let (p, uv, t) = simple_quad();
let cfg = default_local_frame_config();
let res = compute_local_frames(&p, &uv, &t, &cfg);
for i in 0..local_frame_count(&res) {
let tg = local_frame_tangent(&res, i);
let len = (tg[0] * tg[0] + tg[1] * tg[1] + tg[2] * tg[2]).sqrt();
assert!((len - 1.0).abs() < 1e-4, "tangent len={}", len);
}
}
#[test]
fn test_handedness_is_pm_one() {
let (p, uv, t) = simple_quad();
let cfg = default_local_frame_config();
let res = compute_local_frames(&p, &uv, &t, &cfg);
for i in 0..local_frame_count(&res) {
let h = local_frame_handedness(&res, i);
assert!(h == 1.0 || h == -1.0, "handedness={}", h);
}
}
#[test]
fn test_to_json_contains_frames() {
let (p, uv, t) = simple_quad();
let cfg = default_local_frame_config();
let res = compute_local_frames(&p, &uv, &t, &cfg);
let json = local_frames_to_json(&res);
assert!(json.contains("frames"));
assert!(json.contains("orthonormal"));
}
#[test]
fn test_orthonormalize_preserves_count() {
let (p, uv, t) = simple_quad();
let cfg = default_local_frame_config();
let res = compute_local_frames(&p, &uv, &t, &cfg);
let res2 = local_frame_orthonormalize(&res, 1e-7);
assert_eq!(local_frame_count(&res), local_frame_count(&res2));
}
#[test]
fn test_orthonormalize_still_valid() {
let (p, uv, t) = simple_quad();
let cfg = default_local_frame_config();
let res = compute_local_frames(&p, &uv, &t, &cfg);
let res2 = local_frame_orthonormalize(&res, 1e-7);
assert!(local_frames_valid(&res2, 1e-4));
}
#[test]
fn test_bitangent_orthogonal_to_normal() {
let (p, uv, t) = simple_quad();
let cfg = default_local_frame_config();
let res = compute_local_frames(&p, &uv, &t, &cfg);
for i in 0..local_frame_count(&res) {
let bt = local_frame_bitangent(&res, i);
let n = local_frame_normal(&res, i);
let d = v3_dot(bt, n);
assert!(d.abs() < 1e-4, "bt·n={}", d);
}
}
}