Sample an outgoing direction from the BRDF at a surface point. Uses a simple metal/dielectric split: metals are pure specular, dielectrics pick diffuse vs specular by Fresnel-at-normal weight.
(surface: &SurfaceSample, view_world: Vec3, rng: &mut Rng)
| 1232 | /// Uses a simple metal/dielectric split: metals are pure specular, |
| 1233 | /// dielectrics pick diffuse vs specular by Fresnel-at-normal weight. |
| 1234 | fn sample_brdf(surface: &SurfaceSample, view_world: Vec3, rng: &mut Rng) -> BrdfSample { |
| 1235 | let n = surface.normal; |
| 1236 | let alpha = surface.roughness * surface.roughness; |
| 1237 | let (t, bt) = build_tbn(n); |
| 1238 | |
| 1239 | // View in the tangent frame (z-up). |
| 1240 | let v_tangent = Vec3::new(view_world.dot(t), view_world.dot(bt), view_world.dot(n)); |
| 1241 | |
| 1242 | // f0: dielectrics use 0.04; metals use the base color as f0. |
| 1243 | let f0 = Vec3::splat(0.04).lerp(surface.base_color, surface.metallic); |
| 1244 | |
| 1245 | // Decide diffuse vs specular lobe. Weighting by luminance of the |
| 1246 | // Fresnel-at-normal-incidence gives a reasonable importance |
| 1247 | // distribution without a second sample. Pure metals have ~zero |
| 1248 | // diffuse so this collapses naturally. |
| 1249 | let spec_weight = (f0.x + f0.y + f0.z) / 3.0; |
| 1250 | let diff_weight = (1.0 - spec_weight) * (1.0 - surface.metallic); |
| 1251 | let total = spec_weight + diff_weight + 1e-6; |
| 1252 | let p_spec = spec_weight / total; |
| 1253 | let pick_spec = rng.next_f32() < p_spec; |
| 1254 | |
| 1255 | let rand = rng.next_vec2(); |
| 1256 | |
| 1257 | if pick_spec { |
| 1258 | // Sample a microfacet normal via VNDF, then reflect the view |
| 1259 | // direction across it. |
| 1260 | let h_tangent = sample_ggx_vndf(v_tangent, alpha, rand); |
| 1261 | let l_tangent = reflect(-v_tangent, h_tangent); |
| 1262 | if l_tangent.z <= 0.0 { |
| 1263 | return BrdfSample { |
| 1264 | direction_world: Vec3::Z, |
| 1265 | throughput: Vec3::ZERO, |
| 1266 | terminated: true, |
| 1267 | }; |
| 1268 | } |
| 1269 | let h_world = t * h_tangent.x + bt * h_tangent.y + n * h_tangent.z; |
| 1270 | let l_world = t * l_tangent.x + bt * l_tangent.y + n * l_tangent.z; |
| 1271 | let n_dot_l = l_tangent.z; |
| 1272 | let n_dot_v = v_tangent.z.max(1e-4); |
| 1273 | let n_dot_h = h_tangent.z.max(1e-4); |
| 1274 | let v_dot_h = v_tangent.dot(h_tangent).max(1e-4); |
| 1275 | |
| 1276 | // VNDF sampling PDF: D * G1 * max(0, V·H) / N·V. |
| 1277 | // The full BRDF is F * D * V_smith, so the throughput reduces |
| 1278 | // to F * G2/G1 * (V·H / (N·V * N·H))... but with height- |
| 1279 | // correlated V_smith the clean form is: |
| 1280 | // throughput = F * G2_height_correlated / G1(V) |
| 1281 | // which we can write more stably as below. |
| 1282 | let f = fresnel_schlick(v_dot_h, f0); |
| 1283 | let g2 = v_smith(n_dot_v, n_dot_l, alpha) * 4.0 * n_dot_v * n_dot_l; |
| 1284 | // For the VNDF sampler the combined BRDF*cos/PDF simplifies |
| 1285 | // essentially to F * G2/G1. We approximate with the ratio of |
| 1286 | // the correlated V term to the monodir G1 — functionally |
| 1287 | // equivalent and numerically well-behaved. |
| 1288 | let g1_v = smith_g1(n_dot_v, alpha); |
| 1289 | let weight = if g1_v > 0.0 { |
| 1290 | f * g2 / (g1_v + 1e-6) |
| 1291 | } else { |
no test coverage detected