Path: blob/main/crates/bevy_solari/src/realtime/restir_di.wgsl
9511 views
// https://intro-to-restir.cwyman.org/presentations/2023ReSTIR_Course_Notes.pdf
// https://d1qx31qr3h6wln.cloudfront.net/publications/ReSTIR%20GI.pdf
enable wgpu_ray_query;
#import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance
#import bevy_pbr::prepass_bindings::PreviousViewUniforms
#import bevy_pbr::utils::{rand_f, rand_range_u, sample_disk}
#import bevy_render::maths::PI
#import bevy_render::view::View
#import bevy_solari::brdf::{evaluate_brdf, evaluate_diffuse_brdf}
#import bevy_solari::gbuffer_utils::{gpixel_resolve, pixel_dissimilar, permute_pixel}
#import bevy_solari::presample_light_tiles::unpack_resolved_light_sample
#import bevy_solari::sampling::{LightSample, NULL_LIGHT_ID, calculate_resolved_light_contribution, resolve_and_calculate_light_contribution, resolve_light_sample, trace_light_visibility, balance_heuristic}
#import bevy_solari::scene_bindings::{light_sources, previous_frame_light_id_translations, LIGHT_NOT_PRESENT_THIS_FRAME}
#import bevy_solari::specular_gi::SPECULAR_GI_FOR_DI_ROUGHNESS_THRESHOLD
#import bevy_solari::realtime_bindings::{view_output, light_tile_samples, light_tile_resolved_samples, di_reservoirs_a, di_reservoirs_b, gbuffer, depth_buffer, motion_vectors, previous_gbuffer, previous_depth_buffer, view, previous_view, constants, ResolvedLightSamplePacked}
const INITIAL_SAMPLES = 8u;
const SPATIAL_REUSE_RADIUS_PIXELS = 30.0;
const CONFIDENCE_WEIGHT_CAP = 20.0;
@compute @workgroup_size(8, 8, 1)
fn initial_and_temporal(@builtin(workgroup_id) workgroup_id: vec3<u32>, @builtin(global_invocation_id) global_id: vec3<u32>) {
if any(global_id.xy >= vec2u(view.main_pass_viewport.zw)) { return; }
let pixel_index = global_id.x + global_id.y * u32(view.main_pass_viewport.z);
var rng = pixel_index + constants.frame_index;
let depth = textureLoad(depth_buffer, global_id.xy, 0);
if depth == 0.0 {
store_reservoir_b(global_id.xy, empty_reservoir());
return;
}
let surface = gpixel_resolve(textureLoad(gbuffer, global_id.xy, 0), depth, global_id.xy, view.main_pass_viewport.zw, view.world_from_clip);
let diffuse_brdf = surface.material.base_color / PI;
let initial_reservoir = generate_initial_reservoir(surface.world_position, surface.world_normal, diffuse_brdf, workgroup_id.xy, &rng);
let temporal = load_temporal_reservoir(global_id.xy, depth, surface.world_position, surface.world_normal);
let merge_result = merge_reservoirs(initial_reservoir, surface.world_position, surface.world_normal, diffuse_brdf,
temporal.reservoir, temporal.world_position, temporal.world_normal, temporal.diffuse_brdf, &rng);
store_reservoir_b(global_id.xy, merge_result.merged_reservoir);
}
@compute @workgroup_size(8, 8, 1)
fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3<u32>) {
if any(global_id.xy >= vec2u(view.main_pass_viewport.zw)) { return; }
let pixel_index = global_id.x + global_id.y * u32(view.main_pass_viewport.z);
var rng = pixel_index + constants.frame_index;
let depth = textureLoad(depth_buffer, global_id.xy, 0);
if depth == 0.0 {
store_reservoir_a(global_id.xy, empty_reservoir());
return;
}
let surface = gpixel_resolve(textureLoad(gbuffer, global_id.xy, 0), depth, global_id.xy, view.main_pass_viewport.zw, view.world_from_clip);
let diffuse_brdf = surface.material.base_color / PI;
let input_reservoir = load_reservoir_b(global_id.xy);
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, diffuse_brdf,
spatial.reservoir, spatial.world_position, spatial.world_normal, spatial.diffuse_brdf, &rng);
var combined_reservoir = merge_result.merged_reservoir;
// More accuracy, less stability
#ifndef BIASED_RESAMPLING
store_reservoir_a(global_id.xy, combined_reservoir);
#endif
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);
}
// More stability, less accuracy (shadows extend further out than they should)
#ifdef BIASED_RESAMPLING
store_reservoir_a(global_id.xy, combined_reservoir);
#endif
let wo = normalize(view.world_position - surface.world_position);
var brdf: vec3<f32>;
// If the surface is very smooth, let specular GI handle the specular lobe
if surface.material.roughness <= SPECULAR_GI_FOR_DI_ROUGHNESS_THRESHOLD {
brdf = evaluate_diffuse_brdf(surface.world_normal, merge_result.wi, surface.material.base_color, surface.material.metallic);
} else {
brdf = evaluate_brdf(surface.world_normal, wo, merge_result.wi, surface.material);
}
var pixel_color = merge_result.selected_sample_radiance * combined_reservoir.unbiased_contribution_weight;
pixel_color *= brdf;
pixel_color += surface.material.emissive;
pixel_color *= view.exposure;
textureStore(view_output, global_id.xy, vec4(pixel_color, 1.0));
}
fn generate_initial_reservoir(world_position: vec3<f32>, world_normal: vec3<f32>, diffuse_brdf: vec3<f32>, workgroup_id: vec2<u32>, rng: ptr<function, u32>) -> Reservoir {
var workgroup_rng = (workgroup_id.x * 5782582u) + workgroup_id.y;
let light_tile_start = rand_range_u(128u, &workgroup_rng) * 1024u;
var reservoir = empty_reservoir();
var weight_sum = 0.0;
let mis_weight = 1.0 / f32(INITIAL_SAMPLES);
var reservoir_target_function = 0.0;
var light_sample_world_position = vec4(0.0);
var selected_tile_sample = 0u;
for (var i = 0u; i < INITIAL_SAMPLES; i++) {
let tile_sample = light_tile_start + rand_range_u(1024u, rng);
let resolved_light_sample = unpack_resolved_light_sample(light_tile_resolved_samples[tile_sample], view.exposure);
let light_contribution = calculate_resolved_light_contribution(resolved_light_sample, world_position, world_normal);
let target_function = luminance(light_contribution.radiance * diffuse_brdf * saturate(dot(light_contribution.wi, world_normal)));
let resampling_weight = mis_weight * (target_function * light_contribution.inverse_pdf);
weight_sum += resampling_weight;
if rand_f(rng) < resampling_weight / weight_sum {
reservoir_target_function = target_function;
light_sample_world_position = resolved_light_sample.world_position;
selected_tile_sample = tile_sample;
}
}
if reservoir_target_function != 0.0 {
reservoir.sample = light_tile_samples[selected_tile_sample];
}
if reservoir_valid(reservoir) {
let inverse_target_function = select(0.0, 1.0 / reservoir_target_function, reservoir_target_function > 0.0);
reservoir.unbiased_contribution_weight = weight_sum * inverse_target_function;
reservoir.unbiased_contribution_weight *= trace_light_visibility(world_position, light_sample_world_position);
}
reservoir.confidence_weight = 1.0;
return reservoir;
}
fn load_temporal_reservoir(pixel_id: vec2<u32>, depth: f32, world_position: vec3<f32>, world_normal: vec3<f32>) -> NeighborInfo {
let motion_vector = textureLoad(motion_vectors, pixel_id, 0).xy;
let temporal_pixel_id_float = round(vec2<f32>(pixel_id) - (motion_vector * view.main_pass_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
if any(temporal_pixel_id_float < vec2(0.0)) || any(temporal_pixel_id_float >= view.main_pass_viewport.zw) || bool(constants.reset) {
return NeighborInfo(empty_reservoir(), vec3(0.0), vec3(0.0), vec3(0.0));
}
let permuted_temporal_pixel_id = permute_pixel(vec2<u32>(temporal_pixel_id_float), constants.frame_index, view.main_pass_viewport.zw);
var temporal = load_temporal_reservoir_inner(permuted_temporal_pixel_id, depth, world_position, world_normal);
// If permuted reprojection failed (tends to happen on object edges), try point reprojection
if !reservoir_valid(temporal.reservoir) {
temporal = load_temporal_reservoir_inner(vec2<u32>(temporal_pixel_id_float), depth, world_position, world_normal);
}
// Check if the light selected in the previous frame no longer exists in the current frame (e.g. entity despawned)
let previous_light_id = temporal.reservoir.sample.light_id >> 16u;
let triangle_id = temporal.reservoir.sample.light_id & 0xFFFFu;
let light_id = previous_frame_light_id_translations[previous_light_id];
if light_id == LIGHT_NOT_PRESENT_THIS_FRAME {
return NeighborInfo(empty_reservoir(), vec3(0.0), vec3(0.0), vec3(0.0));
}
temporal.reservoir.sample.light_id = (light_id << 16u) | triangle_id;
temporal.reservoir.confidence_weight = min(temporal.reservoir.confidence_weight, CONFIDENCE_WEIGHT_CAP);
return temporal;
}
fn load_temporal_reservoir_inner(temporal_pixel_id: vec2<u32>, depth: f32, world_position: vec3<f32>, world_normal: vec3<f32>) -> NeighborInfo {
// 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));
}
let temporal_reservoir = load_reservoir_a(temporal_pixel_id);
return NeighborInfo(temporal_reservoir, temporal_surface.world_position, temporal_surface.world_normal, temporal_diffuse_brdf);
}
fn load_spatial_reservoir(pixel_id: vec2<u32>, depth: f32, world_position: vec3<f32>, world_normal: vec3<f32>, rng: ptr<function, u32>) -> NeighborInfo {
var search_radius = SPATIAL_REUSE_RADIUS_PIXELS;
for (var i = 0u; i < 5u; i++) {
let spatial_pixel_id = get_neighbor_pixel_id(pixel_id, search_radius, rng);
let spatial_depth = textureLoad(depth_buffer, spatial_pixel_id, 0);
let spatial_surface = gpixel_resolve(textureLoad(gbuffer, spatial_pixel_id, 0), spatial_depth, spatial_pixel_id, view.main_pass_viewport.zw, view.world_from_clip);
let spatial_diffuse_brdf = spatial_surface.material.base_color / PI;
if pixel_dissimilar(depth, world_position, spatial_surface.world_position, world_normal, spatial_surface.world_normal, view) {
search_radius /= 2.0;
continue;
}
let spatial_reservoir = load_reservoir_b(spatial_pixel_id);
return NeighborInfo(spatial_reservoir, spatial_surface.world_position, spatial_surface.world_normal, spatial_diffuse_brdf);
}
return NeighborInfo(empty_reservoir(), world_position, world_normal, vec3(0.0));
}
fn get_neighbor_pixel_id(center_pixel_id: vec2<u32>, search_radius: f32, rng: ptr<function, u32>) -> vec2<u32> {
var spatial_id = vec2<f32>(center_pixel_id) + sample_disk(search_radius, rng);
spatial_id = clamp(spatial_id, vec2(0.0), view.main_pass_viewport.zw - 1.0);
return vec2<u32>(spatial_id);
}
struct NeighborInfo {
reservoir: Reservoir,
world_position: vec3<f32>,
world_normal: vec3<f32>,
diffuse_brdf: vec3<f32>,
}
struct Reservoir {
sample: LightSample,
confidence_weight: f32,
unbiased_contribution_weight: f32,
}
fn empty_reservoir() -> Reservoir {
return Reservoir(
LightSample(NULL_LIGHT_ID, 0u),
0.0,
0.0,
);
}
fn reservoir_valid(reservoir: Reservoir) -> bool {
return reservoir.sample.light_id != NULL_LIGHT_ID;
}
fn pack_reservoir(reservoir: Reservoir) -> vec4<u32> {
let weights = bitcast<vec2<u32>>(vec2<f32>(reservoir.confidence_weight, reservoir.unbiased_contribution_weight));
return vec4<u32>(reservoir.sample.light_id, reservoir.sample.seed, weights);
}
fn store_reservoir_a(pixel: vec2<u32>, reservoir: Reservoir) {
textureStore(di_reservoirs_a, pixel, pack_reservoir(reservoir));
}
fn store_reservoir_b(pixel: vec2<u32>, reservoir: Reservoir) {
textureStore(di_reservoirs_b, pixel, pack_reservoir(reservoir));
}
fn unpack_reservoir(packed: vec4<u32>) -> Reservoir {
let weights = bitcast<vec2<f32>>(packed.zw);
return Reservoir(LightSample(packed.x, packed.y), weights.x, weights.y);
}
fn load_reservoir_a(pixel: vec2<u32>) -> Reservoir {
return unpack_reservoir(textureLoad(di_reservoirs_a, pixel));
}
fn load_reservoir_b(pixel: vec2<u32>) -> Reservoir {
return unpack_reservoir(textureLoad(di_reservoirs_b, pixel));
}
struct ReservoirMergeResult {
merged_reservoir: Reservoir,
selected_sample_radiance: vec3<f32>,
wi: vec3<f32>,
}
fn merge_reservoirs(
canonical_reservoir: Reservoir,
canonical_world_position: vec3<f32>,
canonical_world_normal: vec3<f32>,
canonical_diffuse_brdf: vec3<f32>,
other_reservoir: Reservoir,
other_world_position: vec3<f32>,
other_world_normal: vec3<f32>,
other_diffuse_brdf: vec3<f32>,
rng: ptr<function, u32>,
) -> ReservoirMergeResult {
// Contributions for resampling
let canonical_contribution_canonical_sample = reservoir_contribution(canonical_reservoir, canonical_world_position, canonical_world_normal, canonical_diffuse_brdf);
let canonical_contribution_other_sample = reservoir_contribution(other_reservoir, canonical_world_position, canonical_world_normal, canonical_diffuse_brdf);
// Extra contributions for MIS
let other_contribution_canonical_sample = reservoir_contribution(canonical_reservoir, other_world_position, other_world_normal, other_diffuse_brdf);
let other_contribution_other_sample = reservoir_contribution(other_reservoir, other_world_position, other_world_normal, other_diffuse_brdf);
// Resampling weight for canonical sample
let canonical_sample_mis_weight = balance_heuristic(
canonical_reservoir.confidence_weight * canonical_contribution_canonical_sample.target_function,
other_reservoir.confidence_weight * other_contribution_canonical_sample.target_function,
);
let canonical_sample_resampling_weight = canonical_sample_mis_weight * canonical_contribution_canonical_sample.target_function * canonical_reservoir.unbiased_contribution_weight;
// Resampling weight for other sample
let other_sample_mis_weight = balance_heuristic(
other_reservoir.confidence_weight * other_contribution_other_sample.target_function,
canonical_reservoir.confidence_weight * canonical_contribution_other_sample.target_function,
);
let other_sample_resampling_weight = other_sample_mis_weight * canonical_contribution_other_sample.target_function * other_reservoir.unbiased_contribution_weight;
// Perform resampling
var combined_reservoir = empty_reservoir();
combined_reservoir.confidence_weight = canonical_reservoir.confidence_weight + other_reservoir.confidence_weight;
let weight_sum = canonical_sample_resampling_weight + other_sample_resampling_weight;
if rand_f(rng) < other_sample_resampling_weight / weight_sum {
combined_reservoir.sample = other_reservoir.sample;
let inverse_target_function = select(0.0, 1.0 / canonical_contribution_other_sample.target_function, canonical_contribution_other_sample.target_function > 0.0);
combined_reservoir.unbiased_contribution_weight = weight_sum * inverse_target_function;
return ReservoirMergeResult(combined_reservoir, canonical_contribution_other_sample.radiance, canonical_contribution_other_sample.wi);
} else {
combined_reservoir.sample = canonical_reservoir.sample;
let inverse_target_function = select(0.0, 1.0 / canonical_contribution_canonical_sample.target_function, canonical_contribution_canonical_sample.target_function > 0.0);
combined_reservoir.unbiased_contribution_weight = weight_sum * inverse_target_function;
return ReservoirMergeResult(combined_reservoir, canonical_contribution_canonical_sample.radiance, canonical_contribution_canonical_sample.wi);
}
}
struct ReservoirContribution {
radiance: vec3<f32>,
target_function: f32,
wi: vec3<f32>,
}
// TODO: Have input take ResolvedLightSample instead of reservoir.light_sample
fn reservoir_contribution(reservoir: Reservoir, world_position: vec3<f32>, world_normal: vec3<f32>, diffuse_brdf: vec3<f32>) -> ReservoirContribution {
if !reservoir_valid(reservoir) { return ReservoirContribution(vec3(0.0), 0.0, vec3(0.0)); }
let light_contribution = resolve_and_calculate_light_contribution(reservoir.sample, world_position, world_normal);
let target_function = luminance(light_contribution.radiance * diffuse_brdf * saturate(dot(light_contribution.wi, world_normal)));
return ReservoirContribution(light_contribution.radiance, target_function, light_contribution.wi);
}