# How the Material Node System in `nodes_material.c` and `float_curve_node.c` Integrates with the Iron Engine's Shader Pipeline

> Discover how ArmorPaint's material node system integrates with the Iron engine's shader pipeline. Learn how node graphs become GLSL shaders for efficient GPU rendering.

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

---

**ArmorPaint translates visual node graphs into executable GPU shaders by registering node types in [`nodes_material.c`](https://github.com/armory3d/armorpaint/blob/main/nodes_material.c), generating GLSL snippets via per-node callbacks like `float_curve_node_value()`, and assembling the complete shader source in [`parser_material.c`](https://github.com/armory3d/armorpaint/blob/main/parser_material.c) for final compilation by the Iron engine.**

The **material node system** in the armory3d/armorpaint repository serves as a domain-specific language (DSL) that converts artist-friendly node graphs into optimized shader code. This pipeline bridges the high-level material editor with the low-level Iron rendering engine, enabling real-time PBR material authoring without manual shader programming.

## Node Registration and Type System ([`nodes_material.c`](https://github.com/armory3d/armorpaint/blob/main/nodes_material.c))

The integration begins with node registration during application initialization. The `nodes_material_init()` function in [`paint/sources/nodes_material.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/nodes_material.c) creates categorized arrays for different node types and registers each node's metadata and code-generation callbacks.

### Registering the Float Curve Node Type

When `float_curve_node_init()` is called, it constructs a `ui_node_t` structure describing the **FLOAT_CURVE** node's sockets, default values, and UI behavior. This registration makes the node available in the material editor palette:

```c
void nodes_material_init() {
    nodes_material_utilities = any_array_create_from_raw((void *[]){}, 0);
    float_curve_node_init();  // Registers FLOAT_CURVE into utilities category
    
    nodes_material_list = any_array_create_from_raw(
        (void *[]){
            nodes_material_input,
            nodes_material_texture,
            nodes_material_color,
            nodes_material_utilities,
        }, 4);
}

```

The initialization function also maps the node's type string to its value-generation callback:

```c
any_map_set(parser_material_node_values, "FLOAT_CURVE", float_curve_node_value);
any_map_set(ui_nodes_custom_buttons, "nodes_material_float_curve_button",
             nodes_material_float_curve_button);

```

This mapping ensures that when the shader parser encounters a **FLOAT_CURVE** node, it invokes `float_curve_node_value()` to generate the corresponding GLSL code.

## Per-Node Shader Code Generation ([`float_curve_node.c`](https://github.com/armory3d/armorpaint/blob/main/float_curve_node.c))

Individual nodes act as code generators. The [`float_curve_node.c`](https://github.com/armory3d/armorpaint/blob/main/float_curve_node.c) file implements the **FLOAT_CURVE** node's shader emission logic through the `float_curve_node_value()` callback function.

### The `float_curve_node_value` Callback

When the parser requests a node's output value, this function resolves input sockets and emits shader code:

```c
char *float_curve_node_value(ui_node_t *node, ui_node_socket_t *socket) {
    char *fac = parser_material_parse_value_input(node->inputs->buffer[0], false);
    char *val = parser_material_parse_value_input(node->inputs->buffer[1], false);
    f32_array_t *curves = node->buttons->buffer[0]->default_value;

    if (curves->buffer[32] == 0.0f) float_curve_init(curves);

    i32 num = (i32)curves->buffer[32];
    char *name = parser_material_node_name(node, NULL);
    char *mapped = vector_curves_eval(name, val, curves->buffer, num);
    return string_tmp("lerp(%s, %s, %s)", val, mapped, fac);
}

```

The function performs three critical operations:
- **Resolves inputs**: `parser_material_parse_value_input()` fetches connected socket values or constants
- **Initializes curves**: Ensures the curve data buffer contains valid control points
- **Generates expression**: Returns a `lerp(Value, CurvedValue, Factor)` GLSL expression that blends between the original and remapped values

## The Curve Evaluation Helper ([`vector_curves_node.c`](https://github.com/armory3d/armorpaint/blob/main/vector_curves_node.c))

Complex mathematical operations are abstracted into reusable code generators. The `vector_curves_eval()` function in [`paint/sources/nodes_material/vector_curves_node.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/nodes_material/vector_curves_node.c) transforms curve control points into runtime shader logic.

### Runtime Curve Sampling in GLSL

This function writes temporary variables and conditional logic directly into the shader being constructed:

```c
char *vector_curves_eval(char *name, char *fac, f32 *points, i32 num) {
    char *result_var = string_tmp("%s_result", name);
    char *fac_var = string_tmp("%s_fac", name);
    
    parser_material_write(parser_material_kong,
        string_tmp("var %s: float = %s;", fac_var, fac));

    // Sort points by X coordinate
    i32 sorted[num];
    for (i32 i = 0; i < num; i++) sorted[i] = i;
    vector_curves_sort(sorted, num, points);

    // Generate first curve segment
    parser_material_write(parser_material_kong,
        string_tmp("var %s: float = lerp(%s, %s, %s);",
                   result_var, y0, y1, b01));

    // Generate conditional segments for remaining points
    for (i32 i = 1; i < num - 1; i++) {
        parser_material_write(parser_material_kong,
            string_tmp("if (%s > %s) { %s = lerp(%s, %s, %s); }",
                       fac_var, xi, result_var, yi, yi1, blend));
    }
    return result_var;
}

```

The generated code evaluates the curve entirely on the GPU without texture lookups, using linear interpolation between sorted control points.

## Graph Traversal and Shader Assembly ([`parser_material.c`](https://github.com/armory3d/armorpaint/blob/main/parser_material.c))

The **parser material** system orchestrates the translation from node graph to shader source. Located in [`paint/sources/parser_material.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/parser_material.c), this module traverses the graph starting from the output node and stitches together individual code snippets.

### Building Temporary Variables

As the parser walks connections, it ensures each node output becomes a named temporary variable to prevent duplicate evaluation:

```c
char *parser_material_write_result(ui_node_link_t *l) {
    ui_node_t *from_node = parser_material_get_node(l->from_id);
    ui_node_socket_t *from_socket = from_node->outputs->buffer[l->from_socket];
    char *res_var = parser_material_res_var_name(from_node, from_socket);

    if (string_array_index_of(parser_material_parsed, res_var) < 0) {
        any_array_push(parser_material_parsed, res_var);
        parser_material_write(parser_material_kong,
            string_tmp("var %s: float = %s;", res_var,
                       parser_material_parse_value(from_node, from_socket)));
    }
    return res_var;
}

```

This deduplication ensures that complex node graphs generate efficient shader code without redundant calculations.

### Writing Shader Stages

The parser maintains separate buffers for vertex and fragment stages. Node callbacks write to `parser_material_kong`, a `node_shader_t` structure representing the current shader under construction. The parser tracks which attributes are required:

```c
if (kong->frag_n) {  // Normal attribute required
    node_shader_add_constant(kong, "N: float3x3", "_normal_matrix");
    node_shader_add_out(kong, "wnormal: float");
    node_shader_write_vert(kong,
        "output.wnormal = constants.N * float3(input.nor.xy, input.pos.w);");
}

```

## Iron Engine Integration and Compilation

The final integration layer connects the generated shader source to Iron's rendering pipeline through GPU compilation and context setup.

### Shader Context Finalization

After graph traversal completes, `parser_material_finalize()` processes the accumulated requirements and generates the complete shader header:

```c
// Generated shader structure passed to Iron
node_shader_t *kong = parser_material_kong;
// Add required uniforms based on node flags
if (kong->frag_n) node_shader_add_constant(...);

```

### GPU Compilation Pipeline

The completed shader source transfers to Iron's GPU abstraction layer in [`base/sources/iron_gpu.c`](https://github.com/armory3d/armorpaint/blob/main/base/sources/iron_gpu.c). The `iron_gpu_shader_compile()` function compiles the generated GLSL (or HLSL/Metal) into a executable GPU program:

```c
// Iron compiles the assembled source
iron_gpu_shader_compile(shader_source, vertex_shader, fragment_shader);

```

Iron then binds the compiled shader to the material's render pipeline, utilizing the attributes and uniforms specified during the parsing phase.

## Summary

- **Node Registration**: [`nodes_material.c`](https://github.com/armory3d/armorpaint/blob/main/nodes_material.c) initializes node types and maps type strings to code-generation callbacks like `float_curve_node_value()`
- **Code Generation**: Individual node files emit GLSL snippets; [`float_curve_node.c`](https://github.com/armory3d/armorpaint/blob/main/float_curve_node.c) generates curve evaluation logic while [`vector_curves_node.c`](https://github.com/armory3d/armorpaint/blob/main/vector_curves_node.c) creates the underlying mathematical expressions
- **Graph Parsing**: [`parser_material.c`](https://github.com/armory3d/armorpaint/blob/main/parser_material.c) traverses the node graph, manages temporary variables, and assembles fragment/vertex shader source code
- **Iron Integration**: The parser populates `node_shader_t` structures with required attributes, then passes the final source to Iron's GPU compiler in [`iron_gpu.c`](https://github.com/armory3d/armorpaint/blob/main/iron_gpu.c) for real-time execution

## Frequently Asked Questions

### How does ArmorPaint handle complex node graphs without shader compilation errors?

The **parser material** system in [`parser_material.c`](https://github.com/armory3d/armorpaint/blob/main/parser_material.c) performs topological sorting of the node graph and generates unique temporary variable names for each node output via `parser_material_res_var_name()`. This ensures data dependencies are resolved in the correct order and prevents naming collisions during code generation.

### Can custom nodes be added to the material editor?

Yes, developers can extend the system by creating new node initialization functions similar to `float_curve_node_init()` in [`nodes_material.c`](https://github.com/armory3d/armorpaint/blob/main/nodes_material.c). Each new node must register a type string and implement a value callback function that returns valid GLSL code, which the parser then integrates into the shader pipeline.

### What shader languages does the Iron engine support?

According to the compilation pipeline in [`base/sources/iron_gpu.c`](https://github.com/armory3d/armorpaint/blob/main/base/sources/iron_gpu.c), Iron supports **GLSL** for OpenGL/Vulkan backends, **HLSL** for DirectX, and **Metal Shading Language** for macOS/iOS. The node system's generated code uses generic syntax compatible with these targets, with backend-specific transformations applied during the `iron_gpu_shader_compile()` stage.

### How are curve values stored between the UI and shader?

Curve control points are stored in a `f32_array_t` buffer within the node's `buttons` array. The `float_curve_node_value()` function reads these values and passes them to `vector_curves_eval()`, which hardcodes the curve points as compile-time constants in the generated shader, enabling efficient GPU-side evaluation without runtime texture sampling.