# Architectural Considerations for Adding New Brush Types, Material Nodes, and IO Plugins to ArmorPaint

> Learn architectural considerations for adding new brush types material nodes and IO plugins to ArmorPaint Understand canvas separation node registration and serialization patterns

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

---

**Extending ArmorPaint requires understanding the strict separation between material canvases (CANVAS_TYPE_MATERIAL) and brush canvases (CANVAS_TYPE_BRUSH), registering new node logic in global maps such as nodes_brush_creates, and following established serialization patterns in the IO subsystem.**

ArmorPaint (armory3d/armorpaint) implements a dual canvas architecture that separates material workflows from brush workflows at the engine level. Whether you are extending the node system with custom brush logic or adding support for proprietary file formats, understanding these architectural boundaries is essential for maintaining system stability. The codebase distinguishes between **material nodes** that define surface properties and **brush nodes** that drive procedural painting behavior through distinct context variables and serialization paths.

## Core Node Architecture

ArmorPaint maintains parallel node systems that handle different aspects of the painting workflow. The architecture bifurcates at the canvas level, with each system using dedicated context variables and registration maps.

### Material vs. Brush Canvas Types

**Material nodes** operate on `CANVAS_TYPE_MATERIAL` and are accessed via `ui_nodes_get_canvas(false)`. These nodes populate the material graph that defines surface appearance, serialized under `material.nodes` in project files. The system initializes these through `nodes_material_init()` in [`paint/sources/nodes_material.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/nodes_material.c), which merges arrays like `nodes_material_input` and `nodes_material_texture` into the global `nodes_material_list`.

**Brush nodes** use `CANVAS_TYPE_BRUSH` and are accessed via `ui_nodes_get_canvas(true)` as implemented in `ui_base_show_brush_nodes()` within [`paint/sources/ui/ui_base.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/ui/ui_base.c). Unlike material nodes, brush nodes register through the `nodes_brush_creates` map in [`paint/sources/nodes_brush.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/nodes_brush.c), where functions like `brush_output_node_create` are mapped to string keys (see lines 18-22 of [`paint/sources/nodes_brush/brush_output_node.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/nodes_brush/brush_output_node.c)). These nodes serialize under `brush_nodes` in project files, handled in [`paint/sources/io/export_arm.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/io/export_arm.c) at lines 360-365.

### Context Variables and Preview Generation

The separation extends to runtime context variables. Material nodes interact with `g_context->material`, while brush nodes manipulate `g_context->brush` and specific override fields such as `brush_nodes_radius`, `brush_nodes_scale`, `brush_nodes_uses_random`, and related variables defined in [`paint/sources/context.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/context.c) (lines 120-133). Preview generation follows this split: material nodes use `make_material_parse_node_preview_material()`, whereas brush node previews require `make_material_parse_paint_material()` in [`paint/sources/util/util_render.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/util/util_render.c) which consumes the brush-node overrides stored in the context.

## Adding a New Brush-Type Material Node

Creating a custom brush node involves implementing the logic layer, defining the UI schema, integrating with the brush context, and ensuring proper serialization.

### Implement Node Logic

Create a new C file in `paint/sources/nodes_brush/` (e.g., [`my_brush_node.c`](https://github.com/armory3d/armorpaint/blob/main/my_brush_node.c)). Implement three core functions following the pattern established in [`brush_output_node.c`](https://github.com/armory3d/armorpaint/blob/main/brush_output_node.c):

1. **`my_brush_node_create(ui_node_t *raw, f32_array_t *args)`** – Allocates the node structure using `ALLOC_INIT` and stores references via `logic_node_create()`.
2. **`my_brush_node_run()`** – Executes each frame to read from `g_context->brush` inputs and write to brush-node overrides or the canvas.
3. **`my_brush_node_init()`** – Registers the node in the global map using `any_map_set(nodes_brush_creates, "my_brush_node", my_brush_node_create);`.

```c
/* paint/sources/nodes_brush/my_brush_node.c */
#include "../global.h"

/* 1️⃣ Node allocation */
void *my_brush_node_create(ui_node_t *raw, f32_array_t *args) {
    my_brush_node_t *n = ALLOC_INIT(my_brush_node_t, {0});
    n->base = logic_node_create(n);
    n->raw  = raw;
    return n;
}

/* 2️⃣ Node execution – modifies brush radius */
void my_brush_node_run() {
    // Example: radius = input * 2.0
    logic_node_value_t *input = logic_node_input_get(self->base->inputs->buffer[0]);
    g_context->brush_radius = input->_f32 * 2.0;
}

/* 3️⃣ Registration */
void my_brush_node_init() {
    any_map_set(nodes_brush_creates, "my_brush_node", my_brush_node_create);
}

```

### Define the UI Schema

In the same source file, provide a JSON-like node definition within comments (as seen in [`brush_output_node.c`](https://github.com/armory3d/armorpaint/blob/main/brush_output_node.c) lines 23-33). This schema declares inputs, outputs, and UI elements that the node system parses at runtime to build the interface.

### Integrate with Brush Context

If your node modifies brush parameters, update the relevant `g_context` fields. For deterministic behavior, respect the random flag by checking `brush_nodes_uses_random` before applying overrides, mirroring the logic in `brush_output_node_parse_inputs()` at lines 29-36. After modifying brush parameters that affect the paint material, trigger recompilation by setting `ui_nodes_recompile_mat = true;` as demonstrated at lines 88-90 of [`brush_output_node.c`](https://github.com/armory3d/armorpaint/blob/main/brush_output_node.c).

### Handle Serialization

Brush node canvases serialize automatically via `util_encode_node_canvas()` in [`paint/sources/util/util_encode.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/util/util_encode.c) (lines 270-274). If your node stores custom data beyond the standard canvas structure, extend the encoding logic in [`paint/sources/io/import_legacy.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/io/import_legacy.c) or [`paint/sources/io/export_arm.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/io/export_arm.c) to persist these values.

