Skip to content

Commit d458904

Browse files
authored
Solari: Better path termination heuristic (#22570)
I was using a path termination heuristic designed for a radiance cache, but solari uses an irradiance cache. Whoops. This new heuristic is inspired by RTXGI, and prevents the world cache from showing up in reflections properly. To test, move the camera close to a mirror surface. --- Also contains some bugfixes for bugs introduced in #22468.
1 parent 842032d commit d458904

File tree

3 files changed

+24
-22
lines changed

3 files changed

+24
-22
lines changed

crates/bevy_solari/src/pathtracer/pathtracer.wgsl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3<u32>) {
3939
var throughput = vec3(1.0);
4040
var p_bounce = 0.0;
4141
var bounce_was_perfect_reflection = true;
42-
var previous_normal = vec3(0.0);
4342
loop {
4443
let ray = trace_ray(ray_origin, ray_direction, ray_t_min, RAY_T_MAX, RAY_FLAG_NONE);
4544
if ray.kind != RAY_QUERY_INTERSECTION_NONE {
@@ -75,7 +74,6 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3<u32>) {
7574
ray_t_min = RAY_T_MIN;
7675
p_bounce = next_bounce.pdf;
7776
bounce_was_perfect_reflection = next_bounce.perfectly_specular_bounce;
78-
previous_normal = ray_hit.world_normal;
7977

8078
// Update throughput for next bounce
8179
let brdf = evaluate_brdf(ray_hit.world_normal, wo, next_bounce.wi, ray_hit.material);

crates/bevy_solari/src/realtime/specular_gi.wgsl

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#define_import_path bevy_solari::specular_gi
22

33
#import bevy_pbr::pbr_functions::{calculate_tbn_mikktspace, calculate_diffuse_color, calculate_F0}
4-
#import bevy_pbr::prepass_bindings::PreviousViewUniforms
54
#import bevy_render::maths::{orthonormalize, PI}
65
#import bevy_render::view::View
76
#import bevy_solari::brdf::{evaluate_brdf, evaluate_specular_brdf}
@@ -17,7 +16,6 @@
1716

1817
const DIFFUSE_GI_REUSE_ROUGHNESS_THRESHOLD: f32 = 0.4;
1918
const SPECULAR_GI_FOR_DI_ROUGHNESS_THRESHOLD: f32 = 0.0225;
20-
const TERMINATE_IN_WORLD_CACHE_THRESHOLD: f32 = 0.03;
2119

2220
@compute @workgroup_size(8, 8, 1)
2321
fn specular_gi(@builtin(global_invocation_id) global_id: vec3<u32>) {
@@ -33,7 +31,8 @@ fn specular_gi(@builtin(global_invocation_id) global_id: vec3<u32>) {
3331
let surface = gpixel_resolve(textureLoad(gbuffer, global_id.xy, 0), depth, global_id.xy, view.main_pass_viewport.zw, view.world_from_clip);
3432

3533
let wo_unnormalized = view.world_position - surface.world_position;
36-
let wo = normalize(wo_unnormalized);
34+
let wo_length = length(wo_unnormalized);
35+
let wo = wo_unnormalized / wo_length;
3736

3837
var radiance: vec3<f32>;
3938
var wi: vec3<f32>;
@@ -53,12 +52,7 @@ fn specular_gi(@builtin(global_invocation_id) global_id: vec3<u32>) {
5352
wi = wi_tangent.x * T + wi_tangent.y * B + wi_tangent.z * N;
5453
let pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, surface.material.roughness);
5554

56-
// https://d1qx31qr3h6wln.cloudfront.net/publications/mueller21realtime.pdf#subsection.3.4, equation (4)
57-
let cos_theta = saturate(dot(wo, surface.world_normal));
58-
var a0 = dot(wo_unnormalized, wo_unnormalized) / (4.0 * PI * cos_theta);
59-
a0 *= TERMINATE_IN_WORLD_CACHE_THRESHOLD;
60-
61-
radiance = trace_glossy_path(global_id.xy, surface, wi, pdf, a0, &rng) / pdf;
55+
radiance = trace_glossy_path(global_id.xy, surface, wo_length, wi, pdf, &rng) / pdf;
6256
}
6357

6458
let brdf = evaluate_specular_brdf(surface.world_normal, wo, wi, surface.material.base_color, surface.material.metallic,
@@ -74,15 +68,15 @@ fn specular_gi(@builtin(global_invocation_id) global_id: vec3<u32>) {
7468
#endif
7569
}
7670

77-
fn trace_glossy_path(pixel_id: vec2<u32>, primary_surface: ResolvedGPixel, initial_wi: vec3<f32>, initial_p_bounce: f32, a0: f32, rng: ptr<function, u32>) -> vec3<f32> {
71+
fn trace_glossy_path(pixel_id: vec2<u32>, primary_surface: ResolvedGPixel, initial_ray_t: f32, initial_wi: vec3<f32>, initial_p_bounce: f32, rng: ptr<function, u32>) -> vec3<f32> {
7872
var radiance = vec3(0.0);
7973
var throughput = vec3(1.0);
8074

8175
var ray_origin = primary_surface.world_position;
8276
var wi = initial_wi;
8377
var p_bounce = initial_p_bounce;
8478
var surface_perfect_mirror = false;
85-
var path_spread = 0.0;
79+
var path_spread = path_spread_heuristic(initial_ray_t, primary_surface.material.roughness);
8680

8781
#ifdef DLSS_RR_GUIDE_BUFFERS
8882
var mirror_rotations = reflection_matrix(primary_surface.world_normal);
@@ -111,9 +105,6 @@ fn trace_glossy_path(pixel_id: vec2<u32>, primary_surface: ResolvedGPixel, initi
111105
// Should not perform NEE for mirror-like surfaces
112106
surface_perfect_mirror = ray_hit.material.roughness <= MIRROR_ROUGHNESS_THRESHOLD && ray_hit.material.metallic > 0.9999;
113107

114-
// https://d1qx31qr3h6wln.cloudfront.net/publications/mueller21realtime.pdf#subsection.3.4, equation (3)
115-
path_spread += sqrt((ray.t * ray.t) / (p_bounce * wo_tangent.z));
116-
117108
// Primary surface replacement for perfect mirrors
118109
// https://developer.nvidia.com/blog/rendering-perfect-reflections-and-refractions-in-path-traced-games/#primary_surface_replacement
119110
#ifdef DLSS_RR_GUIDE_BUFFERS
@@ -127,8 +118,12 @@ fn trace_glossy_path(pixel_id: vec2<u32>, primary_surface: ResolvedGPixel, initi
127118
}
128119
#endif
129120

130-
if path_spread * path_spread > a0 * get_cell_size(ray_hit.world_position, view.world_position) {
131-
// Path spread is wide enough, terminate path in the world cache
121+
// Terminate path in the world cache if the ray is long enough and the path spread is large enough
122+
let world_cache_cell_size = get_cell_size(ray_hit.world_position, view.world_position);
123+
let ray_longer_than_cell = ray.t > sqrt(3.0) * world_cache_cell_size;
124+
let path_spread_large_enough = path_spread > world_cache_cell_size * world_cache_cell_size;
125+
126+
if ray_longer_than_cell && path_spread_large_enough {
132127
let diffuse_brdf = ray_hit.material.base_color / PI;
133128
radiance += throughput * diffuse_brdf * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position, ray.t, WORLD_CACHE_CELL_LIFETIME, rng);
134129
break;
@@ -148,8 +143,10 @@ fn trace_glossy_path(pixel_id: vec2<u32>, primary_surface: ResolvedGPixel, initi
148143
// Update throughput for next bounce
149144
p_bounce = ggx_vndf_pdf(wo_tangent, wi_tangent, ray_hit.material.roughness);
150145
let brdf = evaluate_brdf(N, wo, wi, ray_hit.material);
151-
let cos_theta = saturate(dot(wi, N));
152-
throughput *= (brdf * cos_theta) / p_bounce;
146+
throughput *= brdf / p_bounce;
147+
148+
// Path spread increase
149+
path_spread += path_spread_heuristic(ray.t, ray_hit.material.roughness);
153150
}
154151

155152
return radiance;
@@ -186,6 +183,12 @@ fn nee_mis_weight(inverse_p_light: f32, brdf_rays_can_hit: bool, wo_tangent: vec
186183
return power_heuristic(p_light, p_bounce);
187184
}
188185

186+
fn path_spread_heuristic(ray_t: f32, roughness: f32) -> f32 {
187+
let alpha_squared = min(roughness * roughness, 0.99);
188+
let distance_squared = ray_t * ray_t;
189+
return distance_squared * 0.5 * (alpha_squared / (1.0 - alpha_squared));
190+
}
191+
189192
#ifdef DLSS_RR_GUIDE_BUFFERS
190193
// https://en.wikipedia.org/wiki/Householder_transformation
191194
fn reflection_matrix(plane_normal: vec3f) -> mat3x3<f32> {

crates/bevy_solari/src/realtime/world_cache_update.wgsl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,14 @@ fn sample_random_light_ris(world_position: vec3<f32>, world_normal: vec3<f32>, w
9999
let resolved_light_sample = unpack_resolved_light_sample(light_tile_resolved_samples[tile_sample], view.exposure);
100100
let light_contribution = calculate_resolved_light_contribution(resolved_light_sample, world_position, world_normal);
101101

102-
let target_function = luminance(light_contribution.radiance * saturate(dot(light_contribution.wi, world_normal)));
102+
let contribution = light_contribution.radiance * saturate(dot(light_contribution.wi, world_normal));
103+
let target_function = luminance(contribution);
103104
let resampling_weight = mis_weight * (target_function * light_contribution.inverse_pdf);
104105

105106
weight_sum += resampling_weight;
106107

107108
if rand_f(rng) < resampling_weight / weight_sum {
108-
selected_sample_radiance = light_contribution.radiance;
109+
selected_sample_radiance = contribution;
109110
selected_sample_target_function = target_function;
110111
selected_sample_world_position = resolved_light_sample.world_position;
111112
}

0 commit comments

Comments
 (0)