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

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. This struct represents every layer, mask, and group in the layer stack.

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, the layers_new_layer() function initializes the CPU-side structure but schedules texture allocation for the next frame.

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. 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:

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 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)
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 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:

// 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

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

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 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. 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.

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 →