diff --git a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl index 9755dfeba8f03..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, fresnel} +#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); + 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); @@ -95,24 +97,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 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 = 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); - let pdf = (diffuse_weight * diffuse_pdf) + (specular_weight * specular_pdf); - return pdf; -} 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 67d8c470cf4c4..ef44d97b32191 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,19 +16,34 @@ struct EvaluateAndSampleBrdfResult { pdf: f32, } +struct LobeReflectances { + specular: vec3, + diffuse: vec3, +} + +// Hemispherical reflectance of each lobe +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( + rho_specular, + (1.0 - material.metallic) * (1.0 - rho_specular) * material.base_color, + ); +} + 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 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, F_ab); + 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]; @@ -49,19 +64,21 @@ 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, + evaluate_specular_brdf(wo, wi, world_normal, material, F_ab) / 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 = (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 throughput = evaluate_brdf(wo, wi, world_normal, material, F_ab) / pdf; return EvaluateAndSampleBrdfResult(wi, throughput, pdf); } @@ -70,23 +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 { - let diffuse_color = calculate_diffuse_color(material.base_color, material.metallic, 0.0, 0.0) / PI; - +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 layering = (1.0 - fresnel(F0, NdotL)) * (1.0 - fresnel(F0, NdotV)); - - return diffuse_color * layering * NdotL; + 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); @@ -107,10 +122,29 @@ 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, 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, F_ab); + 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 (diffuse_weight * diffuse_pdf) + (specular_weight * specular_pdf); +} + fn fresnel(f0: vec3, LdotH: f32) -> vec3 { return f0 + (1.0 - f0) * pow(1.0 - LdotH, 5.0); }