Skip to main content
Image-Based Lighting (IBL) simulates environment lighting from captured real-world environments. IBL provides both diffuse irradiance and specular reflections, creating realistic global illumination.

Overview

IndirectLight in Filament consists of two components:
  1. Irradiance: Diffuse lighting from the environment
  2. Reflections: Specular reflections from the environment (mirror-like reflections on glossy surfaces)
Environments are typically captured as high-resolution HDR equirectangular images and processed with the cmgen tool.

Creating an IndirectLight

Basic Setup

// Load IBL textures (generated by cmgen)
Texture* iblTexture = loadKtxTexture(engine, "venetian_crossroads_ibl.ktx");

// Load spherical harmonics coefficients
math::float3 sh[9];
loadSphericalHarmonics("venetian_crossroads_sh.txt", sh);

// Create indirect light
IndirectLight* ibl = IndirectLight::Builder()
    .reflections(iblTexture)     // Specular cubemap
    .irradiance(3, sh)           // Diffuse SH (3 bands = 9 coefficients)
    .intensity(30000.0f)         // Intensity in lux
    .build(*engine);

scene->setIndirectLight(ibl);

With Irradiance Cubemap

Alternatively, irradiance can be specified as a cubemap instead of spherical harmonics:
Texture* reflections = loadReflectionsCubemap(engine);
Texture* irradiance = loadIrradianceCubemap(engine);

IndirectLight* ibl = IndirectLight::Builder()
    .reflections(reflections)
    .irradiance(irradiance)  // Cubemap instead of SH
    .intensity(30000.0f)
    .build(*engine);

scene->setIndirectLight(ibl);
When only reflections are provided, irradiance is automatically derived from the reflections cubemap.

The cmgen Tool

The cmgen tool processes environment maps into formats suitable for Filament:

Basic Usage

# Generate IBL from equirectangular HDR image
cmgen -x . --format=ktx --size=256 --extract-blur=0.1 environment.hdr

# This generates:
# - environment_ibl.ktx    (reflections cubemap)
# - environment_skybox.ktx (skybox cubemap)
# - environment_sh.txt     (spherical harmonics)

cmgen Parameters

Essential options:
  • --format=ktx|png|hdr|rgbm|dds: Output format (ktx recommended)
  • --size=N: Cubemap resolution (power of 2, typically 256-1024)
  • --extract-blur=0.0-1.0: Skybox blur amount (0.1 recommended)
  • -x PATH: Output directory
Quality options:
  • --sh=N: Spherical harmonics bands (1, 2, or 3)
  • --sh-shader: Output SH as shader code
  • --sh-irradiance: Compute irradiance SH (default)
  • --no-mirror: Disable horizontal flip
Compression:
  • --compression=COMPRESSION: astc, s3tc_srgb, etc.
  • --compression=astc_fast_ldr: Fast ASTC for mobile

Example: High-Quality IBL

# Desktop quality
cmgen -x output/ \
  --format=ktx \
  --size=512 \
  --extract-blur=0.1 \
  --sh=3 \
  studio.hdr

# Mobile optimized
cmgen -x output/ \
  --format=ktx \
  --size=256 \
  --extract-blur=0.1 \
  --sh=3 \
  --compression=astc_fast_ldr \
  outdoor.hdr

Processing Multiple Faces

For cubemap faces as separate images:
cmgen -x . --format=ktx --size=256 \
  --ibl-irradiance=irradiance/ \
  --ibl-ld=reflections/ \
  px.hdr nx.hdr py.hdr ny.hdr pz.hdr nz.hdr

Spherical Harmonics

Spherical harmonics efficiently represent diffuse irradiance using a small number of coefficients.

SH Bands

Filament supports 1, 2, or 3 bands:
  • 1 band: 1 coefficient (DC component only)
  • 2 bands: 4 coefficients (adds linear terms)
  • 3 bands: 9 coefficients (adds quadratic terms, recommended)

Loading SH from cmgen Output

The cmgen tool outputs SH coefficients in a text file:
// Read SH from cmgen output file
math::float3 sh[9];
std::ifstream shFile("environment_sh.txt");
for (int i = 0; i < 9; i++) {
    shFile >> sh[i].x >> sh[i].y >> sh[i].z;
}

IndirectLight* ibl = IndirectLight::Builder()
    .irradiance(3, sh)  // 3 bands
    .build(*engine);

Radiance vs Irradiance SH

Filament supports two SH formats:
// Irradiance SH (pre-convolved, from cmgen)
builder.irradiance(3, irradianceSH);

// Radiance SH (raw spherical harmonics)
builder.radiance(3, radianceSH);
cmgen outputs irradiance SH by default. Use .irradiance() for cmgen output.

IBL Intensity

The intensity scales the environment lighting:
IndirectLight* ibl = IndirectLight::Builder()
    .reflections(texture)
    .intensity(30000.0f)  // Default: 30,000 lux
    .build(*engine);

// Update at runtime
ibl->setIntensity(50000.0f);
Typical intensities:
  • Outdoor sunny: 30,000-100,000 lux
  • Overcast day: 10,000-20,000 lux
  • Indoor: 300-1,000 lux
  • Studio: 1,000-5,000 lux

IBL Rotation

Rotate the environment map orientation:
// Rotate 90 degrees around Y-axis
float angle = M_PI / 2.0f;
math::mat3f rotation = math::mat3f::rotation(angle, {0, 1, 0});

IndirectLight* ibl = IndirectLight::Builder()
    .reflections(texture)
    .rotation(rotation)
    .build(*engine);

// Update at runtime
ibl->setRotation(rotation);
The rotation matrix must be a rigid-body transform (orthonormal, no scaling).

Complete IBL Setup Example

#include <filament/Engine.h>
#include <filament/Scene.h>
#include <filament/IndirectLight.h>
#include <filament/Skybox.h>
#include <ktxreader/Ktx1Reader.h>

using namespace filament;
using namespace image;

// Load KTX texture
Texture* loadKtxTexture(Engine* engine, const char* path) {
    std::ifstream file(path, std::ios::binary);
    std::vector<uint8_t> data((std::istreambuf_iterator<char>(file)),
                               std::istreambuf_iterator<char>());
    
    Ktx1Bundle bundle(data.data(), data.size());
    return Ktx1Reader::createTexture(engine, &bundle, false);
}

// Setup IBL and skybox
void setupEnvironment(Engine* engine, Scene* scene) {
    // Load textures
    Texture* iblTexture = loadKtxTexture(engine, "env_ibl.ktx");
    Texture* skyTexture = loadKtxTexture(engine, "env_skybox.ktx");
    
    // Load spherical harmonics
    math::float3 sh[9];
    // ... load from file or embed in code ...
    
    // Create skybox
    Skybox* skybox = Skybox::Builder()
        .environment(skyTexture)
        .build(*engine);
    scene->setSkybox(skybox);
    
    // Create indirect light
    IndirectLight* ibl = IndirectLight::Builder()
        .reflections(iblTexture)
        .irradiance(3, sh)
        .intensity(30000.0f)
        .build(*engine);
    scene->setIndirectLight(ibl);
}

Estimating Directional Light from IBL

Filament can estimate a directional light direction and color from IBL spherical harmonics:
math::float3 sh[9];
// ... load spherical harmonics ...

// Estimate dominant light direction
math::float3 sunDirection = IndirectLight::getDirectionEstimate(sh);

// Estimate light color and intensity at that direction
math::float4 sunColor = IndirectLight::getColorEstimate(sh, sunDirection);

// Create matching directional light
LightManager::Builder(LightManager::Type::SUN)
    .direction(sunDirection)
    .color({sunColor.r, sunColor.g, sunColor.b})
    .intensity(sunColor.a * ibl->getIntensity())
    .castShadows(true)
    .build(*engine, sun);
This works best with outdoor HDR environments with a clear dominant light (like the sun). May give unexpected results with indoor or LDR environments.

Skybox

A skybox displays the environment in the background:
Texture* skyboxTexture = loadKtxTexture(engine, "skybox.ktx");

Skybox* skybox = Skybox::Builder()
    .environment(skyboxTexture)
    .build(*engine);

scene->setSkybox(skybox);

Skybox Options

Skybox* skybox = Skybox::Builder()
    .environment(texture)
    .showSun(true)           // Show sun disk (for Type::SUN lights)
    .intensity(30000.0f)     // Skybox intensity
    .color({1.0f, 1.0f, 1.0f}) // Tint color
    .build(*engine);

Runtime Updates

IBL properties can be modified after creation:
// Update intensity
ibl->setIntensity(50000.0f);

// Update rotation
float angle = time * 0.1f;
math::mat3f rotation = math::mat3f::rotation(angle, {0, 1, 0});
ibl->setRotation(rotation);

// Get current textures
Texture* reflections = ibl->getReflectionsTexture();
Texture* irradiance = ibl->getIrradianceTexture();

Destroying IBL

Cleanup IBL and associated resources:
// Remove from scene first
scene->setIndirectLight(nullptr);
scene->setSkybox(nullptr);

// Destroy resources
engine->destroy(ibl);
engine->destroy(skybox);
engine->destroy(iblTexture);
engine->destroy(skyboxTexture);

Best Practices

Texture Sizes

  • Reflections: 256-512 (mobile), 512-1024 (desktop)
  • Skybox: Same or higher than reflections
  • Must be power of 2
  • Must be mipmapped

Format Recommendations

  • Desktop: KTX with no compression or DDS with BC6H
  • Mobile: KTX with ASTC compression
  • Always use HDR source images

Compression

# Mobile (ASTC)
cmgen --compression=astc_fast_ldr ...

# Desktop (no compression, better quality)
cmgen --format=ktx ...

Performance

  • Use spherical harmonics for irradiance (9 coefficients vs cubemap)
  • 3 bands sufficient for most scenes
  • Irradiance cubemap trades ALU for bandwidth
  • Smaller IBL textures for distant/background objects

Troubleshooting

Dark Scene

  • Increase IBL intensity (try 50000-100000)
  • Verify texture loaded correctly
  • Check material baseColor isn’t too dark
  • Add a directional light

Wrong Colors

  • Verify texture is in linear space (not sRGB)
  • Check SH coefficients loaded correctly
  • Verify no gamma correction applied twice

Blocky Reflections

  • Increase reflections texture size
  • Ensure texture is mipmapped
  • Check mipmap generation in cmgen

Missing Reflections

  • Verify material has roughness < 1.0
  • Check reflections texture bound correctly
  • Ensure material is not unlit

Example: Multiple IBLs

Switch between different environments:
std::vector<IndirectLight*> environments;
size_t currentEnv = 0;

// Load multiple IBLs
environments.push_back(loadIBL(engine, "outdoor"));
environments.push_back(loadIBL(engine, "indoor"));
environments.push_back(loadIBL(engine, "studio"));

// Switch environment
void switchEnvironment(Scene* scene, size_t index) {
    currentEnv = index % environments.size();
    scene->setIndirectLight(environments[currentEnv]);
}

Next Steps

Lighting

Configure direct lights in your scene

Materials

Create materials that respond to IBL

Build docs developers (and LLMs) love