How to Bake Normal Maps from Mesh Geometry in ArmorPaint: A Complete Technical Guide
ArmorPaint bakes normal maps by ray-tracing high-poly mesh normals onto low-poly UVs using a tangent-space conversion shader, accessible via the Bake Material dialog or programmatically through the bake context API.
ArmorPaint provides a comprehensive baking system that converts high-poly mesh geometry into tangent-space normal maps for real-time rendering workflows. When you need to bake normal maps from mesh geometry in ArmorPaint, the engine utilizes a specialized ray-trace pipeline that samples surface normals and transforms them into the target mesh's tangent space. This process is implemented across several core source files including make_bake.c and render_path_raytrace_bake.c.
Understanding the Normal Map Bake Pipeline
The baking pipeline consists of three distinct phases: type selection, shader generation, and ray-trace execution. Each phase maps to specific source files in the Armory3D repository.
Bake Type Selection and Context Configuration
The baking process begins with setting the bake type in the global context. According to paint/sources/enums.h at line 247, the system defines BAKE_TYPE_NORMAL as the identifier for tangent-space normal map baking. When you select Normal from the bake type dropdown in the UI, the system assigns this value to g_context->bake_type.
In paint/sources/nodes_material/bake_texture_node.c (lines 145-152), the node interface handles the user selection and updates the bake context accordingly. This configuration determines which shader variant the pipeline will generate during the next phase.
Shader Generation and Tangent-Space Conversion
The core logic for normal map generation resides in paint/sources/render/make_bake.c (lines 34-42). When g_context->bake_type equals BAKE_TYPE_NORMAL, the system constructs a fragment shader that performs the critical tangent-space conversion:
// make_bake.c – normal-map bake (tangent space)
else if (g_context->bake_type == BAKE_TYPE_NORMAL) { // Tangent
kong->frag_n = true;
node_shader_add_texture(kong, "texpaint_undo", "_texpaint_undo"); // high-poly normals
node_shader_write_frag(
kong, "var n0: float3 = sample_lod(texpaint_undo, sampler_linear, tex_coord, 0.0).rgb * float3(2.0, 2.0, 2.0) - float3(1.0, 1.0, 1.0);");
node_shader_add_function(kong, str_cotangent_frame);
node_shader_write_frag(kong, "var invTBN: float3x3 = transpose(cotangent_frame(n, n, tex_coord));");
node_shader_write_frag(kong, "var res: float3 = normalize(invTBN * n0) * float3(0.5, 0.5, 0.5) + float3(0.5, 0.5, 0.5);");
node_shader_write_frag(kong, "output[0] = float4(res, 1.0);");
}
This code block performs four critical operations:
- Samples the high-poly normal texture (
texpaint_undo) and decodes it from 0-1 range to -1 to 1 range - Constructs a cotangent frame (TBN matrix) using the
cotangent_framefunction - Transforms the high-poly normal from world space to tangent space using the inverse TBN matrix
- Re-encodes the result to 0-1 range for texture storage
Ray-Trace Execution and Shader Binding
The actual rendering pass is managed by paint/sources/render/render_path_raytrace_bake.c (lines 15-18). The function render_path_raytrace_bake_get_bake_shader_name() selects the appropriate shader based on the bake type:
// render_path_raytrace_bake.c – shader name selection
char *render_path_raytrace_bake_get_bake_shader_name() {
return g_context->bake_type == BAKE_TYPE_NORMAL ?
string("raytrace_bake_normal%s", render_path_raytrace_ext) :
...;
}
This shader binding connects the generated tangent-space conversion code to the ray-trace pipeline, which casts rays from the low-poly mesh surface to the high-poly geometry to sample the detailed normals.
Step-by-Step Guide to Baking Normal Maps
You can initiate the normal map bake through the ArmorPaint interface or programmatically via the C API.
Baking via the User Interface
To bake normal maps from mesh geometry in ArmorPaint using the graphical interface:
- Open the Bake Material... dialog from the main menu (handled in
paint/sources/ui/ui_menubar.cat lines 328-332). - In the Bake dropdown selector, choose Normal to set
g_context->bake_typetoBAKE_TYPE_NORMAL. - Configure optional parameters such as Samples (ray count per pixel) and Up Axis orientation.
- Click the Bake button to initiate the ray-trace pass.
- After completion, switch the viewport mode to Normal (as defined in
paint/sources/ui/ui_menubar.clines 86-88) to inspect the baked tangent-space normal map on your low-poly mesh.
Baking Programmatically via the API
For developers extending ArmorPaint or automating asset pipelines, you can trigger the bake programmatically:
// Set up bake context (usually done by UI)
g_context->bake_type = BAKE_TYPE_NORMAL; // Choose normal-map bake
g_context->bake_samples = 128; // Number of rays per pixel
g_context->bake_up_axis = BAKE_UP_AXIS_Y; // Y-up (optional)
// Trigger the bake pass
render_path_raytrace_bake_commands(parse_paint_material);
This configuration mimics the UI workflow, setting the bake type to tangent-space normals and specifying 128 samples for quality. The render_path_raytrace_bake_commands() function executes the full pipeline described in the previous section.
Exporting Baked Normal Maps
After the bake operation completes, the resulting normal map resides in the material's Normal texture slot. To export this texture to disk:
// After bake completes:
_box_export_bake_material = true; // Enable "Bake to Textures" export mode
export_texture_run(g_context->texture_export_path, true);
This code triggers the export pipeline, saving the baked tangent-space normal map to the path specified in the global context. You can also access the texture through the Export → Bake to Textures menu option in the standard interface.
Key Source Files and Architecture
The normal baking system spans multiple subsystems within the ArmorPaint source:
paint/sources/enums.h(lines 247-252): Defines thebake_type_tenumeration includingBAKE_TYPE_NORMALandBAKE_TYPE_NORMAL_OBJECT.paint/sources/render/make_bake.c(lines 34-42): Generates the fragment shader responsible for tangent-space conversion using the cotangent frame algorithm.paint/sources/render/render_path_raytrace_bake.c(lines 15-18): Selects the appropriate ray-trace shader variant and manages the render pass execution.paint/sources/ui/ui_menubar.c(lines 86-88, 328-332): Provides the user interface for initiating bakes and switching viewport modes.paint/sources/nodes_material/bake_texture_node.c(lines 145-152): Implements the node-based UI for bake type selection.paint/sources/render/render_path_paint.c(lines 954-960): Handles transitions between different bake modes and manages state cleanup.
Summary
- ArmorPaint bakes normal maps using a ray-trace pipeline that samples high-poly geometry and converts normals to the low-poly mesh's tangent space.
- The bake type is controlled via
g_context->bake_type, withBAKE_TYPE_NORMALselecting tangent-space output as defined inenums.h. - Shader generation occurs in
make_bake.c, which constructs a fragment shader utilizingcotangent_framefor TBN matrix construction and normal transformation. - Execution is handled by
render_path_raytrace_bake.c, which binds the "raytrace_bake_normal" shader and manages the ray-casting process. - You can initiate bakes through the Bake Material... UI dialog or programmatically by configuring the bake context and calling
render_path_raytrace_bake_commands().
Frequently Asked Questions
What is the difference between Normal and Normal Object bake types in ArmorPaint?
ArmorPaint supports two normal map variants as defined in enums.h: BAKE_TYPE_NORMAL (tangent-space) and BAKE_TYPE_NORMAL_OBJECT (object-space). Tangent-space normal maps encode surface details relative to the low-poly mesh's surface orientation, making them ideal for asset deformation and universal lighting calculations. Object-space normal maps store normals in world coordinates, which are useful for static environment pieces but cannot be deformed or reused across different mesh orientations.
How does ArmorPaint convert high-poly normals to tangent space?
The conversion occurs in make_bake.c through a matrix transformation process. The shader first samples the high-poly normal data from texpaint_undo, then constructs a cotangent frame (TBN matrix) using the cotangent_frame function. It transposes this matrix to create an inverse TBN (invTBN), which transforms world-space normals into tangent-space vectors. Finally, it remaps the -1 to 1 range results into 0 to 1 for texture storage.
Can I adjust the number of samples when baking normal maps?
Yes. The sample count is controlled via g_context->bake_samples before invoking the bake. Higher values (such as 128 or 256) reduce noise in the resulting normal map but increase computation time. This parameter is accessible both through the Samples field in the Bake Material dialog and programmatically through the context structure.
Where are the baked normal maps stored after the bake completes?
Upon completion, the baked normal map populates the Normal texture slot of the current material. You can visualize it immediately by switching the viewport to Normal mode. For persistent storage, use the Export → Bake to Textures feature or call export_texture_run() with _box_export_bake_material set to true, which writes the texture to the path specified in g_context->texture_export_path.
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 →