From c3cb7d9d3fe3a584cdd856e856532ab9ab42302a Mon Sep 17 00:00:00 2001 From: Dylan Sechet Date: Tue, 7 Apr 2026 17:29:19 +0200 Subject: [PATCH 1/4] fix: remove mikktspace tbn --- crates/bevy_solari/src/pathtracer/pathtracer.wgsl | 2 +- crates/bevy_solari/src/scene/brdf.wgsl | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl index 0fa945660c879..78c1daa47a59a 100644 --- a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl +++ b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl @@ -71,7 +71,7 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3) { } // 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.world_tangent, ray_hit.material, &rng); + let next_bounce = evaluate_and_sample_brdf(wo, ray_hit.world_normal, ray_hit.material, &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/scene/brdf.wgsl b/crates/bevy_solari/src/scene/brdf.wgsl index 6a207ea982109..43ffafb1f9d5d 100644 --- a/crates/bevy_solari/src/scene/brdf.wgsl +++ b/crates/bevy_solari/src/scene/brdf.wgsl @@ -6,7 +6,7 @@ enable wgpu_ray_query; #import bevy_pbr::lighting::{D_GGX, V_SmithGGXCorrelated, specular_multiscatter} #import bevy_pbr::pbr_functions::{calculate_tbn_mikktspace, calculate_diffuse_color, calculate_F0} #import bevy_pbr::utils::{rand_f, sample_cosine_hemisphere} -#import bevy_render::maths::PI +#import bevy_render::maths::{PI, orthonormalize} #import bevy_solari::sampling::{sample_ggx_vndf, ggx_vndf_pdf, ggx_vndf_sample_invalid} #import bevy_solari::scene_bindings::{ResolvedMaterial, MIRROR_ROUGHNESS_THRESHOLD, brdf_dfg_lut, brdf_dfg_lut_sampler} @@ -19,7 +19,6 @@ struct EvaluateAndSampleBrdfResult { fn evaluate_and_sample_brdf( wo: vec3, world_normal: vec3, - world_tangent: vec4, material: ResolvedMaterial, rng: ptr, ) -> EvaluateAndSampleBrdfResult { @@ -31,7 +30,7 @@ fn evaluate_and_sample_brdf( let diffuse_weight = mix(df, 0.0, material.metallic); let specular_weight = 1.0 - diffuse_weight; - let TBN = calculate_tbn_mikktspace(world_normal, world_tangent); + let TBN = orthonormalize(world_normal); let T = TBN[0]; let B = TBN[1]; let N = TBN[2]; From 717a20f3c9afd408c96e0958fa0fba07eca81d01 Mon Sep 17 00:00:00 2001 From: Dylan Sechet Date: Wed, 8 Apr 2026 11:27:44 +0200 Subject: [PATCH 2/4] fix: brdf fixes --- crates/bevy_solari/src/scene/brdf.wgsl | 98 +++++++++++++++++--------- 1 file changed, 63 insertions(+), 35 deletions(-) diff --git a/crates/bevy_solari/src/scene/brdf.wgsl b/crates/bevy_solari/src/scene/brdf.wgsl index 43ffafb1f9d5d..cfbfb7926a316 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_tbn_mikktspace, calculate_diffuse_color, calculate_F0} +#import bevy_pbr::pbr_functions::calculate_F0_dielectric #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,52 +16,77 @@ struct EvaluateAndSampleBrdfResult { pdf: f32, } +struct LobeReflectances { + rho_spec: vec3, + rho_diff: vec3, +} + +// Hemispherical reflectance of each lobe +fn lobe_reflectances(F0_metal: vec3, F0_dielectric: vec3, material: ResolvedMaterial, NdotV: f32) -> LobeReflectances { + if material.roughness <= MIRROR_ROUGHNESS_THRESHOLD { + let F_m = fresnel(F0_metal, NdotV); + let F_d = fresnel(F0_dielectric, NdotV); + return LobeReflectances( + material.metallic * F_m + (1.0 - material.metallic) * F_d, + (1.0 - material.metallic) * (vec3(1.0) - F_d) * material.base_color, + ); + } + 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_m = (F0_metal * F_ab.x + vec3(F_ab.y)) * (vec3(1.0) + F0_metal * ms_factor); + let rho_spec_d = (F0_dielectric * F_ab.x + vec3(F_ab.y)) * (vec3(1.0) + F0_dielectric * ms_factor); + return LobeReflectances( + material.metallic * rho_spec_m + (1.0 - material.metallic) * rho_spec_d, + (1.0 - material.metallic) * (vec3(1.0) - rho_spec_d) * material.base_color, + ); +} + fn evaluate_and_sample_brdf( wo: vec3, world_normal: vec3, material: ResolvedMaterial, 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 df = 1.0 - luminance(fresnel(F0, NdotV)); - - let diffuse_weight = mix(df, 0.0, material.metallic); - let specular_weight = 1.0 - diffuse_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 NdotV = dot(N, wo); + if NdotV < 0.0001 { return EvaluateAndSampleBrdfResult(vec3(0.0), vec3(0.0), 0.0); } + let F0_metal = material.base_color; + let F0_dielectric = calculate_F0_dielectric(vec3(material.reflectance)); + let rho = lobe_reflectances(F0_metal, F0_dielectric, material, NdotV); + let specular_weight = luminance(rho.rho_spec) / luminance(rho.rho_spec + rho.rho_diff); + + let wo_tangent = vec3(dot(wo, T), dot(wo, B), dot(wo, N)); var wi: vec3; var wi_tangent: vec3; - let diffuse_selected = rand_f(rng) < diffuse_weight; + let diffuse_selected = rand_f(rng) < (1.0 - specular_weight); if diffuse_selected { - wi = sample_cosine_hemisphere(world_normal, rng); + wi = sample_cosine_hemisphere(N, rng); wi_tangent = vec3(dot(wi, T), dot(wi, B), dot(wi, N)); } else { wi_tangent = sample_ggx_vndf(wo_tangent, material.roughness, rng); - if ggx_vndf_sample_invalid(wi_tangent) { - return EvaluateAndSampleBrdfResult(vec3(0.0), vec3(0.0), 0.0); - } + if ggx_vndf_sample_invalid(wi_tangent) { return EvaluateAndSampleBrdfResult(vec3(0.0), vec3(0.0), 0.0); } wi = wi_tangent.x * T + wi_tangent.y * B + wi_tangent.z * N; } - 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; + // Mirror is a delta function + if material.roughness <= MIRROR_ROUGHNESS_THRESHOLD { + if diffuse_selected { + return EvaluateAndSampleBrdfResult(wi, rho.rho_diff / (1.0 - specular_weight), 1.0); + } else { + if dot(N, wi) <= 0.0 { return EvaluateAndSampleBrdfResult(vec3(0.0), vec3(0.0), 0.0); } + 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 = specular_weight * specular_pdf + (1.0 - specular_weight) * diffuse_pdf; + let throughput = evaluate_brdf(wo, wi, world_normal, material) / pdf; return EvaluateAndSampleBrdfResult(wi, throughput, pdf); } @@ -75,31 +100,31 @@ 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 F0_dielectric = calculate_F0_dielectric(vec3(material.reflectance)); + let rho = lobe_reflectances(material.base_color, F0_dielectric, material, NdotV); + return rho.rho_diff / PI * NdotL; } fn evaluate_specular_brdf(wo: vec3, wi: vec3, world_normal: vec3, material: ResolvedMaterial) -> vec3 { let H = normalize(wi + wo); let NdotL = dot(world_normal, wi); let NdotH = dot(world_normal, H); - let LdotH = dot(wi, H); + let HdotV = dot(H, wo); let NdotV = dot(world_normal, wo); - if NdotL < 0.0001 || NdotH < 0.0001 || LdotH < 0.0001 || NdotV < 0.0001 { return vec3(0.0); } + if NdotL < 0.0001 || NdotH < 0.0001 || HdotV < 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); + let F0_metal = material.base_color; + let F0_dielectric = calculate_F0_dielectric(vec3(material.reflectance)); if material.roughness <= MIRROR_ROUGHNESS_THRESHOLD { if abs(NdotH - 1.0) < 0.0001 { - return F; + let F_m = fresnel(F0_metal, HdotV); + let F_d = fresnel(F0_dielectric, HdotV); + return material.metallic * F_m + (1.0 - material.metallic) * F_d; } else { return vec3(0.0); } @@ -108,7 +133,10 @@ 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; + let F_m = fresnel(F0_metal, HdotV); + let F_d = fresnel(F0_dielectric, HdotV); + return (material.metallic * specular_multiscatter(D, Vs, F_m, F0_metal, F_ab, 1.0) + + (1.0 - material.metallic) * specular_multiscatter(D, Vs, F_d, F0_dielectric, F_ab, 1.0)) * NdotL; } fn fresnel(f0: vec3, LdotH: f32) -> vec3 { From 31356c0d007ae9861470c6e345c112cc8dcf8f6d Mon Sep 17 00:00:00 2001 From: Dylan Sechet Date: Tue, 14 Apr 2026 20:23:43 +0200 Subject: [PATCH 3/4] feat: update pathtracer --- .../src/pathtracer/pathtracer.wgsl | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl index 78c1daa47a59a..4a8a6189a372b 100644 --- a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl +++ b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl @@ -1,11 +1,11 @@ enable wgpu_ray_query; #import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance -#import bevy_pbr::pbr_functions::{calculate_tbn_mikktspace, calculate_F0} +#import bevy_pbr::pbr_functions::calculate_F0_dielectric #import bevy_pbr::utils::{rand_f, rand_vec2f} -#import bevy_render::maths::PI +#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} @@ -96,23 +96,21 @@ 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 TBN = calculate_tbn_mikktspace(ray_hit.world_normal, ray_hit.world_tangent); + let TBN = orthonormalize(ray_hit.world_normal); let T = TBN[0]; let B = TBN[1]; let N = TBN[2]; + let NdotV = max(dot(N, wo), 0.0001); + let F0_metal = ray_hit.material.base_color; + let F0_dielectric = calculate_F0_dielectric(vec3(ray_hit.material.reflectance)); + let rho = lobe_reflectances(F0_metal, F0_dielectric, ray_hit.material, NdotV); + let specular_weight = luminance(rho.rho_spec) / luminance(rho.rho_spec + rho.rho_diff); + 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); - let pdf = (diffuse_weight * diffuse_pdf) + (specular_weight * specular_pdf); - return pdf; + return specular_weight * specular_pdf + (1.0 - specular_weight) * diffuse_pdf; } From 6a6b38047fc8d81baa249780d0be0da088ba0a7c Mon Sep 17 00:00:00 2001 From: Dylan Sechet Date: Mon, 20 Apr 2026 00:33:47 +0200 Subject: [PATCH 4/4] fix: remove mirror special-casing for diffuse lobe --- crates/bevy_solari/src/scene/brdf.wgsl | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/crates/bevy_solari/src/scene/brdf.wgsl b/crates/bevy_solari/src/scene/brdf.wgsl index cfbfb7926a316..87be09ce76ae1 100644 --- a/crates/bevy_solari/src/scene/brdf.wgsl +++ b/crates/bevy_solari/src/scene/brdf.wgsl @@ -71,14 +71,9 @@ fn evaluate_and_sample_brdf( wi_tangent = sample_ggx_vndf(wo_tangent, material.roughness, rng); if ggx_vndf_sample_invalid(wi_tangent) { return EvaluateAndSampleBrdfResult(vec3(0.0), vec3(0.0), 0.0); } wi = wi_tangent.x * T + wi_tangent.y * B + wi_tangent.z * N; - } - - // Mirror is a delta function - if material.roughness <= MIRROR_ROUGHNESS_THRESHOLD { - if diffuse_selected { - return EvaluateAndSampleBrdfResult(wi, rho.rho_diff / (1.0 - specular_weight), 1.0); - } else { - if dot(N, wi) <= 0.0 { return EvaluateAndSampleBrdfResult(vec3(0.0), vec3(0.0), 0.0); } + + // Mirror specular is a delta function + if material.roughness <= MIRROR_ROUGHNESS_THRESHOLD { return EvaluateAndSampleBrdfResult(wi, rho.rho_spec / specular_weight, 1.0); } }