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 theblendingfield (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_opacityproperty. - 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):
- Source Capture — Copies
l0->texpaintintolayers_temp_image. - Mask Construction — If
l1contains masks, each mask renders intopipes_temp_mask_image. - Shader Execution — The
layer_merge.kongshader receivestex0(source),texa(destination), optionaltexmask, andtex1(packed channels). Theconstants.blendinguniform selects the blend mode (0-17). - 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 wherefill_materialmatches the active context material, then triggers repaint.layers_update_path_layers()— Identifies path layers referencing the current material and invokesutil_layer_repaint_path()orrender_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->_->layersasslot_layer_tstructures defined inpaint/sources/types.h. - Compositing: The
layers_merge_layer()function drives GPU blending vialayer_merge.kong, using temporary bufferslayers_temp_imageandpipes_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()andlayers_update_path_layers()when materials change. - Export:
layers_flatten()bakes the stack tolayers_expa/b/ctextures with optional height-to-normal conversion. - UI: Preview generation uses
util_layer_update_preview()andlayer_view.kongfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →