How to Implement Custom Filters for Image Post-Processing in LuisaRender
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, 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 insrc/base/filter.h, this class specifies the filter's radius, shift, and the pure virtualevaluate(x)kernel function. It also declares thebuild()method that creates a runtime instance.Filter::Instance: Implemented insrc/base/filter.cpp, this holds a 64-entry lookup-table (LUT) and alias-sampling tables for fast GPU evaluation. It provides thesample(u)method that returns a sub-pixel offset and weight for a 2-D random sample.Pipeline::build_filter: Located insrc/base/pipeline.cpp, this method caches built filter instances in a_filtersmap, ensuring each unique filter node is constructed only once per render.Camera: As implemented insrc/base/camera.cpp, the camera holds a pointer to aFilternode and callsfilter()->sample(u_filter)during ray generation to obtain sampling offsets.
Typical Execution Flow
- Scene parsing:
Scene::load_filter(insrc/base/scene.cpp) reads the"filter"node from JSON/YAML and constructs the appropriate derivedFilterobject. - Pipeline build:
Pipeline::build_filterinvokesFilter::build, which constructs aFilter::Instancecontaining the precomputed LUT and alias tables. - Camera sampling: During ray generation, the camera draws a 2-D uniform sample
u_filterand callsfilter()->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 and 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.
// 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.
// 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 distancexsuch that the function returns zero for|x| > 1.0(i.e., outside the radius).build(): Most filters can rely on the default base implementation insrc/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:
{
"camera": {
"type": "Perspective",
"filter": {
"impl": "MyFilter",
"radius": 2.0
}
}
}
The parser in 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), the system:
- Samples your
evaluate(x)function uniformly over[0, radius]to populate a 64-entry LUT (_lut). - 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) 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, implementevaluate()for the kernel function, and use the base classbuild()method unless you need custom GPU resources. - Registration: Use
LUISA_RENDER_REGISTER_FILTER("<Name>", <Class>)in your.cppfile to expose the filter to the scene loader. - Automatic optimization: The base class handles LUT generation and alias sampling in
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) is the scene-side node that holds construction parameters and the evaluate() kernel definition. The Filter::Instance (created in 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 for a reference implementation. The macro registers your class with the scene loader in src/base/scene.cpp, enabling lookup by the "impl" string.
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 →