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:
Irradiance : Diffuse lighting from the environment
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.0 f ) // 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.0 f )
. build ( * engine);
scene -> setIndirectLight (ibl);
When only reflections are provided, irradiance is automatically derived from the reflections cubemap.
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.0 f ) // Default: 30,000 lux
. build ( * engine);
// Update at runtime
ibl -> setIntensity ( 50000.0 f );
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.0 f ;
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.0 f )
. 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.0 f ) // Skybox intensity
. color ({ 1.0 f , 1.0 f , 1.0 f }) // Tint color
. build ( * engine);
Runtime Updates
IBL properties can be modified after creation:
// Update intensity
ibl -> setIntensity ( 50000.0 f );
// Update rotation
float angle = time * 0.1 f ;
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
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 ...
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