How to Implement Custom Light Samplers for Importance Sampling in LuisaRender
To implement a custom light sampler in LuisaRender, subclass luisa::render::LightSampler, implement the nested Instance type with select() and sampling methods, and register it with LUISA_RENDER_MAKE_SCENE_NODE_PLUGIN.
LuisaRender delegates light source selection to pluggable light sampler components, enabling developers to replace uniform sampling with importance sampling strategies such as power-weighted or texture-driven distributions. Implementing custom light samplers for importance sampling in LuisaRender requires deriving from the abstract base class defined in src/base/light_sampler.h and implementing GPU-bound selection logic. This architecture allows integrators to importance-sample lights without modifying core rendering code.
Core Architecture of Light Samplers
LuisaRender employs a dual-class pattern for GPU-compatible plugins. The outer class handles host-side construction and configuration, while the nested Instance class encapsulates the actual sampling logic compiled for the GPU.
The Base Class Interface
All light samplers must inherit from luisa::render::LightSampler and override the pure virtual build() method. The base class constructor accepts a Scene pointer and a SceneNodeDesc pointer for parsing custom parameters.
// In src/base/light_sampler.h
class LightSampler : public SceneNode {
public:
class Instance;
virtual luisa::unique_ptr<Instance> build(Pipeline &pipeline, CommandBuffer &command_buffer) const noexcept = 0;
// ...
};
The Instance Pattern
The nested Instance class (defined inside your LightSampler subclass) holds compiled GPU kernels and device buffers. It must provide methods that execute on the GPU to select lights and evaluate sampling probabilities. The reference implementation in src/lightsamplers/uniform.cpp demonstrates the required interface contract.
Step-by-Step Implementation Guide
Step 1: Create the Light Sampler Subclass
Define a new class that inherits from LightSampler. The constructor should forward arguments to the base class and extract any custom parameters from the SceneNodeDesc.
class PowerWeightedLightSampler final : public LightSampler {
public:
class Instance final : public LightSampler::Instance {
// GPU-side implementation
};
explicit PowerWeightedLightSampler(Scene *scene, const SceneNodeDesc *desc) noexcept
: LightSampler{scene, desc} {
// Parse host-side parameters here
}
[[nodiscard]] luisa::unique_ptr<Instance> build(Pipeline &pipeline, CommandBuffer &) const noexcept override;
};
Step 2: Implement Required Instance Methods
Your Instance class must override the following pure virtual methods to perform importance sampling:
-
Selection select(const Interaction &it, Expr<float> u, const SampledWavelengths &swl, Expr<float> time)
Returns aSelectionstruct containing a light tag and selection probabilityprob. This variant receives shading context viait. -
Selection select(Expr<float> u, const SampledWavelengths &swl, Expr<float> time)
Environment-only selection overload used when no surface interaction is available. -
Light::Sample _sample_light(const Interaction &it_from, Expr<uint> tag, Expr<float2> u, const SampledWavelengths &swl, Expr<float> time)
Delegates to the concrete light's sampling routine for the tag selected byselect(). -
Environment::Sample _sample_environment(Expr<float2> u, const SampledWavelengths &swl, Expr<float> time)
Samples the environment map when the selection indicates environment lighting (tag == selection_environment). -
LightSampler::Sample _sample_light_le(Expr<uint> tag, Expr<float2> u_light, Expr<float2> u_dir, const SampledWavelengths &swl, Expr<float> time)
Performs light-emission sampling (e.g., for area lights), crucial for bidirectional techniques and light-path tracing.
Optional overrides include evaluate_hit() and evaluate_miss() for analytic PDF weighting when you need to compute multiple importance sampling (MIS) weights outside of standard selection probabilities.
Step 3: Register the Plugin
At the end of your implementation file, invoke the registration macro to make the sampler discoverable by the scene parser:
LUISA_RENDER_MAKE_SCENE_NODE_PLUGIN(luisa::render::PowerWeightedLightSampler)
This macro is defined in src/base/scene_node.h and instantiates the plugin factory.
Step 4: Configure Scene Parameters
If your sampler requires user data (e.g., a power threshold or importance map path), read these values in the constructor via desc->property_float(), desc->property_string(), or similar methods. The JSON scene description will automatically pass these parameters when the sampler is instantiated.
Complete Example: Power-Weighted Importance Sampling
The following skeleton implements a power-weighted distribution, where lights are selected based on their emitted power rather than uniformly. It pre-computes a CDF on the host and uploads it to a Buffer<float> for GPU lookup.
// src/lightsamplers/power_weighted.cpp
#include <base/light_sampler.h>
#include <base/scene_node_desc.h>
namespace luisa::render {
class PowerWeightedLightSampler final : public LightSampler {
public:
class Instance final : public LightSampler::Instance {
private:
Buffer<float> _cdf; // Pre-computed CDF of light powers
[[nodiscard]] uint _draw_light(Expr<float> u) const noexcept {
// Binary search on _cdf to map uniform u to light tag
// Implementation uses luisa::compute kernels
return tag;
}
public:
explicit Instance(const Pipeline &pipeline, const PowerWeightedLightSampler *sampler) noexcept
: LightSampler::Instance{pipeline, sampler} {
// Build _cdf from scene light powers (host side)
// Upload to GPU buffer
}
[[nodiscard]] Selection select(const Interaction &,
Expr<float> u,
const SampledWavelengths &,
Expr<float>) const noexcept override {
uint tag = _draw_light(u);
Float pdf = 0.0f; // Compute from CDF differences
return {.tag = tag, .prob = pdf};
}
[[nodiscard]] Selection select(Expr<float> u,
const SampledWavelengths &,
Expr<float>) const noexcept override {
return select(Interaction{}, u, {}, 0.0f);
}
[[nodiscard]] Light::Sample _sample_light(const Interaction &it_from,
Expr<uint> tag,
Expr<float2> u,
const SampledWavelengths &swl,
Expr<float> time) const noexcept override {
return _pipeline.lights()[tag]->sample(it_from, u, swl, time);
}
[[nodiscard]] Environment::Sample _sample_environment(Expr<float2> u,
const SampledWavelengths &swl,
Expr<float> time) const noexcept override {
return _pipeline.environment()->sample(u, swl, time);
}
[[nodiscard]] LightSampler::Sample _sample_light_le(Expr<uint> tag,
Expr<float2> u_light,
Expr<float2> u_dir,
const SampledWavelengths &swl,
Expr<float> time) const noexcept override {
return _pipeline.lights()[tag]->sample_le(u_light, u_dir, swl, time);
}
[[nodiscard]] Evaluation evaluate_hit(const Interaction &,
Expr<float3>,
const SampledWavelengths &,
Expr<float>) const noexcept override {
return {}; // Not required for standard MIS
}
[[nodiscard]] Evaluation evaluate_miss(Expr<float3>,
const SampledWavelengths &,
Expr<float>) const noexcept override {
return {};
}
};
explicit PowerWeightedLightSampler(Scene *scene, const SceneNodeDesc *desc) noexcept
: LightSampler{scene, desc} {}
[[nodiscard]] luisa::unique_ptr<Instance> build(Pipeline &pipeline, CommandBuffer &cmd) const noexcept override {
return luisa::make_unique<Instance>(pipeline, this);
}
};
LUISA_RENDER_MAKE_SCENE_NODE_PLUGIN(luisa::render::PowerWeightedLightSampler)
} // namespace luisa::render
Connecting to Scene Description
After compilation, reference your sampler in the JSON scene file using the registered type name. Integrators such as DirectLighting or PathTracing will automatically invoke your select() method when choosing lights.
{
"type": "power_weighted",
"param": "value"
}
Verify the implementation by rendering a scene with varying light intensities; the resulting image should exhibit lower noise in regions illuminated by high-power sources compared to uniform sampling.
Summary
- Inherit from
LightSamplerdefined insrc/base/light_sampler.hand implement thebuild()method. - Implement the nested
Instanceclass withselect(),_sample_light(),_sample_environment(), and_sample_light_le()methods to define GPU-side importance sampling logic. - Register the plugin using
LUISA_RENDER_MAKE_SCENE_NODE_PLUGINto enable scene-parser discovery. - Pre-compute distributions (e.g., CDFs) on the host in the
Instanceconstructor, storing data in GPU buffers for kernel access. - Reference
src/lightsamplers/uniform.cppfor the canonical implementation of the interface contract.
Frequently Asked Questions
What is the difference between _sample_light and _sample_light_le?
_sample_light samples the direct lighting contribution from a light source given a shading point, while _sample_light_le samples the light's emission profile independently of any shading context (e.g., sampling a point on an area light and a direction from its emission distribution). The latter is essential for light-path tracing and bidirectional algorithms implemented in LuisaRender.
How do I handle environment lighting in a custom sampler?
Your select() method can return a special tag indicating environment selection (typically selection_environment). When this tag is returned, the integrator will call _sample_environment() instead of _sample_light(). You must ensure your probability calculation in select() accounts for the environment's contribution relative to other lights.
Can I use texture-based importance maps with custom light samplers?
Yes. Store the importance map as a Buffer<float> or Image<float> in your Instance class, initialized from file data in the constructor. In the select() method, use Expr<float2> coordinates to lookup and PDF-weight your selection probability, enabling spatially-varying importance sampling based on sky maps or occlusion data.
Where should I build the sampling distribution (host vs device)?
Build complex data structures like CDFs or KD-trees on the host (CPU) within the Instance constructor, then upload the resulting arrays to device buffers (GPU). The select() and sampling methods only execute device-side kernels and should not perform dynamic memory allocation or complex branching.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →