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 operationspaint_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()andpath_is_texture()classify assets before dispatch - Importer Maps:
any_map_get()queriesimport_mesh_importersorimport_texture_importersfor 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()andcontext_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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →