@@ -85,18 +85,43 @@ float3 probe_emission_estimate(MaterialParameters mat)
8585}
8686
8787// power-proportional light pick weight, lin 2022 §6.1 / restir di reference
88- // returns 0 for lights ineligible for the restir nee pool (point/spot, zero intensity), so the
89- // total weight only counts the lights that sample_light_candidate can actually emit candidates
90- // for, this keeps light_pick_pdf_for_index consistent with the picker
88+ // all four light types (directional, point, spot, area) are eligible, point/spot weights are
89+ // scaled by 1/distance_to_camera^2 approximation via 1/range^2 so a faraway lamp does not
90+ // dominate the picker for a primary near another light, this is the standard veach 1997
91+ // "important light" heuristic adapted for the dirac case where exact 1/r^2 at the primary
92+ // would require per-pixel weight evaluation (too expensive for restir's once-per-frame
93+ // light budget)
9194float light_pick_weight (LightParameters l)
9295{
96+ if (l.intensity <= 0.0f )
97+ return 0.0f ;
98+
9399 bool is_directional = (l.flags & (1u << 0 )) != 0 ;
100+ bool is_point = (l.flags & (1u << 1 )) != 0 ;
101+ bool is_spot = (l.flags & (1u << 2 )) != 0 ;
94102 bool is_area = (l.flags & (1u << 6 )) != 0 ;
95- if (l.intensity <= 0.0f || (!is_directional && !is_area))
96- return 0.0f ;
97103
98104 float lum = max (luminance (l.color.rgb), 1e-3f );
99- return l.intensity * lum;
105+ if (is_directional || is_area)
106+ {
107+ return l.intensity * lum;
108+ }
109+ if (is_point || is_spot)
110+ {
111+ // dirac local light, weight by power proxy (intensity * radiated cone fraction)
112+ // proportional to range^2 so a bright lamp with long reach beats a dim short one,
113+ // spot lights are further weighted by their cone solid angle so a narrow cone gets
114+ // proportionally less budget than a uniform sphere of equal intensity
115+ float range_sq = max (l.range * l.range, 1e-2f );
116+ float cone_factor = 1.0f ;
117+ if (is_spot)
118+ {
119+ float cos_outer = cos (l.angle);
120+ cone_factor = max (1.0f - cos_outer, 0.05f );
121+ }
122+ return l.intensity * lum * range_sq * cone_factor;
123+ }
124+ return 0.0f ;
100125}
101126
102127// total nee pick weight, called once per evaluation, the loop is O(light_count) so this stays
@@ -125,8 +150,9 @@ float light_pick_pdf_for_index(uint light_idx, float total_weight)
125150// computes the nee strategy density at a brdf-sampled candidate, in solid angle at primary
126151// returns the sun cone density for sky candidates that fall inside the cone, zero otherwise
127152// area lights are not in the bvh so the brdf strategy can never produce paths whose rc lies
128- // on an area light rectangle, the area light loop that used to live here was a fictitious
129- // strategy density that darkened the brdf branch by ~50% near walls aligned with area lights
153+ // on an area light rectangle, point/spot are dirac and likewise cannot be hit by a brdf
154+ // bounce, so the nee strategy contributes zero to the brdf-candidate mis denominator for
155+ // those light types and the brdf candidate gets its full single-strategy mis weight
130156float light_nee_pdf_for_candidate (PathSample candidate, float3 primary_pos)
131157{
132158 uint light_count = (uint )buffer_frame.restir_pt_light_count;
@@ -262,9 +288,15 @@ float sky_nee_pdf_at(float3 dir, float3 shading_normal)
262288
263289// evaluates direct lighting (analytical lights + environment probe) at a surface vertex
264290// returns the contribution in the direction of view_dir (i.e. back toward the previous vertex)
265- // always uses the full brdf so metallic/glossy surfaces receive proper nee at every vertex
266- // note: stored rc_radiance is therefore view-dependent at rc (w.r.t. src's incoming direction)
267- // the bias introduced during reconnection shift is bounded by the rc roughness gate
291+ // when diffuse_only_brdf is false, the full brdf (diffuse + ggx specular) is used at this
292+ // vertex, this is correct for suffix vertices past rc which are baked into rc_L_post and not
293+ // individually reused across pixels
294+ // when diffuse_only_brdf is true, the specular lobe is dropped and only the lambert diffuse
295+ // term remains, this is the correct choice for the rc vertex itself because the resulting
296+ // nee radiance must be view-independent so the reconnection shift can reuse it at any dst
297+ // primary without re-evaluating the rc brdf against the dst-correct incoming direction, the
298+ // rc roughness gate at 0.3 makes the missing specular lobe a bounded, negligible energy loss
299+ // at the same vertex
268300float3 direct_lighting_at_vertex (
269301 float3 shading_pos,
270302 float3 shading_normal,
@@ -273,6 +305,7 @@ float3 direct_lighting_at_vertex(
273305 float3 albedo,
274306 float roughness,
275307 float metallic,
308+ bool diffuse_only_brdf,
276309 inout uint seed)
277310{
278311 float3 total = float3 (0 , 0 , 0 );
@@ -373,8 +406,9 @@ float3 direct_lighting_at_vertex(
373406
374407 // secondary vertex always uses the full brdf, primary specular routing only applies
375408 // at the primary vertex itself which is shaded by self_shift_evaluate or the shifts
409+ // at the rc vertex itself we use lambert only so the stored nee stays view-independent
376410 float brdf_pdf;
377- float3 brdf = evaluate_brdf (albedo, roughness, metallic, shading_normal, view_dir, light_dir, brdf_pdf, false );
411+ float3 brdf = evaluate_brdf (albedo, roughness, metallic, shading_normal, view_dir, light_dir, brdf_pdf, diffuse_only_brdf );
378412
379413 // analytical lights are not in the bvh, the brdf bounce can never sample to their
380414 // direction so there is no second strategy to mis with, applying power_heuristic against
@@ -462,7 +496,7 @@ float3 direct_lighting_at_vertex(
462496 if (luminance (probe_emission) > 0.0f )
463497 {
464498 float brdf_pdf_probe;
465- float3 brdf_probe = evaluate_brdf (albedo, roughness, metallic, shading_normal, view_dir, env_dir, brdf_pdf_probe, false );
499+ float3 brdf_probe = evaluate_brdf (albedo, roughness, metallic, shading_normal, view_dir, env_dir, brdf_pdf_probe, diffuse_only_brdf );
466500
467501 float mis_weight = power_heuristic (env_pdf, brdf_pdf_probe);
468502 total += brdf_probe * probe_emission * mis_weight / env_pdf;
@@ -475,7 +509,7 @@ float3 direct_lighting_at_vertex(
475509 env_radiance = clamp_sky_radiance (env_radiance);
476510
477511 float brdf_pdf_env;
478- float3 brdf_env = evaluate_brdf (albedo, roughness, metallic, shading_normal, view_dir, env_dir, brdf_pdf_env, false );
512+ float3 brdf_env = evaluate_brdf (albedo, roughness, metallic, shading_normal, view_dir, env_dir, brdf_pdf_env, diffuse_only_brdf );
479513
480514 float mis_weight_env = power_heuristic (env_pdf, brdf_pdf_env);
481515 total += brdf_env * env_radiance * mis_weight_env / env_pdf;
@@ -583,18 +617,23 @@ void trace_rc_suffix(
583617 // that requires a cpu-built emissive triangle structured buffer which is not part of
584618 // this rewrite, the bounce-emission path is unbiased so this is a variance-only win
585619 out_L_post += throughput * next.emission;
620+ // suffix vertices past rc, full brdf is correct here because L_post is reused as a
621+ // whole only via the rc bsdf factor at shift time, individual suffix vertices are not
622+ // re-evaluated against a different primary direction
586623 out_L_post += throughput * direct_lighting_at_vertex (
587624 next.hit_position, next.hit_normal, next.geometric_normal,
588- -nd, next.albedo, next.roughness, next.metallic, seed);
625+ -nd, next.albedo, next.roughness, next.metallic, false , seed);
589626
590627 cur = next;
591628 view_dir = -nd;
592629 }
593630}
594631
595- // gathers the rc vertex's nee + emission contribution (with rc bsdf baked in at src view dir,
596- // view-dep bias bounded by the get_restir_rc_min_roughness floor on rc reuse) and the suffix
597- // radiance past rc (with rc bsdf factored out into rc_outgoing_dir for paper-faithful shifts)
632+ // gathers the rc vertex's nee + emission contribution (with the rc brdf reduced to lambert
633+ // only so the stored radiance is view-independent and reuses correctly at any dst primary,
634+ // missing specular-lobe energy at rc is bounded by the get_restir_rc_min_roughness floor)
635+ // and the suffix radiance past rc (with the rc brdf factored out into rc_outgoing_dir for
636+ // paper-faithful reconnection shifts)
598637void accumulate_subpath_at_rc (
599638 PathPayload rc,
600639 float3 rc_view_dir,
@@ -605,9 +644,13 @@ void accumulate_subpath_at_rc(
605644 out float3 out_first_dir)
606645{
607646 out_L_nee = rc.emission;
647+ // diffuse-only brdf at rc keeps the stored nee radiance view-independent so the
648+ // reconnection shift can reuse it at any dst primary without re-evaluating the rc brdf
649+ // against the dst-correct incoming direction, the rc roughness gate at 0.3 bounds the
650+ // missing specular lobe energy at this same vertex
608651 out_L_nee += direct_lighting_at_vertex (
609652 rc.hit_position, rc.hit_normal, rc.geometric_normal,
610- rc_view_dir, rc.albedo, rc.roughness, rc.metallic, seed);
653+ rc_view_dir, rc.albedo, rc.roughness, rc.metallic, true , seed);
611654
612655 out_L_post = float3 (0 , 0 , 0 );
613656 out_first_dir = float3 (0 , 0 , 0 );
@@ -766,6 +809,56 @@ PathSample sample_light_candidate(
766809 return s;
767810 }
768811
812+ bool is_point = (light.flags & (1u << 1 )) != 0 ;
813+ bool is_spot = (light.flags & (1u << 2 )) != 0 ;
814+ if (is_point || is_spot)
815+ {
816+ // dirac local light, rc is the light position treated as a point emitter, distance
817+ // attenuation and spot cone are folded into rc_L_nee so the shift-time evaluation
818+ // collapses to brdf_dst * rc_L_nee like the area-light branch, the BRDF strategy
819+ // cannot produce paths that land on a point/spot light (they have zero volume in
820+ // the bvh) so the MIS denominator is single-strategy and we use a unit solid-angle
821+ // pdf to keep ris weights in the same scale as area light candidates (the pdf
822+ // really is a dirac, but a single-strategy weight target/pdf does not care about
823+ // the absolute scale of pdf as long as it is consistent across same-strategy samples)
824+ float3 to = light.position - primary_pos;
825+ float light_dist = length (to);
826+ if (light_dist < 1e-3f )
827+ return s;
828+
829+ float3 dir = to / light_dist;
830+ if (dot (dir, primary_normal) <= MIN_COS_AT_PRIMARY)
831+ return s;
832+
833+ float range_factor = saturate (1.0f - light_dist / max (light.range, 0.01f ));
834+ float attenuation = range_factor * range_factor / max (light_dist * light_dist, 0.01f );
835+
836+ if (is_spot)
837+ {
838+ float cos_angle = dot (-dir, light.direction);
839+ float cos_outer = cos (light.angle);
840+ float cos_inner = cos (light.angle * 0.8f );
841+ float spot = saturate ((cos_angle - cos_outer) / max (cos_inner - cos_outer, 1e-4f ));
842+ attenuation *= spot;
843+ if (attenuation <= 0.0f )
844+ return s;
845+ }
846+
847+ s.rc_pos = light.position;
848+ s.rc_normal = -dir;
849+ s.rc_L_nee = light.color.rgb * light.intensity * attenuation;
850+ s.rc_L_post = float3 (0 , 0 , 0 );
851+ s.flags |= PATH_FLAG_HAS_RC | PATH_FLAG_NEE;
852+
853+ // unit solid-angle pdf keeps dirac candidates on the same numerical scale as the
854+ // area branch so the ris ordering is stable across mixed light types, the actual
855+ // dirac density is infinite which would zero out weights through 1/pdf in mis,
856+ // single-strategy w = target/(n_light * 1) yields the correct estimator for the
857+ // collapsed dirac case (no brdf strategy contribution to balance against)
858+ source_pdf = pick_pdf;
859+ return s;
860+ }
861+
769862 return s;
770863}
771864
@@ -845,28 +938,20 @@ PathSample trace_path_from_primary(
845938 L_nee = max (L_nee, 0.0f );
846939 L_post = max (L_post, 0.0f );
847940
848- // soft luminance clamp, the W clamp + m cap bound the unbiased estimator energy but not
849- // single sample variance, when a glossy rc lands on a bright emitter the rc bsdf baked
850- // into rc_L_nee can spike to thousands of nits and a stable spatial neighbor will then
851- // stamp that pulse across many pixels (boiling), the soft scale preserves chromaticity so
852- // the residual downward bias is uniform across channels and the denoiser absorbs the rest
853- {
854- float lum_nee = luminance (L_nee);
855- float lum_post = luminance (L_post);
856- const float firefly_ceiling = 250.0f ;
857- if (lum_nee > firefly_ceiling) L_nee *= firefly_ceiling / lum_nee;
858- if (lum_post > firefly_ceiling) L_post *= firefly_ceiling / lum_post;
859- }
860-
941+ // stored radiance is left unmodified at sample construction so the resampler scores the
942+ // true integrand, per-sample firefly safety is provided downstream via the w clamp at
943+ // shade time (get_w_clamp_for_sample) and the denoiser's spatial firefly suppression,
944+ // squashing radiance pre-ris would bias the integrand the resampler is trying to estimate
861945 s.rc_L_nee = L_nee;
862946 s.rc_L_post = L_post;
863947 s.rc_outgoing_dir = first_outgoing_dir;
864948 s.path_length = 2 ;
865949
866- // reconnection validity: the rc vertex must be rough enough that the rc_L_nee view-dep
867- // bias (rc bsdf baked at src direction for the nee component) stays small, the suffix
868- // view-dep is now factored out via rc_L_post + rc_outgoing_dir + rc material, distance
869- // must be large enough to keep the solid-angle jacobian well-conditioned
950+ // reconnection validity: the rc vertex must be rough enough that the lambert-only nee at
951+ // rc captures most of the energy (the dropped ggx specular lobe at rc is the remaining
952+ // bias source, bounded by the roughness floor), the suffix view-dep is factored out via
953+ // rc_L_post + rc_outgoing_dir + rc material, distance must be large enough to keep the
954+ // solid-angle jacobian well-conditioned
870955 float rc_min_roughness = get_restir_rc_min_roughness ();
871956 float dist_sq = dot (hit.hit_position - primary_pos, hit.hit_position - primary_pos);
872957 bool rc_valid = (hit.roughness >= rc_min_roughness)
0 commit comments