ArmorPaint Layer System and Compositing Workflows: A Technical Deep Dive

ArmorPaint organizes all paint data as a hierarchical stack of layers that are composited via GPU shaders using temporary render targets, supporting non-destructive blending modes, masks, and procedural filters.

The armory3d/armorpaint repository implements a node-driven texturing engine where every brush stroke, fill, and filter exists as a distinct layer in g_project->_->layers. This ArmorPaint layer system and compositing workflows provide artists with Photoshop-grade layer management while maintaining real-time performance through GPU-accelerated merge operations defined in paint/sources/util/util_layer.c.

Core Layer Architecture

The foundation of the system resides in paint/sources/types.h, which defines the slot_layer_t structure. Each layer maintains GPU textures for base color (texpaint), normal maps (texpaint_nor), and packed material channels (texpaint_pack), alongside metadata for blending modes and opacity.

Layer Types and Storage

ArmorPaint supports five distinct layer types, each serving specific artistic workflows:

  • Paint Layer — Stores base color and optional PBR channels in texpaint. Blending with lower layers is controlled via the blending field (values 0-17 corresponding to Mix, Darken, Multiply, Screen, etc.).
  • Mask Layer — Contains a single-channel mask texture that modulates the parent layer's opacity through the mask_opacity property.
  • Path Layer — Records 3D curve data in path_points* for text and brush strokes that follow vector paths.
  • Group Layer — Acts as a container for hierarchical organization, allowing nested blending and group-level mask application.
  • Filter Layer — Encapsulates a material node graph created via layers_create_filter(), applying post-processing effects to underlying layers.

The GPU-Accelerated Compositing Pipeline

When layers are merged or the canvas resizes, ArmorPaint executes a multi-stage GPU pipeline defined in util_layer.c.

Texture Preparation and Temporary Buffers

When texture resolution changes, layers_resize() rebuilds all GPU textures for every layer in the stack. For merge operations, layers_make_temp_img() allocates layers_temp_image, an RGBA32 render target serving as the source buffer. A second temporary target, pipes_temp_mask_image, is reserved specifically for mask compositing.

The Merge Operation

The core compositing routine is layers_merge_layer(l0, l1, use_mask):

  1. Source Capture — Copies l0->texpaint into layers_temp_image.
  2. Mask Construction — If l1 contains masks, each mask renders into pipes_temp_mask_image.
  3. Shader Execution — The layer_merge.kong shader receives tex0 (source), texa (destination), optional texmask, and tex1 (packed channels). The constants.blending uniform selects the blend mode (0-17).
  4. Output — The GPU writes the blended result back to the destination texture.

Layer Preview Generation

To display thumbnails in the UI, util_layer_update_preview() copies texpaint into texpaint_preview using the pipes_copy pipeline. The layer_view.kong shader then renders specific channels (R/G/B/A or full RGBA) based on the constants.channel uniform, enabling artists to inspect individual material channels.

Mask Application and Blending

Masks in ArmorPaint are not merely alpha channels but fully composited layers. When merging a mask layer (slot_layer_is_mask(l1)), the system executes a specialized GPU path:

_gpu_begin(l0->texpaint, NULL, NULL, GPU_CLEAR_NONE, 0, 0.0);
gpu_set_pipeline(pipes_merge_mask);
gpu_set_texture(pipes_tex0_merge_mask, l1->texpaint);
gpu_set_texture(pipes_texa_merge_mask, layers_temp_image);
gpu_set_float(pipes_opac_merge_mask, slot_layer_get_opacity(l1));
gpu_set_int(pipes_blending_merge_mask, BLEND_TYPE_MIX);
gpu_draw();
_gpu_end();

This operation samples the mask's red channel, multiplies it by the source opacity, and blends it onto the destination using BLEND_TYPE_MIX (value 101), effectively masking the parent layer's visibility.

Dynamic Layer Types: Fill, Path, and Filters

Procedural layers require regeneration when source materials change. The system provides two update mechanisms:

  • layers_update_fill_layers() — Iterates the stack to find layers where fill_material matches the active context material, then triggers repaint.
  • layers_update_path_layers() — Identifies path layers referencing the current material and invokes util_layer_repaint_path() or render_path_paint_commands_paint() to re-render curve-based strokes.

Filter layers created via layers_create_filter() maintain a material node graph that processes underlying layers as a post-effect, updating automatically when the node graph changes.

