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_frame function
  • 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:

  1. Open the Bake Material... dialog from the main menu (handled in paint/sources/ui/ui_menubar.c at lines 328-332).
  2. In the Bake dropdown selector, choose Normal to set g_context->bake_type to BAKE_TYPE_NORMAL.
  3. Configure optional parameters such as Samples (ray count per pixel) and Up Axis orientation.
  4. Click the Bake button to initiate the ray-trace pass.
  5. After completion, switch the viewport mode to Normal (as defined in paint/sources/ui/ui_menubar.c lines 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:

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, with BAKE_TYPE_NORMAL selecting tangent-space output as defined in enums.h.
  • Shader generation occurs in make_bake.c, which constructs a fragment shader utilizing cotangent_frame for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →