Skip to content

Commit f94997c

Browse files
Various Solari improvements (#21649)
* Fix compile error when compiling with DLSS enabled after #21205 * Use permutation sampling for ReSTIR DI temporal reuse to fix artifacts under DLSS-RR * For both DI and GI, removed the spatial raytrace, and moved it to the final reservoir before shading. * Reduced DI initial samples 32 -> 8 for better performance at the cost of quality * Various specular GI improvements and bugfixes (still kinda terrible overall, I need to do some research on how people usually do this kind of thing) * Made the world cache adapt faster / be less stable * Switched spatial hashing collisions from to linear probing --------- Co-authored-by: Jasmine Schweitzer <[email protected]>
1 parent e3814a9 commit f94997c

File tree

7 files changed

+75
-84
lines changed

7 files changed

+75
-84
lines changed

crates/bevy_solari/src/realtime/gbuffer_utils.wgsl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,12 @@ fn pixel_dissimilar(depth: f32, world_position: vec3<f32>, other_world_position:
4444

4545
return tangent_plane_distance / view_z > 0.003 || dot(normal, other_normal) < 0.906;
4646
}
47+
48+
fn permute_pixel(pixel_id: vec2<u32>, frame_index: u32, view_size: vec2<f32>) -> vec2<u32> {
49+
let r = frame_index;
50+
let offset = vec2(r & 3u, (r >> 2u) & 3u);
51+
var shifted_pixel_id = pixel_id + offset;
52+
shifted_pixel_id ^= vec2(3u);
53+
shifted_pixel_id -= offset;
54+
return min(shifted_pixel_id, vec2<u32>(view_size - 1.0));
55+
}

crates/bevy_solari/src/realtime/node.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,8 @@ impl ViewNode for SolariLightingNode {
204204
let bind_group_resolve_dlss_rr_textures = view_dlss_rr_textures.map(|d| {
205205
render_context.render_device().create_bind_group(
206206
"solari_lighting_bind_group_resolve_dlss_rr_textures",
207-
&self.bind_group_layout_resolve_dlss_rr_textures,
207+
&pipeline_cache
208+
.get_bind_group_layout(&self.bind_group_layout_resolve_dlss_rr_textures),
208209
&BindGroupEntries::sequential((
209210
&d.diffuse_albedo.default_view,
210211
&d.specular_albedo.default_view,

crates/bevy_solari/src/realtime/restir_di.wgsl

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#import bevy_render::maths::PI
88
#import bevy_render::view::View
99
#import bevy_solari::brdf::evaluate_brdf
10-
#import bevy_solari::gbuffer_utils::{gpixel_resolve, pixel_dissimilar}
10+
#import bevy_solari::gbuffer_utils::{gpixel_resolve, pixel_dissimilar, permute_pixel}
1111
#import bevy_solari::presample_light_tiles::{ResolvedLightSamplePacked, unpack_resolved_light_sample}
1212
#import bevy_solari::sampling::{LightSample, calculate_resolved_light_contribution, resolve_and_calculate_light_contribution, resolve_light_sample, trace_light_visibility}
1313
#import bevy_solari::scene_bindings::{light_sources, previous_frame_light_id_translations, LIGHT_NOT_PRESENT_THIS_FRAME}
@@ -27,7 +27,7 @@
2727
struct PushConstants { frame_index: u32, reset: u32 }
2828
var<push_constant> constants: PushConstants;
2929

30-
const INITIAL_SAMPLES = 32u;
30+
const INITIAL_SAMPLES = 8u;
3131
const SPATIAL_REUSE_RADIUS_PIXELS = 30.0;
3232
const CONFIDENCE_WEIGHT_CAP = 20.0;
3333

@@ -73,7 +73,12 @@ fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3<u32>) {
7373
let input_reservoir = load_reservoir_b(global_id.xy);
7474
let spatial_reservoir = load_spatial_reservoir(global_id.xy, depth, surface.world_position, surface.world_normal, &rng);
7575
let merge_result = merge_reservoirs(input_reservoir, spatial_reservoir, surface.world_position, surface.world_normal, diffuse_brdf, &rng);
76-
let combined_reservoir = merge_result.merged_reservoir;
76+
var combined_reservoir = merge_result.merged_reservoir;
77+
78+
if reservoir_valid(combined_reservoir) {
79+
let resolved_light_sample = resolve_light_sample(combined_reservoir.sample, light_sources[combined_reservoir.sample.light_id >> 16u]);
80+
combined_reservoir.unbiased_contribution_weight *= trace_light_visibility(surface.world_position, resolved_light_sample.world_position);
81+
}
7782

7883
store_reservoir_a(global_id.xy, combined_reservoir);
7984

@@ -133,7 +138,7 @@ fn generate_initial_reservoir(world_position: vec3<f32>, world_normal: vec3<f32>
133138
fn load_temporal_reservoir(pixel_id: vec2<u32>, depth: f32, world_position: vec3<f32>, world_normal: vec3<f32>) -> Reservoir {
134139
let motion_vector = textureLoad(motion_vectors, pixel_id, 0).xy;
135140
let temporal_pixel_id_float = round(vec2<f32>(pixel_id) - (motion_vector * view.main_pass_viewport.zw));
136-
let temporal_pixel_id = vec2<u32>(temporal_pixel_id_float);
141+
let temporal_pixel_id = permute_pixel(vec2<u32>(temporal_pixel_id_float), constants.frame_index, view.viewport.zw);
137142

138143
// Check if the current pixel was off screen during the previous frame (current pixel is newly visible),
139144
// or if all temporal history should assumed to be invalid
@@ -173,14 +178,7 @@ fn load_spatial_reservoir(pixel_id: vec2<u32>, depth: f32, world_position: vec3<
173178
return empty_reservoir();
174179
}
175180

176-
var spatial_reservoir = load_reservoir_b(spatial_pixel_id);
177-
178-
if reservoir_valid(spatial_reservoir) {
179-
let resolved_light_sample = resolve_light_sample(spatial_reservoir.sample, light_sources[spatial_reservoir.sample.light_id >> 16u]);
180-
spatial_reservoir.unbiased_contribution_weight *= trace_light_visibility(world_position, resolved_light_sample.world_position);
181-
}
182-
183-
return spatial_reservoir;
181+
return load_reservoir_b(spatial_pixel_id);
184182
}
185183

186184
fn get_neighbor_pixel_id(center_pixel_id: vec2<u32>, rng: ptr<function, u32>) -> vec2<u32> {

crates/bevy_solari/src/realtime/restir_gi.wgsl

Lines changed: 24 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
#import bevy_render::maths::PI
77
#import bevy_render::view::View
88
#import bevy_solari::brdf::evaluate_diffuse_brdf
9-
#import bevy_solari::gbuffer_utils::{gpixel_resolve, pixel_dissimilar}
9+
#import bevy_solari::gbuffer_utils::{gpixel_resolve, pixel_dissimilar, permute_pixel}
1010
#import bevy_solari::sampling::{sample_random_light, trace_point_visibility}
1111
#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN, RAY_T_MAX}
1212
#import bevy_solari::world_cache::query_world_cache
@@ -67,7 +67,9 @@ fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3<u32>) {
6767
let spatial = load_spatial_reservoir(global_id.xy, depth, surface.world_position, surface.world_normal, &rng);
6868
let merge_result = merge_reservoirs(input_reservoir, surface.world_position, surface.world_normal, surface.material.base_color / PI,
6969
spatial.reservoir, spatial.world_position, spatial.world_normal, spatial.diffuse_brdf, &rng);
70-
let combined_reservoir = merge_result.merged_reservoir;
70+
var combined_reservoir = merge_result.merged_reservoir;
71+
72+
combined_reservoir.radiance *= trace_point_visibility(surface.world_position, combined_reservoir.sample_point_world_position);
7173

7274
gi_reservoirs_a[pixel_index] = combined_reservoir;
7375

@@ -76,10 +78,6 @@ fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3<u32>) {
7678
var pixel_color = textureLoad(view_output, global_id.xy);
7779
pixel_color += vec4(merge_result.selected_sample_radiance * combined_reservoir.unbiased_contribution_weight * view.exposure * brdf, 0.0);
7880
textureStore(view_output, global_id.xy, pixel_color);
79-
80-
#ifdef VISUALIZE_WORLD_CACHE
81-
textureStore(view_output, global_id.xy, vec4(query_world_cache(surface.world_position, surface.world_normal, view.world_position) * view.exposure, 1.0));
82-
#endif
8381
}
8482

8583
fn generate_initial_reservoir(world_position: vec3<f32>, world_normal: vec3<f32>, rng: ptr<function, u32>) -> Reservoir {
@@ -107,7 +105,7 @@ fn generate_initial_reservoir(world_position: vec3<f32>, world_normal: vec3<f32>
107105
reservoir.radiance = direct_lighting.radiance;
108106
reservoir.unbiased_contribution_weight = direct_lighting.inverse_pdf * uniform_hemisphere_inverse_pdf();
109107
#else
110-
reservoir.radiance = query_world_cache(sample_point.world_position, sample_point.geometric_world_normal, view.world_position);
108+
reservoir.radiance = query_world_cache(sample_point.world_position, sample_point.geometric_world_normal, view.world_position, rng);
111109
reservoir.unbiased_contribution_weight = uniform_hemisphere_inverse_pdf();
112110
#endif
113111

@@ -120,43 +118,28 @@ fn generate_initial_reservoir(world_position: vec3<f32>, world_normal: vec3<f32>
120118
fn load_temporal_reservoir(pixel_id: vec2<u32>, depth: f32, world_position: vec3<f32>, world_normal: vec3<f32>) -> NeighborInfo {
121119
let motion_vector = textureLoad(motion_vectors, pixel_id, 0).xy;
122120
let temporal_pixel_id_float = round(vec2<f32>(pixel_id) - (motion_vector * view.main_pass_viewport.zw));
121+
let temporal_pixel_id = permute_pixel(vec2<u32>(temporal_pixel_id_float), constants.frame_index, view.viewport.zw);
123122

124123
// Check if the current pixel was off screen during the previous frame (current pixel is newly visible),
125124
// or if all temporal history should assumed to be invalid
126125
if any(temporal_pixel_id_float < vec2(0.0)) || any(temporal_pixel_id_float >= view.main_pass_viewport.zw) || bool(constants.reset) {
127126
return NeighborInfo(empty_reservoir(), vec3(0.0), vec3(0.0), vec3(0.0));
128127
}
129128

130-
let temporal_pixel_id_base = vec2<u32>(round(temporal_pixel_id_float));
131-
for (var i = 0u; i < 4u; i++) {
132-
let temporal_pixel_id = permute_pixel(temporal_pixel_id_base, i);
133-
134-
// Check if the pixel features have changed heavily between the current and previous frame
135-
let temporal_depth = textureLoad(previous_depth_buffer, temporal_pixel_id, 0);
136-
let temporal_surface = gpixel_resolve(textureLoad(previous_gbuffer, temporal_pixel_id, 0), temporal_depth, temporal_pixel_id, view.main_pass_viewport.zw, previous_view.world_from_clip);
137-
let temporal_diffuse_brdf = temporal_surface.material.base_color / PI;
138-
if pixel_dissimilar(depth, world_position, temporal_surface.world_position, world_normal, temporal_surface.world_normal, view) {
139-
continue;
140-
}
141-
142-
let temporal_pixel_index = temporal_pixel_id.x + temporal_pixel_id.y * u32(view.main_pass_viewport.z);
143-
var temporal_reservoir = gi_reservoirs_a[temporal_pixel_index];
144-
145-
temporal_reservoir.confidence_weight = min(temporal_reservoir.confidence_weight, CONFIDENCE_WEIGHT_CAP);
146-
147-
return NeighborInfo(temporal_reservoir, temporal_surface.world_position, temporal_surface.world_normal, temporal_diffuse_brdf);
129+
// Check if the pixel features have changed heavily between the current and previous frame
130+
let temporal_depth = textureLoad(previous_depth_buffer, temporal_pixel_id, 0);
131+
let temporal_surface = gpixel_resolve(textureLoad(previous_gbuffer, temporal_pixel_id, 0), temporal_depth, temporal_pixel_id, view.main_pass_viewport.zw, previous_view.world_from_clip);
132+
let temporal_diffuse_brdf = temporal_surface.material.base_color / PI;
133+
if pixel_dissimilar(depth, world_position, temporal_surface.world_position, world_normal, temporal_surface.world_normal, view) {
134+
return NeighborInfo(empty_reservoir(), vec3(0.0), vec3(0.0), vec3(0.0));
148135
}
149136

150-
return NeighborInfo(empty_reservoir(), vec3(0.0), vec3(0.0), vec3(0.0));
151-
}
137+
let temporal_pixel_index = temporal_pixel_id.x + temporal_pixel_id.y * u32(view.main_pass_viewport.z);
138+
var temporal_reservoir = gi_reservoirs_a[temporal_pixel_index];
152139

153-
fn permute_pixel(pixel_id: vec2<u32>, i: u32) -> vec2<u32> {
154-
let r = constants.frame_index + i;
155-
let offset = vec2(r & 3u, (r >> 2u) & 3u);
156-
var shifted_pixel_id = pixel_id + offset;
157-
shifted_pixel_id ^= vec2(3u);
158-
shifted_pixel_id -= offset;
159-
return min(shifted_pixel_id, vec2<u32>(view.main_pass_viewport.zw - 1.0));
140+
temporal_reservoir.confidence_weight = min(temporal_reservoir.confidence_weight, CONFIDENCE_WEIGHT_CAP);
141+
142+
return NeighborInfo(temporal_reservoir, temporal_surface.world_position, temporal_surface.world_normal, temporal_diffuse_brdf);
160143
}
161144

162145
fn load_spatial_reservoir(pixel_id: vec2<u32>, depth: f32, world_position: vec3<f32>, world_normal: vec3<f32>, rng: ptr<function, u32>) -> NeighborInfo {
@@ -170,9 +153,7 @@ fn load_spatial_reservoir(pixel_id: vec2<u32>, depth: f32, world_position: vec3<
170153
}
171154

172155
let spatial_pixel_index = spatial_pixel_id.x + spatial_pixel_id.y * u32(view.main_pass_viewport.z);
173-
var spatial_reservoir = gi_reservoirs_b[spatial_pixel_index];
174-
175-
spatial_reservoir.radiance *= trace_point_visibility(world_position, spatial_reservoir.sample_point_world_position);
156+
let spatial_reservoir = gi_reservoirs_b[spatial_pixel_index];
176157

177158
return NeighborInfo(spatial_reservoir, spatial_surface.world_position, spatial_surface.world_normal, spatial_diffuse_brdf);
178159
}
@@ -252,27 +233,19 @@ fn merge_reservoirs(
252233
rng: ptr<function, u32>,
253234
) -> ReservoirMergeResult {
254235
// Radiances for resampling
255-
let canonical_sample_radiance =
256-
canonical_reservoir.radiance *
257-
saturate(dot(normalize(canonical_reservoir.sample_point_world_position - canonical_world_position), canonical_world_normal));
258-
let other_sample_radiance =
259-
other_reservoir.radiance *
260-
saturate(dot(normalize(other_reservoir.sample_point_world_position - canonical_world_position), canonical_world_normal));
236+
let canonical_sample_radiance = canonical_reservoir.radiance * saturate(dot(normalize(canonical_reservoir.sample_point_world_position - canonical_world_position), canonical_world_normal));
237+
let other_sample_radiance = other_reservoir.radiance * saturate(dot(normalize(other_reservoir.sample_point_world_position - canonical_world_position), canonical_world_normal));
261238

262239
// Target functions for resampling and MIS
263240
let canonical_target_function_canonical_sample = luminance(canonical_sample_radiance * canonical_diffuse_brdf);
264241
let canonical_target_function_other_sample = luminance(other_sample_radiance * canonical_diffuse_brdf);
265242

266243
// Extra target functions for MIS
267244
let other_target_function_canonical_sample = luminance(
268-
canonical_reservoir.radiance *
269-
saturate(dot(normalize(canonical_reservoir.sample_point_world_position - other_world_position), other_world_normal)) *
270-
other_diffuse_brdf
245+
canonical_reservoir.radiance * saturate(dot(normalize(canonical_reservoir.sample_point_world_position - other_world_position), other_world_normal)) * other_diffuse_brdf
271246
);
272247
let other_target_function_other_sample = luminance(
273-
other_reservoir.radiance *
274-
saturate(dot(normalize(other_reservoir.sample_point_world_position - other_world_position), other_world_normal)) *
275-
other_diffuse_brdf
248+
other_reservoir.radiance * saturate(dot(normalize(other_reservoir.sample_point_world_position - other_world_position), other_world_normal)) * other_diffuse_brdf
276249
);
277250

278251
// Jacobians for resampling and MIS
@@ -299,19 +272,14 @@ fn merge_reservoirs(
299272
canonical_reservoir.confidence_weight * canonical_target_function_canonical_sample,
300273
other_reservoir.confidence_weight * other_target_function_canonical_sample * other_target_function_canonical_sample_jacobian,
301274
);
302-
let canonical_sample_resampling_weight = canonical_sample_mis_weight *
303-
canonical_target_function_canonical_sample *
304-
canonical_reservoir.unbiased_contribution_weight;
275+
let canonical_sample_resampling_weight = canonical_sample_mis_weight * canonical_target_function_canonical_sample * canonical_reservoir.unbiased_contribution_weight;
305276

306277
// Resampling weight for other sample
307278
let other_sample_mis_weight = balance_heuristic(
308279
other_reservoir.confidence_weight * other_target_function_other_sample,
309280
canonical_reservoir.confidence_weight * canonical_target_function_other_sample * canonical_target_function_other_sample_jacobian,
310281
);
311-
let other_sample_resampling_weight = other_sample_mis_weight *
312-
canonical_target_function_other_sample *
313-
other_reservoir.unbiased_contribution_weight *
314-
canonical_target_function_other_sample_jacobian;
282+
let other_sample_resampling_weight = other_sample_mis_weight * canonical_target_function_other_sample * other_reservoir.unbiased_contribution_weight * canonical_target_function_other_sample_jacobian;
315283

316284
// Perform resampling
317285
var combined_reservoir = empty_reservoir();

crates/bevy_solari/src/realtime/specular_gi.wgsl

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ fn specular_gi(@builtin(global_invocation_id) global_id: vec3<u32>) {
3232

3333
var radiance: vec3<f32>;
3434
var wi: vec3<f32>;
35-
if surface.material.roughness > 0.04 {
35+
if surface.material.roughness > 0.1 {
3636
// Surface is very rough, reuse the ReSTIR GI reservoir
3737
let gi_reservoir = gi_reservoirs_a[pixel_index];
3838
wi = normalize(gi_reservoir.sample_point_world_position - surface.world_position);
@@ -59,25 +59,31 @@ fn specular_gi(@builtin(global_invocation_id) global_id: vec3<u32>) {
5959
var pixel_color = textureLoad(view_output, global_id.xy);
6060
pixel_color += vec4(radiance, 0.0);
6161
textureStore(view_output, global_id.xy, pixel_color);
62+
63+
#ifdef VISUALIZE_WORLD_CACHE
64+
textureStore(view_output, global_id.xy, vec4(query_world_cache(surface.world_position, surface.world_normal, view.world_position, &rng) * view.exposure, 1.0));
65+
#endif
6266
}
6367

6468
fn trace_glossy_path(initial_ray_origin: vec3<f32>, initial_wi: vec3<f32>, rng: ptr<function, u32>) -> vec3<f32> {
6569
var ray_origin = initial_ray_origin;
6670
var wi = initial_wi;
6771

6872
// Trace up to three bounces, getting the net throughput from them
73+
var radiance = vec3(0.0);
6974
var throughput = vec3(1.0);
7075
for (var i = 0u; i < 3u; i += 1u) {
7176
// Trace ray
7277
let ray = trace_ray(ray_origin, wi, RAY_T_MIN, RAY_T_MAX, RAY_FLAG_NONE);
7378
if ray.kind == RAY_QUERY_INTERSECTION_NONE { break; }
7479
let ray_hit = resolve_ray_hit_full(ray);
7580

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, rng);
84+
7685
// Surface is very rough, terminate path in the world cache
77-
if ray_hit.material.roughness > 0.04 || i == 2u {
78-
let diffuse_brdf = ray_hit.material.base_color / PI;
79-
return throughput * diffuse_brdf * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position);
80-
}
86+
if ray_hit.material.roughness > 0.1 && i != 0u { break; }
8187

8288
// Sample new ray direction from the GGX BRDF for next bounce
8389
let TBN = calculate_tbn_mikktspace(ray_hit.world_normal, ray_hit.world_tangent);
@@ -93,11 +99,11 @@ fn trace_glossy_path(initial_ray_origin: vec3<f32>, initial_wi: vec3<f32>, rng:
9399
// Update throughput for next bounce
94100
let pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, ray_hit.material.roughness);
95101
let brdf = evaluate_brdf(N, wo, wi, ray_hit.material);
96-
let cos_theta = dot(wi, N);
102+
let cos_theta = saturate(dot(wi, N));
97103
throughput *= (brdf * cos_theta) / pdf;
98104
}
99105

100-
return vec3(0.0);
106+
return radiance;
101107
}
102108

103109
// Don't adjust the size of this struct without also adjusting GI_RESERVOIR_STRUCT_SIZE.

0 commit comments

Comments
 (0)