From 1cb0fe45fd47e5bc4ccca5fc4e19786bc0f21f5a Mon Sep 17 00:00:00 2001 From: Dylan Sechet Date: Tue, 5 May 2026 17:27:35 +0200 Subject: [PATCH 1/7] fix: use energy-conserving layering --- .../src/pathtracer/pathtracer.wgsl | 12 ++--- crates/bevy_solari/src/scene/brdf.wgsl | 49 +++++++++++-------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl index 9755dfeba8f03..e0ca519f1560a 100644 --- a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl +++ b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl @@ -5,7 +5,7 @@ enable wgpu_ray_query; #import bevy_pbr::utils::{rand_f, rand_vec2f} #import bevy_render::maths::{PI, orthonormalize} #import bevy_render::view::View -#import bevy_solari::brdf::{evaluate_brdf, evaluate_and_sample_brdf, fresnel} +#import bevy_solari::brdf::{evaluate_brdf, evaluate_and_sample_brdf, lobe_reflectances} #import bevy_solari::sampling::{sample_random_light, random_emissive_light_pdf, ggx_vndf_pdf, power_heuristic} #import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, ResolvedRayHitFull, RAY_T_MIN, RAY_T_MAX, MIRROR_ROUGHNESS_THRESHOLD} @@ -98,10 +98,9 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3) { fn brdf_pdf(wo: vec3, wi: vec3, ray_hit: ResolvedRayHitFull) -> f32 { let NdotV = max(dot(ray_hit.world_normal, wo), 0.0001); let F0 = calculate_F0(ray_hit.material.base_color, ray_hit.material.metallic, vec3(ray_hit.material.reflectance)); - let df = 1.0 - luminance(fresnel(F0, NdotV)); - - let diffuse_weight = mix(df, 0.0, ray_hit.material.metallic); - let specular_weight = 1.0 - diffuse_weight; + let rho = lobe_reflectances(F0, ray_hit.material, NdotV); + let specular_weight = luminance(rho.rho_spec) / luminance(rho.rho_spec + rho.rho_diff); + let diffuse_weight = 1 - specular_weight; let TBN = orthonormalize(ray_hit.world_normal); let T = TBN[0]; @@ -113,6 +112,5 @@ fn brdf_pdf(wo: vec3, wi: vec3, ray_hit: ResolvedRayHitFull) -> f32 { let diffuse_pdf = wi_tangent.z / PI; let specular_pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, ray_hit.material.roughness); - let pdf = (diffuse_weight * diffuse_pdf) + (specular_weight * specular_pdf); - return pdf; + return specular_weight * specular_pdf + diffuse_weight * diffuse_pdf; } diff --git a/crates/bevy_solari/src/scene/brdf.wgsl b/crates/bevy_solari/src/scene/brdf.wgsl index 67d8c470cf4c4..d369a0f98c16c 100644 --- a/crates/bevy_solari/src/scene/brdf.wgsl +++ b/crates/bevy_solari/src/scene/brdf.wgsl @@ -4,7 +4,7 @@ enable wgpu_ray_query; #import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance #import bevy_pbr::lighting::{D_GGX, V_SmithGGXCorrelated, specular_multiscatter} -#import bevy_pbr::pbr_functions::{calculate_diffuse_color, calculate_F0} +#import bevy_pbr::pbr_functions::calculate_F0 #import bevy_pbr::utils::{rand_f, sample_cosine_hemisphere} #import bevy_render::maths::{PI, orthonormalize} #import bevy_solari::sampling::{sample_ggx_vndf, ggx_vndf_pdf, ggx_vndf_sample_invalid} @@ -16,6 +16,22 @@ struct EvaluateAndSampleBrdfResult { pdf: f32, } +struct LobeReflectances { + rho_spec: vec3, + rho_diff: vec3, +} + +// Hemispherical reflectance of each lobe +fn lobe_reflectances(F0: vec3, material: ResolvedMaterial, NdotV: f32) -> LobeReflectances { + let F_ab = F_AB(material.perceptual_roughness, NdotV); + let ms_factor = 1.0 / (F_ab.x + F_ab.y) - 1.0; + let rho_spec = (F0 * F_ab.x + vec3(F_ab.y)) * (vec3(1.0) + F0 * ms_factor); + return LobeReflectances( + rho_spec, + (1.0 - material.metallic) * (vec3(1.0) - rho_spec) * material.base_color, + ); +} + fn evaluate_and_sample_brdf( wo: vec3, world_normal: vec3, @@ -25,10 +41,9 @@ fn evaluate_and_sample_brdf( let NdotV = dot(world_normal, wo); if NdotV < 0.0001 { return EvaluateAndSampleBrdfResult(vec3(0.0), vec3(0.0), 0.0); } let F0 = calculate_F0(material.base_color, material.metallic, vec3(material.reflectance)); - let df = 1.0 - luminance(fresnel(F0, NdotV)); - - let diffuse_weight = mix(df, 0.0, material.metallic); - let specular_weight = 1.0 - diffuse_weight; + let rho = lobe_reflectances(F0, material, NdotV); + let specular_weight = luminance(rho.rho_spec) / luminance(rho.rho_spec + rho.rho_diff); + let diffuse_weight = 1 - specular_weight; let TBN = orthonormalize(world_normal); let T = TBN[0]; @@ -49,19 +64,17 @@ fn evaluate_and_sample_brdf( return EvaluateAndSampleBrdfResult(vec3(0.0), vec3(0.0), 0.0); } wi = wi_tangent.x * T + wi_tangent.y * B + wi_tangent.z * N; + + // Mirror specular is a delta function + if material.roughness <= MIRROR_ROUGHNESS_THRESHOLD { + return EvaluateAndSampleBrdfResult(wi, rho.rho_spec / specular_weight, 1.0); + } } let diffuse_pdf = wi_tangent.z / PI; let specular_pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, material.roughness); - let pdf = (diffuse_weight * diffuse_pdf) + (specular_weight * specular_pdf); - - var throughput = evaluate_brdf(wo, wi, world_normal, material); - if diffuse_selected || material.roughness > MIRROR_ROUGHNESS_THRESHOLD { - throughput /= pdf; - } else { - throughput /= specular_weight; - } - + let pdf = specular_weight * specular_pdf + diffuse_weight * diffuse_pdf; + let throughput = evaluate_brdf(wo, wi, world_normal, material) / pdf; return EvaluateAndSampleBrdfResult(wi, throughput, pdf); } @@ -75,15 +88,12 @@ fn evaluate_brdf( } fn evaluate_diffuse_brdf(wo: vec3, wi: vec3, world_normal: vec3, material: ResolvedMaterial) -> vec3 { - let diffuse_color = calculate_diffuse_color(material.base_color, material.metallic, 0.0, 0.0) / PI; - let NdotL = dot(world_normal, wi); let NdotV = dot(world_normal, wo); if NdotL < 0.0001 || NdotV < 0.0001 { return vec3(0.0); } let F0 = calculate_F0(material.base_color, material.metallic, vec3(material.reflectance)); - let layering = (1.0 - fresnel(F0, NdotL)) * (1.0 - fresnel(F0, NdotV)); - - return diffuse_color * layering * NdotL; + let rho = lobe_reflectances(F0, material, NdotV); + return rho.rho_diff / PI * NdotL; } fn evaluate_specular_brdf(wo: vec3, wi: vec3, world_normal: vec3, material: ResolvedMaterial) -> vec3 { @@ -93,7 +103,6 @@ fn evaluate_specular_brdf(wo: vec3, wi: vec3, world_normal: vec3, let LdotH = dot(wi, H); let NdotV = dot(world_normal, wo); if NdotL < 0.0001 || NdotH < 0.0001 || LdotH < 0.0001 || NdotV < 0.0001 { return vec3(0.0); } - let F0 = calculate_F0(material.base_color, material.metallic, vec3(material.reflectance)); let F = fresnel(F0, LdotH); From 53c141660f2e246ee61bfc33295d75ba03da2328 Mon Sep 17 00:00:00 2001 From: Dylan Sechet Date: Wed, 13 May 2026 10:57:29 +0200 Subject: [PATCH 2/7] refactor: address PR comments --- .../src/pathtracer/pathtracer.wgsl | 23 +---------- crates/bevy_solari/src/scene/brdf.wgsl | 38 +++++++++++++++---- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl index e0ca519f1560a..edf9f958c093a 100644 --- a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl +++ b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl @@ -5,7 +5,7 @@ enable wgpu_ray_query; #import bevy_pbr::utils::{rand_f, rand_vec2f} #import bevy_render::maths::{PI, orthonormalize} #import bevy_render::view::View -#import bevy_solari::brdf::{evaluate_brdf, evaluate_and_sample_brdf, lobe_reflectances} +#import bevy_solari::brdf::{evaluate_brdf, evaluate_and_sample_brdf, brdf_pdf, lobe_reflectances} #import bevy_solari::sampling::{sample_random_light, random_emissive_light_pdf, ggx_vndf_pdf, power_heuristic} #import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, ResolvedRayHitFull, RAY_T_MIN, RAY_T_MAX, MIRROR_ROUGHNESS_THRESHOLD} @@ -62,7 +62,7 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3) { mis_weight = 1.0; if direct_lighting.brdf_rays_can_hit { - let pdf_of_bounce = brdf_pdf(wo, direct_lighting.wi, ray_hit); + let pdf_of_bounce = brdf_pdf(wo, direct_lighting.wi, ray_hit.world_normal, ray_hit.material); mis_weight = power_heuristic(1.0 / direct_lighting.inverse_pdf, pdf_of_bounce); } @@ -95,22 +95,3 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3) { textureStore(view_output, global_id.xy, vec4(new_color, 1.0)); } -fn brdf_pdf(wo: vec3, wi: vec3, ray_hit: ResolvedRayHitFull) -> f32 { - let NdotV = max(dot(ray_hit.world_normal, wo), 0.0001); - let F0 = calculate_F0(ray_hit.material.base_color, ray_hit.material.metallic, vec3(ray_hit.material.reflectance)); - let rho = lobe_reflectances(F0, ray_hit.material, NdotV); - let specular_weight = luminance(rho.rho_spec) / luminance(rho.rho_spec + rho.rho_diff); - let diffuse_weight = 1 - specular_weight; - - let TBN = orthonormalize(ray_hit.world_normal); - let T = TBN[0]; - let B = TBN[1]; - let N = TBN[2]; - - let wo_tangent = vec3(dot(wo, T), dot(wo, B), dot(wo, N)); - let wi_tangent = vec3(dot(wi, T), dot(wi, B), dot(wi, N)); - - let diffuse_pdf = wi_tangent.z / PI; - let specular_pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, ray_hit.material.roughness); - return specular_weight * specular_pdf + diffuse_weight * diffuse_pdf; -} diff --git a/crates/bevy_solari/src/scene/brdf.wgsl b/crates/bevy_solari/src/scene/brdf.wgsl index d369a0f98c16c..d6f8d2e48b66a 100644 --- a/crates/bevy_solari/src/scene/brdf.wgsl +++ b/crates/bevy_solari/src/scene/brdf.wgsl @@ -17,18 +17,18 @@ struct EvaluateAndSampleBrdfResult { } struct LobeReflectances { - rho_spec: vec3, - rho_diff: vec3, + specular: vec3, + diffuse: vec3, } // Hemispherical reflectance of each lobe fn lobe_reflectances(F0: vec3, material: ResolvedMaterial, NdotV: f32) -> LobeReflectances { let F_ab = F_AB(material.perceptual_roughness, NdotV); - let ms_factor = 1.0 / (F_ab.x + F_ab.y) - 1.0; - let rho_spec = (F0 * F_ab.x + vec3(F_ab.y)) * (vec3(1.0) + F0 * ms_factor); + let multiscattering_factor = 1.0 / (F_ab.x + F_ab.y) - 1.0; + let rho_spec = (F0 * F_ab.x + F_ab.y) * (1.0 + F0 * multiscattering_factor); return LobeReflectances( rho_spec, - (1.0 - material.metallic) * (vec3(1.0) - rho_spec) * material.base_color, + (1.0 - material.metallic) * (1.0 - rho_spec) * material.base_color, ); } @@ -42,8 +42,8 @@ fn evaluate_and_sample_brdf( if NdotV < 0.0001 { return EvaluateAndSampleBrdfResult(vec3(0.0), vec3(0.0), 0.0); } let F0 = calculate_F0(material.base_color, material.metallic, vec3(material.reflectance)); let rho = lobe_reflectances(F0, material, NdotV); - let specular_weight = luminance(rho.rho_spec) / luminance(rho.rho_spec + rho.rho_diff); - let diffuse_weight = 1 - specular_weight; + let specular_weight = luminance(rho.specular) / luminance(rho.specular + rho.diffuse); + let diffuse_weight = 1.0 - specular_weight; let TBN = orthonormalize(world_normal); let T = TBN[0]; @@ -93,7 +93,7 @@ fn evaluate_diffuse_brdf(wo: vec3, wi: vec3, world_normal: vec3, if NdotL < 0.0001 || NdotV < 0.0001 { return vec3(0.0); } let F0 = calculate_F0(material.base_color, material.metallic, vec3(material.reflectance)); let rho = lobe_reflectances(F0, material, NdotV); - return rho.rho_diff / PI * NdotL; + return rho.diffuse / PI * NdotL; } fn evaluate_specular_brdf(wo: vec3, wi: vec3, world_normal: vec3, material: ResolvedMaterial) -> vec3 { @@ -103,6 +103,7 @@ fn evaluate_specular_brdf(wo: vec3, wi: vec3, world_normal: vec3, let LdotH = dot(wi, H); let NdotV = dot(world_normal, wo); if NdotL < 0.0001 || NdotH < 0.0001 || LdotH < 0.0001 || NdotV < 0.0001 { return vec3(0.0); } + let F0 = calculate_F0(material.base_color, material.metallic, vec3(material.reflectance)); let F = fresnel(F0, LdotH); @@ -120,6 +121,27 @@ fn evaluate_specular_brdf(wo: vec3, wi: vec3, world_normal: vec3, return specular_multiscatter(D, Vs, F, F0, F_ab, 1.0) * NdotL; } +fn brdf_pdf(wo: vec3, wi: vec3, world_normal: vec3, material: ResolvedMaterial) -> f32 { + let NdotV = max(dot(world_normal, wo), 0.0001); + let F0 = calculate_F0(material.base_color, material.metallic, vec3(material.reflectance)); + let rho = lobe_reflectances(F0, material, NdotV); + let specular_weight = luminance(rho.specular) / luminance(rho.specular + rho.diffuse); + let diffuse_weight = 1.0 - specular_weight; + + let TBN = orthonormalize(world_normal); + let T = TBN[0]; + let B = TBN[1]; + let N = TBN[2]; + + let wo_tangent = vec3(dot(wo, T), dot(wo, B), dot(wo, N)); + let wi_tangent = vec3(dot(wi, T), dot(wi, B), dot(wi, N)); + + let diffuse_pdf = wi_tangent.z / PI; + let specular_pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, material.roughness); + return specular_weight * specular_pdf + diffuse_weight * diffuse_pdf; +} + + fn fresnel(f0: vec3, LdotH: f32) -> vec3 { return f0 + (1.0 - f0) * pow(1.0 - LdotH, 5.0); } From 6afa995969939c556d29e5d1b21e51c4aaaa385a Mon Sep 17 00:00:00 2001 From: Dylan Sechet Date: Wed, 13 May 2026 11:17:33 +0200 Subject: [PATCH 3/7] fix: give mirrors an infinite pdf --- crates/bevy_solari/src/scene/brdf.wgsl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/bevy_solari/src/scene/brdf.wgsl b/crates/bevy_solari/src/scene/brdf.wgsl index d6f8d2e48b66a..30a9e430df20d 100644 --- a/crates/bevy_solari/src/scene/brdf.wgsl +++ b/crates/bevy_solari/src/scene/brdf.wgsl @@ -67,7 +67,11 @@ fn evaluate_and_sample_brdf( // Mirror specular is a delta function if material.roughness <= MIRROR_ROUGHNESS_THRESHOLD { - return EvaluateAndSampleBrdfResult(wi, rho.rho_spec / specular_weight, 1.0); + return EvaluateAndSampleBrdfResult( + wi, + rho.specular / specular_weight, + bitcast(0x7F800000u) // INF + ); } } From 33c85dbbed85f6878497cfe9f0c0dc109a4db404 Mon Sep 17 00:00:00 2001 From: Dylan Sechet Date: Thu, 14 May 2026 00:46:52 +0200 Subject: [PATCH 4/7] fix: use evaluate_specular_brdf in mirror case --- crates/bevy_solari/src/scene/brdf.wgsl | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/bevy_solari/src/scene/brdf.wgsl b/crates/bevy_solari/src/scene/brdf.wgsl index 30a9e430df20d..e7ae054bc2f70 100644 --- a/crates/bevy_solari/src/scene/brdf.wgsl +++ b/crates/bevy_solari/src/scene/brdf.wgsl @@ -25,10 +25,10 @@ struct LobeReflectances { fn lobe_reflectances(F0: vec3, material: ResolvedMaterial, NdotV: f32) -> LobeReflectances { let F_ab = F_AB(material.perceptual_roughness, NdotV); let multiscattering_factor = 1.0 / (F_ab.x + F_ab.y) - 1.0; - let rho_spec = (F0 * F_ab.x + F_ab.y) * (1.0 + F0 * multiscattering_factor); + let rho_specular = (F0 * F_ab.x + F_ab.y) * (1.0 + F0 * multiscattering_factor); return LobeReflectances( - rho_spec, - (1.0 - material.metallic) * (1.0 - rho_spec) * material.base_color, + rho_specular, + (1.0 - material.metallic) * (1.0 - rho_specular) * material.base_color, ); } @@ -69,15 +69,15 @@ fn evaluate_and_sample_brdf( if material.roughness <= MIRROR_ROUGHNESS_THRESHOLD { return EvaluateAndSampleBrdfResult( wi, - rho.specular / specular_weight, + evaluate_specular_brdf(wo, wi, world_normal, material) / specular_weight, bitcast(0x7F800000u) // INF - ); + ); } } let diffuse_pdf = wi_tangent.z / PI; let specular_pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, material.roughness); - let pdf = specular_weight * specular_pdf + diffuse_weight * diffuse_pdf; + let pdf = (diffuse_weight * diffuse_pdf) + (specular_weight * specular_pdf); let throughput = evaluate_brdf(wo, wi, world_normal, material) / pdf; return EvaluateAndSampleBrdfResult(wi, throughput, pdf); } @@ -145,7 +145,6 @@ fn brdf_pdf(wo: vec3, wi: vec3, world_normal: vec3, material: Res return specular_weight * specular_pdf + diffuse_weight * diffuse_pdf; } - fn fresnel(f0: vec3, LdotH: f32) -> vec3 { return f0 + (1.0 - f0) * pow(1.0 - LdotH, 5.0); } From f8770120638d07eec81187e51636ee0336a26fbc Mon Sep 17 00:00:00 2001 From: Dylan Sechet Date: Thu, 14 May 2026 01:23:07 +0200 Subject: [PATCH 5/7] chore: remove extra space --- crates/bevy_solari/src/scene/brdf.wgsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_solari/src/scene/brdf.wgsl b/crates/bevy_solari/src/scene/brdf.wgsl index e7ae054bc2f70..f079e9ef781ed 100644 --- a/crates/bevy_solari/src/scene/brdf.wgsl +++ b/crates/bevy_solari/src/scene/brdf.wgsl @@ -107,7 +107,7 @@ fn evaluate_specular_brdf(wo: vec3, wi: vec3, world_normal: vec3, let LdotH = dot(wi, H); let NdotV = dot(world_normal, wo); if NdotL < 0.0001 || NdotH < 0.0001 || LdotH < 0.0001 || NdotV < 0.0001 { return vec3(0.0); } - + let F0 = calculate_F0(material.base_color, material.metallic, vec3(material.reflectance)); let F = fresnel(F0, LdotH); From 4c523b3bb716364f8d37c297fda7a57f985b0461 Mon Sep 17 00:00:00 2001 From: Dylan Sechet Date: Fri, 15 May 2026 22:17:04 +0200 Subject: [PATCH 6/7] fix: remove unused import --- crates/bevy_solari/src/pathtracer/pathtracer.wgsl | 2 +- crates/bevy_solari/src/scene/brdf.wgsl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl index edf9f958c093a..2b3c7ac64bb30 100644 --- a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl +++ b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl @@ -5,7 +5,7 @@ enable wgpu_ray_query; #import bevy_pbr::utils::{rand_f, rand_vec2f} #import bevy_render::maths::{PI, orthonormalize} #import bevy_render::view::View -#import bevy_solari::brdf::{evaluate_brdf, evaluate_and_sample_brdf, brdf_pdf, lobe_reflectances} +#import bevy_solari::brdf::{evaluate_brdf, evaluate_and_sample_brdf, brdf_pdf} #import bevy_solari::sampling::{sample_random_light, random_emissive_light_pdf, ggx_vndf_pdf, power_heuristic} #import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, ResolvedRayHitFull, RAY_T_MIN, RAY_T_MAX, MIRROR_ROUGHNESS_THRESHOLD} diff --git a/crates/bevy_solari/src/scene/brdf.wgsl b/crates/bevy_solari/src/scene/brdf.wgsl index f079e9ef781ed..145b727865463 100644 --- a/crates/bevy_solari/src/scene/brdf.wgsl +++ b/crates/bevy_solari/src/scene/brdf.wgsl @@ -142,7 +142,7 @@ fn brdf_pdf(wo: vec3, wi: vec3, world_normal: vec3, material: Res let diffuse_pdf = wi_tangent.z / PI; let specular_pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, material.roughness); - return specular_weight * specular_pdf + diffuse_weight * diffuse_pdf; + return (diffuse_weight * diffuse_pdf) + (specular_weight * specular_pdf); } fn fresnel(f0: vec3, LdotH: f32) -> vec3 { From 5f63ce7db50f7ab7f57ab022dc14e03f47d2117e Mon Sep 17 00:00:00 2001 From: Dylan Sechet Date: Sat, 16 May 2026 00:18:34 +0200 Subject: [PATCH 7/7] perf: avoid excess F_ab sampling --- .../src/pathtracer/pathtracer.wgsl | 10 ++++---- .../bevy_solari/src/realtime/restir_di.wgsl | 8 ++++--- .../bevy_solari/src/realtime/restir_gi.wgsl | 6 +++-- .../bevy_solari/src/realtime/specular_gi.wgsl | 12 ++++++---- crates/bevy_solari/src/scene/brdf.wgsl | 24 +++++++++---------- 5 files changed, 35 insertions(+), 25 deletions(-) diff --git a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl index 2b3c7ac64bb30..0054c6764392c 100644 --- a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl +++ b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl @@ -5,7 +5,7 @@ enable wgpu_ray_query; #import bevy_pbr::utils::{rand_f, rand_vec2f} #import bevy_render::maths::{PI, orthonormalize} #import bevy_render::view::View -#import bevy_solari::brdf::{evaluate_brdf, evaluate_and_sample_brdf, brdf_pdf} +#import bevy_solari::brdf::{evaluate_brdf, evaluate_and_sample_brdf, brdf_pdf, F_AB} #import bevy_solari::sampling::{sample_random_light, random_emissive_light_pdf, ggx_vndf_pdf, power_heuristic} #import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, ResolvedRayHitFull, RAY_T_MIN, RAY_T_MAX, MIRROR_ROUGHNESS_THRESHOLD} @@ -45,6 +45,8 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3) { if ray.kind != RAY_QUERY_INTERSECTION_NONE { let ray_hit = resolve_ray_hit_full(ray); let wo = -ray_direction; + let NdotV = max(dot(ray_hit.world_normal, wo), 0.0001); + let F_ab = F_AB(ray_hit.material.perceptual_roughness, NdotV); // Emissive contribution var mis_weight = 1.0; @@ -62,16 +64,16 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3) { mis_weight = 1.0; if direct_lighting.brdf_rays_can_hit { - let pdf_of_bounce = brdf_pdf(wo, direct_lighting.wi, ray_hit.world_normal, ray_hit.material); + let pdf_of_bounce = brdf_pdf(wo, direct_lighting.wi, ray_hit.world_normal, ray_hit.material, F_ab); mis_weight = power_heuristic(1.0 / direct_lighting.inverse_pdf, pdf_of_bounce); } - let direct_lighting_brdf = evaluate_brdf(wo, direct_lighting.wi, ray_hit.world_normal, ray_hit.material); + let direct_lighting_brdf = evaluate_brdf(wo, direct_lighting.wi, ray_hit.world_normal, ray_hit.material, F_ab); radiance += mis_weight * throughput * direct_lighting.radiance * direct_lighting.inverse_pdf * direct_lighting_brdf; } // Sample new ray direction from the material BRDF for next bounce and apply BRDF - let next_bounce = evaluate_and_sample_brdf(wo, ray_hit.world_normal, ray_hit.material, &rng); + let next_bounce = evaluate_and_sample_brdf(wo, ray_hit.world_normal, ray_hit.material, F_ab, &rng); if next_bounce.pdf == 0.0 { break; } ray_direction = next_bounce.wi; ray_origin = ray_hit.world_position + (ray_hit.geometric_world_normal * RAY_T_MIN); diff --git a/crates/bevy_solari/src/realtime/restir_di.wgsl b/crates/bevy_solari/src/realtime/restir_di.wgsl index ab3f18b9fe1fe..1d4ed9a2a9ae0 100644 --- a/crates/bevy_solari/src/realtime/restir_di.wgsl +++ b/crates/bevy_solari/src/realtime/restir_di.wgsl @@ -8,7 +8,7 @@ enable wgpu_ray_query; #import bevy_pbr::utils::{rand_f, rand_range_u, sample_disk} #import bevy_render::maths::PI #import bevy_render::view::View -#import bevy_solari::brdf::{evaluate_diffuse_brdf, evaluate_specular_brdf} +#import bevy_solari::brdf::{evaluate_diffuse_brdf, evaluate_specular_brdf, F_AB} #import bevy_solari::gbuffer_utils::{gpixel_resolve, pixel_dissimilar, permute_pixel} #import bevy_solari::presample_light_tiles::unpack_resolved_light_sample #import bevy_solari::sampling::{LightSample, ResolvedLightSample, NULL_LIGHT_ID, calculate_resolved_light_contribution, resolve_and_calculate_light_contribution, resolve_light_sample, trace_light_visibility, balance_heuristic} @@ -79,10 +79,12 @@ fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3) { #endif let wo = normalize(view.world_position - surface.world_position); - var brdf = evaluate_diffuse_brdf(wo, merge_result.wi, surface.world_normal, surface.material); + let NdotV = max(dot(surface.world_normal, wo), 0.0001); + let F_ab = F_AB(surface.material.perceptual_roughness, NdotV); + var brdf = evaluate_diffuse_brdf(wo, merge_result.wi, surface.world_normal, surface.material, F_ab); // Only consider the specular lobe if the surface is not smooth, else leave it for the specular GI pass to handle if surface.material.roughness > SPECULAR_GI_FOR_DI_ROUGHNESS_THRESHOLD { - brdf += evaluate_specular_brdf(wo, merge_result.wi, surface.world_normal, surface.material); + brdf += evaluate_specular_brdf(wo, merge_result.wi, surface.world_normal, surface.material, F_ab); } var pixel_color = merge_result.selected_sample_radiance * combined_reservoir.unbiased_contribution_weight; diff --git a/crates/bevy_solari/src/realtime/restir_gi.wgsl b/crates/bevy_solari/src/realtime/restir_gi.wgsl index 254d0ec257829..19fd75efa5079 100644 --- a/crates/bevy_solari/src/realtime/restir_gi.wgsl +++ b/crates/bevy_solari/src/realtime/restir_gi.wgsl @@ -6,7 +6,7 @@ enable wgpu_ray_query; #import bevy_pbr::utils::{rand_f, sample_uniform_hemisphere, uniform_hemisphere_inverse_pdf, sample_disk} #import bevy_render::maths::PI #import bevy_render::view::View -#import bevy_solari::brdf::evaluate_diffuse_brdf +#import bevy_solari::brdf::{evaluate_diffuse_brdf, F_AB} #import bevy_solari::gbuffer_utils::{gpixel_resolve, pixel_dissimilar, permute_pixel} #import bevy_solari::sampling::{sample_random_light, trace_point_visibility, balance_heuristic, isnan} #import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN, RAY_T_MAX} @@ -80,7 +80,9 @@ fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3) { #endif let wo = normalize(view.world_position - surface.world_position); - let brdf = evaluate_diffuse_brdf(wo, merge_result.wi, surface.world_normal, surface.material); + let NdotV = max(dot(surface.world_normal, wo), 0.0001); + let F_ab = F_AB(surface.material.perceptual_roughness, NdotV); + let brdf = evaluate_diffuse_brdf(wo, merge_result.wi, surface.world_normal, surface.material, F_ab); var pixel_color = textureLoad(view_output, global_id.xy); pixel_color += vec4(merge_result.selected_sample_radiance * combined_reservoir.unbiased_contribution_weight * view.exposure * brdf, 0.0); diff --git a/crates/bevy_solari/src/realtime/specular_gi.wgsl b/crates/bevy_solari/src/realtime/specular_gi.wgsl index 3033b8e50e166..205aedcdaaf8f 100644 --- a/crates/bevy_solari/src/realtime/specular_gi.wgsl +++ b/crates/bevy_solari/src/realtime/specular_gi.wgsl @@ -7,7 +7,7 @@ enable wgpu_ray_query; #import bevy_pbr::utils::rand_f #import bevy_render::maths::{orthonormalize, PI} #import bevy_render::view::View -#import bevy_solari::brdf::{evaluate_brdf, evaluate_specular_brdf} +#import bevy_solari::brdf::{evaluate_brdf, evaluate_specular_brdf, F_AB} #import bevy_solari::gbuffer_utils::{gpixel_resolve, ResolvedGPixel} #import bevy_solari::sampling::{sample_random_light, random_emissive_light_pdf, sample_ggx_vndf, ggx_vndf_pdf, ggx_vndf_sample_invalid, power_heuristic} #import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, ResolvedRayHitFull, RAY_T_MIN, RAY_T_MAX, MIRROR_ROUGHNESS_THRESHOLD} @@ -67,7 +67,9 @@ fn specular_gi(@builtin(global_invocation_id) global_id: vec3) { } } - let brdf = evaluate_specular_brdf(wo, wi, surface.world_normal, surface.material); + let NdotV = max(dot(surface.world_normal, wo), 0.0001); + let F_ab = F_AB(surface.material.perceptual_roughness, NdotV); + let brdf = evaluate_specular_brdf(wo, wi, surface.world_normal, surface.material, F_ab); radiance *= brdf * view.exposure; var pixel_color = textureLoad(view_output, global_id.xy); @@ -107,6 +109,8 @@ fn trace_glossy_path(pixel_id: vec2, primary_surface: ResolvedGPixel, initi let wo = -wi; let wo_tangent = vec3(dot(wo, T), dot(wo, B), dot(wo, N)); + let NdotV = max(dot(ray_hit.world_normal, wo), 0.0001); + let F_ab = F_AB(ray_hit.material.perceptual_roughness, NdotV); // Add emissive contribution let mis_weight = emissive_mis_weight(i, primary_surface.material.roughness, p_bounce, ray_hit); @@ -140,7 +144,7 @@ fn trace_glossy_path(pixel_id: vec2, primary_surface: ResolvedGPixel, initi } else if !surface_perfect_mirror { // Sample direct lighting (NEE) let direct_lighting = sample_random_light(ray_hit.world_position, ray_hit.world_normal, rng); - let direct_lighting_brdf = evaluate_brdf(wo, direct_lighting.wi, ray_hit.world_normal, ray_hit.material); + let direct_lighting_brdf = evaluate_brdf(wo, direct_lighting.wi, ray_hit.world_normal, ray_hit.material, F_ab); let mis_weight = nee_mis_weight(direct_lighting.inverse_pdf, direct_lighting.brdf_rays_can_hit, wo_tangent, direct_lighting.wi, ray_hit, TBN); radiance += throughput * mis_weight * direct_lighting.radiance * direct_lighting.inverse_pdf * direct_lighting_brdf; } @@ -153,7 +157,7 @@ fn trace_glossy_path(pixel_id: vec2, primary_surface: ResolvedGPixel, initi // Update throughput for next bounce p_bounce = ggx_vndf_pdf(wo_tangent, wi_tangent, ray_hit.material.roughness); - throughput *= evaluate_brdf(wo, wi, N, ray_hit.material); + throughput *= evaluate_brdf(wo, wi, N, ray_hit.material, F_ab); if ray_hit.material.roughness > MIRROR_ROUGHNESS_THRESHOLD { throughput /= p_bounce; } diff --git a/crates/bevy_solari/src/scene/brdf.wgsl b/crates/bevy_solari/src/scene/brdf.wgsl index 145b727865463..ef44d97b32191 100644 --- a/crates/bevy_solari/src/scene/brdf.wgsl +++ b/crates/bevy_solari/src/scene/brdf.wgsl @@ -22,8 +22,7 @@ struct LobeReflectances { } // Hemispherical reflectance of each lobe -fn lobe_reflectances(F0: vec3, material: ResolvedMaterial, NdotV: f32) -> LobeReflectances { - let F_ab = F_AB(material.perceptual_roughness, NdotV); +fn lobe_reflectances(F0: vec3, material: ResolvedMaterial, F_ab: vec2) -> LobeReflectances { let multiscattering_factor = 1.0 / (F_ab.x + F_ab.y) - 1.0; let rho_specular = (F0 * F_ab.x + F_ab.y) * (1.0 + F0 * multiscattering_factor); return LobeReflectances( @@ -36,12 +35,13 @@ fn evaluate_and_sample_brdf( wo: vec3, world_normal: vec3, material: ResolvedMaterial, + F_ab: vec2, rng: ptr, ) -> EvaluateAndSampleBrdfResult { let NdotV = dot(world_normal, wo); if NdotV < 0.0001 { return EvaluateAndSampleBrdfResult(vec3(0.0), vec3(0.0), 0.0); } let F0 = calculate_F0(material.base_color, material.metallic, vec3(material.reflectance)); - let rho = lobe_reflectances(F0, material, NdotV); + let rho = lobe_reflectances(F0, material, F_ab); let specular_weight = luminance(rho.specular) / luminance(rho.specular + rho.diffuse); let diffuse_weight = 1.0 - specular_weight; @@ -69,7 +69,7 @@ fn evaluate_and_sample_brdf( if material.roughness <= MIRROR_ROUGHNESS_THRESHOLD { return EvaluateAndSampleBrdfResult( wi, - evaluate_specular_brdf(wo, wi, world_normal, material) / specular_weight, + evaluate_specular_brdf(wo, wi, world_normal, material, F_ab) / specular_weight, bitcast(0x7F800000u) // INF ); } @@ -78,7 +78,7 @@ fn evaluate_and_sample_brdf( let diffuse_pdf = wi_tangent.z / PI; let specular_pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, material.roughness); let pdf = (diffuse_weight * diffuse_pdf) + (specular_weight * specular_pdf); - let throughput = evaluate_brdf(wo, wi, world_normal, material) / pdf; + let throughput = evaluate_brdf(wo, wi, world_normal, material, F_ab) / pdf; return EvaluateAndSampleBrdfResult(wi, throughput, pdf); } @@ -87,20 +87,21 @@ fn evaluate_brdf( wi: vec3, world_normal: vec3, material: ResolvedMaterial, + F_ab: vec2, ) -> vec3 { - return evaluate_diffuse_brdf(wo, wi, world_normal, material) + evaluate_specular_brdf(wo, wi, world_normal, material); + return evaluate_diffuse_brdf(wo, wi, world_normal, material, F_ab) + evaluate_specular_brdf(wo, wi, world_normal, material, F_ab); } -fn evaluate_diffuse_brdf(wo: vec3, wi: vec3, world_normal: vec3, material: ResolvedMaterial) -> vec3 { +fn evaluate_diffuse_brdf(wo: vec3, wi: vec3, world_normal: vec3, material: ResolvedMaterial, F_ab: vec2) -> vec3 { let NdotL = dot(world_normal, wi); let NdotV = dot(world_normal, wo); if NdotL < 0.0001 || NdotV < 0.0001 { return vec3(0.0); } let F0 = calculate_F0(material.base_color, material.metallic, vec3(material.reflectance)); - let rho = lobe_reflectances(F0, material, NdotV); + let rho = lobe_reflectances(F0, material, F_ab); return rho.diffuse / PI * NdotL; } -fn evaluate_specular_brdf(wo: vec3, wi: vec3, world_normal: vec3, material: ResolvedMaterial) -> vec3 { +fn evaluate_specular_brdf(wo: vec3, wi: vec3, world_normal: vec3, material: ResolvedMaterial, F_ab: vec2) -> vec3 { let H = normalize(wi + wo); let NdotL = dot(world_normal, wi); let NdotH = dot(world_normal, H); @@ -121,14 +122,13 @@ fn evaluate_specular_brdf(wo: vec3, wi: vec3, world_normal: vec3, let D = D_GGX(material.roughness, NdotH); let Vs = V_SmithGGXCorrelated(material.roughness, NdotV, NdotL); - let F_ab = F_AB(material.perceptual_roughness, NdotV); return specular_multiscatter(D, Vs, F, F0, F_ab, 1.0) * NdotL; } -fn brdf_pdf(wo: vec3, wi: vec3, world_normal: vec3, material: ResolvedMaterial) -> f32 { +fn brdf_pdf(wo: vec3, wi: vec3, world_normal: vec3, material: ResolvedMaterial, F_ab: vec2) -> f32 { let NdotV = max(dot(world_normal, wo), 0.0001); let F0 = calculate_F0(material.base_color, material.metallic, vec3(material.reflectance)); - let rho = lobe_reflectances(F0, material, NdotV); + let rho = lobe_reflectances(F0, material, F_ab); let specular_weight = luminance(rho.specular) / luminance(rho.specular + rho.diffuse); let diffuse_weight = 1.0 - specular_weight;