## Adding a New IO Plugin

The IO plugin system in [`paint/plugins/plugins.c`](https://github.com/armory3d/armorpaint/blob/main/paint/plugins/plugins.c) allows extending import capabilities for textures and meshes without modifying core rendering logic.

### Implement the Parser

Write a parser function following the signature `void *io_myformat_parse(char *buf, size_t len);` or `void *import_myformat(char *path)`. For external library dependencies, guard the code with `#ifdef WITH_MYFORMAT` and update the build scripts accordingly. The function should return data structures compatible with existing asset handling (e.g., `gpu_texture_t` for textures).

### Register the Plugin

In [`paint/plugins/plugins.c`](https://github.com/armory3d/armorpaint/blob/main/paint/plugins/plugins.c), add your registration inside `plugins_init()` following this pattern:

```c
/* paint/plugins/plugins.c */
static void *import_myfmt(char *path) {
    buffer_t *b = data_get_blob(path);
    void *res   = io_myfmt_parse((char *)b->buffer, b->length);
    data_delete_blob(path);
    return res;
}

void plugins_init() {
    /* … existing registrations … */
    any_map_set(import_texture_importers, "myfmt", import_myfmt);
    any_array_push(_path_texture_formats, "myfmt");
}

```

For mesh formats, substitute `import_texture_importers` with `import_mesh_importers` and `_path_texture_formats` with `_path_mesh_formats`.

### UI Integration and Asset Handling

The Plugins tab in [`paint/sources/ui/tab_plugins.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/ui/tab_plugins.c) iterates automatically over `_path_texture_formats` and `_path_mesh_formats`, so new formats appear in the UI without explicit modification. Ensure your parser utilizes `data_cached_textures` for caching when appropriate and provide cleanup hooks similar to `plugins_free_raw_mesh()` if your plugin allocates significant resources.

## Common Pitfalls and Edge Cases

Avoid these architectural missteps when extending ArmorPaint:

- **Node Name Collisions**: The global `nodes_brush_creates` map requires unique string keys. Verify uniqueness with `any_map_get` before registration to prevent overwriting existing handlers.
- **Missing Recompilation**: Failing to set `ui_nodes_recompile_mat = true` after brush parameter changes results in stale paint materials and incorrect previews.
- **Serialization Gaps**: Custom node data not handled by `util_encode_node_canvas()` requires explicit extension of the import/export logic in [`paint/sources/io/export_arm.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/io/export_arm.c).
- **Plugin Conflicts**: Registering duplicate file extensions in `_path_texture_formats` overwrites previous handlers. Check existing entries with `any_array_search` before pushing new formats.
- **Platform Dependencies**: OS-specific libraries (e.g., `libpng`) must be guarded with platform macros like `#if defined(IRON_WINDOWS) || defined(IRON_LINUX) || defined(IRON_MACOS)`.

## Summary

- ArmorPaint strictly separates **material nodes** (`CANVAS_TYPE_MATERIAL`) from **brush nodes** (`CANVAS_TYPE_BRUSH`) with distinct context variables, registration maps, and serialization paths.
- New brush nodes require implementation of create, run, and init functions, registration in `nodes_brush_creates`, and setting `ui_nodes_recompile_mat` after parameter changes.
- IO plugins register in [`paint/plugins/plugins.c`](https://github.com/armory3d/armorpaint/blob/main/paint/plugins/plugins.c) using `any_map_set` for importers and `any_array_push` for format lists, with automatic UI exposure through the Plugins tab.
- Brush node canvases serialize automatically via `util_encode_node_canvas()`, but custom data requires manual extension of the IO pipeline in [`export_arm.c`](https://github.com/armory3d/armorpaint/blob/main/export_arm.c) or [`import_legacy.c`](https://github.com/armory3d/armorpaint/blob/main/import_legacy.c).
- Context variables for brush nodes reside in `g_context` and include specific override fields for radius, scale, and randomization flags defined in [`paint/sources/context.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/context.c).

## Frequently Asked Questions

### What is the difference between material nodes and brush nodes in ArmorPaint?

Material nodes define surface properties and operate on `CANVAS_TYPE_MATERIAL` using `g_context->material`, while brush nodes control procedural painting behavior using `CANVAS_TYPE_BRUSH` and manipulate `g_context->brush` along with specific override variables like `brush_nodes_radius` and `brush_nodes_scale`. Material nodes serialize under `material.nodes` whereas brush nodes store under `brush_nodes` in project files, with previews generated by distinct parser functions in [`util_render.c`](https://github.com/armory3d/armorpaint/blob/main/util_render.c).

### How do I register a new brush node in ArmorPaint?

Create a source file in `paint/sources/nodes_brush/` implementing `create`, `run`, and `init` functions. In the init function, register the node using `any_map_set(nodes_brush_creates, "node_name", node_name_create);` following the pattern in lines 18-22 of [`brush_output_node.c`](https://github.com/armory3d/armorpaint/blob/main/brush_output_node.c). Include a JSON-like UI schema definition in comments for the node interface, and call `ui_nodes_recompile_mat = true;` when the node modifies brush parameters that affect the paint material.

### Where do I handle serialization for custom brush nodes?

Standard brush node canvases serialize automatically through `util_encode_node_canvas()` in [`paint/sources/util/util_encode.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/util/util_encode.c) at lines 270-274, called from [`export_arm.c`](https://github.com/armory3d/armorpaint/blob/main/export_arm.c). For custom data structures that extend the standard node canvas, extend the encoding logic in [`paint/sources/io/export_arm.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/io/export_arm.c) and the corresponding decoding logic in [`paint/sources/io/import_legacy.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/io/import_legacy.c).

### How do I add support for a new texture format in ArmorPaint?

Implement a parser function with an appropriate signature, register it in [`paint/plugins/plugins.c`](https://github.com/armory3d/armorpaint/blob/main/paint/plugins/plugins.c) using `any_map_set(import_texture_importers, "ext", import_myformat)`, and add the extension to `_path_texture_formats` using `any_array_push`. The format appears automatically in the Plugins tab UI without requiring modifications to [`tab_plugins.c`](https://github.com/armory3d/armorpaint/blob/main/tab_plugins.c).