Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/armory3d/armorpaint/llms.txt

Use this file to discover all available pages before exploring further.

ArmorPaint’s node editors for materials and brushes are fully extensible through the plugin API. A plugin can register new node categories — which appear as groups in the node search list — and map each node type string to a callback that generates the corresponding Kong/GLSL shader code when the material is compiled. This mechanism is the same one used internally by built-in nodes such as the Script node and the Shader node, making the plugin API a first-class path for custom shader logic.
The node type string registered with plugin_material_custom_nodes_set or plugin_brush_custom_nodes_set must exactly match the type field stored in the canvas JSON for nodes of that kind. Mismatches will cause the code generator to be skipped silently.

How Node Plugins Work

1

Register a category

The plugin calls plugin_material_category_add (or plugin_brush_category_add) with a display name and an any_array_t * listing the node type strings that belong to it. ArmorPaint appends the category to nodes_material_categories and the node list to nodes_material_list, making it appear in the Add Node search popup.
2

Register a code-generation callback

The plugin calls plugin_material_custom_nodes_set(node_type, fn). Internally, ArmorPaint stores the function pointer in parser_material_custom_nodes, keyed by the node type string. When parser_material encounters a node of that type during shader compilation, it calls the registered function to obtain the GLSL/Kong expression for that node’s output.
3

Implement the handler

The callback receives the ui_node_t *node and the output ui_node_socket_t *socket being evaluated, and must return a char * GLSL/Kong expression string (e.g. "0.5" or "constants.my_value"). It can call node_shader_add_constant, node_shader_write_frag, or plugin_material_kong_get to modify the shader being built.
4

Clean up on plugin stop

The on_delete callback removes categories and handler mappings to leave the node editor in a clean state.

Material Node Registration

// my_nodes_plugin.c  (MiniC)

char *my_node_fn(void *node, void *socket) {
    // Return a GLSL/Kong expression for this node's output
    return "0.75";
}

void main() {
    void *plugin = plugin_create();
    plugin_notify_on_delete(plugin, on_delete);

    // Build the node list for the new category
    any_array_t *nodes = any_array_create(0);
    any_array_push(nodes, "my_custom_node");

    // Register the category — it will appear in the Add Node popup
    plugin_material_category_add("My Nodes", nodes);

    // Map the node type to the shader code generator
    plugin_material_custom_nodes_set("my_custom_node", my_node_fn);
}

void on_delete() {
    plugin_material_category_remove("My Nodes");
    plugin_material_custom_nodes_remove("my_custom_node");
}
A category can contain multiple node types. Add one entry per type to the any_array_t before passing it to plugin_material_category_add, then call plugin_material_custom_nodes_set once for each type.

Brush Node Registration

Brush nodes follow the same pattern but use the brush-specific API. The code generator callback for a brush node is stored in parser_logic_custom_nodes.
char *my_brush_fn(void *node, void *socket) {
    return "1.0";
}

void main() {
    void *plugin = plugin_create();
    plugin_notify_on_delete(plugin, on_delete);

    any_array_t *brush_nodes = any_array_create(0);
    any_array_push(brush_nodes, "my_brush_node");

    plugin_brush_category_add("My Brush Nodes", brush_nodes);
    plugin_brush_custom_nodes_set("my_brush_node", my_brush_fn);
}

void on_delete() {
    plugin_brush_category_remove("My Brush Nodes");
    plugin_brush_custom_nodes_remove("my_brush_node");
}

Accessing the Kong Material Context

plugin_material_kong_get returns the active node_shader_t * (the Kong shader context) that parser_material is currently building. Your node callback can use it to add constants, write fragment shader lines, or query whether certain features have been enabled.
char *my_node_fn(void *node, void *socket) {
    void *kong = plugin_material_kong_get();

    // Write a raw GLSL line into the fragment shader
    node_shader_write_frag(kong, "float my_val = sin(sys_time());");

    // Expose a CPU-side constant to the shader
    node_shader_add_constant(kong, "my_param: float", "_my_param");

    return "my_val";
}
Modifying the Kong shader pipeline directly — through node_shader_write_frag, node_shader_add_constant, and related calls — requires a solid understanding of how parser_material.c builds the output shader. Writing syntactically incorrect GLSL or adding duplicate constants can prevent the material from compiling.

