# How to Implement Custom Light Samplers for Importance Sampling in LuisaRender

> Learn to implement custom light samplers for importance sampling in LuisaRender. Subclass LightSampler, implement Instance methods, and register your plugin for advanced rendering control.

- Repository: [LuisaGroup/luisarender](https://github.com/luisagroup/luisarender)
- Tags: how-to-guide
- Published: 2026-03-06

---

**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`](https://github.com/luisagroup/luisarender/blob/main/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.

```cpp
// 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`](https://github.com/luisagroup/luisarender/blob/main/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`.

```cpp
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 a `Selection` struct containing a light tag and selection probability `prob`. This variant receives shading context via `it`.

- **`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 by `select()`.

- **`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:

```cpp
LUISA_RENDER_MAKE_SCENE_NODE_PLUGIN(luisa::render::PowerWeightedLightSampler)

```

This macro is defined in [`src/base/scene_node.h`](https://github.com/luisagroup/luisarender/blob/main/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.

```cpp
// 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.

```json
{
    "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 `LightSampler`** defined in [`src/base/light_sampler.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/light_sampler.h) and implement the `build()` method.
- **Implement the nested `Instance` class** with `select()`, `_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_PLUGIN` to enable scene-parser discovery.
- **Pre-compute distributions** (e.g., CDFs) on the host in the `Instance` constructor, storing data in GPU buffers for kernel access.
- **Reference [`src/lightsamplers/uniform.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/lightsamplers/uniform.cpp)** for 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.