# How ArmorPaint Manages Texture Sets and Layers: A Deep Dive into the Source Code

> Explore how ArmorPaint manages texture sets and layers by diving into its source code. Discover its GPU-accelerated blending and slot_layer_t structure for efficient texture management.

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

---

**ArmorPaint stores every paintable element as a `slot_layer_t` structure that owns a complete texture set—including base color, normal map, packed material channels, and optional sculpt data—and composites these layers through GPU-accelerated blending pipelines.**

ArmorPaint's layer system is built around a hierarchical stack where each layer encapsulates its own texture set. This architecture enables non-destructive painting workflows, allowing artists to blend, mask, and merge material data independently. Understanding how the engine allocates these texture sets and composites them during export reveals the technical foundation of the software's painting capabilities.

## The Layer Data Structure: slot_layer_t

At the core of ArmorPaint's texture management is the `slot_layer_t` structure defined in [`paint/sources/slot_layer.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/slot_layer.c). This struct represents every layer, mask, and group in the layer stack.

```c
typedef struct slot_layer_t {
    char               *name;          // UI name
    i32                 id;            // unique identifier
    layer_slot_type_t   type;          // LAYER, MASK or GROUP
    slot_layer_t       *parent;        // hierarchical parent (for masks / groups)

    // *** Texture set ***
    gpu_texture_t      *texpaint;      // base colour (RGBA32/64/128)
    gpu_texture_t      *texpaint_nor;  // normal map
    gpu_texture_t      *texpaint_pack; // packed channels (occlusion, roughness, metallic, height)
    gpu_texture_t      *texpaint_sculpt; // high‑res sculpt buffer (optional)

    // Rendering state
    f32                 mask_opacity;
    i32                 blending;
    bool                visible;
    // … other per‑layer flags (fill_material, path_material, etc.)
} slot_layer_t;

```

**Every layer** therefore represents an independent **texture set**. The `texpaint` field stores the base color, while `texpaint_nor` handles normal maps and `texpaint_pack` stores occlusion, roughness, metallic, and height data in a single texture. When high-resolution sculpting is enabled, `texpaint_sculpt` provides additional buffer space.

## Creating Layers and Allocating Texture Sets

Layer creation follows a deferred allocation pattern to avoid GPU memory pressure. In [`paint/sources/util/util_layer.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/util/util_layer.c), the `layers_new_layer()` function initializes the CPU-side structure but schedules texture allocation for the next frame.

```c
slot_layer_t *layers_new_layer(bool clear, i32 position, slot_layer_t *parent) {
    if (g_project->_->layers->length > layers_max_layers) return NULL;
    slot_layer_t *l = slot_layer_create("", LAYER_SLOT_TYPE_LAYER, parent);
    // initialise texture set later (on next frame)
    if (clear) sys_notify_on_next_frame(&layers_new_layer_clear, l);
    return l;
}

```

Mask layers follow a similar pattern through `layers_new_mask()`, but hold only single-channel textures suitable for opacity blending.

The actual GPU textures are allocated on demand through helpers like `layers_make_temp_img()`, `layers_make_temp_mask_img()`, and `layers_make_export_img()`. The bit depth depends on the project's `base_bits` setting—**RGBA32**, **RGBA64**, or **RGBA128**—determining whether textures use 8, 16, or 32 bits per channel.

## Live Painting and Preview Updates

During painting operations, ArmorPaint uses a special **live layer** (`render_path_paint_live_layer`) defined in [`paint/sources/render/render_path_paint.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/render/render_path_paint.c). This is a standard `slot_layer_t` that receives brush strokes in real-time, writing directly into its `texpaint`, `texpaint_nor`, and `texpaint_pack` buffers.

To keep the UI responsive, each layer maintains a preview texture (`texpaint_preview`). When layers change, `util_layer_update_preview()` copies the current paint buffer into the thumbnail:

```c
void util_layer_update_preview() {
    if (g_context->layers_preview_dirty) {
        for (i32 i = 0; i < g_project->_->layers->length; ++i) {
            slot_layer_t *l = g_project->_->layers->buffer[i];
            if (!slot_layer_is_group(l) && l->texpaint_preview) {
                draw_begin(l->texpaint_preview, true, 0x00000000);
                draw_set_pipeline(pipes_copy);
                draw_scaled_image(l->texpaint, 0,0,l->texpaint_preview->width,l->texpaint_preview->height);
                draw_end();
            }
        }
    }
}

```

## Merging and Flattening Texture Sets

