Deferred visibility in RIS
7cbd9f9 · 2026-07-08 ↗
This article covers the estimator change in the second part of commit
7cbd9f9. Candidate scoring may omit shadow rays, but the retained candidate
must be evaluated with visibility before its contribution is returned.
The material below is specific to delaying visibility inside the HdRestir RIS and reservoir estimators; the linked references supply only the sampling and light-interface prerequisites.
Estimator and target
For a direct-light sample \(z\), write the contribution as
\[f(z)=V(z)\,g(z),\]where \(g\) is the unoccluded emission–BSDF–geometry contribution and \(V\in\{0,1\}\) is visibility. RIS may use the cheaper scalar target \(\hat p(z)\approx\lVert g(z)\rVert\), select \(Y\), and evaluate visibility once:
\[\widehat I = V(Y)g(Y)\frac{S}{M\hat p(Y)}.\]Assumptions: The proposal covers visible contribution, the unoccluded target is finite and positive wherever visible contribution is non-zero, the candidate weight sum is positive, and visibility is evaluated for the retained sample in the receiving context.
Conditioned on candidates \(X_{1:M}\), selection probability is \(w_i/S\), where \(w_i=\hat p(X_i)/q(X_i)\). Therefore
\[\begin{aligned} \mathbb E[\widehat I\mid X_{1:M}] &=\sum_i \frac{w_i}{S} V(X_i)g(X_i)\frac{S}{M\hat p(X_i)}\\ &=\frac1M\sum_i\frac{V(X_i)g(X_i)}{q(X_i)}. \end{aligned}\]Taking the outer expectation gives \(\int V(z)g(z)\,dz\). Visibility need not be part of the selection target, but it must remain in the final contribution.
If visibility is omitted from both scoring and final evaluation, the estimator integrates unoccluded lighting instead. If stored visibility is reused in a different shading context without validation, the same substitution can occur across frames.
Cost and variance
Let \(M\) be the candidate count and \(C_s\) the cost of one shadow ray. Testing every candidate costs approximately \(M C_s\). Testing only the winner costs approximately \(C_s\), plus winner re-evaluation. The visibility work saved is close to
\[(M-1)C_s,\]provided that candidate generation does not perform equivalent intersection work elsewhere.
This cost expression does not predict variance. In an open scene, \(\lVert g\rVert\) can be a useful predictor of \(\lVert Vg\rVert\). In a heavily occluded scene, RIS can repeatedly select a high-target candidate that becomes zero after the final shadow ray. The estimator remains unbiased under the stated assumptions, but its variance can increase.
Candidate scoring without a shadow ray
The exact skip branch in _evaluateLightSample is:
if (skipVisibility)
return MISContrib{.Emission = RGBToSpectrum(ls.Color, surface.lambda),
.ThroughputMul = RGBToSpectrum(bsdfValue * nDotL, surface.lambda),
.PNee = pNee,
.PBsdf = pBsdf,
.UseMis = useMis,
.VisibilityTested = false};
Emission, BSDF, cosine, and both proposal densities remain present.
VisibilityTested = false records the missing operation as data rather than
leaving it implicit in control flow. The target later uses the luminance of
this unoccluded spectral contribution.
The non-reservoir winner
After weighted selection, the ordinary RIS branch checks the flag:
SampledSpectrum winnerThroughput{chosenCandidate.Throughput};
// ...
if (!chosenCandidate.VisibilityTested && chosenCandidate.Candidate.has_value())
{
const LightCandidate &lc{*chosenCandidate.Candidate};
const MISContrib nee{_evaluateLightSample(isect, *lc.Light, lc.Ls, scene, /*skipVisibility=*/false)};
winnerThroughput = nee.Emission * nee.ThroughputMul;
}
return winnerThroughput * W;
Passing false forces the full light evaluation, including the shadow ray.
The returned winnerThroughput therefore contains \(V(Y)g(Y)\), and W
contains the RIS normalization derived above.
The reservoir winner
The persistent branch performs the same obligation before storing the current reservoir:
if (finalized.ChosenSample.has_value() && !finalized.ChosenSample->VisibilityTested &&
finalized.ChosenSample->Candidate.has_value())
{
RISLightCandidate &winner{*finalized.ChosenSample};
const LightCandidate &lc{*winner.Candidate};
const MISContrib nee{_evaluateLightSample(isect, *lc.Light, lc.Ls, scene,
/*skipVisibility=*/false)};
winner.Throughput = nee.Emission * nee.ThroughputMul;
winner.VisibilityTested = true;
}
The stored sample is marked as tested for the context in which it was finalized. That flag alone does not make the visibility valid in a later receiving context. At this revision, temporal merging still accepts stored visibility more broadly than the final GPU implementation. The history limitations described in step 2 therefore remain relevant.
For a finite light, re-evaluation reconnects the retained light sample to the current surface and traces toward its endpoint. For an environment, the retained direction defines the visibility ray. The contribution, PDF, target, and visibility test must refer to compatible geometric interpretations.
An ordinary beauty render is deliberately omitted here: it cannot reveal whether a shadow ray was traced for every proposal or only for the retained sample. A stronger follow-up experiment would pair these curves with equal-time captures of the shadow-ray counter and hold scene, seed, candidate count, resolution, filtering, and stopping rule fixed.