From c1a171045da3974842e5f80b9bf85bfd4a3bd18d Mon Sep 17 00:00:00 2001 From: Jasmine Schweitzer Date: Fri, 24 Oct 2025 16:11:41 -0400 Subject: [PATCH 1/6] Various Solari improvements --- crates/bevy_solari/src/realtime/node.rs | 4 +- .../bevy_solari/src/realtime/restir_di.wgsl | 29 ++++++++----- .../bevy_solari/src/realtime/restir_gi.wgsl | 42 ++++++++----------- .../bevy_solari/src/realtime/specular_gi.wgsl | 16 +++---- .../src/realtime/world_cache_query.wgsl | 8 ++-- 5 files changed, 52 insertions(+), 47 deletions(-) diff --git a/crates/bevy_solari/src/realtime/node.rs b/crates/bevy_solari/src/realtime/node.rs index 01f61381e445c..f8d4df339cd60 100644 --- a/crates/bevy_solari/src/realtime/node.rs +++ b/crates/bevy_solari/src/realtime/node.rs @@ -204,7 +204,9 @@ impl ViewNode for SolariLightingNode { let bind_group_resolve_dlss_rr_textures = view_dlss_rr_textures.map(|d| { render_context.render_device().create_bind_group( "solari_lighting_bind_group_resolve_dlss_rr_textures", - &self.bind_group_layout_resolve_dlss_rr_textures, + &pipeline_cache.get_bind_group_layout( + &self.bind_group_layout_resolve_dlss_rr_textures, + ), &BindGroupEntries::sequential(( &d.diffuse_albedo.default_view, &d.specular_albedo.default_view, diff --git a/crates/bevy_solari/src/realtime/restir_di.wgsl b/crates/bevy_solari/src/realtime/restir_di.wgsl index a1484d9e2349f..14bea9e8aa0cc 100644 --- a/crates/bevy_solari/src/realtime/restir_di.wgsl +++ b/crates/bevy_solari/src/realtime/restir_di.wgsl @@ -27,7 +27,7 @@ struct PushConstants { frame_index: u32, reset: u32 } var constants: PushConstants; -const INITIAL_SAMPLES = 32u; +const INITIAL_SAMPLES = 8u; const SPATIAL_REUSE_RADIUS_PIXELS = 30.0; const CONFIDENCE_WEIGHT_CAP = 20.0; @@ -73,7 +73,12 @@ fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3) { let input_reservoir = load_reservoir_b(global_id.xy); let spatial_reservoir = load_spatial_reservoir(global_id.xy, depth, surface.world_position, surface.world_normal, &rng); let merge_result = merge_reservoirs(input_reservoir, spatial_reservoir, surface.world_position, surface.world_normal, diffuse_brdf, &rng); - let combined_reservoir = merge_result.merged_reservoir; + var combined_reservoir = merge_result.merged_reservoir; + + if reservoir_valid(combined_reservoir) { + let resolved_light_sample = resolve_light_sample(combined_reservoir.sample, light_sources[combined_reservoir.sample.light_id >> 16u]); + combined_reservoir.unbiased_contribution_weight *= trace_light_visibility(surface.world_position, resolved_light_sample.world_position); + } store_reservoir_a(global_id.xy, combined_reservoir); @@ -133,7 +138,7 @@ fn generate_initial_reservoir(world_position: vec3, world_normal: vec3 fn load_temporal_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3) -> Reservoir { let motion_vector = textureLoad(motion_vectors, pixel_id, 0).xy; let temporal_pixel_id_float = round(vec2(pixel_id) - (motion_vector * view.main_pass_viewport.zw)); - let temporal_pixel_id = vec2(temporal_pixel_id_float); + let temporal_pixel_id = permute_pixel(vec2(temporal_pixel_id_float)); // Check if the current pixel was off screen during the previous frame (current pixel is newly visible), // or if all temporal history should assumed to be invalid @@ -164,6 +169,15 @@ fn load_temporal_reservoir(pixel_id: vec2, depth: f32, world_position: vec3 return temporal_reservoir; } +fn permute_pixel(pixel_id: vec2) -> vec2 { + let r = constants.frame_index; + let offset = vec2(r & 3u, (r >> 2u) & 3u); + var shifted_pixel_id = pixel_id + offset; + shifted_pixel_id ^= vec2(3u); + shifted_pixel_id -= offset; + return min(shifted_pixel_id, vec2(view.main_pass_viewport.zw - 1.0)); +} + fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3, rng: ptr) -> Reservoir { let spatial_pixel_id = get_neighbor_pixel_id(pixel_id, rng); @@ -173,14 +187,7 @@ fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3< return empty_reservoir(); } - var spatial_reservoir = load_reservoir_b(spatial_pixel_id); - - if reservoir_valid(spatial_reservoir) { - let resolved_light_sample = resolve_light_sample(spatial_reservoir.sample, light_sources[spatial_reservoir.sample.light_id >> 16u]); - spatial_reservoir.unbiased_contribution_weight *= trace_light_visibility(world_position, resolved_light_sample.world_position); - } - - return spatial_reservoir; + return load_reservoir_b(spatial_pixel_id); } fn get_neighbor_pixel_id(center_pixel_id: vec2, rng: ptr) -> vec2 { diff --git a/crates/bevy_solari/src/realtime/restir_gi.wgsl b/crates/bevy_solari/src/realtime/restir_gi.wgsl index 1017993f08648..14833ddf448e1 100644 --- a/crates/bevy_solari/src/realtime/restir_gi.wgsl +++ b/crates/bevy_solari/src/realtime/restir_gi.wgsl @@ -67,7 +67,9 @@ fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3) { let spatial = load_spatial_reservoir(global_id.xy, depth, surface.world_position, surface.world_normal, &rng); let merge_result = merge_reservoirs(input_reservoir, surface.world_position, surface.world_normal, surface.material.base_color / PI, spatial.reservoir, spatial.world_position, spatial.world_normal, spatial.diffuse_brdf, &rng); - let combined_reservoir = merge_result.merged_reservoir; + var combined_reservoir = merge_result.merged_reservoir; + + combined_reservoir.radiance *= trace_point_visibility(surface.world_position, combined_reservoir.sample_point_world_position); gi_reservoirs_a[pixel_index] = combined_reservoir; @@ -120,6 +122,7 @@ fn generate_initial_reservoir(world_position: vec3, world_normal: vec3 fn load_temporal_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3) -> NeighborInfo { let motion_vector = textureLoad(motion_vectors, pixel_id, 0).xy; let temporal_pixel_id_float = round(vec2(pixel_id) - (motion_vector * view.main_pass_viewport.zw)); + let temporal_pixel_id = permute_pixel(vec2(temporal_pixel_id_float)); // Check if the current pixel was off screen during the previous frame (current pixel is newly visible), // or if all temporal history should assumed to be invalid @@ -127,31 +130,24 @@ fn load_temporal_reservoir(pixel_id: vec2, depth: f32, world_position: vec3 return NeighborInfo(empty_reservoir(), vec3(0.0), vec3(0.0), vec3(0.0)); } - let temporal_pixel_id_base = vec2(round(temporal_pixel_id_float)); - for (var i = 0u; i < 4u; i++) { - let temporal_pixel_id = permute_pixel(temporal_pixel_id_base, i); - - // Check if the pixel features have changed heavily between the current and previous frame - let temporal_depth = textureLoad(previous_depth_buffer, temporal_pixel_id, 0); - 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); - let temporal_diffuse_brdf = temporal_surface.material.base_color / PI; - if pixel_dissimilar(depth, world_position, temporal_surface.world_position, world_normal, temporal_surface.world_normal, view) { - continue; - } - - let temporal_pixel_index = temporal_pixel_id.x + temporal_pixel_id.y * u32(view.main_pass_viewport.z); - var temporal_reservoir = gi_reservoirs_a[temporal_pixel_index]; + // Check if the pixel features have changed heavily between the current and previous frame + let temporal_depth = textureLoad(previous_depth_buffer, temporal_pixel_id, 0); + 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); + let temporal_diffuse_brdf = temporal_surface.material.base_color / PI; + if pixel_dissimilar(depth, world_position, temporal_surface.world_position, world_normal, temporal_surface.world_normal, view) { + return NeighborInfo(empty_reservoir(), vec3(0.0), vec3(0.0), vec3(0.0)); + } - temporal_reservoir.confidence_weight = min(temporal_reservoir.confidence_weight, CONFIDENCE_WEIGHT_CAP); + let temporal_pixel_index = temporal_pixel_id.x + temporal_pixel_id.y * u32(view.main_pass_viewport.z); + var temporal_reservoir = gi_reservoirs_a[temporal_pixel_index]; - return NeighborInfo(temporal_reservoir, temporal_surface.world_position, temporal_surface.world_normal, temporal_diffuse_brdf); - } + temporal_reservoir.confidence_weight = min(temporal_reservoir.confidence_weight, CONFIDENCE_WEIGHT_CAP); - return NeighborInfo(empty_reservoir(), vec3(0.0), vec3(0.0), vec3(0.0)); + return NeighborInfo(temporal_reservoir, temporal_surface.world_position, temporal_surface.world_normal, temporal_diffuse_brdf); } -fn permute_pixel(pixel_id: vec2, i: u32) -> vec2 { - let r = constants.frame_index + i; +fn permute_pixel(pixel_id: vec2) -> vec2 { + let r = constants.frame_index; let offset = vec2(r & 3u, (r >> 2u) & 3u); var shifted_pixel_id = pixel_id + offset; shifted_pixel_id ^= vec2(3u); @@ -170,9 +166,7 @@ fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3< } let spatial_pixel_index = spatial_pixel_id.x + spatial_pixel_id.y * u32(view.main_pass_viewport.z); - var spatial_reservoir = gi_reservoirs_b[spatial_pixel_index]; - - spatial_reservoir.radiance *= trace_point_visibility(world_position, spatial_reservoir.sample_point_world_position); + let spatial_reservoir = gi_reservoirs_b[spatial_pixel_index]; return NeighborInfo(spatial_reservoir, spatial_surface.world_position, spatial_surface.world_normal, spatial_diffuse_brdf); } diff --git a/crates/bevy_solari/src/realtime/specular_gi.wgsl b/crates/bevy_solari/src/realtime/specular_gi.wgsl index 07a28136ffc17..79f6513f16525 100644 --- a/crates/bevy_solari/src/realtime/specular_gi.wgsl +++ b/crates/bevy_solari/src/realtime/specular_gi.wgsl @@ -32,7 +32,7 @@ fn specular_gi(@builtin(global_invocation_id) global_id: vec3) { var radiance: vec3; var wi: vec3; - if surface.material.roughness > 0.04 { + if surface.material.roughness > 0.1 { // Surface is very rough, reuse the ReSTIR GI reservoir let gi_reservoir = gi_reservoirs_a[pixel_index]; wi = normalize(gi_reservoir.sample_point_world_position - surface.world_position); @@ -66,6 +66,7 @@ fn trace_glossy_path(initial_ray_origin: vec3, initial_wi: vec3, rng: var wi = initial_wi; // Trace up to three bounces, getting the net throughput from them + var radiance = vec3(0.0); var throughput = vec3(1.0); for (var i = 0u; i < 3u; i += 1u) { // Trace ray @@ -73,11 +74,12 @@ fn trace_glossy_path(initial_ray_origin: vec3, initial_wi: vec3, rng: if ray.kind == RAY_QUERY_INTERSECTION_NONE { break; } let ray_hit = resolve_ray_hit_full(ray); + // Add world cache contribution + let diffuse_brdf = ray_hit.material.base_color / PI; + radiance += throughput * diffuse_brdf * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position); + // Surface is very rough, terminate path in the world cache - if ray_hit.material.roughness > 0.04 || i == 2u { - let diffuse_brdf = ray_hit.material.base_color / PI; - return throughput * diffuse_brdf * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position); - } + if ray_hit.material.roughness > 0.04 && i != 0u { break; } // Sample new ray direction from the GGX BRDF for next bounce let TBN = calculate_tbn_mikktspace(ray_hit.world_normal, ray_hit.world_tangent); @@ -93,11 +95,11 @@ fn trace_glossy_path(initial_ray_origin: vec3, initial_wi: vec3, rng: // Update throughput for next bounce let pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, ray_hit.material.roughness); let brdf = evaluate_brdf(N, wo, wi, ray_hit.material); - let cos_theta = dot(wi, N); + let cos_theta = saturate(dot(wi, N)); throughput *= (brdf * cos_theta) / pdf; } - return vec3(0.0); + return radiance; } // Don't adjust the size of this struct without also adjusting GI_RESERVOIR_STRUCT_SIZE. diff --git a/crates/bevy_solari/src/realtime/world_cache_query.wgsl b/crates/bevy_solari/src/realtime/world_cache_query.wgsl index 46f0fe920f2ce..67c380ab73cc3 100644 --- a/crates/bevy_solari/src/realtime/world_cache_query.wgsl +++ b/crates/bevy_solari/src/realtime/world_cache_query.wgsl @@ -1,9 +1,9 @@ #define_import_path bevy_solari::world_cache /// How responsive the world cache is to changes in lighting (higher is less responsive, lower is more responsive) -const WORLD_CACHE_MAX_TEMPORAL_SAMPLES: f32 = 20.0; +const WORLD_CACHE_MAX_TEMPORAL_SAMPLES: f32 = 5.0; /// Maximum amount of frames a cell can live for without being queried -const WORLD_CACHE_CELL_LIFETIME: u32 = 30u; +const WORLD_CACHE_CELL_LIFETIME: u32 = 4u; /// Maximum amount of attempts to find a cache entry after a hash collision const WORLD_CACHE_MAX_SEARCH_STEPS: u32 = 3u; @@ -57,8 +57,8 @@ fn query_world_cache(world_position: vec3, world_normal: vec3, view_po world_cache_geometry_data[key].world_normal = world_normal; return vec3(0.0); } else { - // Collision - jump to another entry - key = wrap_key(pcg_hash(key)); + // Collision - linear probe to next entry + key += 1u; } } From dac71e9a3e24ad1a95ca8d30ec8692a3e9013361 Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Fri, 24 Oct 2025 21:13:38 -0400 Subject: [PATCH 2/6] Fmt --- crates/bevy_solari/src/realtime/node.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/bevy_solari/src/realtime/node.rs b/crates/bevy_solari/src/realtime/node.rs index f8d4df339cd60..27fa95cd298bd 100644 --- a/crates/bevy_solari/src/realtime/node.rs +++ b/crates/bevy_solari/src/realtime/node.rs @@ -204,9 +204,8 @@ impl ViewNode for SolariLightingNode { let bind_group_resolve_dlss_rr_textures = view_dlss_rr_textures.map(|d| { render_context.render_device().create_bind_group( "solari_lighting_bind_group_resolve_dlss_rr_textures", - &pipeline_cache.get_bind_group_layout( - &self.bind_group_layout_resolve_dlss_rr_textures, - ), + &pipeline_cache + .get_bind_group_layout(&self.bind_group_layout_resolve_dlss_rr_textures), &BindGroupEntries::sequential(( &d.diffuse_albedo.default_view, &d.specular_albedo.default_view, From 49c9f8ff2826c2818751eb818bc5c1a22b7e1f67 Mon Sep 17 00:00:00 2001 From: Jasmine Schweitzer Date: Fri, 31 Oct 2025 09:58:11 -0400 Subject: [PATCH 3/6] Extract permute_pixel --- .../src/realtime/gbuffer_utils.wgsl | 9 ++++ .../bevy_solari/src/realtime/restir_di.wgsl | 18 +++----- .../bevy_solari/src/realtime/restir_gi.wgsl | 43 ++++++------------- 3 files changed, 27 insertions(+), 43 deletions(-) diff --git a/crates/bevy_solari/src/realtime/gbuffer_utils.wgsl b/crates/bevy_solari/src/realtime/gbuffer_utils.wgsl index 8b5a6c00df2da..632809081ec2c 100644 --- a/crates/bevy_solari/src/realtime/gbuffer_utils.wgsl +++ b/crates/bevy_solari/src/realtime/gbuffer_utils.wgsl @@ -44,3 +44,12 @@ fn pixel_dissimilar(depth: f32, world_position: vec3, other_world_position: return tangent_plane_distance / view_z > 0.003 || dot(normal, other_normal) < 0.906; } + +fn permute_pixel(pixel_id: vec2, frame_index: u32, view_size: vec2) -> vec2 { + let r = frame_index; + let offset = vec2(r & 3u, (r >> 2u) & 3u); + var shifted_pixel_id = pixel_id + offset; + shifted_pixel_id ^= vec2(3u); + shifted_pixel_id -= offset; + return min(shifted_pixel_id, vec2(view_size - 1.0)); +} \ No newline at end of file diff --git a/crates/bevy_solari/src/realtime/restir_di.wgsl b/crates/bevy_solari/src/realtime/restir_di.wgsl index 14bea9e8aa0cc..49010a3014ba4 100644 --- a/crates/bevy_solari/src/realtime/restir_di.wgsl +++ b/crates/bevy_solari/src/realtime/restir_di.wgsl @@ -7,7 +7,7 @@ #import bevy_render::maths::PI #import bevy_render::view::View #import bevy_solari::brdf::evaluate_brdf -#import bevy_solari::gbuffer_utils::{gpixel_resolve, pixel_dissimilar} +#import bevy_solari::gbuffer_utils::{gpixel_resolve, pixel_dissimilar, permute_pixel} #import bevy_solari::presample_light_tiles::{ResolvedLightSamplePacked, unpack_resolved_light_sample} #import bevy_solari::sampling::{LightSample, calculate_resolved_light_contribution, resolve_and_calculate_light_contribution, resolve_light_sample, trace_light_visibility} #import bevy_solari::scene_bindings::{light_sources, previous_frame_light_id_translations, LIGHT_NOT_PRESENT_THIS_FRAME} @@ -24,7 +24,10 @@ @group(1) @binding(11) var previous_depth_buffer: texture_depth_2d; @group(1) @binding(12) var view: View; @group(1) @binding(13) var previous_view: PreviousViewUniforms; -struct PushConstants { frame_index: u32, reset: u32 } +struct PushConstants { + frame_index: u32, + reset: u32 +} var constants: PushConstants; const INITIAL_SAMPLES = 8u; @@ -138,7 +141,7 @@ fn generate_initial_reservoir(world_position: vec3, world_normal: vec3 fn load_temporal_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3) -> Reservoir { let motion_vector = textureLoad(motion_vectors, pixel_id, 0).xy; let temporal_pixel_id_float = round(vec2(pixel_id) - (motion_vector * view.main_pass_viewport.zw)); - let temporal_pixel_id = permute_pixel(vec2(temporal_pixel_id_float)); + let temporal_pixel_id = permute_pixel(vec2(temporal_pixel_id_float), constants.frame_index, view.viewport.zw); // Check if the current pixel was off screen during the previous frame (current pixel is newly visible), // or if all temporal history should assumed to be invalid @@ -169,15 +172,6 @@ fn load_temporal_reservoir(pixel_id: vec2, depth: f32, world_position: vec3 return temporal_reservoir; } -fn permute_pixel(pixel_id: vec2) -> vec2 { - let r = constants.frame_index; - let offset = vec2(r & 3u, (r >> 2u) & 3u); - var shifted_pixel_id = pixel_id + offset; - shifted_pixel_id ^= vec2(3u); - shifted_pixel_id -= offset; - return min(shifted_pixel_id, vec2(view.main_pass_viewport.zw - 1.0)); -} - fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3, rng: ptr) -> Reservoir { let spatial_pixel_id = get_neighbor_pixel_id(pixel_id, rng); diff --git a/crates/bevy_solari/src/realtime/restir_gi.wgsl b/crates/bevy_solari/src/realtime/restir_gi.wgsl index 14833ddf448e1..4f679e7279753 100644 --- a/crates/bevy_solari/src/realtime/restir_gi.wgsl +++ b/crates/bevy_solari/src/realtime/restir_gi.wgsl @@ -6,7 +6,7 @@ #import bevy_render::maths::PI #import bevy_render::view::View #import bevy_solari::brdf::evaluate_diffuse_brdf -#import bevy_solari::gbuffer_utils::{gpixel_resolve, pixel_dissimilar} +#import bevy_solari::gbuffer_utils::{gpixel_resolve, pixel_dissimilar, permute_pixel} #import bevy_solari::sampling::{sample_random_light, trace_point_visibility} #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 @@ -21,7 +21,10 @@ @group(1) @binding(11) var previous_depth_buffer: texture_depth_2d; @group(1) @binding(12) var view: View; @group(1) @binding(13) var previous_view: PreviousViewUniforms; -struct PushConstants { frame_index: u32, reset: u32 } +struct PushConstants { + frame_index: u32, + reset: u32 +} var constants: PushConstants; const SPATIAL_REUSE_RADIUS_PIXELS = 30.0; @@ -122,7 +125,7 @@ fn generate_initial_reservoir(world_position: vec3, world_normal: vec3 fn load_temporal_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3) -> NeighborInfo { let motion_vector = textureLoad(motion_vectors, pixel_id, 0).xy; let temporal_pixel_id_float = round(vec2(pixel_id) - (motion_vector * view.main_pass_viewport.zw)); - let temporal_pixel_id = permute_pixel(vec2(temporal_pixel_id_float)); + let temporal_pixel_id = permute_pixel(vec2(temporal_pixel_id_float), constants.frame_index, view.viewport.zw); // Check if the current pixel was off screen during the previous frame (current pixel is newly visible), // or if all temporal history should assumed to be invalid @@ -146,15 +149,6 @@ fn load_temporal_reservoir(pixel_id: vec2, depth: f32, world_position: vec3 return NeighborInfo(temporal_reservoir, temporal_surface.world_position, temporal_surface.world_normal, temporal_diffuse_brdf); } -fn permute_pixel(pixel_id: vec2) -> vec2 { - let r = constants.frame_index; - let offset = vec2(r & 3u, (r >> 2u) & 3u); - var shifted_pixel_id = pixel_id + offset; - shifted_pixel_id ^= vec2(3u); - shifted_pixel_id -= offset; - return min(shifted_pixel_id, vec2(view.main_pass_viewport.zw - 1.0)); -} - fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3, rng: ptr) -> NeighborInfo { let spatial_pixel_id = get_neighbor_pixel_id(pixel_id, rng); @@ -246,12 +240,8 @@ fn merge_reservoirs( rng: ptr, ) -> ReservoirMergeResult { // Radiances for resampling - let canonical_sample_radiance = - canonical_reservoir.radiance * - saturate(dot(normalize(canonical_reservoir.sample_point_world_position - canonical_world_position), canonical_world_normal)); - let other_sample_radiance = - other_reservoir.radiance * - saturate(dot(normalize(other_reservoir.sample_point_world_position - canonical_world_position), canonical_world_normal)); + let canonical_sample_radiance = canonical_reservoir.radiance * saturate(dot(normalize(canonical_reservoir.sample_point_world_position - canonical_world_position), canonical_world_normal)); + let other_sample_radiance = other_reservoir.radiance * saturate(dot(normalize(other_reservoir.sample_point_world_position - canonical_world_position), canonical_world_normal)); // Target functions for resampling and MIS let canonical_target_function_canonical_sample = luminance(canonical_sample_radiance * canonical_diffuse_brdf); @@ -259,14 +249,10 @@ fn merge_reservoirs( // Extra target functions for MIS let other_target_function_canonical_sample = luminance( - canonical_reservoir.radiance * - saturate(dot(normalize(canonical_reservoir.sample_point_world_position - other_world_position), other_world_normal)) * - other_diffuse_brdf + canonical_reservoir.radiance * saturate(dot(normalize(canonical_reservoir.sample_point_world_position - other_world_position), other_world_normal)) * other_diffuse_brdf ); let other_target_function_other_sample = luminance( - other_reservoir.radiance * - saturate(dot(normalize(other_reservoir.sample_point_world_position - other_world_position), other_world_normal)) * - other_diffuse_brdf + other_reservoir.radiance * saturate(dot(normalize(other_reservoir.sample_point_world_position - other_world_position), other_world_normal)) * other_diffuse_brdf ); // Jacobians for resampling and MIS @@ -293,19 +279,14 @@ fn merge_reservoirs( canonical_reservoir.confidence_weight * canonical_target_function_canonical_sample, other_reservoir.confidence_weight * other_target_function_canonical_sample * other_target_function_canonical_sample_jacobian, ); - let canonical_sample_resampling_weight = canonical_sample_mis_weight * - canonical_target_function_canonical_sample * - canonical_reservoir.unbiased_contribution_weight; + let canonical_sample_resampling_weight = canonical_sample_mis_weight * canonical_target_function_canonical_sample * canonical_reservoir.unbiased_contribution_weight; // Resampling weight for other sample let other_sample_mis_weight = balance_heuristic( other_reservoir.confidence_weight * other_target_function_other_sample, canonical_reservoir.confidence_weight * canonical_target_function_other_sample * canonical_target_function_other_sample_jacobian, ); - 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; + 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; // Perform resampling var combined_reservoir = empty_reservoir(); From 191b79414d2fa0e7592196e9673b926041ce6e0a Mon Sep 17 00:00:00 2001 From: Jasmine Schweitzer Date: Fri, 31 Oct 2025 09:59:06 -0400 Subject: [PATCH 4/6] Fix roughness cutoff --- crates/bevy_solari/src/realtime/restir_di.wgsl | 5 +---- crates/bevy_solari/src/realtime/restir_gi.wgsl | 5 +---- crates/bevy_solari/src/realtime/specular_gi.wgsl | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/bevy_solari/src/realtime/restir_di.wgsl b/crates/bevy_solari/src/realtime/restir_di.wgsl index 49010a3014ba4..31c8fa445b5cc 100644 --- a/crates/bevy_solari/src/realtime/restir_di.wgsl +++ b/crates/bevy_solari/src/realtime/restir_di.wgsl @@ -24,10 +24,7 @@ @group(1) @binding(11) var previous_depth_buffer: texture_depth_2d; @group(1) @binding(12) var view: View; @group(1) @binding(13) var previous_view: PreviousViewUniforms; -struct PushConstants { - frame_index: u32, - reset: u32 -} +struct PushConstants { frame_index: u32, reset: u32 } var constants: PushConstants; const INITIAL_SAMPLES = 8u; diff --git a/crates/bevy_solari/src/realtime/restir_gi.wgsl b/crates/bevy_solari/src/realtime/restir_gi.wgsl index 4f679e7279753..2a15e4e67d42e 100644 --- a/crates/bevy_solari/src/realtime/restir_gi.wgsl +++ b/crates/bevy_solari/src/realtime/restir_gi.wgsl @@ -21,10 +21,7 @@ @group(1) @binding(11) var previous_depth_buffer: texture_depth_2d; @group(1) @binding(12) var view: View; @group(1) @binding(13) var previous_view: PreviousViewUniforms; -struct PushConstants { - frame_index: u32, - reset: u32 -} +struct PushConstants { frame_index: u32, reset: u32 } var constants: PushConstants; const SPATIAL_REUSE_RADIUS_PIXELS = 30.0; diff --git a/crates/bevy_solari/src/realtime/specular_gi.wgsl b/crates/bevy_solari/src/realtime/specular_gi.wgsl index 79f6513f16525..468edaaaea89c 100644 --- a/crates/bevy_solari/src/realtime/specular_gi.wgsl +++ b/crates/bevy_solari/src/realtime/specular_gi.wgsl @@ -79,7 +79,7 @@ fn trace_glossy_path(initial_ray_origin: vec3, initial_wi: vec3, rng: radiance += throughput * diffuse_brdf * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position); // Surface is very rough, terminate path in the world cache - if ray_hit.material.roughness > 0.04 && i != 0u { break; } + if ray_hit.material.roughness > 0.1 && i != 0u { break; } // Sample new ray direction from the GGX BRDF for next bounce let TBN = calculate_tbn_mikktspace(ray_hit.world_normal, ray_hit.world_tangent); From a50470500a3204c1ebd8d7fafc7abd4f5573863d Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Sat, 1 Nov 2025 01:03:11 -0400 Subject: [PATCH 5/6] Jitter in tangent plane before looking up in world cache --- crates/bevy_solari/src/realtime/gbuffer_utils.wgsl | 2 +- crates/bevy_solari/src/realtime/restir_gi.wgsl | 6 +----- crates/bevy_solari/src/realtime/specular_gi.wgsl | 6 +++++- .../src/realtime/world_cache_query.wgsl | 14 ++++++++++---- .../src/realtime/world_cache_update.wgsl | 2 +- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/crates/bevy_solari/src/realtime/gbuffer_utils.wgsl b/crates/bevy_solari/src/realtime/gbuffer_utils.wgsl index 632809081ec2c..a9d513f77e8e1 100644 --- a/crates/bevy_solari/src/realtime/gbuffer_utils.wgsl +++ b/crates/bevy_solari/src/realtime/gbuffer_utils.wgsl @@ -52,4 +52,4 @@ fn permute_pixel(pixel_id: vec2, frame_index: u32, view_size: vec2) -> shifted_pixel_id ^= vec2(3u); shifted_pixel_id -= offset; return min(shifted_pixel_id, vec2(view_size - 1.0)); -} \ No newline at end of file +} diff --git a/crates/bevy_solari/src/realtime/restir_gi.wgsl b/crates/bevy_solari/src/realtime/restir_gi.wgsl index 2a15e4e67d42e..ed51cb9f5ace6 100644 --- a/crates/bevy_solari/src/realtime/restir_gi.wgsl +++ b/crates/bevy_solari/src/realtime/restir_gi.wgsl @@ -78,10 +78,6 @@ fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3) { 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); textureStore(view_output, global_id.xy, pixel_color); - -#ifdef VISUALIZE_WORLD_CACHE - textureStore(view_output, global_id.xy, vec4(query_world_cache(surface.world_position, surface.world_normal, view.world_position) * view.exposure, 1.0)); -#endif } fn generate_initial_reservoir(world_position: vec3, world_normal: vec3, rng: ptr) -> Reservoir { @@ -109,7 +105,7 @@ fn generate_initial_reservoir(world_position: vec3, world_normal: vec3 reservoir.radiance = direct_lighting.radiance; reservoir.unbiased_contribution_weight = direct_lighting.inverse_pdf * uniform_hemisphere_inverse_pdf(); #else - reservoir.radiance = query_world_cache(sample_point.world_position, sample_point.geometric_world_normal, view.world_position); + reservoir.radiance = query_world_cache(sample_point.world_position, sample_point.geometric_world_normal, view.world_position, rng); reservoir.unbiased_contribution_weight = uniform_hemisphere_inverse_pdf(); #endif diff --git a/crates/bevy_solari/src/realtime/specular_gi.wgsl b/crates/bevy_solari/src/realtime/specular_gi.wgsl index 468edaaaea89c..9900b190db9fc 100644 --- a/crates/bevy_solari/src/realtime/specular_gi.wgsl +++ b/crates/bevy_solari/src/realtime/specular_gi.wgsl @@ -59,6 +59,10 @@ fn specular_gi(@builtin(global_invocation_id) global_id: vec3) { var pixel_color = textureLoad(view_output, global_id.xy); pixel_color += vec4(radiance, 0.0); textureStore(view_output, global_id.xy, pixel_color); + +#ifdef VISUALIZE_WORLD_CACHE + textureStore(view_output, global_id.xy, vec4(query_world_cache(surface.world_position, surface.world_normal, view.world_position, &rng) * view.exposure, 1.0)); +#endif } fn trace_glossy_path(initial_ray_origin: vec3, initial_wi: vec3, rng: ptr) -> vec3 { @@ -76,7 +80,7 @@ fn trace_glossy_path(initial_ray_origin: vec3, initial_wi: vec3, rng: // Add world cache contribution let diffuse_brdf = ray_hit.material.base_color / PI; - radiance += throughput * diffuse_brdf * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position); + radiance += throughput * diffuse_brdf * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position, rng); // Surface is very rough, terminate path in the world cache if ray_hit.material.roughness > 0.1 && i != 0u { break; } diff --git a/crates/bevy_solari/src/realtime/world_cache_query.wgsl b/crates/bevy_solari/src/realtime/world_cache_query.wgsl index 67c380ab73cc3..f72ae7467435c 100644 --- a/crates/bevy_solari/src/realtime/world_cache_query.wgsl +++ b/crates/bevy_solari/src/realtime/world_cache_query.wgsl @@ -1,7 +1,7 @@ #define_import_path bevy_solari::world_cache /// How responsive the world cache is to changes in lighting (higher is less responsive, lower is more responsive) -const WORLD_CACHE_MAX_TEMPORAL_SAMPLES: f32 = 5.0; +const WORLD_CACHE_MAX_TEMPORAL_SAMPLES: f32 = 10.0; /// Maximum amount of frames a cell can live for without being queried const WORLD_CACHE_CELL_LIFETIME: u32 = 4u; /// Maximum amount of attempts to find a cache entry after a hash collision @@ -37,9 +37,15 @@ struct WorldCacheGeometryData { @group(1) @binding(22) var world_cache_active_cells_count: u32; #ifndef WORLD_CACHE_NON_ATOMIC_LIFE_BUFFER -fn query_world_cache(world_position: vec3, world_normal: vec3, view_position: vec3) -> vec3 { +fn query_world_cache(world_position: vec3, world_normal: vec3, view_position: vec3, rng: ptr) -> vec3 { let cell_size = get_cell_size(world_position, view_position); - let world_position_quantized = bitcast>(quantize_position(world_position, cell_size)); + + // https://tomclabault.github.io/blog/2025/regir, jitter_world_position_tangent_plane + let TBN = orthonormalize(world_normal); + let offset = (rand_vec2f(rng) * 2.0 - 1.0) * cell_size * 0.5; + let jittered_position = world_position + offset.x * TBN[0] + offset.y * TBN[1]; + + let world_position_quantized = bitcast>(quantize_position(jittered_position, cell_size)); let world_normal_quantized = bitcast>(quantize_normal(world_normal)); var key = compute_key(world_position_quantized, world_normal_quantized); let checksum = compute_checksum(world_position_quantized, world_normal_quantized); @@ -53,7 +59,7 @@ fn query_world_cache(world_position: vec3, world_normal: vec3, view_po } else if existing_checksum == WORLD_CACHE_EMPTY_CELL { // Cell is empty - reset cell lifetime so that it starts getting updated next frame atomicStore(&world_cache_life[key], WORLD_CACHE_CELL_LIFETIME); - world_cache_geometry_data[key].world_position = world_position; + world_cache_geometry_data[key].world_position = jittered_position; world_cache_geometry_data[key].world_normal = world_normal; return vec3(0.0); } else { diff --git a/crates/bevy_solari/src/realtime/world_cache_update.wgsl b/crates/bevy_solari/src/realtime/world_cache_update.wgsl index 43b11f279f51b..44a387c8fce71 100644 --- a/crates/bevy_solari/src/realtime/world_cache_update.wgsl +++ b/crates/bevy_solari/src/realtime/world_cache_update.wgsl @@ -37,7 +37,7 @@ fn sample_radiance(@builtin(workgroup_id) workgroup_id: vec3, @builtin(glob let ray_hit = trace_ray(geometry_data.world_position, ray_direction, RAY_T_MIN, RAY_T_MAX, RAY_FLAG_NONE); if ray_hit.kind != RAY_QUERY_INTERSECTION_NONE { let ray_hit = resolve_ray_hit_full(ray_hit); - new_radiance += ray_hit.material.base_color * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position); + new_radiance += ray_hit.material.base_color * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position, &rng); } #endif From 3669f59ad13ef1e5169b39811d8fdee2d9c39d99 Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:48:02 -0400 Subject: [PATCH 6/6] Add imports --- crates/bevy_solari/src/realtime/world_cache_query.wgsl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bevy_solari/src/realtime/world_cache_query.wgsl b/crates/bevy_solari/src/realtime/world_cache_query.wgsl index f72ae7467435c..dac506836badc 100644 --- a/crates/bevy_solari/src/realtime/world_cache_query.wgsl +++ b/crates/bevy_solari/src/realtime/world_cache_query.wgsl @@ -1,5 +1,8 @@ #define_import_path bevy_solari::world_cache +#import bevy_pbr::utils::rand_vec2f +#import bevy_render::maths::orthonormalize + /// How responsive the world cache is to changes in lighting (higher is less responsive, lower is more responsive) const WORLD_CACHE_MAX_TEMPORAL_SAMPLES: f32 = 10.0; /// Maximum amount of frames a cell can live for without being queried