# ArmorPaint Layer System and Compositing Workflows: A Technical Deep Dive

> Explore ArmorPaint's powerful layer system and compositing workflows. Learn how GPU shaders enable non-destructive blending, masks, and procedural filters for efficient 3D texturing.

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

---

**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`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/util/util_layer.c).

## Core Layer Architecture

The foundation of the system resides in [`paint/sources/types.h`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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:

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

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

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

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

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

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

```c
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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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.