feat(renderer): add tangent to MeshVertex with computation in OBJ parser and sphere generator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 21:18:26 +09:00
parent 88fabf2905
commit 4d7ff5a122
5 changed files with 218 additions and 3 deletions

View File

@@ -0,0 +1,131 @@
/// Van der Corput sequence via bit-reversal.
pub fn radical_inverse_vdc(mut bits: u32) -> f32 {
bits = (bits << 16) | (bits >> 16);
bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1);
bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2);
bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4);
bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8);
bits as f32 * 2.328_306_4e-10 // / 0x100000000
}
/// Hammersley low-discrepancy 2D sample.
pub fn hammersley(i: u32, n: u32) -> [f32; 2] {
[i as f32 / n as f32, radical_inverse_vdc(i)]
}
/// GGX importance-sampled half vector in tangent space (N = (0,0,1)).
pub fn importance_sample_ggx(xi: [f32; 2], roughness: f32) -> [f32; 3] {
let a = roughness * roughness;
let phi = 2.0 * std::f32::consts::PI * xi[0];
let cos_theta = ((1.0 - xi[1]) / (1.0 + (a * a - 1.0) * xi[1])).sqrt();
let sin_theta = (1.0 - cos_theta * cos_theta).max(0.0).sqrt();
[phi.cos() * sin_theta, phi.sin() * sin_theta, cos_theta]
}
/// Smith geometry function for IBL: k = a²/2.
pub fn geometry_smith_ibl(n_dot_v: f32, n_dot_l: f32, roughness: f32) -> f32 {
let a = roughness * roughness;
let k = a / 2.0;
let ggx_v = n_dot_v / (n_dot_v * (1.0 - k) + k);
let ggx_l = n_dot_l / (n_dot_l * (1.0 - k) + k);
ggx_v * ggx_l
}
/// Monte Carlo integration of the split-sum BRDF for a given NdotV and roughness.
/// Returns (scale, bias) such that F_env ≈ F0 * scale + bias.
pub fn integrate_brdf(n_dot_v: f32, roughness: f32) -> (f32, f32) {
const NUM_SAMPLES: u32 = 1024;
// View vector in tangent space where N = (0,0,1).
let v = [
(1.0 - n_dot_v * n_dot_v).max(0.0).sqrt(),
0.0_f32,
n_dot_v,
];
let mut scale = 0.0_f32;
let mut bias = 0.0_f32;
for i in 0..NUM_SAMPLES {
let xi = hammersley(i, NUM_SAMPLES);
let h = importance_sample_ggx(xi, roughness);
// dot(V, H)
let v_dot_h = (v[0] * h[0] + v[1] * h[1] + v[2] * h[2]).max(0.0);
// Reflect V around H to get L.
let l = [
2.0 * v_dot_h * h[0] - v[0],
2.0 * v_dot_h * h[1] - v[1],
2.0 * v_dot_h * h[2] - v[2],
];
let n_dot_l = l[2].max(0.0); // L.z in tangent space
let n_dot_h = h[2].max(0.0);
if n_dot_l > 0.0 {
let g = geometry_smith_ibl(n_dot_v, n_dot_l, roughness);
let g_vis = g * v_dot_h / (n_dot_h * n_dot_v).max(0.001);
let fc = (1.0 - v_dot_h).powi(5);
scale += g_vis * (1.0 - fc);
bias += g_vis * fc;
}
}
(scale / NUM_SAMPLES as f32, bias / NUM_SAMPLES as f32)
}
/// Generate the BRDF LUT for the split-sum IBL approximation.
///
/// Returns `size * size` elements. Each element is `[scale, bias]` where
/// the x-axis (u) maps NdotV in [0, 1] and the y-axis (v) maps roughness in [0, 1].
pub fn generate_brdf_lut(size: u32) -> Vec<[f32; 2]> {
let mut lut = Vec::with_capacity((size * size) as usize);
for row in 0..size {
// v maps to roughness (row 0 → roughness near 0, last row → 1).
let roughness = ((row as f32 + 0.5) / size as f32).clamp(0.0, 1.0);
for col in 0..size {
// u maps to NdotV.
let n_dot_v = ((col as f32 + 0.5) / size as f32).clamp(0.0, 1.0);
let (scale, bias) = integrate_brdf(n_dot_v, roughness);
lut.push([scale, bias]);
}
}
lut
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_brdf_lut_dimensions() {
let size = 64u32;
let lut = generate_brdf_lut(size);
assert_eq!(lut.len(), (size * size) as usize);
}
#[test]
fn test_brdf_lut_values_in_range() {
let lut = generate_brdf_lut(64);
for pixel in &lut {
assert!(
pixel[0] >= 0.0 && pixel[0] <= 1.5,
"scale {} out of range",
pixel[0]
);
assert!(
pixel[1] >= 0.0 && pixel[1] <= 1.5,
"bias {} out of range",
pixel[1]
);
}
}
#[test]
fn test_hammersley() {
let n = 1024u32;
let sample = hammersley(0, n);
assert_eq!(sample[0], 0.0, "hammersley(0, N).x should be 0");
}
}

View File

