Architectural Considerations for Adding New Brush Types, Material Nodes, and IO Plugins to ArmorPaint
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, 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. Unlike material nodes, brush nodes register through the nodes_brush_creates map in 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). These nodes serialize under brush_nodes in project files, handled in 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 (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 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). Implement three core functions following the pattern established in brush_output_node.c:
my_brush_node_create(ui_node_t *raw, f32_array_t *args)– Allocates the node structure usingALLOC_INITand stores references vialogic_node_create().my_brush_node_run()– Executes each frame to read fromg_context->brushinputs and write to brush-node overrides or the canvas.my_brush_node_init()– Registers the node in the global map usingany_map_set(nodes_brush_creates, "my_brush_node", my_brush_node_create);.
/* 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 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.
Handle Serialization
Brush node canvases serialize automatically via util_encode_node_canvas() in 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 or paint/sources/io/export_arm.c to persist these values.
Adding a New IO Plugin
The IO plugin system in 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, add your registration inside plugins_init() following this pattern:
/* 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 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_createsmap requires unique string keys. Verify uniqueness withany_map_getbefore registration to prevent overwriting existing handlers. - Missing Recompilation: Failing to set
ui_nodes_recompile_mat = trueafter 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 inpaint/sources/io/export_arm.c. - Plugin Conflicts: Registering duplicate file extensions in
_path_texture_formatsoverwrites previous handlers. Check existing entries withany_array_searchbefore 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 settingui_nodes_recompile_matafter parameter changes. - IO plugins register in
paint/plugins/plugins.cusingany_map_setfor importers andany_array_pushfor 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 inexport_arm.corimport_legacy.c. - Context variables for brush nodes reside in
g_contextand include specific override fields for radius, scale, and randomization flags defined inpaint/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.
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. 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 at lines 270-274, called from export_arm.c. For custom data structures that extend the standard node canvas, extend the encoding logic in paint/sources/io/export_arm.c and the corresponding decoding logic in 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 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.
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 →