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 texture
  • empty_envmap – A fallback 1x1 black texture
  • envmap_loaded – Boolean flag indicating valid data
  • envmap_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() in paint/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_data in paint/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() and envmap_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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →