How LuisaRender's Spectrum System Implements Physically-Based Color Representation
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, 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 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
rgbto 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)wherexandyare ratios of the other channels to the max, andzzis 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, the HeroWavelengthSpectrum class implements the default "hero" spectrum approach. When decoding, it constructs an RGBSigmoidPolynomial from the coefficients:
auto spec = RGBAlbedoSpectrum{ RGBSigmoidPolynomial{ v.xyz() } };
The polynomial evaluates a wavelength λ through the RGBSigmoidPolynomial::operator():
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 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 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.
Decoding to SampledSpectrum
When shaders need actual spectral values, they call the decode methods:
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), 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:
// 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 and 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
RGB2SpectrumTableinsrc/spectra/srgb2spec.cppprovides 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
SampledWavelengthsto optimize Monte Carlo integration - Energy conservation: The CIE-Y luminance stored in the
wcomponent of encoded spectra ensures correct luminous power representation - GPU integration: Bindless texture registration in
src/base/pipeline.henables 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 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), 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. 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.
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 →