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.

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)}.\]
Proof Deferring visibility until after RIS selection preserves the expectation

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.

Commit contents Files discussed in this chapter ris_direct_light_integrator.cpp · restir_render_settings.cpp · RenderSetup.usda
Complete commit diff ↗

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.

Convergence curves comparing per-candidate visibility with visibility evaluated only for the retained sample
The plot compares error against elapsed time for the two visibility policies in the committed test configuration. Measurement scope: The result depends on scene occlusion, candidate count, renderer revision, hardware, and reference image. Source: deferred-visibility commit 7cbd9f9
Record this: Use a scene with partial occlusion and many lights. Record equal-time runs with risSkipVisibility false and true, showing candidate count and shadow-ray count. Keep denoising disabled.
Caption to keep: Deferred visibility reduces candidate shadow rays; its variance depends on how accurately the unoccluded target predicts visible contribution.