pub fn compute_vertex_tangent_frames(normals: &[[f32; 3]]) -> Vec<([f32; 3], [f32; 3])> {
normals.iter().map(|&n| gram_schmidt_tangent(n)).collect()
}
pub fn tangents_from_explicit(
normals: &[[f32; 3]],
tangents: &[[f32; 4]],
) -> Vec<([f32; 3], [f32; 3])> {
normals
.iter()
.zip(tangents.iter())
.map(|(&n, &t)| {
let normal = glam::Vec3::from(n);
let tangent = glam::Vec3::new(t[0], t[1], t[2]);
let handedness = t[3];
let bitangent = normal.cross(tangent) * handedness;
(tangent.to_array(), bitangent.to_array())
})
.collect()
}
pub fn compute_face_tangent_frames(
positions: &[[f32; 3]],
indices: &[u32],
) -> Vec<([f32; 3], [f32; 3])> {
let num_tris = indices.len() / 3;
let mut frames = Vec::with_capacity(num_tris);
for tri in 0..num_tris {
let i0 = indices[3 * tri] as usize;
let i1 = indices[3 * tri + 1] as usize;
let i2 = indices[3 * tri + 2] as usize;
let p0 = glam::Vec3::from(positions[i0]);
let p1 = glam::Vec3::from(positions[i1]);
let p2 = glam::Vec3::from(positions[i2]);
let e0 = p1 - p0;
let e1 = p2 - p0;
let face_normal_raw = e0.cross(e1);
let normal = if face_normal_raw.length_squared() > 1e-12 {
face_normal_raw.normalize()
} else {
glam::Vec3::Z
};
let (tangent, bitangent) = if e0.length_squared() > 1e-12 {
let t = (e0 - e0.dot(normal) * normal).normalize_or_zero();
if t.length_squared() > 0.5 {
let b = normal.cross(t);
(t, b)
} else {
gram_schmidt_tangent_vec(normal)
}
} else {
gram_schmidt_tangent_vec(normal)
};
frames.push((tangent.to_array(), bitangent.to_array()));
}
frames
}
pub fn gram_schmidt_tangent(normal: [f32; 3]) -> ([f32; 3], [f32; 3]) {
let (t, b) = gram_schmidt_tangent_vec(glam::Vec3::from(normal));
(t.to_array(), b.to_array())
}
fn gram_schmidt_tangent_vec(n: glam::Vec3) -> (glam::Vec3, glam::Vec3) {
let reference = if n.x.abs() < 0.9 {
glam::Vec3::X
} else {
glam::Vec3::Y
};
let t = (reference - reference.dot(n) * n).normalize_or_zero();
let t = if t.length_squared() < 0.5 {
let fallback = glam::Vec3::Z;
(fallback - fallback.dot(n) * n).normalize_or_zero()
} else {
t
};
let b = n.cross(t);
(t, b)
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_orthonormal(name: &str, normal: [f32; 3], tangent: [f32; 3], bitangent: [f32; 3]) {
let n = glam::Vec3::from(normal);
let t = glam::Vec3::from(tangent);
let b = glam::Vec3::from(bitangent);
assert!(
(t.length() - 1.0).abs() < 1e-4,
"{name}: tangent not unit length: {}",
t.length()
);
assert!(
(b.length() - 1.0).abs() < 1e-4,
"{name}: bitangent not unit length: {}",
b.length()
);
assert!(
n.dot(t).abs() < 1e-4,
"{name}: tangent not perpendicular to normal: {}",
n.dot(t)
);
assert!(
n.dot(b).abs() < 1e-4,
"{name}: bitangent not perpendicular to normal: {}",
n.dot(b)
);
assert!(
t.dot(b).abs() < 1e-4,
"{name}: tangent not perpendicular to bitangent: {}",
t.dot(b)
);
}
#[test]
fn gram_schmidt_axis_aligned_normals() {
let normals = [
([1.0, 0.0, 0.0], "+X"),
([-1.0, 0.0, 0.0], "-X"),
([0.0, 1.0, 0.0], "+Y"),
([0.0, -1.0, 0.0], "-Y"),
([0.0, 0.0, 1.0], "+Z"),
([0.0, 0.0, -1.0], "-Z"),
];
for (n, name) in &normals {
let (t, b) = gram_schmidt_tangent(*n);
assert_orthonormal(name, *n, t, b);
}
}
#[test]
fn gram_schmidt_diagonal_normal() {
let s = 1.0 / 3.0f32.sqrt();
let n = [s, s, s];
let (t, b) = gram_schmidt_tangent(n);
assert_orthonormal("diagonal", n, t, b);
}
#[test]
fn compute_vertex_tangent_frames_length_matches() {
let normals = vec![[0.0, 1.0, 0.0]; 10];
let frames = compute_vertex_tangent_frames(&normals);
assert_eq!(frames.len(), normals.len());
}
#[test]
fn compute_vertex_tangent_frames_all_orthonormal() {
let normals = vec![[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
let frames = compute_vertex_tangent_frames(&normals);
for (i, (t, b)) in frames.iter().enumerate() {
assert_orthonormal(&format!("vtx_{i}"), normals[i], *t, *b);
}
}
#[test]
fn compute_face_tangent_frames_right_triangle() {
let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let indices = vec![0u32, 1, 2];
let frames = compute_face_tangent_frames(&positions, &indices);
assert_eq!(frames.len(), 1);
let (t, b) = frames[0];
assert!((t[0] - 1.0).abs() < 1e-4, "tangent X should be ~1.0");
assert!(t[1].abs() < 1e-4);
assert!(t[2].abs() < 1e-4);
assert!(b[0].abs() < 1e-4);
assert!((b[1] - 1.0).abs() < 1e-4, "bitangent Y should be ~1.0");
}
#[test]
fn compute_face_tangent_frames_degenerate_no_panic() {
let positions = vec![[0.0; 3]; 3]; let indices = vec![0u32, 1, 2];
let frames = compute_face_tangent_frames(&positions, &indices);
assert_eq!(frames.len(), 1); }
#[test]
fn tangents_from_explicit_positive_handedness() {
let normals = vec![[0.0, 0.0, 1.0]]; let tangents = vec![[1.0, 0.0, 0.0, 1.0]]; let frames = tangents_from_explicit(&normals, &tangents);
let (t, b) = frames[0];
assert!((t[0] - 1.0).abs() < 1e-5);
assert!((b[1] - 1.0).abs() < 1e-5);
}
#[test]
fn tangents_from_explicit_negative_handedness() {
let normals = vec![[0.0, 0.0, 1.0]]; let tangents = vec![[1.0, 0.0, 0.0, -1.0]]; let frames = tangents_from_explicit(&normals, &tangents);
let (_, b) = frames[0];
assert!((b[1] - (-1.0)).abs() < 1e-5);
}
#[test]
fn tangents_from_explicit_length_matches() {
let normals = vec![[0.0, 1.0, 0.0]; 5];
let tangents = vec![[1.0, 0.0, 0.0, 1.0]; 5];
let frames = tangents_from_explicit(&normals, &tangents);
assert_eq!(frames.len(), 5);
}
}