# How ArmorPaint Handles Lighting and Environment Maps: IBL Implementation Guide

> Discover how ArmorPaint implements image-based lighting (IBL) for real-time diffuse and specular environment lighting. Learn about its pre-filtered radiance mip-maps and spherical-harmonic irradiance coefficients.

- Repository: [Armory 3D/armorpaint](https://github.com/armory3d/armorpaint)
- Tags: deep-dive
- Published: 2026-09-13

---

**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`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/ui/ui_menubar.c). The function `ui_menubar_import_envmap()` opens a file chooser dialog and triggers the import pipeline:

```c
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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/render/render_pathsphere.c) and `make_envsphere_shader()` in [`paint/sources/render/render_envsphere.c`](https://github.com/armory3d/armorpaint/blob/main/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`:

```c
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:

```c
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`:

```c
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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/render_pathsphere.c) demonstrates the full IBL integration:

```c
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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/types.h)), tracking textures, rotation angles, strength, and blur parameters.
- **Uniforms** (`_envmap_data` in [`paint/sources/uniforms.c`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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.