Skip to content

Commit 6c22c9b

Browse files
authored
Solari: Improve specular GI (#21907)
Go back to only sampling the world cache for rough and non-first-bounce surfaces, but sample direct lighting when that doesn't happen. Mainline <img width="3206" height="1875" alt="image" src="https://github.com/user-attachments/assets/1247d357-d190-4fdc-a7b3-396d42478b99" /> This PR <img width="3206" height="1875" alt="image" src="https://github.com/user-attachments/assets/927bca68-4cac-4aef-87d5-dce079eb54af" /> Pathtraced reference <img width="3206" height="1875" alt="image" src="https://github.com/user-attachments/assets/d6668bde-521c-47c7-b4d5-0ba6c8db5cc8" /> Fixes #21967.
1 parent e44d659 commit 6c22c9b

File tree

3 files changed

+73
-21
lines changed

3 files changed

+73
-21
lines changed

crates/bevy_solari/src/pathtracer/pathtracer.wgsl

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#import bevy_render::maths::PI
55
#import bevy_render::view::View
66
#import bevy_solari::brdf::evaluate_brdf
7-
#import bevy_solari::sampling::{sample_random_light, random_light_pdf, sample_ggx_vndf, ggx_vndf_pdf, balance_heuristic, power_heuristic}
7+
#import bevy_solari::sampling::{sample_random_light, random_emissive_light_pdf, sample_ggx_vndf, ggx_vndf_pdf, power_heuristic}
88
#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, ResolvedRayHitFull, RAY_T_MIN, RAY_T_MAX}
99

1010
@group(1) @binding(0) var accumulation_texture: texture_storage_2d<rgba32float, read_write>;
@@ -48,7 +48,7 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3<u32>) {
4848

4949
var mis_weight = 1.0;
5050
if !bounce_was_perfect_reflection {
51-
let p_light = random_light_pdf(ray_hit);
51+
let p_light = random_emissive_light_pdf(ray_hit);
5252
mis_weight = power_heuristic(p_bounce, p_light);
5353
}
5454
radiance += mis_weight * throughput * ray_hit.material.emissive;
@@ -57,8 +57,13 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3<u32>) {
5757
let is_perfectly_specular = ray_hit.material.roughness <= 0.001 && ray_hit.material.metallic > 0.9999;
5858
if !is_perfectly_specular {
5959
let direct_lighting = sample_random_light(ray_hit.world_position, ray_hit.world_normal, &rng);
60-
let pdf_of_bounce = brdf_pdf(wo, direct_lighting.wi, ray_hit);
61-
mis_weight = power_heuristic(1.0 / direct_lighting.inverse_pdf, pdf_of_bounce);
60+
61+
mis_weight = 1.0;
62+
if direct_lighting.brdf_rays_can_hit {
63+
let pdf_of_bounce = brdf_pdf(wo, direct_lighting.wi, ray_hit);
64+
mis_weight = power_heuristic(1.0 / direct_lighting.inverse_pdf, pdf_of_bounce);
65+
}
66+
6267
let direct_lighting_brdf = evaluate_brdf(ray_hit.world_normal, wo, direct_lighting.wi, ray_hit.material);
6368
radiance += mis_weight * throughput * direct_lighting.radiance * direct_lighting.inverse_pdf * direct_lighting_brdf;
6469
}

crates/bevy_solari/src/realtime/specular_gi.wgsl

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
#import bevy_render::view::View
44
#import bevy_solari::brdf::{evaluate_brdf, evaluate_specular_brdf}
55
#import bevy_solari::gbuffer_utils::gpixel_resolve
6-
#import bevy_solari::sampling::{sample_ggx_vndf, ggx_vndf_pdf}
7-
#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN, RAY_T_MAX}
6+
#import bevy_solari::sampling::{sample_random_light, random_emissive_light_pdf, sample_ggx_vndf, ggx_vndf_pdf, power_heuristic}
7+
#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, ResolvedRayHitFull, RAY_T_MIN, RAY_T_MAX}
88
#import bevy_solari::world_cache::{query_world_cache, WORLD_CACHE_CELL_LIFETIME}
99

1010
@group(1) @binding(0) var view_output: texture_storage_2d<rgba16float, read_write>;
@@ -15,6 +15,9 @@
1515
struct PushConstants { frame_index: u32, reset: u32 }
1616
var<push_constant> constants: PushConstants;
1717

18+
const DIFFUSE_GI_REUSE_ROUGHNESS_THRESHOLD: f32 = 0.4;
19+
const WORLD_CACHE_TERMINATION_ROUGHNESS_THRESHOLD: f32 = 0.4;
20+
1821
@compute @workgroup_size(8, 8, 1)
1922
fn specular_gi(@builtin(global_invocation_id) global_id: vec3<u32>) {
2023
if any(global_id.xy >= vec2u(view.main_pass_viewport.zw)) { return; }
@@ -32,7 +35,7 @@ fn specular_gi(@builtin(global_invocation_id) global_id: vec3<u32>) {
3235

3336
var radiance: vec3<f32>;
3437
var wi: vec3<f32>;
35-
if surface.material.roughness > 0.1 {
38+
if surface.material.roughness > DIFFUSE_GI_REUSE_ROUGHNESS_THRESHOLD {
3639
// Surface is very rough, reuse the ReSTIR GI reservoir
3740
let gi_reservoir = gi_reservoirs_a[pixel_index];
3841
wi = normalize(gi_reservoir.sample_point_world_position - surface.world_position);
@@ -68,6 +71,8 @@ fn specular_gi(@builtin(global_invocation_id) global_id: vec3<u32>) {
6871
fn trace_glossy_path(initial_ray_origin: vec3<f32>, initial_wi: vec3<f32>, rng: ptr<function, u32>) -> vec3<f32> {
6972
var ray_origin = initial_ray_origin;
7073
var wi = initial_wi;
74+
var surface_perfectly_specular = false;
75+
var p_bounce = 0.0;
7176

7277
// Trace up to three bounces, getting the net throughput from them
7378
var radiance = vec3(0.0);
@@ -78,34 +83,72 @@ fn trace_glossy_path(initial_ray_origin: vec3<f32>, initial_wi: vec3<f32>, rng:
7883
if ray.kind == RAY_QUERY_INTERSECTION_NONE { break; }
7984
let ray_hit = resolve_ray_hit_full(ray);
8085

81-
// Add world cache contribution
82-
let diffuse_brdf = ray_hit.material.base_color / PI;
83-
radiance += throughput * diffuse_brdf * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position, WORLD_CACHE_CELL_LIFETIME, rng);
84-
85-
// Surface is very rough, terminate path in the world cache
86-
if ray_hit.material.roughness > 0.1 && i != 0u { break; }
87-
88-
// Sample new ray direction from the GGX BRDF for next bounce
8986
let TBN = calculate_tbn_mikktspace(ray_hit.world_normal, ray_hit.world_tangent);
9087
let T = TBN[0];
9188
let B = TBN[1];
9289
let N = TBN[2];
90+
9391
let wo = -wi;
9492
let wo_tangent = vec3(dot(wo, T), dot(wo, B), dot(wo, N));
93+
94+
// Add emissive contribution (but not on the first bounce, since ReSTIR DI handles that)
95+
if i != 0u {
96+
radiance += throughput * emissive_mis_weight(p_bounce, ray_hit, surface_perfectly_specular) * ray_hit.material.emissive;
97+
}
98+
99+
// Should not perform NEE for mirror-like surfaces
100+
surface_perfectly_specular = ray_hit.material.roughness <= 0.001 && ray_hit.material.metallic > 0.9999;
101+
102+
if ray_hit.material.roughness > WORLD_CACHE_TERMINATION_ROUGHNESS_THRESHOLD && i != 0u {
103+
// Surface is very rough, terminate path in the world cache
104+
let diffuse_brdf = ray_hit.material.base_color / PI;
105+
radiance += throughput * diffuse_brdf * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position, WORLD_CACHE_CELL_LIFETIME, rng);
106+
break;
107+
} else if !surface_perfectly_specular {
108+
// Sample direct lighting (NEE)
109+
let direct_lighting = sample_random_light(ray_hit.world_position, ray_hit.world_normal, rng);
110+
let direct_lighting_brdf = evaluate_brdf(ray_hit.world_normal, wo, direct_lighting.wi, ray_hit.material);
111+
let mis_weight = nee_mis_weight(direct_lighting.inverse_pdf, direct_lighting.brdf_rays_can_hit, wo_tangent, direct_lighting.wi, ray_hit, TBN);
112+
radiance += throughput * mis_weight * direct_lighting.radiance * direct_lighting.inverse_pdf * direct_lighting_brdf;
113+
}
114+
115+
// Sample new ray direction from the GGX BRDF for next bounce
95116
let wi_tangent = sample_ggx_vndf(wo_tangent, ray_hit.material.roughness, rng);
96117
wi = wi_tangent.x * T + wi_tangent.y * B + wi_tangent.z * N;
97118
ray_origin = ray_hit.world_position;
98119

99120
// Update throughput for next bounce
100-
let pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, ray_hit.material.roughness);
121+
p_bounce = ggx_vndf_pdf(wo_tangent, wi_tangent, ray_hit.material.roughness);
101122
let brdf = evaluate_brdf(N, wo, wi, ray_hit.material);
102123
let cos_theta = saturate(dot(wi, N));
103-
throughput *= (brdf * cos_theta) / pdf;
124+
throughput *= (brdf * cos_theta) / p_bounce;
104125
}
105126

106127
return radiance;
107128
}
108129

130+
fn emissive_mis_weight(p_bounce: f32, ray_hit: ResolvedRayHitFull, previous_surface_perfectly_specular: bool) -> f32 {
131+
if previous_surface_perfectly_specular { return 1.0; }
132+
133+
let p_light = random_emissive_light_pdf(ray_hit);
134+
return power_heuristic(p_bounce, p_light);
135+
}
136+
137+
fn nee_mis_weight(inverse_p_light: f32, brdf_rays_can_hit: bool, wo_tangent: vec3<f32>, wi: vec3<f32>, ray_hit: ResolvedRayHitFull, TBN: mat3x3<f32>) -> f32 {
138+
if !brdf_rays_can_hit {
139+
return 1.0;
140+
}
141+
142+
let T = TBN[0];
143+
let B = TBN[1];
144+
let N = TBN[2];
145+
let wi_tangent = vec3(dot(wi, T), dot(wi, B), dot(wi, N));
146+
147+
let p_light = 1.0 / inverse_p_light;
148+
let p_bounce = ggx_vndf_pdf(wo_tangent, wi_tangent, ray_hit.material.roughness);
149+
return max(0.0, power_heuristic(p_light, p_bounce));
150+
}
151+
109152
// Don't adjust the size of this struct without also adjusting GI_RESERVOIR_STRUCT_SIZE.
110153
struct Reservoir {
111154
sample_point_world_position: vec3<f32>,

crates/bevy_solari/src/scene/sampling.wgsl

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ fn balance_heuristic(f: f32, g: f32) -> f32 {
1515

1616
// https://gpuopen.com/download/Bounded_VNDF_Sampling_for_Smith-GGX_Reflections.pdf (Listing 1)
1717
fn sample_ggx_vndf(wi_tangent: vec3<f32>, roughness: f32, rng: ptr<function, u32>) -> vec3<f32> {
18+
if roughness <= 0.01 {
19+
return vec3(-wi_tangent.xy, wi_tangent.z);
20+
}
21+
1822
let i = wi_tangent;
1923
let rand = rand_vec2f(rng);
2024
let i_std = normalize(vec3(i.xy * roughness, i.z));
@@ -69,6 +73,7 @@ struct LightContribution {
6973
radiance: vec3<f32>,
7074
inverse_pdf: f32,
7175
wi: vec3<f32>,
76+
brdf_rays_can_hit: bool,
7277
}
7378

7479
struct LightContributionNoPdf {
@@ -88,10 +93,9 @@ fn sample_random_light(ray_origin: vec3<f32>, origin_world_normal: vec3<f32>, rn
8893
return light_contribution;
8994
}
9095

91-
fn random_light_pdf(hit: ResolvedRayHitFull) -> f32 {
96+
fn random_emissive_light_pdf(hit: ResolvedRayHitFull) -> f32 {
9297
let light_count = arrayLength(&light_sources);
93-
let p_light = 1.0 / f32(light_count);
94-
return p_light / (hit.triangle_area * f32(hit.triangle_count));
98+
return 1.0 / (f32(light_count) * f32(hit.triangle_count) * hit.triangle_area);
9599
}
96100

97101
fn generate_random_light_sample(rng: ptr<function, u32>) -> GenerateRandomLightSampleResult {
@@ -169,7 +173,7 @@ fn calculate_resolved_light_contribution(resolved_light_sample: ResolvedLightSam
169173

170174
let radiance = resolved_light_sample.radiance * cos_theta_origin * (cos_theta_light / light_distance_squared);
171175

172-
return LightContribution(radiance, resolved_light_sample.inverse_pdf, wi);
176+
return LightContribution(radiance, resolved_light_sample.inverse_pdf, wi, resolved_light_sample.world_position.w == 1.0);
173177
}
174178

175179
fn resolve_and_calculate_light_contribution(light_sample: LightSample, ray_origin: vec3<f32>, origin_world_normal: vec3<f32>) -> LightContributionNoPdf {

0 commit comments

Comments
 (0)