How the Material Node System in `nodes_material.c` and `float_curve_node.c` Integrates with the Iron Engine's Shader Pipeline
ArmorPaint translates visual node graphs into executable GPU shaders by registering node types in 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 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)
The integration begins with node registration during application initialization. The nodes_material_init() function in 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:
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:
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)
Individual nodes act as code generators. The 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:
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)
Complex mathematical operations are abstracted into reusable code generators. The vector_curves_eval() function in 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:
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)
The parser material system orchestrates the translation from node graph to shader source. Located in 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:
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:
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:
// 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. The iron_gpu_shader_compile() function compiles the generated GLSL (or HLSL/Metal) into a executable GPU program:
// 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.cinitializes node types and maps type strings to code-generation callbacks likefloat_curve_node_value() - Code Generation: Individual node files emit GLSL snippets;
float_curve_node.cgenerates curve evaluation logic whilevector_curves_node.ccreates the underlying mathematical expressions - Graph Parsing:
parser_material.ctraverses the node graph, manages temporary variables, and assembles fragment/vertex shader source code - Iron Integration: The parser populates
node_shader_tstructures with required attributes, then passes the final source to Iron's GPU compiler iniron_gpu.cfor 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 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. 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, 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.
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 →