ArmorPaint Asset Management: How Models and Textures Are Imported and Organized

ArmorPaint uses a centralized import pipeline that detects file formats via path_is_mesh() and path_is_texture(), dispatches to format-specific importers through function pointer maps, and stores all assets in the global g_project structure for synchronous access across the engine.

The armory3d/armorpaint repository implements a robust asset management system that decouples the UI from raw file handling. This article examines how ArmorPaint asset management processes 3D models and textures through a unified pipeline, from format detection to scene integration.

Centralized Import Pipeline Architecture

File Type Detection and Dispatcher

Every asset import begins with type detection in import_mesh.c (lines 13–20) and import_texture.c. The system uses boolean checks to classify incoming files:

// From import_mesh.c
if (!path_is_mesh(path)) {
    if (!context_enable_import_plugin(path)) {
        console_error(strings_unknown_asset_format());
        return;
    }
}

When a user drops a file or selects Import Mesh, the pipeline extracts the extension and queries one of two global dispatch tables: import_mesh_importers or import_texture_importers. These any_map_t structures map file extensions to importer function pointers populated at startup from modules in the io/ directory.

Importer Lookup and Execution

The dispatcher retrieves the appropriate loader via any_map_get():

// From import_mesh.c, line 40
char *ext = string_tmp("%s", path + string_last_index_of(path, ".") + 1);
raw_mesh_t *(*importer)(char *path) = any_map_get(import_mesh_importers, ext);
raw_mesh_t *mesh = importer(path);  // Actual file parsing occurs here

This design allows the engine to support OBJ, FBX, BLEND, KTX, and custom formats without hardcoding logic into the UI layer.

Mesh Import Implementation

The import_mesh_run() function in import_mesh.c orchestrates the complete geometry pipeline. After parsing, raw data converts to engine objects through import_mesh_make_mesh() or import_mesh_add_mesh():

void import_mesh_run(char *path, bool clear_layers,
                     bool replace_existing, bool keep_camera) {
    // 1. Detect format
    if (!path_is_mesh(path)) {
        if (!context_enable_import_plugin(path)) {
            console_error(strings_unknown_asset_format());
            return;
        }
    }

    // 2. Pick the correct importer
    char *ext = string_tmp("%s", path + string_last_index_of(path, ".") + 1);
    raw_mesh_t *(*importer)(char *path) = any_map_get(import_mesh_importers, ext);
    raw_mesh_t *mesh = importer(path);

    // 3. Create engine mesh object(s)
    replace_existing ? import_mesh_make_mesh(mesh)
                     : import_mesh_add_mesh(mesh);

    // 4. Store source path for reloads
    g_project->mesh_assets = any_array_create_from_raw((void *[]){ path }, 1);
}

Scene Integration and Asset Tracking

Post-import steps occur in import_mesh.c (lines 88–115). The system handles re-parenting, UV-unwrapping, and scaling to view bounds via util_mesh_merge(). The active paint object updates through context_select_paint_object(), while source paths persist in g_project->mesh_assets for future reload operations.

Texture Import and Material Binding

Texture processing follows a parallel path in import_texture.c. The pipeline creates texture_t objects and binds them to materials immediately:

void import_texture_run(char *path) {
    char *ext = string_tmp("%s", path + string_last_index_of(path, ".") + 1);
    raw_texture_t *(*importer)(char *path) = any_map_get(import_texture_importers, ext);
    raw_texture_t *tex = importer(path);

    // Create engine texture and update material system
    texture_t *t = texture_create(tex->data, tex->width, tex->height, tex->format);
    material_set_texture(current_material, t);
}

Textures integrate directly into the material system rather than appearing as standalone scene objects, ensuring immediate availability for painting operations.

Global Project State and Resource Management

All imported assets ultimately reference the global g_project structure defined in context.c. This centralized state maintains:

  • mesh_assets: Array of source paths for reload operations
  • paint_objects: Active mesh instances for the current project
  • Material definitions: Linked textures and shaders

Generic Resource Loading

For non-project assets like icons and default materials, resource.c provides resource_load():

// From ui_base.c (lines 121-124)
void ui_base_draw() {
    string_array_t *resources = string_array_create();
    string_array_push(resources, "icons");
    string_array_push(resources, "default_material");
    resource_load(resources);  // Pulls from /paint/assets/
}

Loading Behavior

Standard asset loading operates synchronously on the main thread using iron_load_blob(). However, large downloads such as neural-node models utilize iron_file_download() with callbacks to maintain UI responsiveness during transfer.

Practical Code Examples

Importing a Mesh Programmatically

Custom plugins can trigger imports directly through the pipeline:

char *my_mesh_path = "C:/Models/character.obj";
import_mesh_run(my_mesh_path, true, false, false);  
// clear_layers = true, replace_existing = false, keep_camera = false

Replacing an Existing Texture

Update the active material's diffuse map at runtime:

char *new_tex = "C:/Textures/diffuse.png";
import_texture_run(new_tex);  // Swaps the active material's texture reference

Accessing Loaded Mesh Objects

Iterate the global project state to find specific assets by name:

mesh_object_t *obj = NULL;
for (i32 i = 0; i < g_project->_->paint_objects->length; ++i) {
    mesh_object_t *p = g_project->_->paint_objects->buffer[i];
    if (string_equals(p->base->name, "Character")) {
        obj = p;
        break;
    }
}
if (obj) {
    // Apply masks, export, or modify geometry
}

Summary

  • Format Detection: path_is_mesh() and path_is_texture() classify assets before dispatch
  • Importer Maps: any_map_get() queries import_mesh_importers or import_texture_importers for format-specific parsers
  • Global State: All assets register with g_project, enabling cross-referencing between UI, rendering, and painting systems
  • Scene Integration: util_mesh_merge() and context_select_paint_object() handle post-import scene updates
  • Resource Loader: resource_load() manages shared assets like icons independently of project files

Frequently Asked Questions

How does ArmorPaint detect file formats?

ArmorPaint checks file extensions using path_is_mesh() and path_is_texture() in import_mesh.c (lines 13–20). If the extension matches supported types like OBJ or PNG, the system proceeds to importer lookup; otherwise, it attempts to load a plugin via context_enable_import_plugin().

Where are imported assets stored after loading?

All assets reference the global g_project structure. Meshes append to g_project->mesh_assets for path tracking and g_project->paint_objects for scene instances. Textures bind directly to the material system rather than persisting in standalone arrays.

Is asset loading synchronous or asynchronous?

Standard imports operate synchronously on the main thread through iron_load_blob(). Large network resources utilize iron_file_download() with asynchronous callbacks to prevent UI blocking during lengthy transfers.

How can developers extend the import system?

Developers register custom importers by populating import_mesh_importers or import_texture_importers with extension-to-function mappings. The any_map_t architecture allows runtime registration of new parsers without modifying core UI code.

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 →