How ArmorPaint Handles Lighting and Environment Maps: IBL Implementation Guide
ArmorPaint uses image-based lighting (IBL) with pre-filtered radiance mip-maps and spherical-harmonic irradiance coefficients to render real-time diffuse and specular environment lighting in the viewport, controlled via the Kong shader context and _envmap_data uniforms.
The armory3d/armorpaint repository implements a complete physically-based rendering (PBR) pipeline that converts imported HDR environment maps into GPU-ready irradiance data. This system allows artists to rotate, blur, and adjust the strength of environment lighting directly in the 3D viewport while maintaining interactive frame rates.
Architecture Overview
ArmorPaint’s lighting system revolves around two core data structures: spherical-harmonic irradiance for diffuse reflections and pre-filtered radiance textures for specular highlights. When a user imports an HDR file, the engine generates a mip-mapped radiance cube map and extracts SH coefficients, storing both in the global context. The renderer then combines these with material roughness and view direction to compute final pixel values in real-time.
Environment Map Import and Storage
Importing HDR Files via the UI
Environment map import begins in paint/sources/ui/ui_menubar.c. The function ui_menubar_import_envmap() opens a file chooser dialog and triggers the import pipeline:
void ui_menubar_import_envmap(char *path) {
import_envmap_run(path, NULL);
context_update_envmap();
}
The import_envmap_run() function processes the HDR equirectangular texture into pre-filtered mip-maps, while context_update_envmap() computes spherical-harmonic irradiance coefficients and updates the GPU uniform buffer.
Context Storage in types.h
All environment-related state lives in the context_t structure defined in paint/sources/types.h. Key fields include:
saved_envmap– The primary pre-filtered radiance textureempty_envmap– A fallback 1x1 black textureenvmap_loaded– Boolean flag indicating valid dataenvmap_angle,envmap_strength,envmap_blur– User-controllable parameters
Shader Pipeline and Uniforms
Uniform Buffer Setup
The file paint/sources/uniforms.c packs environment parameters into the _envmap_data constant, which contains the rotation angle, sine and cosine of that angle, and the user-defined strength multiplier. A secondary _envmap_data_world constant provides the same data with fallbacks for tone-mapping operations.
Shader Context Creation
ArmorPaint dynamically constructs shader code for objects requiring IBL. The functions make_pathsphere_shader() in paint/sources/render/render_pathsphere.c and make_envsphere_shader() in paint/sources/render/render_envsphere.c append environment sampling logic to the Kong shader context. These shaders are responsible for the path-trace preview sphere and the small metallic/diffuse viewport helpers.
Diffuse and Specular IBL Implementation
Spherical Harmonic Diffuse
For diffuse lighting, the shader evaluates the rotated normal against pre-computed SH coefficients. The sh_irradiance() function samples coefficients shirr0 through shirr6, rotating the normal vector using the sine and cosine stored in constants.envmap_data:
var indirect: float3 = albedo * (sh_irradiance(float3(
n.x * constants.envmap_data.z + n.y * constants.envmap_data.y,
n.y * constants.envmap_data.z - n.x * constants.envmap_data.y,
n.z)) / 3.14159265);
This approach provides a cheap, analytically-integrated diffuse term that avoids expensive texture lookups for Lambertian surfaces.
Pre-filtered Specular Sampling
Specular reflections use a split-sum approximation. The reflection vector is converted to equirectangular UV coordinates via envmap_equirect(), then sampled from the radiance mip-chain at a level determined by roughness:
var envmap_coord: float2 = envmap_equirect(wreflect, constants.envmap_data.x);
var lodc0: float3 = envmap_sample(lod0, envmap_coord);
var lodc1: float3 = envmap_sample(lod1, envmap_coord);
var prefiltered_color: float3 = lerp3(lodc0, lodc1, lodf);
The roughness value drives the mip-level selection (envlod = roughness * 5.0), where higher roughness samples blurrier mip-maps to approximate micro-facet distribution.
BRDF Approximation
The specular term is modulated by env_brdf_approx(), a fast split-sum approximation for the Fresnel integral. The final indirect lighting combines both diffuse and specular terms, scaled by the user-controlled strength stored in constants.envmap_data.w:
indirect = indirect + prefiltered_color * env_brdf_approx(f0, roughness, dotnv);
indirect = indirect * constants.envmap_data.w;
Runtime Controls and Preview
Users manipulate environment lighting via the viewport UI. Changing the envmap_angle rotates the lighting direction by updating the sine/cosine uniforms. The envmap_strength scalar multiplies the final indirect term, while envmap_blur adjusts the specular roughness offset. The render_envsphere.c module draws small preview spheres in the viewport corner to visualize the current environment on both metallic and diffuse materials.
Complete Code Example
The following pattern from render_pathsphere.c demonstrates the full IBL integration:
void make_pathsphere_shader() {
// Specular IBL sampling
node_shader_write_frag(kong,
"var envmap_coord: float2 = envmap_equirect(wreflect, constants.envmap_data.x);");
node_shader_write_frag(kong,
"var lodc0: float3 = envmap_sample(lod0, envmap_coord);");
node_shader_write_frag(kong,
"var lodc1: float3 = envmap_sample(lod1, envmap_coord);");
node_shader_write_frag(kong,
"var prefiltered_color: float3 = lerp3(lodc0, lodc1, lodf);");
// Diffuse SH irradiance
node_shader_write_frag(kong,
"var indirect: float3 = albedo * (sh_irradiance(float3("
"n.x * constants.envmap_data.z + n.y * constants.envmap_data.y,"
"n.y * constants.envmap_data.z - n.x * constants.envmap_data.y,"
"n.z)) / 3.14159265);");
// Combine with BRDF and strength
node_shader_write_frag(kong,
"indirect = indirect + prefiltered_color * env_brdf_approx(f0, roughness, dotnv);");
node_shader_write_frag(kong,
"indirect = indirect * constants.envmap_data.w;");
}
Summary
- Environment maps are imported via
ui_menubar_import_envmap()inpaint/sources/ui/ui_menubar.c, which generates pre-filtered radiance mip-maps and spherical-harmonic coefficients. - Storage occurs in
context_t(paint/sources/types.h), tracking textures, rotation angles, strength, and blur parameters. - Uniforms (
_envmap_datainpaint/sources/uniforms.c) pack angle, sine, cosine, and strength for shader consumption. - Diffuse lighting uses rotated spherical-harmonic irradiance via
sh_irradiance()for performant Lambertian reflections. - Specular lighting samples pre-filtered radiance mip-levels selected by roughness using
envmap_equirect()andenvmap_sample(). - Composition applies a fast
env_brdf_approx()and multiplies by user strength before tone-mapping.
Frequently Asked Questions
How does ArmorPaint rotate environment maps?
ArmorPaint rotates environment lighting by updating the _envmap_data uniform with a new angle value. The sine and cosine of this angle (constants.envmap_data.y and constants.envmap_data.z) rotate the normal vector before sampling spherical-harmonic irradiance, causing the diffuse lighting to rotate. The same angle rotates the reflection vector for specular lookups.
What file format does ArmorPaint use for environment maps?
ArmorPaint imports HDR equirectangular textures through the Import → Envmap menu. The source code processes these into GPU textures with pre-filtered mip-maps for specular sampling and extracts spherical-harmonic coefficients for diffuse irradiance, storing both in g_context->saved_envmap.
Where is the environment lighting strength controlled?
The strength multiplier lives in constants.envmap_data.w, updated via the UI controls in ui_menubar.c. This value scales the combined diffuse and specular indirect lighting terms in the fragment shader generated by make_pathsphere_shader() or make_envsphere_shader(), allowing real-time adjustment of environment map intensity.
Does ArmorPaint support real-time environment map blur?
Yes. The envmap_blur parameter in context_t affects the roughness calculation when selecting radiance mip-levels. Higher blur values increase the effective roughness, causing envmap_sample() to read from higher mip-levels (blurrier pre-filtered data), creating the appearance of a blurred environment map.
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 →