When flattening the stack or merging a mask, ArmorPaint composites texture sets using GPU pipelines. The `layers_merge_layer()` function in [`util_layer.c`](https://github.com/armory3d/armorpaint/blob/main/util_layer.c) handles this blending:

1.  Copy the destination layer to a temporary image (`layers_temp_image`)
2.  Build the mask if needed (blending into `pipes_temp_mask_image`)
3.  Execute GPU pipelines (`pipes_merge`, `pipes_merge_r`, `pipes_merge_g`, `pipes_merge_b`) to composite color, normal, and packed channels separately

For complete stack flattening, `layers_flatten()` iterates over visible layers and blends them into three export render targets:

- **expa** – Base color (RGBA)
- **expb** – Normals (RGB)  
- **expc** – Packed channels (occlusion, roughness, metallic, height)

```c
slot_layer_t *layers_flatten(bool height_to_normal, slot_layer_t_array_t *layers) {
    layers_make_temp_img();
    layers_make_export_img();
    // … loop over layers, blend into expa/expb/expc …
    // optionally convert height to normal at the end
    return ALLOC_INIT(slot_layer_t, {.texpaint=layers_expa,
                                    .texpaint_nor=layers_expb,
                                    .texpaint_pack=layers_expc});
}

```

The function returns a new `slot_layer_t` containing the final texture set, which [`paint/sources/io/export_texture.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/io/export_texture.c) then writes to disk in formats like PNG or EXR.

## Practical Code Examples

### Creating a New Color Layer

To programmatically create a layer with specific material properties:

```c
// Request a new colour layer at the current stack position
_layers_base_color = color_from_floats(0.8f, 0.1f, 0.1f, 1.0f); // red
_layers_occlusion   = 1.0f;
_layers_roughness   = 0.5f;
_layers_metallic    = 0.0f;
_layers_position    = -1;                      // insert after current layer
layers_create_color_layer(_layers_base_color,
                          _layers_occlusion,
                          _layers_roughness,
                          _layers_metallic,
                          _layers_position);

```

Internally, `layers_create_color_layer()` schedules `layers_new_layer()`, then `slot_layer_clear()` populates the texture set with the supplied base color on the next frame update.

### Adding a Mask to the Current Layer

```c
slot_layer_t *mask = layers_new_mask(true, g_context->layer, -1);
slot_layer_clear(mask, 0x00000000, NULL, 1.0f,
                 layers_default_rough, 0.0f);   // initialise empty mask texture

```

The mask's texture set consists of a single-channel `texpaint` (R8) which is later blended via `layers_merge_layer()`.

### Flattening for Export

```c
slot_layer_t *final_set = layers_flatten(true, NULL); // true -> convert height to normal
// final_set now holds:
//   final_set->texpaint     -> base colour (RGBA)
//   final_set->texpaint_nor -> normal map
//   final_set->texpaint_pack-> packed (occlusion, roughness, metallic, height)

```

The resulting `final_set` can be passed directly to the exporter or bound to a material for real-time preview.

## Summary

- **ArmorPaint layers are self-contained texture sets**: Each `slot_layer_t` owns GPU textures for base color, normals, packed material data, and optional sculpting buffers.
- **Deferred allocation improves performance**: GPU memory is allocated on the next frame via `sys_notify_on_next_frame()`, preventing stutter during rapid layer creation.
- **Hierarchical composition respects masks**: The `layers_merge_layer()` function uses dedicated GPU pipelines to blend texture sets while respecting mask opacity and blending modes.
- **Flattening creates export-ready data**: `layers_flatten()` composites the entire stack into three specific render targets (`expa`, `expb`, `expc`) suitable for game engine export.

## Frequently Asked Questions

### How does ArmorPaint store texture data per layer?

ArmorPaint stores texture data in the `slot_layer_t` structure, which contains four primary GPU texture pointers: `texpaint` for base color, `texpaint_nor` for normal maps, `texpaint_pack` for packed material channels (occlusion, roughness, metallic, height), and optionally `texpaint_sculpt` for high-resolution sculpt data. These textures are allocated in [`util_layer.c`](https://github.com/armory3d/armorpaint/blob/main/util_layer.c) based on the project's bit depth setting (32, 64, or 128 bits per pixel).

### What is the difference between a layer and a mask in ArmorPaint?

A **layer** (`LAYER_SLOT_TYPE_LAYER`) contains a full texture set with multiple channels (RGBA color, normals, packed maps), while a **mask** (`LAYER_SLOT_TYPE_MASK`) holds only a single-channel texture (R8) used for opacity blending. Masks are linked to parent layers via the `parent` pointer in `slot_layer_t` and are composited during merge operations to control visibility of the underlying layer.

### How does ArmorPaint handle layer merging for export?

ArmorPaint merges layers using GPU-accelerated pipelines defined in [`util_layer.c`](https://github.com/armory3d/armorpaint/blob/main/util_layer.c). The `layers_flatten()` function iterates through visible layers, composites their texture sets using blend modes and mask data, and outputs to three specific render targets. This process respects opacity settings, blending modes, and can optionally convert height data to normal maps before finalizing the export set.

### When are GPU textures allocated for new layers?

GPU textures are allocated **deferred**—not immediately when calling `layers_new_layer()`, but on the next frame via `sys_notify_on_next_frame()`. This triggers functions like `layers_new_layer_clear()`, which call `slot_layer_clear()` to create the actual GPU render targets (`gpu_create_render_target`). This pattern prevents memory fragmentation and UI freezing during batch layer operations.