Search across packages, versions, files, declarations, and comments. Use multiple terms to narrow results (AND), and add @vertex, @fragment, or @compute to filter by shader stage.
1import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance;
2import bevy_pbr::render::utils::{rand_f, rand_range_u};
3import bevy_render::maths::PI;
4import bevy_render::utils::octahedral_encode;
5import package::scene::brdf::{brdf_pdf, evaluate_and_sample_brdf, evaluate_brdf, EvaluateAndSampleBrdfResult, F_AB};
6import package::realtime::presample_light_tiles::unpack_resolved_light_sample;
7import package::realtime::bindings::{empty_reservoir, light_tile_resolved_samples, light_tile_samples, Reservoir, constants, view};
8import package::scene::sampling::{calculate_resolved_light_contribution, isinf, LightSample, NULL_LIGHT_ID, power_heuristic, trace_visibility, sample_environment_map_light};
9import package::scene::bindings::{light_sources, RAY_T_MAX, RAY_T_MIN, resolve_ray_hit_full, ResolvedMaterial, ResolvedRayHitFull, trace_ray};
10import package::realtime::world_cache_query::{get_cell_size, query_world_cache, WORLD_CACHE_CELL_LIFETIME};
12@if(DLSS_RR_GUIDE_BUFFERS)
13import package::realtime::resolve_dlss_rr_textures::{is_delta_mirror, primary_surface_glossy, psr_bounce_reflectance, psr_chain_mirror, psr_environment_miss, psr_init, replace_primary_surface};
15enable wgpu_ray_query;
17const RECONNECTION_FOOTPRINT_KAPPA = 0.02;
18const RECONNECTION_ROUGHNESS_MIN = 0.6;
19const RECONNECTION_RELAX_DISTANCE = 1.0;
21const CACHE_TERMINATION_MIN_SOLID_ANGLE = PI;
23/// What tracing one path produced.
24///
25/// `radiance` is shaded straight into the pixel. With ReSTIR the reservoir additionally carries the
26/// candidate chosen for reuse, and `radiance` holds the radiance that can not be reused.
27/// Without ReSTIR there is no reservoir and `radiance` is the entire estimate.
28struct InitialSamplingResult {
29 @if(RESTIR) reservoir: Reservoir,
30 radiance: vec3<f32>,
33/// Path vertices use the following convention: x0 = camera, x1 = primary ray hit (the G-buffer
34/// surface), x2 = first BRDF-sampled hit (the reconnection vertex).
35struct PathState {
36 ray_origin: vec3<f32>,
37 normal: vec3<f32>,
38 wo: vec3<f32>,
39 material: ResolvedMaterial,
40 /// Throughput past x1. With ReSTIR the brdf*cos at x1 is factored out of it, see x1_brdf.
41 throughput_past_first_hit: vec3<f32>,
42 /// Reconnection vertex x2, the first BRDF-sampled hit shared by every length >= 2 candidate
43 @if(RESTIR) x2_position: vec3<f32>,
44 @if(RESTIR) x2_normal: vec3<f32>,
45 /// If false, candidates built on x2 are shaded directly into radiance instead of
46 /// published to the reservoir
47 @if(RESTIR) x2_reusable: bool,
48 /// brdf*cos at x1 for the direction toward x2, applied at shade time
49 @if(RESTIR) x1_brdf: vec3<f32>,
50 /// Radiance shaded directly at this pixel
51 radiance: vec3<f32>,
52 @if(RESTIR) reservoir: Reservoir,
53 @if(RESTIR) weight_sum: f32,
54 @if(RESTIR) selected_target_function: f32,
57fn new_path_state(world_position: vec3<f32>, world_normal: vec3<f32>, wo: vec3<f32>, material: ResolvedMaterial) -> PathState {
58 var path: PathState;
59 path.ray_origin = world_position + (world_normal * RAY_T_MIN);
60 path.normal = world_normal;
61 path.wo = wo;
62 path.material = material;
63 path.throughput_past_first_hit = vec3(1.0);
64 path.radiance = vec3(0.0);
65 @if(RESTIR) {
66 path.x1_brdf = vec3(0.0);
67 path.x2_position = vec3(0.0);
68 path.x2_normal = vec3(0.0);
69 path.x2_reusable = false;
70 path.reservoir = empty_reservoir();
71 path.reservoir.confidence_weight = 1.0;
72 path.weight_sum = 0.0;
73 path.selected_target_function = 0.0;
74 }
75 return path;
78fn generate_initial_path(world_position: vec3<f32>, world_normal: vec3<f32>, material: ResolvedMaterial, workgroup_id: vec2<u32>, pixel_id: vec2<u32>, rng: ptr<function, u32>) -> InitialSamplingResult {
79 let wo = normalize(view.world_position - world_position);
80 let primary_NdotV = max(dot(world_normal, wo), 0.0001);
81 let primary_F_ab = F_AB(material.perceptual_roughness, primary_NdotV);
83 var path = new_path_state(world_position, world_normal, wo, material);
85 @if(DLSS_RR_GUIDE_BUFFERS)
86 var psr = psr_init(world_normal, material);
88 for (var bounce = 0u; bounce < constants.max_bounces; bounce++) {
89 let NdotV = max(dot(path.normal, path.wo), 0.0001);
90 let F_ab = F_AB(path.material.perceptual_roughness, NdotV);
92 // Stochastic NEE, with probability proportional to how diffuse the vertex is. Mirror-like
93 // metals have too narrow a lobe for NEE to help, so mostly skip it there and let
94 // BRDF-sampled emissive do the work. Pure dielectrics always run NEE.
95 let p_nee = mix(1.0, path.material.perceptual_roughness, path.material.metallic);
96 let di_samples = select(constants.secondary_di_samples, constants.primary_di_samples, bounce == 0u);
97 generate_nee_candidate(&path, F_ab, p_nee, di_samples, workgroup_id, bounce, rng);
99 // Sample the BRDF and trace the next ray
100 let next_bounce = evaluate_and_sample_brdf(path.wo, path.normal, path.material, F_ab, rng);
101 if next_bounce.pdf == 0.0 { break; }
102 let ray = trace_ray(path.ray_origin, next_bounce.wi, RAY_T_MIN, RAY_T_MAX, RAY_FLAG_NONE);
104 if ray.kind == RAY_QUERY_INTERSECTION_NONE {
105 @if(DLSS_RR_GUIDE_BUFFERS)
106 if !psr.finished && !next_bounce.diffuse_selected {
107 psr_environment_miss(pixel_id, wo);
108 }
110 let environment_map_light_radiance = sample_environment_map_light(next_bounce.wi);
111 @if(RESTIR) {
112 generate_environment_map_light_candidate(&path, next_bounce, environment_map_light_radiance, primary_F_ab, bounce, rng);
113 } @else {
114 path.radiance += path.throughput_past_first_hit * next_bounce.throughput * environment_map_light_radiance;
115 }
117 break;
118 }
120 let ray_hit = resolve_ray_hit_full(ray);
121 let p_brdf = next_bounce.pdf;
123 @if(DLSS_RR_GUIDE_BUFFERS)
124 if !psr.finished {
125 if next_bounce.diffuse_selected {
126 // Took the non-specular lobe, so not a mirror reflection. Keep the guide-buffer defaults.
127 psr.finished = true;
128 } else {
129 psr.throughput *= psr_bounce_reflectance(path.wo, next_bounce.wi, path.normal, path.material, F_ab, isinf(p_brdf));
130 psr.reflection_length += ray.t;
132 if !primary_surface_glossy(material) && is_delta_mirror(ray_hit.material) {
133 // Still in the mirror chain, fold this mirror's reflection into the chain.
134 psr = psr_chain_mirror(psr, ray_hit.world_normal);
135 } else {
136 psr.finished = true;
137 replace_primary_surface(pixel_id, psr, ray_hit, material, world_position, world_normal, wo, primary_F_ab);
138 }
139 }
140 }
142 // Capture x2, the first BRDF-sampled hit
143 if bounce == 0u {
144 @if(RESTIR) {
145 path.x2_position = ray_hit.world_position;
146 path.x2_normal = ray_hit.world_normal;
148 path.x1_brdf = evaluate_brdf(wo, next_bounce.wi, world_normal, material, primary_F_ab);
150 path.x2_reusable = reconnection_reusable(ray.t, p_brdf, next_bounce.wi, next_bounce.diffuse_selected, ray_hit, world_position, material.perceptual_roughness, primary_NdotV);
152 // The primary brdf*cos is applied at shade time, so divide it out of next_bounce.throughput
153 // to leave 1/pdf (or 1/specular_weight for mirrors, avoiding the 1/INF = 0 that would kill
154 // mirror GI).
155 path.throughput_past_first_hit *= next_bounce.throughput / max(path.x1_brdf, vec3(0.0001));
156 } @else {
157 // Nothing is reused, so there is no shade-time reconnection to factor the primary
158 // brdf*cos out for.
159 path.throughput_past_first_hit *= next_bounce.throughput;
160 }
161 } else {
162 // Later bounces keep the full brdf*cos/pdf for L_at_reconnection.
163 path.throughput_past_first_hit *= next_bounce.throughput;
164 }
166 // Resample emissive hits
167 if any(ray_hit.material.emissive > vec3(0.0)) && dot(ray_hit.world_normal, -next_bounce.wi) > 0.0 {
168 generate_emissive_candidate(&path, ray_hit, next_bounce.wi, p_brdf, ray.t, p_nee, di_samples, bounce, rng);
169 }
171 // Try terminating into the world cache
172 if terminate_into_cache(&path, ray_hit, ray.t, p_brdf, bounce, rng) {
173 break;
174 }
176 // Advance to the next vertex
177 path.ray_origin = ray_hit.world_position + (ray_hit.geometric_world_normal * RAY_T_MIN);
178 path.normal = ray_hit.world_normal;
179 path.wo = -next_bounce.wi;
180 path.material = ray_hit.material;
182 // Russian roulette for early termination
183 if bounce > 0u {
184 // With ReSTIR, throughput_past_first_hit has the primary brdf*cos divided out (so it can be
185 // re-applied at shade time), which inflates it. Multiply x1_brdf back in to get the true
186 // energy-bounded path throughput, which is the correct quantity for the RR survival
187 // probability. Without ReSTIR it already is that quantity.
188 var full_throughput = path.throughput_past_first_hit;
189 @if(RESTIR)
190 full_throughput *= max(path.x1_brdf, vec3(0.0001));
191 let rr = saturate(luminance(full_throughput));
192 if rand_f(rng) >= rr { break; }
193 path.throughput_past_first_hit /= rr;
194 }
195 }
197 var result: InitialSamplingResult;
198 result.radiance = path.radiance;
199 @if(RESTIR) {
200 if path.selected_target_function > 0.0 {
201 path.reservoir.unbiased_contribution_weight = path.weight_sum / path.selected_target_function;
202 }
203 result.reservoir = path.reservoir;
204 }
205 return result;
208fn generate_nee_candidate(
209 path: ptr<function, PathState>,
210 F_ab: vec2<f32>,
211 p_nee: f32,
212 di_samples: u32,
213 workgroup_id: vec2<u32>,
214 bounce: u32,
215 rng: ptr<function, u32>,
216) {
217 if rand_f(rng) >= p_nee { return; }
219 let di = sample_light_ris((*path).ray_origin, (*path).normal, (*path).wo, (*path).material, F_ab, di_samples, workgroup_id, bounce, rng);
220 let di_target_function = luminance(di.brdf_radiance);
221 if di_target_function <= 0.0 { return; }
223 // MIS against the BRDF strategy. RIS over N candidates makes the effective NEE pdf at the
224 // winner roughly N * light_pdf(winner), so scale by p_nee for the stochastic gate.
225 var nee_mis_weight = 1.0;
226 if di.brdf_rays_can_hit && di.inverse_solid_angle_pdf > 0.0 {
227 let p_nee_strategy = f32(di_samples) * (1.0 / di.inverse_solid_angle_pdf) * p_nee;
228 let p_brdf_at_nee = brdf_pdf((*path).wo, di.wi, (*path).normal, (*path).material, F_ab);
229 nee_mis_weight = power_heuristic(p_nee_strategy, p_brdf_at_nee);
230 }
232 if bounce == 0u {
233 @if(RESTIR) {
234 // Bounce 0: Candidate is the light sample, stored by reference and re-resolved each frame
235 // nee_mis_weight goes into the target function since it gets recomputed per-pixel during reuse
236 let target_function = di_target_function * nee_mis_weight;
237 let resampling_weight = target_function * di.unbiased_contribution_weight / p_nee;
239 (*path).weight_sum += resampling_weight;
240 if rand_f(rng) * (*path).weight_sum < resampling_weight {
241 (*path).reservoir.light_sample = di.light_sample;
242 (*path).selected_target_function = target_function;
243 }
244 } @else {
245 (*path).radiance += di.brdf_radiance * di.unbiased_contribution_weight * nee_mis_weight / p_nee;
246 }
247 } else {
248 // Deeper bounces: Candidate is the reconnection radiance at x2
249 let L_at_reconnection = (*path).throughput_past_first_hit * di.brdf_radiance * di.unbiased_contribution_weight * nee_mis_weight / p_nee;
251 @if(RESTIR) {
252 resample_reconnection_candidate(path, L_at_reconnection, rng);
253 } @else {
254 (*path).radiance += L_at_reconnection;
255 }
256 }
259struct DiSample {
260 unbiased_contribution_weight: f32,
261 light_sample: LightSample,
262 wi: vec3<f32>,
263 brdf_radiance: vec3<f32>,
264 inverse_solid_angle_pdf: f32,
265 brdf_rays_can_hit: bool,
268fn sample_light_ris(ray_origin: vec3<f32>, normal: vec3<f32>, wo: vec3<f32>, material: ResolvedMaterial, F_ab: vec2<f32>, di_samples: u32, workgroup_id: vec2<u32>, bounce: u32, rng: ptr<function, u32>) -> DiSample {
269 var workgroup_rng = (workgroup_id.x * 5782582u) + workgroup_id.y + bounce;
270 let light_tile_start = rand_range_u(128u, &workgroup_rng) * 1024u;
272 var weight_sum = 0.0;
273 var selected_target_function = 0.0;
274 var selected_tile_sample = 0u;
275 var selected_world_position = vec4(0.0);
276 var selected_wi = vec3(0.0);
277 var selected_brdf_radiance = vec3(0.0);
278 var selected_inverse_solid_angle_pdf = 0.0;
279 var selected_brdf_rays_can_hit = false;
280 let mis_weight = 1.0 / f32(di_samples);
281 for (var i = 0u; i < di_samples; i++) {
282 let tile_sample = light_tile_start + rand_range_u(1024u, rng);
283 let resolved_light_sample = unpack_resolved_light_sample(light_tile_resolved_samples[tile_sample], view.exposure);
284 let light_contribution = calculate_resolved_light_contribution(resolved_light_sample, ray_origin, normal);
285 let brdf_current = evaluate_brdf(wo, light_contribution.wi, normal, material, F_ab);
286 let brdf_radiance = brdf_current * light_contribution.radiance;
288 let target_function = luminance(brdf_radiance);
289 let resampling_weight = mis_weight * (target_function * light_contribution.inverse_pdf);
291 weight_sum += resampling_weight;
293 if rand_f(rng) * weight_sum < resampling_weight {
294 selected_target_function = target_function;
295 selected_tile_sample = tile_sample;
296 selected_world_position = resolved_light_sample.world_position;
297 selected_wi = light_contribution.wi;
298 selected_inverse_solid_angle_pdf = light_contribution.inverse_solid_angle_pdf;
299 selected_brdf_rays_can_hit = light_contribution.brdf_rays_can_hit;
300 selected_brdf_radiance = brdf_radiance;
301 }
302 }
304 var unbiased_contribution_weight = 0.0;
305 if selected_target_function > 0.0 {
306 unbiased_contribution_weight = weight_sum / selected_target_function;
307 unbiased_contribution_weight *= trace_visibility(ray_origin, selected_world_position);
308 }
310 return DiSample(unbiased_contribution_weight, light_tile_samples[selected_tile_sample], selected_wi, selected_brdf_radiance, selected_inverse_solid_angle_pdf, selected_brdf_rays_can_hit);
313fn generate_emissive_candidate(
314 path: ptr<function, PathState>,
315 ray_hit: ResolvedRayHitFull,
316 wi: vec3<f32>,
317 p_brdf: f32,
318 ray_t: f32,
319 p_nee: f32,
320 di_samples: u32,
321 bounce: u32,
322 rng: ptr<function, u32>,
323) {
324 let NdotV_hit = max(dot(ray_hit.world_normal, -wi), 0.0001);
325 let light_count = arrayLength(&light_sources);
326 let area_pdf = 1.0 / (f32(light_count) * f32(ray_hit.triangle_count) * ray_hit.triangle_area);
327 let p_light = area_pdf * ray_t * ray_t / NdotV_hit;
328 let emissive_mis_weight = power_heuristic(p_brdf, p_light * p_nee * f32(di_samples));
330 @if(RESTIR) {
331 if !(*path).x2_reusable {
332 // x1 -> x2 not reuse-safe (mirror/sharp lobe or failed gate): shade directly at this pixel
333 // instead of publishing, since a reuse shift would waste it or make a firefly. Mirror lobes
334 // always land here (p_brdf = INF, footprint 0), where emissive_mis_weight is 1.
335 (*path).radiance += (*path).x1_brdf * (*path).throughput_past_first_hit * ray_hit.material.emissive * emissive_mis_weight;
336 } else if bounce == 0u {
337 // Bounce 0: Candidate is the emissive hit
338 let target_function = luminance((*path).x1_brdf * ray_hit.material.emissive) * emissive_mis_weight;
339 let resampling_weight = luminance((*path).x1_brdf * (*path).throughput_past_first_hit * ray_hit.material.emissive) * emissive_mis_weight;
341 (*path).weight_sum += resampling_weight;
342 if rand_f(rng) * (*path).weight_sum < resampling_weight {
343 (*path).reservoir.light_sample = LightSample(NULL_LIGHT_ID, bitcast<u32>(area_pdf));
344 (*path).reservoir.sample_point_world_position = (*path).x2_position;
345 (*path).reservoir.sample_point_world_normal = octahedral_encode((*path).x2_normal);
346 (*path).reservoir.radiance = ray_hit.material.emissive;
347 (*path).selected_target_function = target_function;
348 }
349 } else {
350 // Deeper bounces: Candidate is the reconnection radiance at x2
351 let emissive_L_at_reconnection = (*path).throughput_past_first_hit * ray_hit.material.emissive * emissive_mis_weight;
352 resample_reconnection_candidate(path, emissive_L_at_reconnection, rng);
353 }
354 } @else {
355 (*path).radiance += (*path).throughput_past_first_hit * ray_hit.material.emissive * emissive_mis_weight;
356 }
359@if(RESTIR)
360fn generate_environment_map_light_candidate(
361 path: ptr<function, PathState>,
362 next_bounce: EvaluateAndSampleBrdfResult,
363 environment_map_light_radiance: vec3<f32>,
364 primary_F_ab: vec2<f32>,
365 bounce: u32,
366 rng: ptr<function, u32>,
367) {
368 if all(environment_map_light_radiance == vec3(0.0)) { return; }
370 if bounce == 0u {
371 // Bounce 0: Candidate is the sky direction
372 if !x1_lobe_reusable(next_bounce.pdf, next_bounce.diffuse_selected, (*path).material.perceptual_roughness) {
373 (*path).radiance += next_bounce.throughput * environment_map_light_radiance;
374 return;
375 }
377 // Publish the candidate as a point RAY_T_MAX along wi.
378 // Neighbors see almost the same direction, and the reconnection jacobian evaluates to ~1.
379 //
380 // TODO: Once the environment map light is a proper LightSource that we sample during NEE,
381 // store this as a LightSample instead.
382 let x1_brdf = evaluate_brdf((*path).wo, next_bounce.wi, (*path).normal, (*path).material, primary_F_ab);
383 let target_function = luminance(x1_brdf * environment_map_light_radiance);
384 let resampling_weight = luminance(next_bounce.throughput * environment_map_light_radiance);
386 (*path).weight_sum += resampling_weight;
387 if rand_f(rng) * (*path).weight_sum < resampling_weight {
388 (*path).reservoir.light_sample = LightSample(NULL_LIGHT_ID, 0u);
389 (*path).reservoir.sample_point_world_position = (*path).ray_origin + next_bounce.wi * RAY_T_MAX;
390 (*path).reservoir.sample_point_world_normal = octahedral_encode(-next_bounce.wi);
391 (*path).reservoir.radiance = environment_map_light_radiance;
392 (*path).selected_target_function = target_function;
393 }
394 } else {
395 // Deeper bounces: Candidate is the reconnection radiance at x2
396 let L_at_reconnection = (*path).throughput_past_first_hit * next_bounce.throughput * environment_map_light_radiance;
397 resample_reconnection_candidate(path, L_at_reconnection, rng);
398 }
401fn terminate_into_cache(
402 path: ptr<function, PathState>,
403 ray_hit: ResolvedRayHitFull,
404 ray_t: f32,
405 p_brdf: f32,
406 bounce: u32,
407 rng: ptr<function, u32>,
408) -> bool {
409 // Only terminate into the world cache when the bounce was from a wide-enough BRDF sample
410 // because the cache is less noisy than continuing the path for rough surfaces,
411 // but less accurate for smooth surfaces
412 let lobe_solid_angle = 1.0 / p_brdf;
413 let broad_enough_to_terminate = lobe_solid_angle >= CACHE_TERMINATION_MIN_SOLID_ANGLE;
414 let forced_terminate = bounce == constants.max_bounces - 1u;
415 if !(broad_enough_to_terminate || forced_terminate) { return false; }
417 // Only use the cache when the ray cleared the cache cell (diagonal = sqrt(3) * cell_size). Short
418 // rays land in a cell that may straddle occluders and leak light through corners.
419 var rng_copy = *rng;
420 let world_cache_cell_size = get_cell_size(ray_hit.world_position, view.world_position, ray_t, &rng_copy);
421 if ray_t <= sqrt(3.0) * world_cache_cell_size { return false; }
423 let cached_radiance = query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position, ray_t, WORLD_CACHE_CELL_LIFETIME, rng);
425 let cache_outgoing = (ray_hit.material.base_color / PI) * cached_radiance;
426 let cache_L_at_reconnection = (*path).throughput_past_first_hit * cache_outgoing;
428 @if(RESTIR) {
429 resample_reconnection_candidate(path, cache_L_at_reconnection, rng);
430 } @else {
431 (*path).radiance += cache_L_at_reconnection;
432 }
434 return true;
437/// Resample radiance arriving at x1 through the reconnection vertex x2. `L_at_reconnection` excludes the
438/// brdf*cos at x1, which is re-applied at shade time from whichever pixel ends up reusing the candidate.
439@if(RESTIR)
440fn resample_reconnection_candidate(path: ptr<function, PathState>, L_at_reconnection: vec3<f32>, rng: ptr<function, u32>) {
441 if !(*path).x2_reusable {
442 // x1 -> x2 not reuse-safe: shade directly at this pixel instead of publishing.
443 (*path).radiance += (*path).x1_brdf * L_at_reconnection;
444 return;
445 }
447 let target_function = luminance((*path).x1_brdf * L_at_reconnection);
448 let resampling_weight = target_function;
450 (*path).weight_sum += resampling_weight;
451 if rand_f(rng) * (*path).weight_sum < resampling_weight {
452 (*path).reservoir.light_sample = LightSample(NULL_LIGHT_ID, 0u);
453 (*path).reservoir.sample_point_world_position = (*path).x2_position;
454 (*path).reservoir.sample_point_world_normal = octahedral_encode((*path).x2_normal);
455 (*path).reservoir.radiance = L_at_reconnection;
456 (*path).selected_target_function = target_function;
457 }
460/// ReSTIR PT Enhanced: Algorithmic Advances for Faster and More Robust ReSTIR Path Tracing
461/// Section 4 (sorta)
462/// https://research.nvidia.com/labs/rtr/publication/lin2026restirptenhanced/lin2026restirptenhanced.pdf
463@if(RESTIR)
464fn reconnection_reusable(ray_t: f32, p_brdf: f32, wi: vec3<f32>, diffuse_selected: bool, ray_hit: ResolvedRayHitFull, world_position: vec3<f32>, x1_perceptual_roughness: f32, primary_NdotV: f32) -> bool {
465 // ray_footprint = t^2 / (p_brdf * cos_x2) is the area a sample represents at x2. It goes to 0 for
466 // mirror lobes (p_brdf = INF) and shrinks for sharp lobes or short segments. Compared against a
467 // uniform 1/(4*PI) primary footprint, so the test trades roughness against distance.
468 let cos_x2 = max(dot(ray_hit.world_normal, -wi), 0.0001);
469 let ray_footprint = (ray_t * ray_t) / (p_brdf * cos_x2);
470 let primary_dist = length(view.world_position - world_position);
471 let primary_footprint = 4.0 * PI * primary_dist * primary_dist / primary_NdotV;
472 let footprint_ok = ray_footprint >= (RECONNECTION_FOOTPRINT_KAPPA / 100.0) * primary_footprint;
474 let x1_lobe_ok = x1_lobe_reusable(p_brdf, diffuse_selected, x1_perceptual_roughness);
476 // Guard at x2. A sharp reflector there makes the stored radiance view-dependent and wrong to
477 // reuse from a neighbor's direction. The roughness floor relaxes with segment length: a distant
478 // glossy x2 is seen by neighbors from nearly the same direction, so the view-dependence washes out.
479 // Diffuse, rough, and emissive vertices are always reuse-safe.
480 let x2_is_light = any(ray_hit.material.emissive > vec3(0.0));
481 let x2_roughness = mix(1.0, ray_hit.material.perceptual_roughness, ray_hit.material.metallic);
482 let x2_roughness_floor = RECONNECTION_ROUGHNESS_MIN * saturate(RECONNECTION_RELAX_DISTANCE / ray_t);
483 let x2_end_ok = x2_is_light || x2_roughness >= x2_roughness_floor;
485 return footprint_ok && x1_lobe_ok && x2_end_ok;
488/// Whether the lobe sampled at x1 is wide enough for its candidates to be reused by neighbors.
489@if(RESTIR)
490fn x1_lobe_reusable(p_brdf: f32, diffuse_selected: bool, x1_perceptual_roughness: f32) -> bool {
491 return !isinf(p_brdf) && (diffuse_selected || x1_perceptual_roughness >= RECONNECTION_ROUGHNESS_MIN);
Contents