# Sampling Strategies in luisa-render: Sobol, PMJ02, and Custom Implementation

> Explore Sobol PMJ02 and custom sampling strategies in luisa-render. Learn to implement your own low-discrepancy generators easily and enhance your rendering with flexible sampler options.

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

---

**luisa-render provides five built-in sampling strategies—including Sobol, ZSobol, PaddedSobol, Independent, and PMJ02BN—and exposes an abstract `Sampler` interface in [`src/base/sampler.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/sampler.h) that allows developers to implement custom low-discrepancy generators by subclassing `Sampler` and `Sampler::Instance`.**

The luisa-render framework ships with a flexible, GPU-accelerated sampling system for generating quasi-random numbers required by path tracing integrators. Understanding these **sampling strategies** and their implementation patterns is essential for optimizing rendering convergence and noise characteristics. This article examines the built-in samplers and provides a complete guide to extending the system with custom variants.

## Built-in Sampling Strategies

luisa-render implements both low-discrepancy sequences and independent random generators. All classes inherit from `luisa::render::Sampler` defined in [`src/base/sampler.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/sampler.h).

### SobolSampler

The **SobolSampler** generates the classic Sobol sequence with optional Owen scrambling, operating on a per-pixel basis with support for arbitrary dimensions. The core logic resides in [`src/samplers/sobol.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/samplers/sobol.cpp), where the template call `_sobol_sample<true>` applies scrambling to raw Sobol values, and `_sobol_interval_to_index` converts pixel intervals to sequence indices.

### ZSobolSampler

The **ZSobolSampler** variant optimizes memory locality for 2D sampling by storing Sobol matrices in pairs (2 × SobolMatrixSize). As implemented in [`src/samplers/zsobol.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/samplers/zsobol.cpp), the constructor copies matrices using the command buffer syntax: `command_buffer << _sobol_matrices.copy_from(SobolMatrices32);`.

### PaddedSobolSampler

To prevent out-of-bounds memory access when dimension indices wrap around, the **PaddedSobolSampler** adds one extra column to the Sobol matrix. This safety padding is defined in [`src/samplers/padded_sobol.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/samplers/padded_sobol.cpp).

### IndependentSampler

For pure Monte-Carlo sampling without correlation, the **IndependentSampler** utilizes a per-pixel XorShift RNG. This generator produces statistically independent random numbers and is implemented in [`src/samplers/independent.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/samplers/independent.cpp).

### PMJ02BNSampler

