Skip to main content

anvilkit_render/renderer/
ibl.rs

1//! # IBL (Image-Based Lighting) 工具
2//!
3//! 提供 BRDF 积分 LUT 的 CPU 生成。
4//! BRDF LUT 用于 PBR 渲染中的 split-sum 近似,
5//! 存储 (F0_scale, F0_bias) 以实现能量守恒的环境光镜面反射。
6
7use std::f32::consts::PI;
8
9/// 生成 BRDF 积分查找表 (LUT)
10///
11/// 对每个 (NdotV, roughness) 组合进行重要性采样,计算 GGX BRDF 的
12/// Fresnel 缩放因子和偏置因子。结果存储为 Rgba8Unorm 格式
13/// (R=scale, G=bias, B=0, A=255)。
14///
15/// # 参数
16///
17/// - `size`: LUT 的宽高(正方形,推荐 256)
18///
19/// # 返回
20///
21/// RGBA8 像素数据,`size * size * 4` 字节
22///
23/// # 示例
24///
25/// ```rust
26/// use anvilkit_render::renderer::ibl::generate_brdf_lut;
27///
28/// let data = generate_brdf_lut(64);
29/// assert_eq!(data.len(), 64 * 64 * 4);
30/// ```
31pub fn generate_brdf_lut(size: u32) -> Vec<u8> {
32    let sample_count = 1024u32;
33    let mut data = Vec::with_capacity((size * size * 4) as usize);
34
35    for y in 0..size {
36        for x in 0..size {
37            let n_dot_v = ((x as f32) + 0.5) / size as f32;
38            let roughness = ((y as f32) + 0.5) / size as f32;
39            let n_dot_v = n_dot_v.max(0.001);
40
41            let (scale, bias) = integrate_brdf(n_dot_v, roughness, sample_count);
42
43            data.push((scale.clamp(0.0, 1.0) * 255.0) as u8);
44            data.push((bias.clamp(0.0, 1.0) * 255.0) as u8);
45            data.push(0);
46            data.push(255);
47        }
48    }
49
50    data
51}
52
53/// 对单个 (NdotV, roughness) 点积分 BRDF
54fn integrate_brdf(n_dot_v: f32, roughness: f32, sample_count: u32) -> (f32, f32) {
55    let v = glam::Vec3::new((1.0 - n_dot_v * n_dot_v).sqrt(), 0.0, n_dot_v);
56    let n = glam::Vec3::Z;
57
58    let mut a = 0.0f32;
59    let mut b = 0.0f32;
60
61    for i in 0..sample_count {
62        let xi = hammersley(i, sample_count);
63        let h = importance_sample_ggx(xi, n, roughness);
64        let l = (2.0 * v.dot(h) * h - v).normalize();
65
66        let n_dot_l = l.z.max(0.0);
67        let n_dot_h = h.z.max(0.0);
68        let v_dot_h = v.dot(h).max(0.0);
69
70        if n_dot_l > 0.0 {
71            let g = geometry_smith_ibl(n_dot_v, n_dot_l, roughness);
72            let g_vis = (g * v_dot_h) / (n_dot_h * n_dot_v).max(0.0001);
73            let fc = (1.0 - v_dot_h).powf(5.0);
74
75            a += (1.0 - fc) * g_vis;
76            b += fc * g_vis;
77        }
78    }
79
80    let inv = 1.0 / sample_count as f32;
81    (a * inv, b * inv)
82}
83
84/// Hammersley 低差异序列
85fn hammersley(i: u32, n: u32) -> glam::Vec2 {
86    glam::Vec2::new(i as f32 / n as f32, radical_inverse_vdc(i))
87}
88
89/// Van der Corput 基数逆序列
90fn radical_inverse_vdc(mut bits: u32) -> f32 {
91    bits = (bits << 16) | (bits >> 16);
92    bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1);
93    bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2);
94    bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4);
95    bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8);
96    bits as f32 * 2.3283064365386963e-10 // 0x100000000
97}
98
99/// GGX 重要性采样
100fn importance_sample_ggx(xi: glam::Vec2, n: glam::Vec3, roughness: f32) -> glam::Vec3 {
101    let a = roughness * roughness;
102
103    let phi = 2.0 * PI * xi.x;
104    let cos_theta = ((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y)).sqrt();
105    let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
106
107    // 球面坐标 → 切线空间笛卡尔坐标
108    let h = glam::Vec3::new(phi.cos() * sin_theta, phi.sin() * sin_theta, cos_theta);
109
110    // 切线空间 → 世界空间
111    let up = if n.z.abs() < 0.999 {
112        glam::Vec3::Z
113    } else {
114        glam::Vec3::X
115    };
116    let tangent = up.cross(n).normalize();
117    let bitangent = n.cross(tangent);
118
119    (tangent * h.x + bitangent * h.y + n * h.z).normalize()
120}
121
122/// Smith GGX 几何函数 (IBL 版本,k = roughness² / 2)
123fn geometry_smith_ibl(n_dot_v: f32, n_dot_l: f32, roughness: f32) -> f32 {
124    let a = roughness;
125    let k = (a * a) / 2.0;
126
127    let ggx_v = n_dot_v / (n_dot_v * (1.0 - k) + k);
128    let ggx_l = n_dot_l / (n_dot_l * (1.0 - k) + k);
129
130    ggx_v * ggx_l
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_generate_brdf_lut_size() {
139        let data = generate_brdf_lut(32);
140        assert_eq!(data.len(), 32 * 32 * 4);
141    }
142
143    #[test]
144    fn test_brdf_lut_values_in_range() {
145        let data = generate_brdf_lut(16);
146        for chunk in data.chunks(4) {
147            // B is always 0, A is always 255
148            assert_eq!(chunk[2], 0);
149            assert_eq!(chunk[3], 255);
150        }
151    }
152
153    #[test]
154    fn test_hammersley_sequence() {
155        let h0 = hammersley(0, 16);
156        assert_eq!(h0.x, 0.0);
157
158        let h8 = hammersley(8, 16);
159        assert!((h8.x - 0.5).abs() < 0.001);
160    }
161
162    #[test]
163    fn test_radical_inverse() {
164        assert!((radical_inverse_vdc(0) - 0.0).abs() < 0.001);
165        assert!((radical_inverse_vdc(1) - 0.5).abs() < 0.001);
166    }
167
168    #[test]
169    fn test_brdf_lut_smooth_surface() {
170        // At roughness near 0 and NdotV near 1, scale should be high, bias low
171        let (scale, bias) = integrate_brdf(0.9, 0.05, 512);
172        assert!(scale > 0.5, "scale={}", scale);
173        assert!(bias < 0.2, "bias={}", bias);
174    }
175}