From 4d7ff5a122adf94b2db6c247809ace77170e3c28 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Tue, 24 Mar 2026 21:18:26 +0900 Subject: [PATCH] feat(renderer): add tangent to MeshVertex with computation in OBJ parser and sphere generator Co-Authored-By: Claude Sonnet 4.6 --- crates/voltex_renderer/src/brdf_lut.rs | 131 ++++++++++++++++++ crates/voltex_renderer/src/obj.rs | 68 +++++++++ crates/voltex_renderer/src/shadow_shader.wgsl | 1 + crates/voltex_renderer/src/sphere.rs | 11 +- crates/voltex_renderer/src/vertex.rs | 10 +- 5 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 crates/voltex_renderer/src/brdf_lut.rs diff --git a/crates/voltex_renderer/src/brdf_lut.rs b/crates/voltex_renderer/src/brdf_lut.rs new file mode 100644 index 0000000..cb9db0c --- /dev/null +++ b/crates/voltex_renderer/src/brdf_lut.rs @@ -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"); + } +} diff --git a/crates/voltex_renderer/src/obj.rs b/crates/voltex_renderer/src/obj.rs index a0910e2..04fe3df 100644 --- a/crates/voltex_renderer/src/obj.rs +++ b/crates/voltex_renderer/src/obj.rs @@ -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) { diff --git a/crates/voltex_renderer/src/shadow_shader.wgsl b/crates/voltex_renderer/src/shadow_shader.wgsl index df2fcf1..8969ad9 100644 --- a/crates/voltex_renderer/src/shadow_shader.wgsl +++ b/crates/voltex_renderer/src/shadow_shader.wgsl @@ -8,6 +8,7 @@ struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, + @location(3) tangent: vec4, }; @vertex diff --git a/crates/voltex_renderer/src/sphere.rs b/crates/voltex_renderer/src/sphere.rs index fec002f..040bcee 100644 --- a/crates/voltex_renderer/src/sphere.rs +++ b/crates/voltex_renderer/src/sphere.rs @@ -29,7 +29,16 @@ pub fn generate_sphere(radius: f32, sectors: u32, stacks: u32) -> (Vec() 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, + }, ], }; }