diff --git a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl index 0fa945660c879..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} @@ -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); @@ -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; } diff --git a/crates/bevy_solari/src/scene/brdf.wgsl b/crates/bevy_solari/src/scene/brdf.wgsl index 6a207ea982109..87be09ce76ae1 100644 --- a/crates/bevy_solari/src/scene/brdf.wgsl +++ b/crates/bevy_solari/src/scene/brdf.wgsl @@ -4,9 +4,9 @@ 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 +#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} @@ -16,53 +16,72 @@ 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, - world_tangent: vec4, 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 = calculate_tbn_mikktspace(world_normal, world_tangent); + 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; + + // 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 + (1.0 - specular_weight) * diffuse_pdf; + let throughput = evaluate_brdf(wo, wi, world_normal, material) / pdf; return EvaluateAndSampleBrdfResult(wi, throughput, pdf); } @@ -76,31 +95,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); } @@ -109,7 +128,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 {