# How to Implement Custom Filters for Image Post-Processing in LuisaRender

> Learn to implement custom image post-processing filters in LuisaRender. Subclass Filter, override evaluate, and register your implementation for advanced image effects.

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

---

**Implement a custom image post-processing filter in LuisaRender by subclassing `luisa::render::Filter`, overriding the `evaluate()` method to define the kernel function, and registering the implementation with `LUISA_RENDER_REGISTER_FILTER` to make it selectable in scene files.**

LuisaRender treats pixel reconstruction and tone-mapping kernels as first-class **Filter** scene nodes that are sampled per-pixel during camera ray generation. To implement custom filters for image post-processing in LuisaRender, you extend the abstract base class defined in [`src/base/filter.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/filter.h), provide the kernel evaluation logic, and expose the filter through the scene description JSON. This guide covers the architecture, implementation steps, and registration process using actual source paths from the luisagroup/luisarender repository.

## Filter Architecture and Runtime Flow

The filter system in LuisaRender separates the scene-node definition from the GPU-side sampling instance. Understanding this separation is critical for implementing custom kernels correctly.

### Core Components

- **`luisa::render::Filter`** (abstract base): Defined in [`src/base/filter.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/filter.h), this class specifies the filter's radius, shift, and the pure virtual `evaluate(x)` kernel function. It also declares the `build()` method that creates a runtime **instance**.
- **`Filter::Instance`**: Implemented in [`src/base/filter.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/filter.cpp), this holds a 64-entry lookup-table (LUT) and alias-sampling tables for fast GPU evaluation. It provides the `sample(u)` method that returns a sub-pixel offset and weight for a 2-D random sample.
- **`Pipeline::build_filter`**: Located in [`src/base/pipeline.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/pipeline.cpp), this method caches built filter instances in a `_filters` map, ensuring each unique filter node is constructed only once per render.
- **`Camera`**: As implemented in [`src/base/camera.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/camera.cpp), the camera holds a pointer to a `Filter` node and calls `filter()->sample(u_filter)` during ray generation to obtain sampling offsets.

### Typical Execution Flow

1. **Scene parsing**: `Scene::load_filter` (in [`src/base/scene.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/scene.cpp)) reads the `"filter"` node from JSON/YAML and constructs the appropriate derived `Filter` object.
2. **Pipeline build**: `Pipeline::build_filter` invokes `Filter::build`, which constructs a `Filter::Instance` containing the precomputed LUT and alias tables.
3. **Camera sampling**: During ray generation, the camera draws a 2-D uniform sample `u_filter` and calls `filter()->sample(u_filter)` to retrieve the offset and weighting for that pixel.

## Creating a Custom Filter Implementation

