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