@@ -118,6 +118,7 @@ pub fn parse_obj(source: &str) -> ObjData {
position,
normal,
uv,
tangent: [0.0; 4],
});
vertex_map.insert(key, new_idx);
new_idx
@@ -127,9 +128,76 @@ pub fn parse_obj(source: &str) -> ObjData {
}
}
compute_tangents(&mut vertices, &indices);
ObjData { vertices, indices }
}
pub fn compute_tangents(vertices: &mut [MeshVertex], indices: &[u32]) {
// Accumulate tangent per vertex from triangles
let mut tangents = vec![[0.0f32; 3]; vertices.len()];
let mut bitangents = vec![[0.0f32; 3]; vertices.len()];
for tri in indices.chunks(3) {
if tri.len() < 3 { continue; }
let i0 = tri[0] as usize;
let i1 = tri[1] as usize;
let i2 = tri[2] as usize;
let v0 = vertices[i0]; let v1 = vertices[i1]; let v2 = vertices[i2];
let edge1 = [v1.position[0]-v0.position[0], v1.position[1]-v0.position[1], v1.position[2]-v0.position[2]];
let edge2 = [v2.position[0]-v0.position[0], v2.position[1]-v0.position[1], v2.position[2]-v0.position[2]];
let duv1 = [v1.uv[0]-v0.uv[0], v1.uv[1]-v0.uv[1]];
let duv2 = [v2.uv[0]-v0.uv[0], v2.uv[1]-v0.uv[1]];
let det = duv1[0]*duv2[1] - duv2[0]*duv1[1];
if det.abs() < 1e-8 { continue; }
let f = 1.0 / det;
let t = [
f * (duv2[1]*edge1[0] - duv1[1]*edge2[0]),
f * (duv2[1]*edge1[1] - duv1[1]*edge2[1]),
f * (duv2[1]*edge1[2] - duv1[1]*edge2[2]),
];
let b = [
f * (-duv2[0]*edge1[0] + duv1[0]*edge2[0]),
f * (-duv2[0]*edge1[1] + duv1[0]*edge2[1]),
f * (-duv2[0]*edge1[2] + duv1[0]*edge2[2]),
];
for &idx in &[i0, i1, i2] {
tangents[idx] = [tangents[idx][0]+t[0], tangents[idx][1]+t[1], tangents[idx][2]+t[2]];
bitangents[idx] = [bitangents[idx][0]+b[0], bitangents[idx][1]+b[1], bitangents[idx][2]+b[2]];
}
}
// Orthogonalize and compute handedness
for (i, v) in vertices.iter_mut().enumerate() {
let n = v.normal;
let t = tangents[i];
// Gram-Schmidt orthogonalize: T' = normalize(T - N * dot(N, T))
let n_dot_t = n[0]*t[0] + n[1]*t[1] + n[2]*t[2];
let ortho = [t[0]-n[0]*n_dot_t, t[1]-n[1]*n_dot_t, t[2]-n[2]*n_dot_t];
let len = (ortho[0]*ortho[0] + ortho[1]*ortho[1] + ortho[2]*ortho[2]).sqrt();
if len > 1e-8 {
let normalized = [ortho[0]/len, ortho[1]/len, ortho[2]/len];
// Handedness: sign of dot(cross(N, T'), B)
let cross = [
n[1]*normalized[2] - n[2]*normalized[1],
n[2]*normalized[0] - n[0]*normalized[2],
n[0]*normalized[1] - n[1]*normalized[0],
];
let b = bitangents[i];
let dot_b = cross[0]*b[0] + cross[1]*b[1] + cross[2]*b[2];
let w = if dot_b < 0.0 { -1.0 } else { 1.0 };
v.tangent = [normalized[0], normalized[1], normalized[2], w];
} else {
v.tangent = [1.0, 0.0, 0.0, 1.0]; // fallback
}
}
}
/// Parse a face vertex token of the form "v", "v/vt", "v//vn", or "v/vt/vn".
/// Returns (v_idx, vt_idx, vn_idx) where 0 means absent.
fn parse_face_vertex(token: &str) -> (u32, u32, u32) {

View File

@@ -8,6 +8,7 @@ struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) uv: vec2<f32>,
@location(3) tangent: vec4<f32>,
};
@vertex

View File

@@ -29,7 +29,16 @@ pub fn generate_sphere(radius: f32, sectors: u32, stacks: u32) -> (Vec<MeshVerte
i as f32 / stacks as f32,
];
vertices.push(MeshVertex { position, normal, uv });
// Tangent follows the longitude direction (increasing sector angle) in XZ plane
let tangent_x = -sector_angle.sin();
let tangent_z = sector_angle.cos();
vertices.push(MeshVertex {
position,
normal,
uv,
tangent: [tangent_x, 0.0, tangent_z, 1.0],
});
}
}

View File

@@ -32,6 +32,7 @@ pub struct MeshVertex {
pub position: [f32; 3],
pub normal: [f32; 3],
pub uv: [f32; 2],
pub tangent: [f32; 4],
}
impl MeshVertex {
@@ -45,15 +46,20 @@ impl MeshVertex {
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
offset: 12,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: (std::mem::size_of::<[f32; 3]>() * 2) as wgpu::BufferAddress,
offset: 24,
shader_location: 2,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: 32,
shader_location: 3,
format: wgpu::VertexFormat::Float32x4,
},
],
};
}