From 86c237851286df32d3436768c1a19c28a1339b2b Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:35:36 -0400 Subject: [PATCH 1/2] Solari: Fix MIS not using consistent PDF measures --- .../bevy_solari/src/pathtracer/pathtracer.wgsl | 5 ++--- crates/bevy_solari/src/realtime/restir_gi.wgsl | 6 +----- crates/bevy_solari/src/realtime/specular_gi.wgsl | 8 ++++---- crates/bevy_solari/src/scene/sampling.wgsl | 16 ++++++++++++---- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl index fdb8ca7210489..5a0d7b5862cb2 100644 --- a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl +++ b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl @@ -50,7 +50,7 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3) { // Emissive contribution var mis_weight = 1.0; if p_bounce != 0.0 { // Not first bounce - let p_light = random_emissive_light_pdf(ray_hit); + let p_light = random_emissive_light_pdf(ray_hit, ray.t, NdotV); mis_weight = power_heuristic(p_bounce, p_light); } radiance += mis_weight * throughput * ray_hit.material.emissive; @@ -64,7 +64,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.world_normal, ray_hit.material, F_ab); - mis_weight = power_heuristic(1.0 / direct_lighting.inverse_pdf, pdf_of_bounce); + mis_weight = power_heuristic(1.0 / direct_lighting.inverse_solid_angle_pdf, pdf_of_bounce); } let direct_lighting_brdf = evaluate_brdf(wo, direct_lighting.wi, ray_hit.world_normal, ray_hit.material, F_ab); @@ -95,4 +95,3 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3) { textureStore(accumulation_texture, global_id.xy, vec4(new_color, old_color.a + 1.0)); textureStore(view_output, global_id.xy, vec4(new_color, 1.0)); } - diff --git a/crates/bevy_solari/src/realtime/restir_gi.wgsl b/crates/bevy_solari/src/realtime/restir_gi.wgsl index 19fd75efa5079..1a6b796ad9568 100644 --- a/crates/bevy_solari/src/realtime/restir_gi.wgsl +++ b/crates/bevy_solari/src/realtime/restir_gi.wgsl @@ -8,7 +8,7 @@ enable wgpu_ray_query; #import bevy_render::view::View #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::sampling::{sample_random_light, trace_point_visibility, balance_heuristic, isinf, isnan} #import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN, RAY_T_MAX} #import bevy_solari::world_cache::{query_world_cache, WORLD_CACHE_CELL_LIFETIME} #import bevy_solari::realtime_bindings::{view_output, gi_reservoirs_a, gi_reservoirs_b, gbuffer, depth_buffer, motion_vectors, previous_gbuffer, previous_depth_buffer, view, previous_view, constants, Reservoir} @@ -208,10 +208,6 @@ fn jacobian( return select(jacobian, 0.0, isinf(jacobian) || isnan(jacobian)); } -fn isinf(x: f32) -> bool { - return (bitcast(x) & 0x7fffffffu) == 0x7f800000u; -} - fn empty_reservoir() -> Reservoir { return Reservoir( vec3(0.0), diff --git a/crates/bevy_solari/src/realtime/specular_gi.wgsl b/crates/bevy_solari/src/realtime/specular_gi.wgsl index 205aedcdaaf8f..263afdca8124a 100644 --- a/crates/bevy_solari/src/realtime/specular_gi.wgsl +++ b/crates/bevy_solari/src/realtime/specular_gi.wgsl @@ -113,7 +113,7 @@ fn trace_glossy_path(pixel_id: vec2, primary_surface: ResolvedGPixel, initi 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); + let mis_weight = emissive_mis_weight(i, primary_surface.material.roughness, p_bounce, ray_hit, ray.t, NdotV); radiance += throughput * mis_weight * ray_hit.material.emissive; // Should not perform NEE for mirror-like surfaces @@ -145,7 +145,7 @@ fn trace_glossy_path(pixel_id: vec2, primary_surface: ResolvedGPixel, initi // 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, 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); + let mis_weight = nee_mis_weight(direct_lighting.inverse_solid_angle_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; } @@ -172,9 +172,9 @@ fn trace_glossy_path(pixel_id: vec2, primary_surface: ResolvedGPixel, initi return radiance; } -fn emissive_mis_weight(i: u32, initial_roughness: f32, p_bounce: f32, ray_hit: ResolvedRayHitFull) -> f32 { +fn emissive_mis_weight(i: u32, initial_roughness: f32, p_bounce: f32, ray_hit: ResolvedRayHitFull, ray_distance: f32, NdotV: f32) -> f32 { if i != 0u { - let p_light = random_emissive_light_pdf(ray_hit); + let p_light = random_emissive_light_pdf(ray_hit, ray_distance, NdotV); return power_heuristic(p_bounce, p_light); } else { // The first bounce gets MIS weight 0.0 or 1.0 depending on if ReSTIR DI shaded using the specular lobe or not diff --git a/crates/bevy_solari/src/scene/sampling.wgsl b/crates/bevy_solari/src/scene/sampling.wgsl index 4b22150c6d241..c26854352db25 100644 --- a/crates/bevy_solari/src/scene/sampling.wgsl +++ b/crates/bevy_solari/src/scene/sampling.wgsl @@ -83,6 +83,10 @@ fn ggx_vndf_pdf(wi_tangent: vec3, wo_tangent: vec3, roughness: f32) -> return select(pdf, 0.0, isnan(pdf)); } +fn isinf(x: f32) -> bool { + return (bitcast(x) & 0x7fffffffu) == 0x7f800000u; +} + fn isnan(x: f32) -> bool { return (bitcast(x) & 0x7fffffffu) > 0x7f800000u; } @@ -104,6 +108,7 @@ struct ResolvedLightSample { struct LightContribution { radiance: vec3, inverse_pdf: f32, + inverse_solid_angle_pdf: f32, wi: vec3, brdf_rays_can_hit: bool, } @@ -125,9 +130,10 @@ fn sample_random_light(ray_origin: vec3, origin_world_normal: vec3, rn return light_contribution; } -fn random_emissive_light_pdf(hit: ResolvedRayHitFull) -> f32 { +fn random_emissive_light_pdf(hit: ResolvedRayHitFull, ray_distance: f32, NdotV: f32) -> f32 { let light_count = arrayLength(&light_sources); - return 1.0 / (f32(light_count) * f32(hit.triangle_count) * hit.triangle_area); + let area_pdf = 1.0 / (f32(light_count) * f32(hit.triangle_count) * hit.triangle_area); + return area_pdf * (ray_distance * ray_distance) / NdotV; } fn generate_random_light_sample(rng: ptr) -> GenerateRandomLightSampleResult { @@ -201,10 +207,12 @@ fn calculate_resolved_light_contribution(resolved_light_sample: ResolvedLightSam let cos_theta_light = saturate(dot(-wi, resolved_light_sample.world_normal)); let light_distance_squared = light_distance * light_distance; + let denominator = cos_theta_light / light_distance_squared; - let radiance = resolved_light_sample.radiance * (cos_theta_light / light_distance_squared); + let radiance = resolved_light_sample.radiance * denominator; + let inverse_solid_angle_pdf = resolved_light_sample.inverse_pdf * denominator; - return LightContribution(radiance, resolved_light_sample.inverse_pdf, wi, resolved_light_sample.world_position.w == 1.0); + return LightContribution(radiance, resolved_light_sample.inverse_pdf, inverse_solid_angle_pdf, wi, resolved_light_sample.world_position.w == 1.0); } fn resolve_and_calculate_light_contribution(light_sample: LightSample, ray_origin: vec3, origin_world_normal: vec3) -> LightContributionNoPdf { From f122ea130e46dc029e943f0eaba52c997cf2a7d7 Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:04:20 -0400 Subject: [PATCH 2/2] Add comment --- crates/bevy_solari/src/scene/sampling.wgsl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bevy_solari/src/scene/sampling.wgsl b/crates/bevy_solari/src/scene/sampling.wgsl index c26854352db25..1267d86751bdf 100644 --- a/crates/bevy_solari/src/scene/sampling.wgsl +++ b/crates/bevy_solari/src/scene/sampling.wgsl @@ -108,6 +108,9 @@ struct ResolvedLightSample { struct LightContribution { radiance: vec3, inverse_pdf: f32, + // inverse_pdf may not be in solid angle measure (e.g. area measure) + // but inverse_solid_angle_pdf is always in solid angle measure + // for cases where it's required, e.g. MIS between light sampling techniques inverse_solid_angle_pdf: f32, wi: vec3, brdf_rays_can_hit: bool,