Script Node — Built-in Custom Shader Injection

The Script node (SCRIPT_CPU type, defined in script_node.c) is ArmorPaint’s built-in example of custom shader injection. It exposes a single text field in which you write a GLSL/Kong expression. During material parsing, script_node_value is called; it reads the expression string, registers a CPU-side shader constant named after the node, and returns a reference to that constant for use in downstream nodes.
// From script_node.c (simplified):
char *script_node_value(ui_node_t *node, ui_node_socket_t *socket) {
    buffer_t *script = node->buttons->buffer[0]->default_value;
    char     *str    = sys_buffer_to_string(script);
    char     *link   = parser_material_node_name(node, NULL);
    any_map_set(parser_material_script_links, link, str);
    node_shader_add_constant(parser_material_kong,
                              string("%s: float", link),
                              string("_%s", link));
    return string("constants.%s", link);
}
The node is registered on startup via script_node_init, which pushes script_node_def onto nodes_material_input and maps "SCRIPT_CPU" in parser_material_node_values. This mirrors exactly what a plugin does — making the Script node a useful reference when implementing your own custom nodes. The Snippets button on the Script node (implemented via ui_nodes_custom_buttons) shows that custom node UI can also be added using any_map_set(ui_nodes_custom_buttons, "my_button_fn", my_button_fn).

Shader Node — Raw Kong Snippet Injection

The Shader node (SHADER_GPU type, defined in shader_node.c) lets you type a raw Kong/GLSL expression directly into the node graph. Its value function is even simpler — it reads the text field and returns it verbatim as the node’s output expression:
// From shader_node.c (simplified):
char *shader_node_value(ui_node_t *node, ui_node_socket_t *socket) {
    buffer_t *shader = node->buttons->buffer[0]->default_value;
    char     *str    = sys_buffer_to_string(shader);
    return string_equals(str, "") ? "0.0" : str;
}
Registered as "SHADER_GPU" in parser_material_node_values, the Shader node demonstrates that the simplest possible custom node handler is a one-liner that returns a constant or expression string.

Cleanup: Removing Categories on Plugin Stop

Always remove your categories and custom node handlers in on_delete to prevent stale entries in the node search list and dangling function pointers in parser_material_custom_nodes.
void on_delete() {
    // Remove material category and its node handler
    plugin_material_category_remove("My Nodes");
    plugin_material_custom_nodes_remove("my_custom_node");

    // Remove brush category and its node handler
    plugin_brush_category_remove("My Brush Nodes");
    plugin_brush_custom_nodes_remove("my_brush_node");
}

Full Plugin Lifecycle Example

1

Create the plugin and declare the delete handler

void on_delete();  // forward declaration

void main() {
    void *plugin = plugin_create();
    plugin_notify_on_delete(plugin, on_delete);
2

Register the node category

    any_array_t *nodes = any_array_create(0);
    any_array_push(nodes, "my_custom_node");
    plugin_material_category_add("My Nodes", nodes);
3

Register the code-generation callback

    plugin_material_custom_nodes_set("my_custom_node", my_node_fn);
}  // end main
4

Implement the node handler

char *my_node_fn(void *node, void *socket) {
    // Return any valid GLSL/Kong expression
    void *kong = plugin_material_kong_get();
    node_shader_write_frag(kong, "float my_val = 0.5;");
    return "my_val";
}
5

Remove everything on delete

void on_delete() {
    plugin_material_category_remove("My Nodes");
    plugin_material_custom_nodes_remove("my_custom_node");
}

Build docs developers (and LLMs) love