Loading...
Searching...
No Matches
ModelImporter.cpp
Go to the documentation of this file.
1//
2// Created by denzel on 08/12/2025.
3//
4
6
7
8#include <fstream>
9#include <assimp/Importer.hpp>
10#include <assimp/scene.h>
11#include <assimp/postprocess.h>
12
13#include "glm/detail/type_mat4x4.hpp"
14#include "glm/gtc/quaternion.hpp"
15#include "glm/gtx/matrix_decompose.hpp"
16#include "hellfire/graphics/Mesh.h"
17#include "hellfire/graphics/Vertex.h"
18#include "hellfire/graphics/material/MaterialData.h"
19#include "hellfire/serializers/MaterialSerializer.h"
20#include "hellfire/serializers/MeshSerializer.h"
21#include "hellfire/serializers/TextureSerializer.h"
22
23namespace hellfire {
25 const std::filesystem::path &output_dir) : registry_(registry),
27 create_directories(output_dir_);
28 }
29
30 ImportResult ModelImporter::import(const std::filesystem::path &source_path, const ImportSettings &settings) {
31 ImportResult result;
32
33 source_path_ = source_path;
34 source_dir_ = source_path.parent_path();
35 base_name_ = source_path.stem().string();
36
37 Assimp::Importer importer;
38 ai_scene_ = importer.ReadFile(source_path.string(), build_import_flags(settings));
39
40 if (!ai_scene_ || !ai_scene_->mRootNode) {
41 result.error_message = importer.GetErrorString();
42 return result;
43 }
44
45 // Process hierarchy starting from root
46 process_node_hierarchy(ai_scene_->mRootNode, result);
47
48 result.success = true;
49 ai_scene_ = nullptr;
50
51 return result;
52 }
53
54 unsigned int ModelImporter::build_import_flags(const ImportSettings &settings) const {
55 unsigned int flags = aiProcess_ValidateDataStructure;
56
57 if (settings.triangulate)
58 flags |= aiProcess_Triangulate;
59 if (settings.generate_normals)
60 flags |= aiProcess_GenSmoothNormals;
61 if (settings.generate_tangents)
62 flags |= aiProcess_CalcTangentSpace;
63 if (settings.flip_uvs)
64 flags |= aiProcess_FlipUVs;
65 if (settings.optimize_meshes)
66 flags |= aiProcess_OptimizeMeshes | aiProcess_OptimizeGraph;
67
68 flags |= aiProcess_JoinIdenticalVertices;
69 flags |= aiProcess_ImproveCacheLocality;
70
71 return flags;
72 }
73
74 void ModelImporter::process_node_hierarchy(const aiNode *node, ImportResult &result, size_t parent_index) {
75 const size_t current_index = result.nodes.size();
76 result.nodes.push_back(convert_node(node));
77 auto &imported_node = result.nodes.back();
78
79 // Link to parent
80 if (parent_index != SIZE_MAX) {
81 result.nodes[parent_index].child_indices.push_back(current_index);
82 } else {
83 result.root_node_index = current_index;
84 }
85
86 // Process meshes attached to this node
87 for (unsigned int i = 0; i < node->mNumMeshes; i++) {
88 const unsigned int mesh_idx = node->mMeshes[i];
89 const aiMesh *ai_mesh = ai_scene_->mMeshes[mesh_idx];
90
91 ImportedMesh imported_mesh;
92 imported_mesh.name = ai_mesh->mName.length > 0
93 ? ai_mesh->mName.C_Str()
94 : make_unique_name(base_name_, "mesh", mesh_idx);
95
96 // Process and serialize mesh
97 imported_mesh.mesh_asset = process_mesh(ai_mesh, mesh_idx);
98 result.created_mesh_assets.push_back(imported_mesh.mesh_asset);
99
100 // Process and serialize material
101 if (ai_mesh->mMaterialIndex < ai_scene_->mNumMaterials) {
102 const aiMaterial *material = ai_scene_->mMaterials[ai_mesh->mMaterialIndex];
103 imported_mesh.material_asset = process_material(material, ai_mesh->mMaterialIndex);
104
105 if (imported_mesh.material_asset != INVALID_ASSET_ID) {
106 result.created_material_assets.push_back(imported_mesh.material_asset);
107 }
108 }
109
110 imported_node.mesh_indices.push_back(result.meshes.size());
111 result.meshes.push_back(imported_mesh);
112 }
113
114 // Recurse into children
115 for (unsigned int i = 0; i < node->mNumChildren; i++) {
116 process_node_hierarchy(node->mChildren[i], result, current_index);
117 }
118 }
119
120 ImportedNode ModelImporter::convert_node(const aiNode *node) const {
121 ImportedNode result;
122
123 result.name = node->mName.length > 0 ? node->mName.C_Str() : "Node";
124
125 glm::mat4 transform = convert_matrix(node->mTransformation);
126 glm::vec3 skew;
127 glm::vec4 perspective;
128 glm::quat rotation;
129
130 decompose(transform, result.scale, rotation, result.position, skew, perspective);
131 result.rotation = eulerAngles(rotation);
132
133 return result;
134 }
135
136 AssetID ModelImporter::process_mesh(const aiMesh *ai_mesh, size_t mesh_index) {
137 std::vector<Vertex> vertices;
138 std::vector<unsigned int> indices;
139
140 vertices.reserve(ai_mesh->mNumVertices);
141 indices.reserve(ai_mesh->mNumFaces * 3);
142
143 // Extract vertices
144 for (unsigned int i = 0; i < ai_mesh->mNumVertices; i++) {
145 Vertex v{};
146
147 v.position = {
148 ai_mesh->mVertices[i].x,
149 ai_mesh->mVertices[i].y,
150 ai_mesh->mVertices[i].z
151 };
152
153 if (ai_mesh->HasNormals()) {
154 v.normal = {
155 ai_mesh->mNormals[i].x,
156 ai_mesh->mNormals[i].y,
157 ai_mesh->mNormals[i].z
158 };
159 }
160
161 if (ai_mesh->HasTangentsAndBitangents()) {
162 v.tangent = {
163 ai_mesh->mTangents[i].x,
164 ai_mesh->mTangents[i].y,
165 ai_mesh->mTangents[i].z
166 };
167 v.bitangent = {
168 ai_mesh->mBitangents[i].x,
169 ai_mesh->mBitangents[i].y,
170 ai_mesh->mBitangents[i].z
171 };
172 }
173
174 if (ai_mesh->HasTextureCoords(0)) {
175 v.texCoords = {
176 ai_mesh->mTextureCoords[0][i].x,
177 ai_mesh->mTextureCoords[0][i].y
178 };
179 }
180
181 if (ai_mesh->HasVertexColors(0)) {
182 v.color = {
183 ai_mesh->mColors[0][i].r,
184 ai_mesh->mColors[0][i].g,
185 ai_mesh->mColors[0][i].b
186 };
187 } else {
188 v.color = glm::vec3(1.0f);
189 }
190
191 vertices.push_back(v);
192 }
193
194 // Extract indices
195 for (unsigned int i = 0; i < ai_mesh->mNumFaces; i++) {
196 const aiFace &face = ai_mesh->mFaces[i];
197 for (unsigned int j = 0; j < face.mNumIndices; j++) {
198 indices.push_back(face.mIndices[j]);
199 }
200 }
201
202 // Serialize to file
203 Mesh mesh(vertices, indices, true); // defer building, because opengl isn't thread safe
204 const std::string filename = make_unique_name(base_name_, "mesh", mesh_index) + ".hfmesh";
205 const auto filepath = output_dir_ / filename;
206
207
208 if (!MeshSerializer::save(filepath, mesh)) {
209 std::cerr << "Failed to save mesh: " << filepath << std::endl;
210 return INVALID_ASSET_ID;
211 }
212
213 return registry_.register_asset(filepath, AssetType::MESH);
214 }
215
216 AssetID ModelImporter::process_material(const aiMaterial *ai_mat, size_t material_index) {
217 MaterialData data;
218
219 // Get name
220 aiString name;
221 if (ai_mat->Get(AI_MATKEY_NAME, name) == AI_SUCCESS) {
222 data.name = name.C_Str();
223 } else {
224 data.name = make_unique_name(base_name_, "material", material_index);
225 }
226
227 // Colors
228 aiColor3D color;
229 if (ai_mat->Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS)
230 data.diffuse_color = {color.r, color.g, color.b};
231 if (ai_mat->Get(AI_MATKEY_COLOR_AMBIENT, color) == AI_SUCCESS)
232 data.ambient_color = {color.r, color.g, color.b};
233 if (ai_mat->Get(AI_MATKEY_COLOR_SPECULAR, color) == AI_SUCCESS)
234 data.specular_color = {color.r, color.g, color.b};
235 if (ai_mat->Get(AI_MATKEY_COLOR_EMISSIVE, color) == AI_SUCCESS)
236 data.emissive_color = {color.r, color.g, color.b};
237
238 // Scalars
239 float value;
240 if (ai_mat->Get(AI_MATKEY_OPACITY, value) == AI_SUCCESS)
241 data.opacity = value;
242 if (ai_mat->Get(AI_MATKEY_SHININESS, value) == AI_SUCCESS)
243 data.shininess = std::max(value, 1.0f);
244 if (ai_mat->Get(AI_MATKEY_METALLIC_FACTOR, value) == AI_SUCCESS)
245 data.metallic = value;
246 if (ai_mat->Get(AI_MATKEY_ROUGHNESS_FACTOR, value) == AI_SUCCESS)
247 data.roughness = value;
248
249 // Textures
250 auto try_load_texture = [&](aiTextureType ai_type, TextureType hf_type, const char* type_name) {
251 unsigned int count = ai_mat->GetTextureCount(ai_type);
252 if (count == 0) return;
253
254 std::cout << " Found " << count << " texture(s) for type: " << type_name << std::endl;
255
256 aiString tex_path;
257 if (ai_mat->GetTexture(ai_type, 0, &tex_path) != AI_SUCCESS) return;
258
259 std::cout << " -> Path: " << tex_path.C_Str() << std::endl;
260
261 AssetID tex_asset = process_texture(tex_path.C_Str(), hf_type);
262 if (tex_asset != INVALID_ASSET_ID) {
263 data.texture_assets[hf_type] = tex_asset;
264 }
265 };
266
267 try_load_texture(aiTextureType_DIFFUSE, TextureType::DIFFUSE, "DIFFUSE");
268 try_load_texture(aiTextureType_NORMALS, TextureType::NORMAL, "NORMALS");
269 try_load_texture(aiTextureType_SPECULAR, TextureType::SPECULAR, "SPECULAR");
270 try_load_texture(aiTextureType_METALNESS, TextureType::METALNESS, "METALNESS");
271 try_load_texture(aiTextureType_DIFFUSE_ROUGHNESS, TextureType::ROUGHNESS, "ROUGHNESS");
272 try_load_texture(aiTextureType_AMBIENT_OCCLUSION, TextureType::AMBIENT_OCCLUSION, "AO");
273 try_load_texture(aiTextureType_EMISSIVE, TextureType::EMISSIVE, "EMISSIVE");
274
275 try_load_texture(aiTextureType_BASE_COLOR, TextureType::DIFFUSE, "BASE_COLOR (glTF)");
276 try_load_texture(aiTextureType_NORMAL_CAMERA, TextureType::NORMAL, "NORMAL_CAMERA");
277
278 // Serialize
279 const std::string filename = data.name + ".hfmat";
280 const auto filepath = output_dir_ / filename;
281
282 if (!MaterialSerializer::save(filepath, data)) {
283 std::cerr << "Failed to save material: " << filepath << std::endl;
284 return INVALID_ASSET_ID;
285 }
286
287 return registry_.register_asset(filepath, AssetType::MATERIAL);
288 }
289
290 AssetID ModelImporter::process_texture(const std::string &texture_ref, TextureType type) {
291 std::filesystem::path resolved_path;
292
293 if (is_embedded_texture(texture_ref)) {
294 size_t index = std::stoul(texture_ref.substr(1));
295 resolved_path = extract_embedded_texture(index);
296 } else {
297 auto path_opt = resolve_texture_path(texture_ref);
298 if (!path_opt) {
299 std::cerr << "Could not resolve texture: " << texture_ref << std::endl;
300 return INVALID_ASSET_ID;
301 }
302 resolved_path = *path_opt;
303 }
304
305 TextureMetadata tex_meta;
306 tex_meta.type = type;
307 tex_meta.generate_mipmaps = true;
308 tex_meta.srgb = (type == TextureType::DIFFUSE || type == TextureType::EMISSIVE);
309
310 TextureSerializer::save_metadata(resolved_path, tex_meta);
311
312 // Check if already registered
313 if (const auto existing = registry_.get_uuid_by_path(resolved_path)) {
314 return *existing;
315 }
316
317 // For textures, we just register the original file
319 }
320
321 std::optional<std::filesystem::path> ModelImporter::resolve_texture_path(const std::string &texture_ref) const {
322 const std::filesystem::path tex_filename = std::filesystem::path(texture_ref).filename();
323
324 const std::vector search_paths = {
325 source_dir_ / texture_ref,
326 source_dir_ / tex_filename,
327 source_dir_ / "textures" / tex_filename,
328 source_dir_ / "Textures" / tex_filename,
329 source_dir_ / "materials" / tex_filename,
330 source_dir_.parent_path() / "textures" / tex_filename,
331 };
332
333 for (const auto &path: search_paths) {
334 if (exists(path)) {
335 return canonical(path);
336 }
337 }
338
339 return std::nullopt;
340 }
341
342 bool ModelImporter::is_embedded_texture(const std::string &path) const {
343 return !path.empty() && path[0] == '*';
344 }
345
346 std::filesystem::path ModelImporter::extract_embedded_texture(size_t index) {
347 if (index >= ai_scene_->mNumTextures) {
348 return {};
349 }
350
351 const aiTexture *tex = ai_scene_->mTextures[index];
352
353 std::string extension = tex->achFormatHint;
354 if (extension.empty()) {
355 // Detect from magic bytes
356 const auto *data = reinterpret_cast<const unsigned char *>(tex->pcData);
357 if (data[0] == 0xFF && data[1] == 0xD8) extension = "jpg";
358 else if (data[0] == 0x89 && data[1] == 0x50) extension = "png";
359 else extension = "bin";
360 }
361
362 const std::string filename = make_unique_name(base_name_, "texture", index)
363 + "." + extension;
364 auto filepath = output_dir_ / filename;
365
366 std::ofstream file(filepath, std::ios::binary);
367 if (!file) return {};
368
369 // mHeight == 0 means compressed format, mWidth is byte count
370 const size_t size = (tex->mHeight == 0)
371 ? tex->mWidth
372 : tex->mWidth * tex->mHeight * 4;
373
374 file.write(reinterpret_cast<const char *>(tex->pcData), size);
375
376 return filepath;
377 }
378
379 glm::mat4 ModelImporter::convert_matrix(const aiMatrix4x4 &m) {
380 return {
381 m.a1, m.b1, m.c1, m.d1,
382 m.a2, m.b2, m.c2, m.d2,
383 m.a3, m.b3, m.c3, m.d3,
384 m.a4, m.b4, m.c4, m.d4
385 };
386 }
387
388 bool ModelImporter::is_identity(const aiMatrix4x4 &m) {
389 constexpr float epsilon = 0.0001f;
390 return std::abs(m.a1 - 1.0f) < epsilon && std::abs(m.b2 - 1.0f) < epsilon
391 && std::abs(m.c3 - 1.0f) < epsilon && std::abs(m.d4 - 1.0f) < epsilon
392 && std::abs(m.a2) < epsilon && std::abs(m.a3) < epsilon && std::abs(m.a4) < epsilon
393 && std::abs(m.b1) < epsilon && std::abs(m.b3) < epsilon && std::abs(m.b4) < epsilon
394 && std::abs(m.c1) < epsilon && std::abs(m.c2) < epsilon && std::abs(m.c4) < epsilon
395 && std::abs(m.d1) < epsilon && std::abs(m.d2) < epsilon && std::abs(m.d3) < epsilon;
396 }
397
398 std::string ModelImporter::make_unique_name(const std::string &base, const std::string &suffix,
399 size_t index) const {
400 return base + "_" + suffix + "_" + std::to_string(index);
401 }
402} // hellfire
Registry for storing assets.
AssetID register_asset(const std::filesystem::path &filepath, AssetType type)
static bool save(const std::filesystem::path &filepath, const MaterialData &material)
static bool save(const std::filesystem::path &filepath, const Mesh &mesh)
Converts external model formats (FBX, GLTF, OBJ) into internal assets this runs once during asset imp...
unsigned int build_import_flags(const ImportSettings &settings) const
std::filesystem::path extract_embedded_texture(size_t index)
ImportedNode convert_node(const aiNode *node) const
ModelImporter(AssetRegistry &registry, const std::filesystem::path &output_dir)
ImportResult import(const std::filesystem::path &source_path, const ImportSettings &settings={})
AssetID process_mesh(const aiMesh *mesh, size_t mesh_index)
void process_node_hierarchy(const aiNode *node, ImportResult &result, size_t parent_index=SIZE_MAX)
AssetID process_material(const aiMaterial *ai_mat, size_t material_index)
AssetRegistry & registry_
static bool save_metadata(const std::filesystem::path &texture_path, const TextureMetadata &meta)
TextureType
Definition Texture.h:13
constexpr AssetID INVALID_ASSET_ID
Definition Vertex.h:5
Complete result of importing a model file.
std::string error_message
Represents an imported mesh with its material binding.
Represents a node in the imported model hierarchy.
Serializable material data (separate from runtime Material class)
Metadata for texture assets.