To add a new reconstruction kernel, create a header and implementation pair in `src/filters/`, such as [`myfilter.h`](https://github.com/luisagroup/luisarender/blob/main/myfilter.h) and [`myfilter.cpp`](https://github.com/luisagroup/luisarender/blob/main/myfilter.cpp).

### Header Definition

The header must inherit from `Filter` and implement the `evaluate()` method. You may optionally override `build()` if your filter requires custom GPU resources beyond the standard LUT.

```cpp
// src/filters/myfilter.h
#pragma once
#include <base/filter.h>

namespace luisa::render {

class MyFilter final : public Filter {
public:
    explicit MyFilter(Scene *scene, const SceneNodeDesc *desc) noexcept
        : Filter{scene, desc} {
        // Read custom parameters from the scene description
        _radius = desc->property_float_or_default("radius", 1.5f);
    }

    // Kernel evaluation: x is distance from center normalized to [0,1]
    [[nodiscard]] float evaluate(float x) const noexcept override {
        const float sigma = 2.0f;
        const float s = x * sigma;
        if (s < 1.0f) {
            return 1.0f - 2.0f * s * s + s * s * s;
        }
        if (s < 2.0f) {
            const float t = 2.0f - s;
            return t * t * t;
        }
        return 0.0f;
    }

    // Optional: override only if you need custom GPU resources
    [[nodiscard]] luisa::unique_ptr<Instance> build(
        Pipeline &pipeline, CommandBuffer &command_buffer) const noexcept override {
        return Filter::build(pipeline, command_buffer); // Use base implementation
    }
};

}// namespace luisa::render

```

### Implementation and Registration

The implementation file registers the filter with the scene-node factory using the `LUISA_RENDER_REGISTER_FILTER` macro.

```cpp
// src/filters/myfilter.cpp
#include <filters/myfilter.h>

namespace luisa::render {

LUISA_RENDER_REGISTER_FILTER("MyFilter", MyFilter)

}// namespace luisa::render

```

### Key Implementation Requirements

- **Base class**: Must inherit from `luisa::render::Filter`.
- **Constructor signature**: Must accept `(Scene *scene, const SceneNodeDesc *desc)` to receive scene context and property access.
- **`evaluate()`**: Must return non-negative weights and handle the normalized distance `x` such that the function returns zero for `|x| > 1.0` (i.e., outside the radius).
- **`build()`**: Most filters can rely on the default base implementation in [`src/base/filter.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/filter.cpp). Override only to allocate additional GPU buffers or textures.
- **Registration**: The `LUISA_RENDER_REGISTER_FILTER("<ImplName>", <Class>)` macro (defined in the scene registration system) makes the filter selectable via the `"impl"` string in scene files.

## Registering and Configuring the Filter

Once registered, the filter is available in scene descriptions using the `"filter"` node within compatible scene elements like cameras.

### Scene JSON Configuration

Specify the filter implementation name and any custom parameters defined in your constructor:

```json
{
  "camera": {
    "type": "Perspective",
    "filter": {
      "impl": "MyFilter",
      "radius": 2.0
    }
  }
}

```

The parser in [`src/base/scene.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/scene.cpp) reads the `"impl"` field, looks up the registered constructor, and passes the node description to your filter's constructor.

## Runtime Sampling and GPU Integration

When the pipeline builds your filter, the base class automatically handles performance-critical sampling infrastructure.

### LUT and Alias Sampling Construction

During `Filter::build` (in [`src/base/filter.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/filter.cpp)), the system:
1. Samples your `evaluate(x)` function uniformly over `[0, radius]` to populate a 64-entry LUT (`_lut`).
2. Constructs discrete alias tables (`_pdf`, `_alias_probs`, `_alias_indices`) enabling **O(1)** importance sampling of the kernel distribution.

### Camera Integration

At render time, the camera (in [`src/base/camera.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/camera.cpp)) calls `Instance::sample(u)` with a 2-D uniform random sample `u`. This method uses the alias table to select a LUT entry and returns the corresponding sub-pixel offset and weight, applying your custom kernel without per-sample kernel evaluation overhead.

## Summary

- **Filters are scene nodes**: They are parsed from JSON, built into GPU instances by the pipeline, and sampled by the camera during ray generation.
- **Minimal implementation**: Subclass `Filter`, implement `evaluate()` for the kernel function, and use the base class `build()` method unless you need custom GPU resources.
- **Registration**: Use `LUISA_RENDER_REGISTER_FILTER("<Name>", <Class>)` in your `.cpp` file to expose the filter to the scene loader.
- **Automatic optimization**: The base class handles LUT generation and alias sampling in [`src/base/filter.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/filter.cpp), providing fast GPU sampling without additional code.
- **Scene usage**: Reference your filter in the camera configuration using `"impl": "<Name>"` and any custom properties defined in your constructor.

## Frequently Asked Questions

### What is the difference between the Filter class and Filter::Instance?

The **`Filter`** class (defined in [`src/base/filter.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/filter.h)) is the scene-side node that holds construction parameters and the `evaluate()` kernel definition. The **`Filter::Instance`** (created in [`src/base/filter.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/filter.cpp)) is the GPU-side runtime object that holds precomputed lookup tables and implements the `sample()` method used during rendering. The pipeline caches instances via `Pipeline::build_filter` to avoid redundant construction.

### How does the radius parameter affect filter behavior?

The **radius** determines the spatial support of the filter in world units. In your constructor, you read this from the scene description via `desc->property_float_or_default()`. The `evaluate(x)` method receives `x` normalized to `[0,1]`, where `1.0` corresponds to the full radius. The filter must return `0.0` when `x > 1.0` to indicate no contribution outside the support region.

### Can I use textures or buffers in my custom filter?

Yes. If your filter requires additional GPU resources (e.g., a pre-computed kernel texture or neural network weights), override the `build()` method in your header. Instead of calling `Filter::build()`, allocate your resources using the provided `Pipeline` and `CommandBuffer`, then construct and return a custom `Instance` subclass that holds these resources. Most simple kernels can rely on the default base implementation.

### Where should I place the LUISA_RENDER_REGISTER_FILTER macro?

Place the **macro in your `.cpp` implementation file**, not in the header. This ensures the registration occurs exactly once during linkage. Existing filters like `Box`, `Gaussian`, and `Mitchell` in `src/filters/` follow this pattern; examine [`src/filters/gaussian.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/filters/gaussian.cpp) for a reference implementation. The macro registers your class with the scene loader in [`src/base/scene.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/scene.cpp), enabling lookup by the `"impl"` string.