1#include "hellfire/assets/ModelLoader.h"
5#include <glm/gtc/quaternion.hpp>
6#include <glm/gtx/matrix_decompose.hpp>
8#include "assimp/Importer.hpp"
9#include "hellfire/ecs/RenderableComponent.h"
10#include "../ecs/Entity.h"
11#include "hellfire/ecs/TransformComponent.h"
12#include "hellfire/ecs/components/MeshComponent.h"
13#include "hellfire/scene/Scene.h"
15namespace fs = std::filesystem;
17#define MODEL_LOADER_DEBUG 0
26 const auto start_time = std::chrono::high_resolution_clock::now();
27 std::cout <<
"Loading model: " << filepath << std::endl;
29 Assimp::Importer importer;
32 if (import_flags == 0) {
37 const aiScene *ai_scene = importer.ReadFile(filepath.string(), import_flags);
39 if (!ai_scene || ai_scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !ai_scene->mRootNode) {
40 std::cerr <<
"ModelLoader: Cannot load " << filepath <<
" - "
41 << importer.GetErrorString() << std::endl;
47 debug_scene_info(ai_scene, filepath.string());
51 preprocess_materials(ai_scene, filepath.string());
54 const EntityID imported_model = process_node(scene, ai_scene->mRootNode, ai_scene, filepath.string());
57 const auto end_time = std::chrono::high_resolution_clock::now();
58 const auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
59 std::cout <<
"Model loaded in: " << duration.count() <<
"ms" << std::endl;
61 return imported_model;
64 void ModelLoader::debug_scene_info(
const aiScene *scene,
const std::string &filepath) {
65 std::cout <<
"=== Scene Debug Info for " << fs::path(filepath).filename() <<
" ===" << std::endl;
66 std::cout <<
"Size: " << fs::file_size(filepath) / 1024 <<
"(KB)" << std::endl;
67 std::cout <<
"Materials: " << scene->mNumMaterials << std::endl;
68 std::cout <<
"Meshes: " << scene->mNumMeshes << std::endl;
69 std::cout <<
"Embedded Textures: " << scene->mNumTextures << std::endl;
72 for (
unsigned int i = 0; i < scene->mNumTextures; i++) {
73 const aiTexture *tex = scene->mTextures[i];
74 std::cout <<
" Embedded texture " << i <<
": "
75 << tex->mWidth <<
"x" << tex->mHeight
76 <<
" format: " << tex->achFormatHint << std::endl;
79 std::cout <<
"=============================================" << std::endl;
82 void ModelLoader::preprocess_materials(
const aiScene *scene,
const std::string &filepath) {
84 std::cout <<
"Preprocessing " << scene->mNumMaterials <<
" materials..." << std::endl;
87 for (
unsigned int i = 0; i < scene->mNumMaterials; i++) {
88 const aiMaterial *ai_material = scene->mMaterials[i];
91 std::string cache_key = create_material_key(ai_material, filepath, i);
94 if (material_cache.find(cache_key) != material_cache.end()) {
99 const auto material = create_material(ai_material, scene, filepath);
100 material_cache[cache_key] = material;
126 EntityID
ModelLoader::process_node(
Scene *scene, aiNode *node,
const aiScene *ai_scene,
const std::string &filepath,
127 EntityID parent_id) {
128 bool should_create_entity = node->mNumMeshes > 0 ||
129 node->mNumChildren > 0 ||
130 !is_identity_transform(node->mTransformation);
132 EntityID entity_id = 0;
135 if (should_create_entity) {
136 std::string node_name = node->mName.length > 0
137 ? node->mName.C_Str()
138 : (
"Node_" + std::to_string(
reinterpret_cast<uintptr_t>(node)));
144 glm::mat4 transform = aiMatrix4x4ToGlm(node->mTransformation);
145 glm::vec3 translation, scale, skew;
147 glm::vec4 perspective;
148 glm::decompose(transform, scale, rotation, translation, skew, perspective);
151 entity->transform()->set_scale(scale);
154 if (parent_id != 0) {
160 if (entity_id != 0) {
161 for (
unsigned int i = 0; i < node->mNumMeshes; i++) {
162 unsigned int mesh_index = node->mMeshes[i];
163 aiMesh *ai_mesh = ai_scene->mMeshes[mesh_index];
165 std::shared_ptr<
Mesh> processed_mesh = process_mesh(ai_mesh, ai_scene, filepath);
168 std::shared_ptr<
Material> material =
nullptr;
169 if (ai_mesh->mMaterialIndex >= 0) {
170 const aiMaterial *ai_material = ai_scene->mMaterials[ai_mesh->mMaterialIndex];
171 std::string material_key = create_material_key(ai_material, filepath, ai_mesh->mMaterialIndex);
173 auto cached_material = material_cache.find(material_key);
174 if (cached_material != material_cache.end()) {
175 material = cached_material->second;
177 material = create_material(ai_material, ai_scene, filepath);
178 material_cache[material_key] = material;
183 EntityID mesh_entity_id;
186 if (node->mNumMeshes == 1) {
188 mesh_entity_id = entity_id;
189 mesh_entity = entity;
192 std::string mesh_name = ai_mesh->mName.length > 0
193 ? ai_mesh->mName.C_Str()
194 : (std::string(node->mName.C_Str()) +
"_Mesh_" + std::to_string(i));
202 auto *mesh_comp = mesh_entity->add_component<MeshComponent>();
203 mesh_comp->set_mesh(processed_mesh);
205 auto *renderable = mesh_entity->add_component<RenderableComponent>();
207 renderable->set_material(material);
213 for (
unsigned int i = 0; i < node->mNumChildren; i++) {
214 process_node(scene, node->mChildren[i], ai_scene, filepath,
215 entity_id != 0 ? entity_id : parent_id);
221 bool ModelLoader::is_identity_transform(
const aiMatrix4x4 &matrix) {
222 const float epsilon = 0.0001f;
223 return std::abs(matrix.a1 - 1.0f) < epsilon && std::abs(matrix.b2 - 1.0f) < epsilon &&
224 std::abs(matrix.c3 - 1.0f) < epsilon && std::abs(matrix.d4 - 1.0f) < epsilon &&
225 std::abs(matrix.a2) < epsilon && std::abs(matrix.a3) < epsilon && std::abs(matrix.a4) < epsilon &&
226 std::abs(matrix.b1) < epsilon && std::abs(matrix.b3) < epsilon && std::abs(matrix.b4) < epsilon &&
227 std::abs(matrix.c1) < epsilon && std::abs(matrix.c2) < epsilon && std::abs(matrix.c4) < epsilon &&
228 std::abs(matrix.d1) < epsilon && std::abs(matrix.d2) < epsilon && std::abs(matrix.d3) < epsilon;
231 void ModelLoader::process_mesh_vertices(aiMesh *mesh, std::vector<Vertex> &vertices,
232 std::vector<
unsigned int> &indices) {
233 vertices.reserve(mesh->mNumVertices);
234 indices.reserve(mesh->mNumFaces * 3);
236 for (
unsigned int i = 0; i < mesh->mNumVertices; i++) {
237 Vertex &v = vertices.emplace_back();
239 const auto &pos = mesh->mVertices[i];
240 v.position = glm::vec3(pos.x, pos.y, pos.z);
243 if (mesh->HasNormals()) {
244 const auto &norm = mesh->mNormals[i];
245 v.normal = glm::vec3(norm.x, norm.y, norm.z);
247 v.normal = glm::vec3(0.0f, 1.0f, 0.0f);
250 if (mesh->HasTangentsAndBitangents()) {
251 const auto &bitan = mesh->mBitangents[i];
252 const auto &tan = mesh->mTangents[i];
253 v.tangent = glm::vec3(tan.x, tan.y, tan.z);
254 v.bitangent = glm::vec3(bitan.x, bitan.y, bitan.z);
256 v.tangent = glm::vec3(1.0f, 0.0f, 0.0f);
257 v.bitangent = glm::vec3(0.0f, 1.0f, 0.0f);
261 if (mesh->HasVertexColors(0)) {
262 const auto &col = mesh->mColors[0][i];
263 v.color = glm::vec3(col.r, col.g, col.b);
265 v.color = glm::vec3(1.0f);
269 if (mesh->HasTextureCoords(0)) {
270 const auto &tex = mesh->mTextureCoords[0][i];
271 v.texCoords = glm::vec2(tex.x, tex.y);
273 v.texCoords = glm::vec2(0.0f);
277 for (
unsigned int i = 0; i < mesh->mNumFaces; i++) {
278 const aiFace &face = mesh->mFaces[i];
279 for (
unsigned int j = 0; j < face.mNumIndices; j++) {
280 indices.push_back(face.mIndices[j]);
285 std::string
ModelLoader::create_mesh_key(aiMesh *mesh,
const std::string &filepath) {
286 return filepath +
"_mesh_" + std::string(mesh->mName.C_Str()) +
"_" + std::to_string(mesh->mMaterialIndex);
289 std::string
ModelLoader::create_material_key(
const aiMaterial *ai_material,
const std::string &filepath,
290 unsigned int material_index) {
291 aiString material_name;
292 ai_material->Get(AI_MATKEY_NAME, material_name);
293 return filepath +
"_mat_" + std::to_string(material_index) +
"_" +
294 (material_name.length > 0 ? material_name.C_Str() :
"unnamed");
297 std::shared_ptr<
Mesh>
ModelLoader::process_mesh(aiMesh *mesh,
const aiScene *scene,
const std::string &filepath) {
299 const std::string mesh_key = create_mesh_key(mesh, filepath);
300 const auto cached = mesh_cache.find(mesh_key);
301 if (cached != mesh_cache.end()) {
302 std::cout <<
"Using cached mesh: " << mesh_key << std::endl;
303 return cached->second;
306 std::vector<Vertex> vertices;
307 std::vector<
unsigned int> indices;
308 process_mesh_vertices(mesh, vertices, indices);
311 auto processed_mesh = std::make_shared<
Mesh>(vertices, indices);
314 mesh_cache[mesh_key] = processed_mesh;
316 return processed_mesh;
320 std::shared_ptr<
Material>
ModelLoader::create_material(
const aiMaterial *ai_material,
const aiScene *scene,
321 const std::string &filepath
324 aiString material_name;
325 ai_material->Get(AI_MATKEY_NAME, material_name);
326 const std::string name = material_name.length > 0 ? material_name.C_Str() :
"ImportedMaterial";
332 load_essential_material_properties(ai_material, *material);
335 load_material_textures(ai_material, *material, scene, filepath);
345 if (ai_material->Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS) {
346 material.set_diffuse_color(glm::vec3(color.r, color.g, color.b));
349 if (ai_material->Get(AI_MATKEY_COLOR_AMBIENT, color) == AI_SUCCESS) {
350 material.set_ambient_color(glm::vec3(color.r, color.g, color.b));
353 if (ai_material->Get(AI_MATKEY_COLOR_SPECULAR, color) == AI_SUCCESS) {
354 material.set_specular_color(glm::vec3(color.r, color.g, color.b));
357 if (ai_material->Get(AI_MATKEY_COLOR_EMISSIVE, color) == AI_SUCCESS) {
358 material.set_emissive_color(glm::vec3(color.r, color.g, color.b));
362 if (ai_material->Get(AI_MATKEY_OPACITY, value) == AI_SUCCESS) {
366 if (ai_material->Get(AI_MATKEY_SHININESS, value) == AI_SUCCESS) {
370 if (ai_material->Get(AI_MATKEY_METALLIC_FACTOR, value) == AI_SUCCESS) {
374 if (ai_material->Get(AI_MATKEY_ROUGHNESS_FACTOR, value) == AI_SUCCESS) {
379 std::vector<std::string>
ModelLoader::get_texture_search_paths(
const std::string &filepath) {
380 const std::filesystem::path model_dir = std::filesystem::path(filepath).parent_path();
383 model_dir.string() +
"/textures/",
384 model_dir.string() +
"/Textures/",
385 model_dir.string() +
"/texture/",
386 model_dir.string() +
"/materials/",
387 model_dir.string() +
"/source/",
388 model_dir.string() +
"/../textures/",
389 model_dir.string() +
"/",
395 void ModelLoader::load_material_textures(
const aiMaterial *ai_material,
Material &material,
const aiScene *scene,
396 const std::string &filepath) {
397 auto load_texture = [&](aiTextureType ai_type,
TextureType dcr_type) {
398 if (ai_material->GetTextureCount(ai_type) == 0)
return;
400 aiString texture_path;
401 if (ai_material->GetTexture(ai_type, 0, &texture_path) != AI_SUCCESS)
return;
403 const std::string path_str = texture_path.C_Str();
406 if (try_load_embedded_texture_unified(path_str, scene, dcr_type, material)) {
415 std::cerr <<
"Failed to load texture: " << path_str << std::endl;
419 load_texture(aiTextureType_DIFFUSE, TextureType::DIFFUSE);
420 load_texture(aiTextureType_NORMALS, TextureType::NORMAL);
421 load_texture(aiTextureType_SPECULAR, TextureType::SPECULAR);
422 load_texture(aiTextureType_METALNESS, TextureType::METALNESS);
423 load_texture(aiTextureType_DIFFUSE_ROUGHNESS, TextureType::ROUGHNESS);
424 load_texture(aiTextureType_AMBIENT_OCCLUSION, TextureType::AMBIENT_OCCLUSION);
425 load_texture(aiTextureType_EMISSIVE, TextureType::EMISSIVE);
428 bool ModelLoader::try_load_embedded_texture_unified(
const std::string &path_str,
429 const aiScene *scene,
432 if (path_str.empty() || path_str[0] !=
'*') {
437 const int texture_index = std::stoi(path_str.substr(1));
439 if (!scene || texture_index >=
static_cast<
int>(scene->mNumTextures)) {
440 std::cerr <<
"Embedded texture index " << texture_index <<
" is out of range!" << std::endl;
444 const aiTexture *embedded_texture = scene->mTextures[texture_index];
447 if (embedded_texture->mHeight == 0) {
448 std::string extension = embedded_texture->achFormatHint;
449 if (extension.empty() || extension.length() > 4) {
450 const unsigned char *data =
reinterpret_cast<
const unsigned char *>(embedded_texture->pcData);
451 if (data && embedded_texture->mWidth >= 2) {
452 if (data[0] == 0xFF && data[1] == 0xD8) extension =
"jpg";
453 else if (data[0] == 0x89 && data[1] == 0x50) extension =
"png";
454 else if (data[0] ==
'B' && data[1] ==
'M') extension =
"bmp";
455 else extension =
"bin";
461 const std::string temp_filename =
"temp_texture_" + std::to_string(texture_index) +
"." + extension;
463 std::ofstream temp_file(temp_filename, std::ios::binary);
465 std::cerr <<
"Failed to create temp file: " << temp_filename << std::endl;
469 temp_file.write(
reinterpret_cast<
const char *>(embedded_texture->pcData), embedded_texture->mWidth);
474 material.set_texture(temp_filename, type, 0);
477 std::filesystem::remove(temp_filename);
479 }
catch (
const std::exception &e) {
480 std::cerr <<
"Exception loading embedded texture: " << e.what() << std::endl;
483 std::filesystem::remove(temp_filename);
488 std::cout <<
"Uncompressed embedded texture found - needs direct GPU upload implementation" <<
492 }
catch (
const std::exception &e) {
493 std::cerr <<
"Error processing embedded texture: " << e.what() << std::endl;
499 const std::string &filepath,
502 const std::string texture_filename = std::filesystem::path(path_str).filename().string();
503 const std::filesystem::path model_dir = std::filesystem::path(filepath).parent_path();
505 const std::vector<std::string> possible_paths = {
507 model_dir.string() +
"/" + path_str,
508 model_dir.string() +
"/textures/" + texture_filename,
509 model_dir.string() +
"/Textures/" + texture_filename,
510 model_dir.string() +
"/texture/" + texture_filename,
511 model_dir.string() +
"/materials/" + texture_filename,
512 model_dir.string() +
"/source/" + texture_filename,
513 model_dir.string() +
"/../textures/" + texture_filename,
514 model_dir.string() +
"/" + texture_filename,
515 "assets/models/" + texture_filename,
516 "assets/textures/" + texture_filename
519 for (
const auto &test_path: possible_paths) {
520 if (std::filesystem::exists(test_path)) {
521 auto texture = load_cached_texture(test_path, type);
524 material.set_texture(texture, 0);
533 const auto it = texture_cache.find(path);
534 if (it != texture_cache.end()) {
535 std::cout <<
"Using cached texture: " << path << std::endl;
540 auto texture = std::make_shared<
Texture>(path, type);
541 if (texture->is_valid()) {
542 texture_cache[path] = texture;
545 }
catch (
const std::exception &e) {
546 std::cerr <<
"Failed to load texture " << path <<
": " << e.what() << std::endl;
553 if (str.empty())
return str;
554 std::string result = str;
555 result[0] = std::toupper(result[0]);
561 material_cache.clear();
562 texture_cache.clear();
563 std::cout <<
"ModelLoader caches cleared" << std::endl;
567 std::cout <<
"=== ModelLoader Cache Statistics ===" << std::endl;
568 std::cout <<
"Cached meshes: " << mesh_cache.size() << std::endl;
569 std::cout <<
"Cached materials: " << material_cache.size() << std::endl;
570 std::cout <<
"====================================" << std::endl;
#define MODEL_LOADER_DEBUG
static std::shared_ptr< Texture > load_cached_texture(const std::string &path, TextureType type)
static void print_cache_stats()
static std::unordered_map< std::string, std::shared_ptr< Texture > > texture_cache
static bool try_load_external_texture_unified(const std::string &path_str, const std::string &filepath, TextureType type, Material &material)
static std::unordered_map< std::string, std::shared_ptr< Mesh > > mesh_cache
static void clear_cache()
static std::string capitalize_first(const std::string &str)
static std::unordered_map< std::string, std::shared_ptr< Material > > material_cache
static EntityID load_model(Scene *scene, const std::filesystem::path &filepath, unsigned int import_flags=0)
TransformComponent * transform()
void set_roughness(float roughness)
void set_metallic(float metallic)
void set_opacity(float opacity)
void set_shininess(float shininess)
Manages a collection of entities and their hierarchical relationships.
void set_parent(EntityID child_id, EntityID parent_id)
Sets the parent of an entity.
void update_world_matrices()
Updates world transformation matrices for all entities Propagates transformations through the entity ...
Entity * get_entity(EntityID id)
Retrieves an entity by its ID.
EntityID create_entity(const std::string &name="GameObject")
Creates a new entity in the scene.
glm::mat4 aiMatrix4x4ToGlm(const aiMatrix4x4 &from)
static constexpr unsigned int HIGH_QUALITY