The **PMJ02BNSampler** provides Progressive Multi-Jittered blue-noise sampling using pre-computed tables (`PMJ02bnSamples`) supporting up to 65,536 samples per pixel. The main sampling logic appears in [`src/samplers/pmj02bn.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/samplers/pmj02bn.cpp), while the lookup tables are defined in [`src/util/pmj02tables.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/util/pmj02tables.cpp) and exposed via [`src/util/pmj02tables.h`](https://github.com/luisagroup/luisarender/blob/main/src/util/pmj02tables.h).

## Implementing a Custom Sampler

Adding a new **sampling strategy** requires implementing two classes: a factory `Sampler` subclass and a stateful `Sampler::Instance` subclass. The factory creates GPU resources, while the instance generates samples per pixel.

### 1. Define the Sampler Subclass

Create a new `.cpp` file in `src/samplers/` that subclasses `Sampler` and implements the `build` factory method and `impl_type` identifier:

```cpp
class MySampler final : public Sampler {
public:
    MySampler(Scene *scene, const SceneNodeDesc *desc) noexcept 
        : Sampler{scene, desc} {}
    [[nodiscard]] luisa::unique_ptr<Instance> 
    build(Pipeline&, CommandBuffer&) const noexcept override;
    [[nodiscard]] luisa::string_view impl_type() const noexcept override { 
        return LUISA_RENDER_PLUGIN_NAME; 
    }
};

```

Reference [`src/samplers/sobol.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/samplers/sobol.cpp) lines 15-22 for the canonical pattern.

### 2. Implement the Instance Interface

The `Sampler::Instance` subclass must implement virtual methods called by integrators each frame:

- **reset** – Allocate or resize GPU buffers, compute lookup tables, and store resolution-dependent data
- **start** – Set the pixel location and sample index for the current pixel  
- **save_state** / **load_state** – Serialize the internal RNG index and per-pixel counters so progressive rendering can resume
- **generate_1d** and **generate_2d** – Return uniformly distributed values (or low-discrepancy values) in `[0,1)`

See [`src/samplers/sobol.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/samplers/sobol.cpp) lines 31-78 for a complete implementation of these methods. For table-lookup sampling patterns, reference [`src/samplers/pmj02bn.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/samplers/pmj02bn.cpp) around line 50.

### 3. Register the Plugin

Expose the sampler to the scene graph using the registration macro at the bottom of your source file:

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

```

This macro is used at the bottom of every built-in sampler, such as [`src/samplers/sobol.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/samplers/sobol.cpp) line 184.

### 4. Update the Build System

Add the new source file to [`src/samplers/CMakeLists.txt`](https://github.com/luisagroup/luisarender/blob/main/src/samplers/CMakeLists.txt) to ensure it compiles and links with the rest of the framework.

## Complete Custom Sampler Example

The following implementation demonstrates a minimal uniform random sampler using XorShift hashing. It allocates a state buffer in `reset`, seeds the RNG by pixel index in `start`, and generates 1D/2D samples:

```cpp
// file: src/samplers/my_uniform.cpp
#include <base/sampler.h>
#include <base/pipeline.h>
#include <dsl/sugar.h>

namespace luisa::render {

class MyUniformSampler final : public Sampler {
public:
    MyUniformSampler(Scene *scene, const SceneNodeDesc *desc) noexcept
        : Sampler{scene, desc} {}
    [[nodiscard]] luisa::unique_ptr<Instance>
    build(Pipeline &pipeline, CommandBuffer &command_buffer) const noexcept override;
    [[nodiscard]] luisa::string_view impl_type() const noexcept override {
        return LUISA_RENDER_PLUGIN_NAME;
    }
};

class MyUniformSamplerInstance final : public Sampler::Instance {
private:
    uint2 _resolution;
    uint _pixel_dim{};
    Buffer<uint4> _state_buf;

public:
    explicit MyUniformSamplerInstance(const Pipeline &pipeline,
                                     CommandBuffer &cmd,
                                     const MyUniformSampler *s) noexcept
        : Sampler::Instance{pipeline, s} {
        _state_buf = pipeline.device().create_buffer<uint4>(1);
    }

    void reset(CommandBuffer &, uint2 resolution,
               uint state_count, uint) noexcept override {
        _resolution = resolution;
        if (!_state_buf || _state_buf.size() < state_count) {
            _state_buf = pipeline().device().create_buffer<uint4>(next_pow2(state_count));
        }
    }

    void start(Expr<uint2> pixel, Expr<uint>) noexcept override {
        _pixel_dim = pixel.x + pixel.y * _resolution.x;
    }

    void save_state(Expr<uint>) noexcept override {}
    void load_state(Expr<uint>) noexcept override {}

    [[nodiscard]] Float generate_1d() noexcept override {
        auto state = make_uint4(_pixel_dim, 0u, 0u, 0u);
        auto r = xxhash32(state.xyz());
        return cast<float>(r) * (1.f / 0xffffffffu);
    }

    [[nodiscard]] Float2 generate_2d() noexcept override {
        Float x = generate_1d();
        Float y = generate_1d();
        return make_float2(x, y);
    }
};

luisa::unique_ptr<Sampler::Instance>
MyUniformSampler::build(Pipeline &p, CommandBuffer &c) const noexcept {
    return luisa::make_unique<MyUniformSamplerInstance>(p, c, this);
}

LUISA_RENDER_MAKE_SCENE_NODE_PLUGIN(luisa::render::MyUniformSampler)

} // namespace luisa::render

```

## Using Samplers in Scene Descriptions

Integrators retrieve the active sampler via `scene->sampler()` and interact only with the abstract `Sampler::Instance` interface. This design allows you to switch between **sampling strategies** without code modifications:

```cpp
// In your integrator (e.g., src/integrators/megasampler.cpp)
auto sampler = scene->sampler();
sampler->reset(command_buffer, resolution, state_count, spp);
for (uint2 pixel : image_pixels) {
    sampler->start(pixel, frame_index);
    Float2 u = sampler->generate_2d();
    // Use u for importance sampling...
}

```

Configure the specific sampler declaratively in your scene YAML:

```yaml
render:
  sampler:
    impl: Sobol
    seed: 12345

```

Valid `impl` values include `Sobol`, `ZSobol`, `PaddedSobol`, `Independent`, and `PMJ02BN`.

## Summary

- luisa-render provides five built-in **sampling strategies**: **SobolSampler**, **ZSobolSampler**, **PaddedSobolSampler**, **IndependentSampler**, and **PMJ02BNSampler**
- All samplers inherit from `Sampler` in [`src/base/sampler.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/sampler.h) and implement the `Sampler::Instance` interface
- Required implementation methods: `reset`, `start`, `save_state`, `load_state`, `generate_1d`, and `generate_2d`
- Register custom samplers with `LUISA_RENDER_MAKE_SCENE_NODE_PLUGIN` and add them to [`src/samplers/CMakeLists.txt`](https://github.com/luisagroup/luisarender/blob/main/src/samplers/CMakeLists.txt)
- Integrators call the virtual interface, allowing hot-swapping between Sobol, PMJ02BN, or custom samplers via scene configuration

## Frequently Asked Questions

### What is the difference between SobolSampler and ZSobolSampler?

**ZSobolSampler** stores Sobol matrices in pairs (2 × matrix size) to improve GPU memory locality when performing 2D sampling operations, whereas **SobolSampler** uses the standard matrix layout optimized for arbitrary dimensions. Both are implemented in [`src/samplers/sobol.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/samplers/sobol.cpp) and [`src/samplers/zsobol.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/samplers/zsobol.cpp) respectively.

### How many samples per pixel does PMJ02BNSampler support?

The **PMJ02BNSampler** supports up to 65,536 samples per pixel using pre-computed tables defined in [`src/util/pmj02tables.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/util/pmj02tables.cpp). These tables provide progressive multi-jittered blue-noise values without runtime generation overhead.

### Do I need to modify integrator code when switching between samplers?

No. Integrators interact with the abstract `Sampler::Instance` interface through methods like `generate_2d()`, `reset()`, and `start()`. You can switch between Sobol, PMJ02BN, or custom samplers by changing the `impl` field in the scene description YAML without recompiling integrator code.

### Where are the Sobol direction numbers defined?

The Sobol matrices (`SobolMatrices32`, `VdCSobolMatrices`) are declared in [`src/util/sobolmatrices.h`](https://github.com/luisagroup/luisarender/blob/main/src/util/sobolmatrices.h) and defined in [`src/util/sobolmatrices.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/util/sobolmatrices.cpp). These constants are used by `SobolSampler` and its variants to generate low-discrepancy sequences.