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:
131
crates/voltex_renderer/src/brdf_lut.rs
Normal file
131
crates/voltex_renderer/src/brdf_lut.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user