Direct-light integrators and multiple importance sampling
e6fb9bb · 2026-05-26 ↗
This commit separates two operations that were previously implemented together:
- Path integration: how does a ray continue through multiple bounces?
- Direct-light integration: which light connection should be evaluated at the current surface?
PathIntegrator retains multi-bounce transport. A new
IDirectLightIntegrator interface receives a shaded point and evaluates the
direct-light term. ILightSampler separately owns the proposal used to select
a light.
classDiagram
class PathIntegrator {
+Li(ray, scene, rng, wavelengths)
}
class IDirectLightIntegrator {
<<interface>>
+Li(surface, scene, rng)
}
class MISDirectLightIntegrator
class ILightSampler {
<<interface>>
+Sample(scene, rng)
}
class UniformLightSampler
PathIntegrator --> IDirectLightIntegrator
IDirectLightIntegrator <|-- MISDirectLightIntegrator
MISDirectLightIntegrator --> ILightSampler
ILightSampler <|-- UniformLightSampler
Why the light-selection probability belongs in the PDF
With \(L\) lights, UniformLightSampler selects light \(\ell\) with probability
\(1/L\), then samples a point or direction conditionally with density
\(p(z\mid\ell)\). The joint proposal is
Forgetting \(1/L\) makes the estimate darker as more lights are added.
The sampled domain is the pair \((\ell,z)\), not only the point \(z\). Different lights may contain geometrically identical positions or directions, but their emission and selection probabilities remain associated with the light identity. Treating the light index as part of the random variable avoids an ambiguous mixture PDF.
Assumptions: A light is selected uniformly and its conditional sampler covers the contribution on that light.
The NEE estimator is \(F(Z,\ell)/p_N(Z,\ell)\). Its expectation is
\[\sum_{\ell=1}^{L}\int \frac1L p(z\mid\ell) \frac{F(z,\ell)}{(1/L)p(z\mid\ell)}\,dz =\sum_{\ell=1}^{L}\int F(z,\ell)\,dz.\]The probability of choosing the light cancels exactly. If the denominator omits it, the result becomes \(1/L\) of the desired sum.
Combining light and BSDF sampling
The MISDirectLightIntegrator evaluates light and BSDF proposals with the
power heuristic
As in the previous chapter, \(\sum_t w_t(z)=1\), so the estimator remains unbiased. Squaring emphasizes the technique whose density is larger at the sample and often reduces variance. The exponent changes variance, not the partition-of-unity proof.
CUDA VolPT provides useful historical context in its path-tracing discussion, including the benefit and GPU control-flow cost of next-event estimation, and in its bidirectional path-tracing discussion, where multiple sampling strategies are combined with MIS. HdRestir applies that combination locally to light and BSDF proposals at one surface.
Area, solid-angle, and discrete-delta PDFs cannot be mixed blindly. HdRestir’s
Pdf type records the measure and performs the rectangle-light Jacobian
conversion before MIS. Delta lights use probability mass and are exclusive
from continuous competitors.
For a point or directional delta light, the BSDF technique has zero probability of generating exactly the same event under a continuous solid-angle distribution. Applying a continuous two-technique heuristic would compare incompatible measures and suppress a contribution that has no actual competitor. The implementation therefore assigns the NEE delta contribution a weight of one.
For finite area lights, both NEE and BSDF sampling can generate the same direction. Their PDFs must include all conditional choices and be expressed in solid angle at the shading point before the power heuristic is evaluated.
The following model applies the same formula to one non-delta direction. Moving either density changes the relative MIS weight; changing the exponent changes how strongly the larger density is preferred. The two displayed weights always sum to one because the numerator terms are normalized by their sum.
Increasing β moves more weight toward the technique with the larger PDF. The two weights still form a partition of unity. Delta events require the separate treatment described below.
Read the refactor from the seam inward
The new call boundary is the semantic part of this refactor. The interface is read first, followed by the default MIS implementation that satisfies it.
Direct-light integrator interface
class IDirectLightIntegrator : public IIntegrator {
public:
~IDirectLightIntegrator() override = default;
[[nodiscard]] virtual SampledSpectrum Li(
const ShadingPoint& surface,
const IScene& scene,
Rng& rng,
const std::optional<BsdfBounceConnection>& bsdfConnection) const = 0;
};
The input is already a shaded surface, not a raw ray. The optional
BsdfBounceConnection carries the light or environment reached by the BSDF
sample. That gives the direct-light implementation both competing techniques:
an explicit light proposal and the already-generated BSDF proposal.
The interface returns only radiance. It does not decide how the path continues;
that responsibility remains in PathIntegrator.
This ownership rule prevents the direct-light strategy from advancing path depth, changing persistent path throughput, applying Russian roulette, selecting the continuation ray, or deciding when the full path terminates. It may trace shadow rays and evaluate the connection already produced by BSDF sampling, but those operations estimate direct radiance at the current vertex.
Light-selection probability
if (_lights.empty()) {
return std::nullopt;
}
const std::size_t lightIndex{
std::min(static_cast<std::size_t>(
rng.NextFloat() * _lights.size()), _lights.size() - 1u)};
const ILight& light{*_lights[lightIndex]};
const auto lightSample{light.SampleLight(hitPos, rng)};
// ...
const float lightSelectPdf{1.0f / static_cast<float>(_lights.size())};
// ...
LightSample ls = *lightSample;
ls.Pdf.value *= lightSelectPdf;
The empty-list guard is required before evaluating _lights.size() - 1u or
indexing the vector. For a non-empty list there are two random choices: which
light, then where on that light. Their joint density is the product.
Multiplying the conditional LightSample PDF here means every downstream
estimator receives the complete density and cannot accidentally forget the
\(1/L\) factor proved above.
Strategy construction in the path integrator
auto directLightIntegrator{_directLightFactory(scene)};
// ...
const BounceWithConnectionResult firstBounceResult{
Detail::SampleBounceWithConnection(
firstMaterial, firstSurface, config, bounceState, scene, rng)};
const std::optional<BsdfBounceConnection> firstConnection{
std::holds_alternative<BsdfBounceConnection>(firstBounceResult)
? std::make_optional(
std::get<BsdfBounceConnection>(firstBounceResult))
: std::nullopt};
totalRadiance += directLightIntegrator->Li(
firstSurface, scene, rng, firstConnection);
The factory creates one direct-light strategy for the current scene. The path integrator still owns bounce sampling and throughput; it hands the connection to the strategy only when computing direct radiance. RIS can therefore replace MIS later without duplicating the bounce loop.
The strategy is created from the current scene because a light sampler may cache the set of lights or build a scene-dependent distribution. The factory also becomes the place where a later ReSTIR implementation can stage persistent buffers without adding reservoir requirements to the ordinary MIS integrator.
MIS application to the direct-light contribution
const MISContrib nee{
_evaluateNEE(surface, *candidate->Light, candidate->Ls, scene)};
if (nee.PNee > 0.0f) {
const float misWeight{(nee.IsDelta || !useBsdfTechnique)
? 1.0f
: PowerHeuristic(nee.PNee, nee.PBsdf)};
totalRadiance += nee.Radiance * misWeight;
}
_evaluateNEE has already converted the light PDF to solid angle, traced the
shadow ray, and divided the contribution by PNee. This block supplies only
the MIS partition weight. Delta lights receive weight one because the
continuous BSDF technique cannot sample the same discrete event.
The BSDF-sampled branch performs the complementary computation. When a sampled bounce reaches an emissive surface or environment, it evaluates the NEE proposal density for the same connection and applies the BSDF-side MIS weight. Implementing only the light-sampled side would omit valid paths and would not form the two-technique estimator derived above.
Validation cases for this commit
The refactor should preserve the previous path tracer’s estimator while changing ownership. Small scenes isolate the main conditions:
| Scene or test | Failure it exposes |
|---|---|
| One point light | Incorrect delta-light weighting |
| One rectangle light on a diffuse surface | Area-to-solid-angle conversion |
| Small area light on a glossy surface | Missing BSDF-sampled light connection |
| Two identical lights | Missing \(1/L\) selection probability |
| Environment only | Incorrect environment proposal handling |
| No lights and a black environment | Empty sampler and zero-radiance handling |
The two-identical-light case has a direct expected result: if the second light has the same transform and emission, mean radiance should double. If the selection probability is missing from the proposal density, the result scales incorrectly when the light count changes.
Two diagnostic scenes
mis_area_lights_scene.usda places differently sized area lights where neither
proposal has the lower variance everywhere. many_lights_scene.usda makes the
\(1/L\) selection factor observable. Each scene isolates one estimator
condition while keeping the remaining render configuration fixed.
Caption to keep: MIS follows the light proposal where it is effective and the BSDF proposal where that is effective, while its weights sum to one.
Code map
direct_light_integrator_interface.hmis_direct_light_integrator.cppuniform_light_sampler.cpp- Full commit diff
The interface now accepts a different direct-light strategy. The next commit uses it to insert RIS.