# How LuisaRender's Spectrum System Implements Physically-Based Color Representation

> Discover how LuisaRender's spectrum system achieves physically-based color with continuous spectral coefficients. Explore wavelength-accurate light transport beyond RGB.

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

---

**LuisaRender implements physically-based color representation by encoding sRGB values as continuous sigmoid-polynomial spectral coefficients stored in a 4D lookup table, enabling wavelength-accurate light transport beyond traditional three-channel RGB limits.**

LuisaRender treats color as a continuous spectral function rather than discrete RGB values, using a hero wavelength sampling approach grounded in actual spectral power distributions. This article explains how the spectrum system in `luisagroup/luisarender` converts conventional sRGB inputs into physically accurate spectral representations for realistic rendering.

## From sRGB to Spectral Coefficients

The pipeline begins when a user supplies an sRGB color (`float3 rgb`) that must be converted into a spectral representation for physically-based rendering.

### The 4D Lookup Table Architecture

At the core of this conversion is the **`RGB2SpectrumTable`** class defined in [`src/spectra/srgb2spec.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/spectra/srgb2spec.cpp), which manages a pre-computed 4D lookup table with **64³ resolution**. This table maps any sRGB triplet to three polynomial coefficients (`c0`, `c1`, `c2`) per color channel. The raw table data lives in [`src/spectra/srgb.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/spectra/srgb.cpp) as `sRGBToSpectrumTable_Data`.

### Encoding Albedo with RGB2SpectrumTable

The conversion happens through `HeroWavelengthSpectrum::encode_static_srgb_albedo`, which forwards the color to the singleton table via `RGB2SpectrumTable::srgb().decode_albedo(rgb)`. The decoding process performs the following steps:

- Normalizes the input `rgb` to the range `[0,1]`
- For neutral colors (`r==g==b`), uses an analytical formula based on CIE-Y
- For chromatic colors, identifies the **max-channel** (`maxc`) and builds a 2D remapping `(x, y, zz)` where `x` and `y` are ratios of the other channels to the max, and `zz` is a double-inverse-smooth-step of the max-channel
- Performs **trilinear interpolation** on `_coefficients[maxc][z][y][x][i]` to retrieve the three sigmoid-polynomial coefficients

The function returns a `float4` where the `xyz` components contain the coefficients and the `w` component stores the CIE-Y luminance.

## Continuous Sigmoid-Polynomial Spectral Representation

Once encoded, colors are stored as **sigmoid-polynomials** that can be evaluated at arbitrary wavelengths, providing true spectral continuity.

### The HeroWavelengthSpectrum Implementation

In [`src/spectra/hero.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/spectra/hero.cpp), the `HeroWavelengthSpectrum` class implements the default "hero" spectrum approach. When decoding, it constructs an `RGBSigmoidPolynomial` from the coefficients:

```cpp
auto spec = RGBAlbedoSpectrum{ RGBSigmoidPolynomial{ v.xyz() } };

```

The polynomial evaluates a wavelength `λ` through the `RGBSigmoidPolynomial::operator()`:

```cpp
Float operator()(Expr<float> lambda) const noexcept {
    return _s(polynomial(lambda, _c.z, _c.y, _c.x));
}

```

Here, `polynomial()` computes a cubic term from the coefficients, and `_s()` applies a smooth sigmoid mapping that guarantees the result stays within `[0,1]`. This sigmoid constraint ensures the reconstructed spectral distribution is physically plausible and energy-conserving.

## Wavelength Sampling Strategies

To perform Monte Carlo integration, the system must sample specific wavelengths from the continuous spectrum.

### SampledWavelengths and Sampling Methods

The `HeroWavelengthSpectrumInstance::sample(u)` method in [`src/spectra/hero.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/spectra/hero.cpp) returns a `SampledWavelengths` container based on the selected sampling strategy (configured via the `"sample"` property, defaulting to `"visible"`):

- **Visible sampling**: Draws wavelengths from `visible_wavelengths_pdf`, concentrating samples where the human eye is most sensitive to reduce variance
- **Uniform sampling**: Distributes samples linearly across the visible range `[380 nm, 780 nm]`

The `SampledWavelengths` and `SampledSpectrum` containers defined in [`src/base/spectrum.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/spectrum.cpp) hold the discrete set of wavelengths and their corresponding sampled values for integrators and shaders.

## Integration with the Rendering Pipeline

During the build phase in `HeroWavelengthSpectrum::build`, the system prepares GPU resources by creating three 3D textures from the lookup table and registering them as bindless textures (`_rgb2spec_t0`, `_rgb2spec_t1`, `_rgb2spec_t2`) via the pipeline interface in [`src/base/pipeline.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/pipeline.h).

### Decoding to SampledSpectrum

When shaders need actual spectral values, they call the decode methods:

```cpp
for (auto i = 0u; i < s.dimension(); i++) {
    s[i] = spec.sample(swl.lambda(i));
}

```

The `spec` object may represent an **albedo** spectrum, an **illuminant** (scaled by an SPD like CIE-D65 from [`src/base/spd.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/spd.h)), or an **unbounded** spectrum. The resulting `SampledSpectrum` provides per-wavelength values for color-accurate light transport calculations.

## Practical Code Example: Encoding and Sampling Spectra

The following example demonstrates the complete workflow using the hero spectrum system:

```cpp
// 1. Create a scene node description
auto desc = SceneNodeDesc::make();
desc->set_property("dimension", 4u);          // 4-sample hero spectrum
desc->set_property("sample",    "visible");   // use visible-weighted sampling

// 2. Build the spectrum object
auto *hero = new luisa::render::HeroWavelengthSpectrum(scene, desc);
auto instance = hero->build(pipeline, command_buffer);

// 3. Encode an sRGB colour to its spectral representation
float3 rgb = make_float3(0.8f, 0.2f, 0.1f);          // a strong red
float4 spec_coeff = hero->encode_static_srgb_albedo(rgb);
// spec_coeff.xyz = polynomial coefficients, spec_coeff.w = CIE-Y

// 4. Sample a wavelength set for a ray (u from RNG)
float u = rng.uniform();
auto sampled_wl = instance->sample(u);               // SampledWavelengths

// 5. Decode the albedo spectrum for those wavelengths
auto decode = instance->decode_albedo(sampled_wl, spec_coeff);
SampledSpectrum albedo = decode.value;               // per-wavelength values
float strength      = decode.strength;              // same as spec_coeff.w

// 6. Convert to CIE-XYZ for display processing
float3 xyz = hero->cie_xyz(sampled_wl, albedo);

```

All calls reference the actual source functions in [`src/spectra/hero.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/spectra/hero.cpp) and [`src/base/spectrum.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/spectrum.h).

## Summary

- **Continuous representation**: LuisaRender stores colors as sigmoid-polynomial coefficients rather than fixed RGB values, enabling evaluation at any wavelength
- **4D lookup table**: The `RGB2SpectrumTable` in [`src/spectra/srgb2spec.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/spectra/srgb2spec.cpp) provides fast conversion from sRGB to spectral coefficients via trilinear interpolation of a 64³ resolution table
- **Physically-based sampling**: The system supports both visible-weighted and uniform wavelength sampling through `SampledWavelengths` to optimize Monte Carlo integration
- **Energy conservation**: The CIE-Y luminance stored in the `w` component of encoded spectra ensures correct luminous power representation
- **GPU integration**: Bindless texture registration in [`src/base/pipeline.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/pipeline.h) enables efficient GPU lookup of spectral coefficients during shading

## Frequently Asked Questions

### Why does LuisaRender use sigmoid-polynomials instead of raw RGB for spectral representation?

Raw RGB defines color at only three discrete wavelengths corresponding to display primaries, which cannot represent metamerism or fluorescent effects accurately. The **sigmoid-polynomial** representation in [`src/spectra/hero.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/spectra/hero.cpp) stores coefficients that generate a smooth, continuous spectral curve across the full visible range `[380 nm, 780 nm]`. The sigmoid constraint ensures the reconstructed spectrum remains positive and bounded, producing physically plausible albedo values that conserve energy during light transport simulations.

### What is the difference between visible and uniform wavelength sampling?

**Visible sampling** uses a probability density function matching human photopic response (`visible_wavelengths_pdf`), concentrating Monte Carlo samples around 555 nm where the eye is most sensitive. This reduces variance in rendered images but requires the `visible` method selection in the scene node. **Uniform sampling** distributes wavelengths evenly across the spectrum, which is useful for non-photographic simulations or when rendering spectral phenomena outside human vision. The mode is set via the `"sample"` property when constructing `HeroWavelengthSpectrum`.

### How does the spectrum system ensure energy conservation?

Energy conservation is maintained through multiple mechanisms: the **sigmoid mapping** guarantees albedo values remain in `[0,1]`, preventing negative or excessive energy. The **CIE-Y luminance** stored in the `w` component of encoded spectra (`spec_coeff.w`) carries the actual luminous power, ensuring that spectral reconstruction respects the correct luminance when converting from sRGB. Additionally, `RGBIlluminantSpectrum` multiplies albedo polynomials by real spectral power distributions (like CIE-D65 from [`src/base/spd.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/spd.cpp)), ensuring light sources emit physically accurate radiant power.

### Where is the spectral lookup table stored during GPU rendering?

During the build phase (`HeroWavelengthSpectrum::build`), the 4D lookup table data is split into three 3D textures and registered as **bindless textures** (`_rgb2spec_t0`, `_rgb2spec_t1`, `_rgb2spec_t2`) through the pipeline interface in [`src/base/pipeline.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/pipeline.h). The `HeroWavelengthSpectrumInstance` stores the index of the first texture (`_rgb2spec_t0`) and uses it to perform hardware-accelerated trilinear interpolation on the GPU when decoding spectral coefficients during shader execution.