Flattening and Export Workflows

For final asset export, layers_flatten() collapses the non-destructive stack into baked textures:

slot_layer_t *export = layers_flatten(
    /*height_to_normal*/ true,
    /*layers*/ NULL);   // NULL processes the entire stack

This function walks the layer hierarchy, repeatedly calling layers_merge_layer() to produce three export images: layers_expa (color), layers_expb (normal), and layers_expc (packed ORM). When height_to_normal is enabled, the function bakes height map data into the normal map export.

Practical Implementation Examples

Creating a New Color Layer

To programmatically create a paint layer with specific PBR values:

layers_create_color_layer(
    /*base_color*/ 0xff808080,   // 50% grey
    /*occlusion*/ 1.0f,
    /*roughness*/ 0.5f,
    /*metallic*/  0.0f,
    /*position*/  -1);          // -1 inserts after active layer

The implementation chain progresses through layers_create_color_layer() → layers_create_color_layer_on_next_frame() → slot_layer_clear(), which initializes the GPU textures.

Adding a Mask to the Active Layer

To attach a mask to the current layer:

slot_layer_t *mask = layers_new_mask(
    /*clear*/ true,
    /*parent*/ g_context->layer,
    /*position*/ -1);

This calls slot_layer_create() → slot_layer_clear() → tab_stages_add_layer(), which adds the mask name to the UI while linking it to the parent layer's opacity.

Merging Layers Downward

To combine the active layer with the one beneath it:

layers_merge_down();

This triggers layers_merge_layer() passing the active layer and its immediate parent, followed by deletion of the merged layer.

Flattening for Export

To generate final textures from the entire stack:

layers_flatten(
    /*height_to_normal*/ true,
    /*layers*/ NULL);

The resulting textures (layers_expa, layers_expb, layers_expc) are ready for export to game engines or rendering pipelines.

Visualizing Specific Channels

To force a UI preview of a specific channel:

g_context->mask_preview_rgba32 = NULL;
tab_layers_make_mask_preview_rgba32(g_context->layer);

This schedules asynchronous preview generation; the shader layer_view.kong interprets constants.channel to display individual R, G, B, or A channels.

Summary

  • Storage: All layers exist in g_project->_->layers as slot_layer_t structures defined in paint/sources/types.h.
  • Compositing: The layers_merge_layer() function drives GPU blending via layer_merge.kong, using temporary buffers layers_temp_image and pipes_temp_mask_image.
  • Masks: Single-channel mask layers modulate parent opacity using BLEND_TYPE_MIX (101) during the merge pass.
  • Dynamics: Fill and path layers update automatically via layers_update_fill_layers() and layers_update_path_layers() when materials change.
  • Export: layers_flatten() bakes the stack to layers_expa/b/c textures with optional height-to-normal conversion.
  • UI: Preview generation uses util_layer_update_preview() and layer_view.kong for channel-isolated visualization.

Frequently Asked Questions

How does ArmorPaint store layer data internally?

According to the armory3d/armorpaint source code, layer data resides in the global project structure at g_project->_->layers, where each element is a slot_layer_t defined in paint/sources/types.h. Each layer maintains separate GPU textures for color (texpaint), normals (texpaint_nor), and packed channels (texpaint_pack), allowing independent channel editing within a unified stack.

What blending modes are available in the layer system?

The system supports 18 distinct blending modes, including Mix, Darken, Multiply, Screen, Overlay, Soft Light, and Value, selected via the constants.blending uniform passed to layer_merge.kong (values 0-17). Additionally, mask layers use a special BLEND_TYPE_MIX mode (value 101) to composite alpha channels separately from color blending.

How are masks composited onto paint layers?

When merging a mask layer, layers_merge_layer() detects slot_layer_is_mask(l1) and executes a GPU pipeline that samples the mask's red channel, multiplies it by the layer opacity, and blends it onto the destination using the pipes_merge_mask pipeline with BLEND_TYPE_MIX. This occurs before the color merge, ensuring the mask modulates the final opacity.

What is the difference between merging and flattening layers?

Merging (layers_merge_down() or layers_merge_layer()) combines two adjacent layers into a single layer, preserving the rest of the stack's editability. Flattening (layers_flatten()) recursively merges an entire layer range (or the full stack) into three final export textures (layers_expa, layers_expb, layers_expc), destroying layer separation but producing optimized, game-ready texture maps.

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 →