#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum TangentHandedness {
Right,
Left,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TangentExportConfig {
pub handedness: TangentHandedness,
pub mikktspace: bool,
pub normalize: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TangentData {
pub tangents: Vec<[f32; 4]>,
pub bitangents: Vec<[f32; 3]>,
pub vertex_count: usize,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TangentExportResult {
pub data: TangentData,
pub degenerate_count: usize,
pub success: bool,
}
#[allow(dead_code)]
pub fn default_tangent_export_config() -> TangentExportConfig {
TangentExportConfig {
handedness: TangentHandedness::Right,
mikktspace: true,
normalize: true,
}
}
#[allow(clippy::too_many_arguments)]
#[allow(dead_code)]
pub fn compute_tangents(
positions: &[[f32; 3]],
normals: &[[f32; 3]],
uvs: &[[f32; 2]],
triangles: &[[u32; 3]],
cfg: &TangentExportConfig,
) -> TangentExportResult {
let vertex_count = positions.len();
let mut tangent_accum = vec![[0.0f32; 3]; vertex_count];
let mut bitangent_accum = vec![[0.0f32; 3]; vertex_count];
let mut degenerate_count = 0usize;
for tri in triangles {
let i0 = tri[0] as usize;
let i1 = tri[1] as usize;
let i2 = tri[2] as usize;
if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
degenerate_count += 1;
continue;
}
let p = [positions[i0], positions[i1], positions[i2]];
let uv = [uvs[i0], uvs[i1], uvs[i2]];
let (t, b) = tangent_for_triangle(&p, &uv);
if is_degenerate_tangent(t) {
degenerate_count += 1;
continue;
}
for vi in [i0, i1, i2] {
for k in 0..3 {
tangent_accum[vi][k] += t[k];
bitangent_accum[vi][k] += b[k];
}
}
}
let handedness_sign = match cfg.handedness {
TangentHandedness::Right => 1.0f32,
TangentHandedness::Left => -1.0f32,
};
let mut tangents = Vec::with_capacity(vertex_count);
let mut bitangents = Vec::with_capacity(vertex_count);
for vi in 0..vertex_count {
let t_raw = tangent_accum[vi];
let b_raw = bitangent_accum[vi];
let n = normals.get(vi).copied().unwrap_or([0.0, 1.0, 0.0]);
let t = if cfg.normalize {
normalize_v3_tan(t_raw)
} else {
t_raw
};
let b = if cfg.normalize {
normalize_v3_tan(b_raw)
} else {
b_raw
};
let packed = pack_tangent_w(t, b, n);
let w = packed[3] * handedness_sign;
tangents.push([t[0], t[1], t[2], w]);
bitangents.push(b);
}
let data = TangentData {
tangents,
bitangents,
vertex_count,
};
TangentExportResult {
success: true,
degenerate_count,
data,
}
}
#[allow(dead_code)]
pub fn tangent_for_triangle(p: &[[f32; 3]; 3], uv: &[[f32; 2]; 3]) -> ([f32; 3], [f32; 3]) {
let e1 = sub3(p[1], p[0]);
let e2 = sub3(p[2], p[0]);
let du1 = uv[1][0] - uv[0][0];
let dv1 = uv[1][1] - uv[0][1];
let du2 = uv[2][0] - uv[0][0];
let dv2 = uv[2][1] - uv[0][1];
let denom = du1 * dv2 - du2 * dv1;
if denom.abs() < 1e-12 {
return ([0.0; 3], [0.0; 3]);
}
let r = 1.0 / denom;
let tangent = [
(dv2 * e1[0] - dv1 * e2[0]) * r,
(dv2 * e1[1] - dv1 * e2[1]) * r,
(dv2 * e1[2] - dv1 * e2[2]) * r,
];
let bitangent = [
(du1 * e2[0] - du2 * e1[0]) * r,
(du1 * e2[1] - du2 * e1[1]) * r,
(du1 * e2[2] - du2 * e1[2]) * r,
];
(tangent, bitangent)
}
#[allow(dead_code)]
pub fn normalize_v3_tan(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; 3];
}
[v[0] / len, v[1] / len, v[2] / len]
}
#[allow(dead_code)]
pub fn tangent_export_to_json(r: &TangentExportResult) -> String {
format!(
r#"{{"vertex_count":{},"degenerate_count":{},"success":{}}}"#,
r.data.vertex_count, r.degenerate_count, r.success
)
}
#[allow(dead_code)]
pub fn is_degenerate_tangent(t: [f32; 3]) -> bool {
let len2 = t[0] * t[0] + t[1] * t[1] + t[2] * t[2];
len2 < 1e-12
}
#[allow(dead_code)]
pub fn handedness_name(cfg: &TangentExportConfig) -> &'static str {
match cfg.handedness {
TangentHandedness::Right => "Right",
TangentHandedness::Left => "Left",
}
}
#[allow(dead_code)]
pub fn tangent_data_vertex_count(data: &TangentData) -> usize {
data.vertex_count
}
#[allow(dead_code)]
pub fn pack_tangent_w(tangent: [f32; 3], bitangent: [f32; 3], normal: [f32; 3]) -> [f32; 4] {
let cross = cross3(normal, tangent);
let dot = cross[0] * bitangent[0] + cross[1] * bitangent[1] + cross[2] * bitangent[2];
let w = if dot < 0.0 { -1.0f32 } else { 1.0f32 };
[tangent[0], tangent[1], tangent[2], w]
}
#[allow(dead_code)]
pub fn validate_tangent_data(data: &TangentData) -> bool {
data.tangents.len() == data.vertex_count && data.bitangents.len() == data.vertex_count
}
fn sub3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn cross3(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],
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_is_right_handed_mikkt() {
let cfg = default_tangent_export_config();
assert_eq!(cfg.handedness, TangentHandedness::Right);
assert!(cfg.mikktspace);
assert!(cfg.normalize);
}
#[test]
fn handedness_name_right() {
let cfg = default_tangent_export_config();
assert_eq!(handedness_name(&cfg), "Right");
}
#[test]
fn handedness_name_left() {
let cfg = TangentExportConfig {
handedness: TangentHandedness::Left,
mikktspace: false,
normalize: true,
};
assert_eq!(handedness_name(&cfg), "Left");
}
#[test]
fn is_degenerate_tangent_zero_vector() {
assert!(is_degenerate_tangent([0.0, 0.0, 0.0]));
}
#[test]
fn is_degenerate_tangent_unit_vector() {
assert!(!is_degenerate_tangent([1.0, 0.0, 0.0]));
}
#[test]
fn normalize_v3_tan_unit_x() {
let v = normalize_v3_tan([3.0, 0.0, 0.0]);
assert!((v[0] - 1.0).abs() < 1e-6);
assert!(v[1].abs() < 1e-6);
assert!(v[2].abs() < 1e-6);
}
#[test]
fn normalize_v3_tan_zero_returns_zero() {
let v = normalize_v3_tan([0.0, 0.0, 0.0]);
assert_eq!(v, [0.0, 0.0, 0.0]);
}
#[test]
fn tangent_for_triangle_xy_plane() {
let p = [[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let uv = [[0.0f32, 0.0], [1.0, 0.0], [0.0, 1.0]];
let (t, _b) = tangent_for_triangle(&p, &uv);
assert!((t[0] - 1.0).abs() < 1e-5);
}
#[test]
fn compute_tangents_single_triangle() {
let positions = vec![
[0.0f32, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
];
let normals = vec![[0.0f32, 0.0, 1.0]; 3];
let uvs = vec![[0.0f32, 0.0], [1.0, 0.0], [0.0, 1.0]];
let triangles = vec![[0u32, 1, 2]];
let cfg = default_tangent_export_config();
let result = compute_tangents(&positions, &normals, &uvs, &triangles, &cfg);
assert!(result.success);
assert_eq!(result.data.vertex_count, 3);
assert_eq!(result.data.tangents.len(), 3);
assert_eq!(result.data.bitangents.len(), 3);
}
#[test]
fn validate_tangent_data_ok() {
let data = TangentData {
tangents: vec![[1.0, 0.0, 0.0, 1.0]; 4],
bitangents: vec![[0.0, 1.0, 0.0]; 4],
vertex_count: 4,
};
assert!(validate_tangent_data(&data));
}
#[test]
fn validate_tangent_data_mismatch() {
let data = TangentData {
tangents: vec![[1.0, 0.0, 0.0, 1.0]; 3],
bitangents: vec![[0.0, 1.0, 0.0]; 4],
vertex_count: 4,
};
assert!(!validate_tangent_data(&data));
}
#[test]
fn pack_tangent_w_right_handed() {
let t = [1.0f32, 0.0, 0.0];
let b = [0.0f32, 1.0, 0.0];
let n = [0.0f32, 0.0, 1.0];
let packed = pack_tangent_w(t, b, n);
assert!((packed[3] - 1.0).abs() < 1e-6);
}
#[test]
fn tangent_export_to_json_contains_vertex_count() {
let result = TangentExportResult {
data: TangentData {
tangents: vec![],
bitangents: vec![],
vertex_count: 99,
},
degenerate_count: 0,
success: true,
};
let json = tangent_export_to_json(&result);
assert!(json.contains("99"));
}
#[test]
fn tangent_data_vertex_count_accessor() {
let data = TangentData {
tangents: vec![],
bitangents: vec![],
vertex_count: 7,
};
assert_eq!(tangent_data_vertex_count(&data), 7);
}
}