SleakEngine 0.1.0
C++23 multi-backend game engine
Loading...
Searching...
No Matches
ModelLoader.cpp
Go to the documentation of this file.
2
3#include <Core/GameObject.hpp>
9#include <Runtime/Texture.hpp>
10#include <Runtime/Skeleton.hpp>
14#include <Memory/RefPtr.hpp>
15#include <Core/Logger.hpp>
16
17#include <Runtime/MeshData.hpp>
19
20#include <assimp/Importer.hpp>
21#include <assimp/scene.h>
22#include <assimp/postprocess.h>
23
24#include <stb_image.h>
25
26#include <filesystem>
27
28namespace Sleak {
29
30Math::Matrix4 ModelLoader::ConvertMatrix(const void* aiMatPtr) {
31 const auto& m = *static_cast<const aiMatrix4x4*>(
32 static_cast<const void*>(aiMatPtr));
33 Math::Matrix4 result;
34 // Assimp stores column-convention (translation at col 3: a4, b4, c4).
35 // Engine uses row-convention (translation at row 3: (3,0), (3,1), (3,2)).
36 // Transpose during conversion so bone matrices match engine convention.
37 result(0, 0) = m.a1; result(0, 1) = m.b1; result(0, 2) = m.c1; result(0, 3) = m.d1;
38 result(1, 0) = m.a2; result(1, 1) = m.b2; result(1, 2) = m.c2; result(1, 3) = m.d2;
39 result(2, 0) = m.a3; result(2, 1) = m.b3; result(2, 2) = m.c3; result(2, 3) = m.d3;
40 result(3, 0) = m.a4; result(3, 1) = m.b4; result(3, 2) = m.c4; result(3, 3) = m.d4;
41 return result;
42}
43
44GameObject* ModelLoader::Load(const std::string& filePath,
45 const ModelLoadOptions& options) {
46 Assimp::Importer importer;
47
48 // First pass: read without PreTransformVertices to check for animations
49 unsigned int baseFlags = aiProcess_Triangulate
50 | aiProcess_GenSmoothNormals
51 | aiProcess_CalcTangentSpace
52 | aiProcess_JoinIdenticalVertices
53 | aiProcess_OptimizeMeshes;
54
55 if (options.flipUVs)
56 baseFlags |= aiProcess_FlipUVs;
57
58 const aiScene* scene = importer.ReadFile(filePath, baseFlags);
59
60 if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
61 SLEAK_ERROR("Assimp: Failed to load model '{}': {}", filePath,
62 importer.GetErrorString());
63 return nullptr;
64 }
65
66 bool hasAnimations = scene->HasAnimations();
67
68 SLEAK_INFO("Model loaded: {} ({} meshes, {} materials, {} textures, {} animations)",
69 filePath, scene->mNumMeshes, scene->mNumMaterials,
70 scene->mNumTextures, scene->mNumAnimations);
71
72 std::string directory = std::filesystem::path(filePath).parent_path().string();
73 auto* root = new GameObject(std::filesystem::path(filePath).stem().string());
74 TextureCache textureCache;
75 MaterialCache materialCache;
76
77 if (!hasAnimations) {
78 // Static model: reimport with PreTransformVertices for optimization
79 importer.FreeScene();
80 unsigned int staticFlags = baseFlags | aiProcess_PreTransformVertices;
81 scene = importer.ReadFile(filePath, staticFlags);
82
83 if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
84 SLEAK_ERROR("Assimp: Failed to reimport model '{}': {}", filePath,
85 importer.GetErrorString());
86 delete root;
87 return nullptr;
88 }
89
90 ProcessNode(scene->mRootNode, scene, root, directory, options,
91 textureCache, materialCache);
92 } else {
93 // Animated model: extract skeleton and animations
94 Skeleton* skeleton = ExtractSkeleton(scene);
95 std::vector<AnimationClip*> clips = ExtractAnimations(scene, skeleton);
96
97 SLEAK_INFO(" Skeleton: {} bones, {} animation clips",
98 skeleton->GetBoneCount(), clips.size());
99
100 ProcessNodeAnimated(scene->mRootNode, scene, root, directory, options,
101 textureCache, materialCache, skeleton, clips);
102 }
103
104 root->Initialize();
105 return root;
106}
107
108Skeleton* ModelLoader::ExtractSkeleton(const aiScene* scene) {
109 auto* skeleton = new Skeleton();
110
111 // First, collect all bones from all meshes
112 for (unsigned int m = 0; m < scene->mNumMeshes; ++m) {
113 aiMesh* mesh = scene->mMeshes[m];
114 if (!mesh->HasBones()) continue;
115
116 for (unsigned int b = 0; b < mesh->mNumBones; ++b) {
117 aiBone* bone = mesh->mBones[b];
118 std::string boneName = bone->mName.C_Str();
119
120 if (skeleton->FindBoneId(boneName) == -1) {
121 Bone newBone;
122 newBone.name = boneName;
123 newBone.offsetMatrix = ConvertMatrix(&bone->mOffsetMatrix);
124 skeleton->AddBone(newBone);
125 }
126 }
127 }
128
129 // Build parent-child hierarchy from scene node tree
130 BuildBoneHierarchy(scene->mRootNode, skeleton, -1);
131
132 // Build full node tree (includes non-bone nodes for correct hierarchy traversal)
133 BuildNodeTree(scene->mRootNode, skeleton);
134
135 // Compute global inverse transform from root node
136 aiMatrix4x4 rootTransform = scene->mRootNode->mTransformation;
137 rootTransform.Inverse();
138 skeleton->SetGlobalInverseTransform(ConvertMatrix(&rootTransform));
139
140 return skeleton;
141}
142
143int ModelLoader::BuildNodeTree(const aiNode* node, Skeleton* skeleton) {
144 NodeData nodeData;
145 nodeData.name = node->mName.C_Str();
146 nodeData.defaultTransform = ConvertMatrix(&node->mTransformation);
147 nodeData.boneIndex = skeleton->FindBoneId(nodeData.name);
148
149 int nodeIdx = skeleton->AddNode(nodeData);
150
151 for (unsigned int i = 0; i < node->mNumChildren; ++i) {
152 int childIdx = BuildNodeTree(node->mChildren[i], skeleton);
153 skeleton->AddNodeChild(nodeIdx, childIdx);
154 }
155
156 return nodeIdx;
157}
158
159void ModelLoader::BuildBoneHierarchy(const aiNode* node, Skeleton* skeleton,
160 int parentId) {
161 std::string nodeName = node->mName.C_Str();
162 int boneId = skeleton->FindBoneId(nodeName);
163
164 // If this node is a bone, set its parent
165 int nextParent = parentId;
166 if (boneId != -1) {
167 skeleton->SetBoneParent(boneId, parentId);
168 nextParent = boneId;
169 }
170
171 for (unsigned int i = 0; i < node->mNumChildren; ++i) {
172 BuildBoneHierarchy(node->mChildren[i], skeleton, nextParent);
173 }
174}
175
176std::vector<AnimationClip*> ModelLoader::ExtractAnimations(
177 const aiScene* scene, Skeleton* skeleton) {
178 std::vector<AnimationClip*> clips;
179
180 for (unsigned int i = 0; i < scene->mNumAnimations; ++i) {
181 aiAnimation* anim = scene->mAnimations[i];
182 auto* clip = new AnimationClip();
183
184 clip->name = anim->mName.C_Str();
185 if (clip->name.empty())
186 clip->name = "Animation_" + std::to_string(i);
187
188 clip->duration = static_cast<float>(anim->mDuration);
189 clip->ticksPerSecond = anim->mTicksPerSecond > 0
190 ? static_cast<float>(anim->mTicksPerSecond)
191 : 25.0f;
192
193 for (unsigned int c = 0; c < anim->mNumChannels; ++c) {
194 aiNodeAnim* nodeAnim = anim->mChannels[c];
195 AnimationChannel channel;
196
197 channel.boneName = nodeAnim->mNodeName.C_Str();
198 channel.boneId = skeleton->FindBoneId(channel.boneName);
199
200 // Position keyframes
201 for (unsigned int k = 0; k < nodeAnim->mNumPositionKeys; ++k) {
202 auto& key = nodeAnim->mPositionKeys[k];
203 channel.positionKeys.push_back({
204 static_cast<float>(key.mTime),
205 Math::Vector3D(key.mValue.x, key.mValue.y, key.mValue.z)
206 });
207 }
208
209 // Rotation keyframes
210 for (unsigned int k = 0; k < nodeAnim->mNumRotationKeys; ++k) {
211 auto& key = nodeAnim->mRotationKeys[k];
212 channel.rotationKeys.push_back({
213 static_cast<float>(key.mTime),
214 Math::Quaternion(key.mValue.w, key.mValue.x,
215 key.mValue.y, key.mValue.z)
216 });
217 }
218
219 // Scale keyframes
220 for (unsigned int k = 0; k < nodeAnim->mNumScalingKeys; ++k) {
221 auto& key = nodeAnim->mScalingKeys[k];
222 channel.scaleKeys.push_back({
223 static_cast<float>(key.mTime),
224 Math::Vector3D(key.mValue.x, key.mValue.y, key.mValue.z)
225 });
226 }
227
228 clip->channels.push_back(std::move(channel));
229 }
230
231 clip->BuildLookup();
232
233 SLEAK_INFO(" Animation '{}': {} channels, {:.1f}s",
234 clip->name, clip->channels.size(),
235 clip->GetDurationInSeconds());
236
237 clips.push_back(clip);
238 }
239
240 return clips;
241}
242
243void ModelLoader::ProcessNode(aiNode* node, const aiScene* scene,
244 GameObject* parent, const std::string& directory,
245 const ModelLoadOptions& options,
246 TextureCache& textureCache,
247 MaterialCache& materialCache) {
248 std::string nodeName = node->mName.C_Str();
249 if (nodeName.empty()) nodeName = "Node";
250
251 for (unsigned int i = 0; i < node->mNumMeshes; ++i) {
252 aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
253
254 std::string meshName = mesh->mName.C_Str();
255 if (meshName.empty()) meshName = nodeName + "_Mesh" + std::to_string(i);
256
257 auto* meshObj = new GameObject(meshName);
258
259 float sf = options.scaleFactor;
260 meshObj->AddComponent<TransformComponent>(
261 options.position,
262 options.rotation,
263 Math::Vector3D(sf, sf, sf));
264
265 if (mesh->mMaterialIndex < scene->mNumMaterials) {
266 auto& material = materialCache[uint64_t(mesh->mMaterialIndex) << 1];
267 if (!material)
268 material =
269 ProcessMaterial(scene->mMaterials[mesh->mMaterialIndex],
270 scene, directory, textureCache);
271 meshObj->AddComponent<MaterialComponent>(material);
272 }
273
274 MeshData meshData = ProcessMesh(mesh, options);
275 meshObj->AddComponent<ColliderComponent>(meshData, Physics::ColliderType::AABB);
276 meshObj->AddComponent<RigidbodyComponent>(BodyType::Static);
277 meshObj->AddComponent<MeshComponent>(std::move(meshData));
278
279 meshObj->SetParent(parent);
280 }
281
282 for (unsigned int i = 0; i < node->mNumChildren; ++i) {
283 ProcessNode(node->mChildren[i], scene, parent, directory, options,
284 textureCache, materialCache);
285 }
286}
287
288void ModelLoader::ProcessNodeAnimated(
289 aiNode* node, const aiScene* scene, GameObject* parent,
290 const std::string& directory, const ModelLoadOptions& options,
291 TextureCache& textureCache, MaterialCache& materialCache,
292 Skeleton* skeleton, std::vector<AnimationClip*>& clips) {
293 std::string nodeName = node->mName.C_Str();
294 if (nodeName.empty()) nodeName = "Node";
295
296 for (unsigned int i = 0; i < node->mNumMeshes; ++i) {
297 aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
298
299 std::string meshName = mesh->mName.C_Str();
300 if (meshName.empty()) meshName = nodeName + "_Mesh" + std::to_string(i);
301
302 auto* meshObj = new GameObject(meshName);
303
304 float sf = options.scaleFactor;
305 meshObj->AddComponent<TransformComponent>(
306 options.position,
307 options.rotation,
308 Math::Vector3D(sf, sf, sf));
309
310 // Use skinned shader for animated models
311 bool hasBones = mesh->HasBones();
312 if (mesh->mMaterialIndex < scene->mNumMaterials) {
313 uint64_t key =
314 (uint64_t(mesh->mMaterialIndex) << 1) | (hasBones ? 1u : 0u);
315 auto& material = materialCache[key];
316 if (!material)
317 material =
318 ProcessMaterial(scene->mMaterials[mesh->mMaterialIndex],
319 scene, directory, textureCache, hasBones);
320 meshObj->AddComponent<MaterialComponent>(material);
321 }
322
323 // Process mesh with bone data extraction
324 MeshData meshData = ProcessMesh(mesh, options, skeleton);
325 meshObj->AddComponent<ColliderComponent>(meshData, Physics::ColliderType::AABB);
326 meshObj->AddComponent<RigidbodyComponent>(BodyType::Static);
327 meshObj->AddComponent<MeshComponent>(std::move(meshData));
328
329 // Add AnimatorComponent if mesh has bones
330 if (hasBones && skeleton->GetBoneCount() > 0 && !clips.empty()) {
331 meshObj->AddComponent<AnimatorComponent>(skeleton, clips);
332 }
333
334 meshObj->SetParent(parent);
335 }
336
337 for (unsigned int i = 0; i < node->mNumChildren; ++i) {
338 ProcessNodeAnimated(node->mChildren[i], scene, parent, directory,
339 options, textureCache, materialCache, skeleton,
340 clips);
341 }
342}
343
344MeshData ModelLoader::ProcessMesh(aiMesh* mesh, const ModelLoadOptions& options,
345 Skeleton* skeleton) {
346 MeshData data;
347 float nSign = options.flipNormals ? -1.0f : 1.0f;
348
349 // Vertices
350 for (unsigned int i = 0; i < mesh->mNumVertices; ++i) {
351 Vertex v{};
352
353 v.px = mesh->mVertices[i].x;
354 v.py = mesh->mVertices[i].y;
355 v.pz = mesh->mVertices[i].z;
356
357 if (mesh->HasNormals()) {
358 v.nx = nSign * mesh->mNormals[i].x;
359 v.ny = nSign * mesh->mNormals[i].y;
360 v.nz = nSign * mesh->mNormals[i].z;
361 }
362
363 if (mesh->HasTangentsAndBitangents()) {
364 v.tx = mesh->mTangents[i].x;
365 v.ty = mesh->mTangents[i].y;
366 v.tz = mesh->mTangents[i].z;
367 v.tw = 1.0f;
368 }
369
370 if (mesh->HasVertexColors(0)) {
371 v.r = mesh->mColors[0][i].r;
372 v.g = mesh->mColors[0][i].g;
373 v.b = mesh->mColors[0][i].b;
374 v.a = mesh->mColors[0][i].a;
375 } else {
376 v.r = 1.0f; v.g = 1.0f; v.b = 1.0f; v.a = 1.0f;
377 }
378
379 if (mesh->HasTextureCoords(0)) {
380 v.u = mesh->mTextureCoords[0][i].x;
381 v.v = mesh->mTextureCoords[0][i].y;
382 }
383
384 // Bone data defaults already set (-1 IDs, 0 weights)
385 data.vertices.AddVertex(v);
386 }
387
388 // Extract bone weights for vertices
389 if (mesh->HasBones() && skeleton) {
390 for (unsigned int b = 0; b < mesh->mNumBones; ++b) {
391 aiBone* bone = mesh->mBones[b];
392 std::string boneName = bone->mName.C_Str();
393 int boneId = skeleton->FindBoneId(boneName);
394
395 if (boneId == -1) {
396 // Bone not in skeleton yet — add it
397 Bone newBone;
398 newBone.name = boneName;
399 newBone.offsetMatrix = ConvertMatrix(&bone->mOffsetMatrix);
400 boneId = skeleton->AddBone(newBone);
401 }
402
403 for (unsigned int w = 0; w < bone->mNumWeights; ++w) {
404 unsigned int vertexId = bone->mWeights[w].mVertexId;
405 float weight = bone->mWeights[w].mWeight;
406
407 if (vertexId >= data.vertices.GetSize()) continue;
408
409 Vertex* vPtr = data.vertices.GetMutableData() + vertexId;
410
411 // Find first empty bone slot
412 for (int s = 0; s < MAX_BONE_INFLUENCE; ++s) {
413 if (vPtr->boneIDs[s] < 0) {
414 vPtr->boneIDs[s] = boneId;
415 vPtr->boneWeights[s] = weight;
416 break;
417 }
418 }
419 }
420 }
421 }
422
423 // Indices
424 for (unsigned int i = 0; i < mesh->mNumFaces; ++i) {
425 const aiFace& face = mesh->mFaces[i];
426 if (options.flipWinding && face.mNumIndices == 3) {
427 data.indices.add(static_cast<IndexType>(face.mIndices[0]));
428 data.indices.add(static_cast<IndexType>(face.mIndices[2]));
429 data.indices.add(static_cast<IndexType>(face.mIndices[1]));
430 } else {
431 for (unsigned int j = 0; j < face.mNumIndices; ++j) {
432 data.indices.add(static_cast<IndexType>(face.mIndices[j]));
433 }
434 }
435 }
436
437 SLEAK_INFO(" Mesh '{}': {} vertices, {} indices{}",
438 mesh->mName.C_Str(), mesh->mNumVertices,
439 data.indices.GetSize(),
440 mesh->HasBones() ? " (skinned)" : "");
441
442 return data;
443}
444
445RefPtr<Material> ModelLoader::ProcessMaterial(aiMaterial* mat,
446 const aiScene* scene,
447 const std::string& directory,
448 TextureCache& textureCache,
449 bool skinned) {
450 auto* material = new Material();
451 material->SetShader(skinned ? "assets/shaders/skinned_shader.hlsl"
452 : "assets/shaders/default_shader.hlsl");
453
454 aiColor4D color;
455 if (mat->Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS) {
456 material->SetDiffuseColor(color.r, color.g, color.b, color.a);
457 }
458 if (mat->Get(AI_MATKEY_COLOR_SPECULAR, color) == AI_SUCCESS) {
459 material->SetSpecularColor(Math::Color(
460 static_cast<uint8_t>(color.r * 255),
461 static_cast<uint8_t>(color.g * 255),
462 static_cast<uint8_t>(color.b * 255)));
463 }
464 if (mat->Get(AI_MATKEY_COLOR_EMISSIVE, color) == AI_SUCCESS) {
465 material->SetEmissiveColor(color.r, color.g, color.b);
466 }
467
468 float val;
469 if (mat->Get(AI_MATKEY_SHININESS, val) == AI_SUCCESS)
470 material->SetShininess(val);
471 if (mat->Get(AI_MATKEY_OPACITY, val) == AI_SUCCESS)
472 material->SetOpacity(val);
473 if (mat->Get(AI_MATKEY_METALLIC_FACTOR, val) == AI_SUCCESS)
474 material->SetMetallic(val);
475 if (mat->Get(AI_MATKEY_ROUGHNESS_FACTOR, val) == AI_SUCCESS)
476 material->SetRoughness(val);
477
478 RefPtr<Texture> tex;
479
480 tex = LoadMaterialTexture(mat, aiTextureType_DIFFUSE, scene, directory, textureCache);
481 if (!tex) tex = LoadMaterialTexture(mat, aiTextureType_BASE_COLOR, scene, directory, textureCache);
482 if (tex) material->SetDiffuseTexture(tex);
483
484 tex = LoadMaterialTexture(mat, aiTextureType_NORMALS, scene, directory, textureCache);
485 if (tex) material->SetNormalTexture(tex);
486
487 tex = LoadMaterialTexture(mat, aiTextureType_SPECULAR, scene, directory, textureCache);
488 if (tex) material->SetSpecularTexture(tex);
489
490 tex = LoadMaterialTexture(mat, aiTextureType_EMISSIVE, scene, directory, textureCache);
491 if (tex) material->SetEmissiveTexture(tex);
492
493 tex = LoadMaterialTexture(mat, aiTextureType_METALNESS, scene, directory, textureCache);
494 if (tex) material->SetMetallicTexture(tex);
495
496 tex = LoadMaterialTexture(mat, aiTextureType_DIFFUSE_ROUGHNESS, scene, directory, textureCache);
497 if (tex) material->SetRoughnessTexture(tex);
498
499 return RefPtr<Material>(material);
500}
501
502RefPtr<::Sleak::Texture> ModelLoader::LoadMaterialTexture(
503 aiMaterial* mat, int type, const aiScene* scene,
504 const std::string& directory, TextureCache& textureCache) {
505 auto texType = static_cast<aiTextureType>(type);
506
507 if (mat->GetTextureCount(texType) == 0) return RefPtr<Texture>();
508
509 aiString aiPath;
510 mat->GetTexture(texType, 0, &aiPath);
511 std::string texPath = aiPath.C_Str();
512
513 // Check cache first (keyed by texture path + type to avoid conflicts)
514 std::string cacheKey = texPath + ":" + std::to_string(type);
515 auto it = textureCache.find(cacheKey);
516 if (it != textureCache.end())
517 return it->second;
518
519 // Check for embedded texture
520 const aiTexture* embedded = scene->GetEmbeddedTexture(texPath.c_str());
521 if (embedded) {
522 ::Sleak::Texture* tex = nullptr;
523 if (embedded->mHeight == 0) {
524 // Compressed embedded texture (PNG/JPG stored as blob)
525 int w, h, channels;
526 unsigned char* pixels = stbi_load_from_memory(
527 reinterpret_cast<const unsigned char*>(embedded->pcData),
528 static_cast<int>(embedded->mWidth),
529 &w, &h, &channels, 4); // Force RGBA
530
531 if (!pixels) {
532 SLEAK_ERROR(" Failed to decode embedded texture: {}", texPath);
533 return RefPtr<Texture>();
534 }
535
537 pixels, static_cast<uint32_t>(w), static_cast<uint32_t>(h),
539
540 stbi_image_free(pixels);
541
542 if (tex) {
543 SLEAK_INFO(" Loaded embedded texture: {} ({}x{})", texPath, w, h);
544 }
545 } else {
546 // Raw uncompressed ARGB8888 data
547 uint32_t w = embedded->mWidth;
548 uint32_t h = embedded->mHeight;
549
550 // Convert ARGB to RGBA
551 std::vector<uint8_t> rgba(w * h * 4);
552 const auto* src = reinterpret_cast<const uint8_t*>(embedded->pcData);
553 for (uint32_t i = 0; i < w * h; ++i) {
554 rgba[i * 4 + 0] = src[i * 4 + 1]; // R
555 rgba[i * 4 + 1] = src[i * 4 + 2]; // G
556 rgba[i * 4 + 2] = src[i * 4 + 3]; // B
557 rgba[i * 4 + 3] = src[i * 4 + 0]; // A
558 }
559
561 rgba.data(), w, h, TextureFormat::RGBA8);
562
563 if (tex) {
564 SLEAK_INFO(" Loaded embedded raw texture: {} ({}x{})", texPath, w, h);
565 }
566 }
567
568 RefPtr<Texture> owned(tex);
569 if (owned) textureCache[cacheKey] = owned;
570 return owned;
571 }
572
573 // External texture file
574 std::string fullPath = directory + "/" + texPath;
575
576 // Normalize path separators
577 std::replace(fullPath.begin(), fullPath.end(), '\\', '/');
578
579 if (!std::filesystem::exists(fullPath)) {
580 SLEAK_WARN(" Texture file not found: {}", fullPath);
581 return RefPtr<Texture>();
582 }
583
584 RefPtr<Texture> tex(RenderEngine::ResourceManager::CreateTexture(fullPath));
585 if (tex) {
586 SLEAK_INFO(" Loaded texture: {}", fullPath);
587 textureCache[cacheKey] = tex;
588 }
589 return tex;
590}
591
592std::vector<AnimationClip*> ModelLoader::LoadAnimationsOnly(
593 const std::string& filePath, Skeleton* skeleton) {
594 std::vector<AnimationClip*> result;
595
596 if (!skeleton) {
597 SLEAK_ERROR("LoadAnimationsOnly: null skeleton");
598 return result;
599 }
600
601 Assimp::Importer importer;
602 unsigned int flags = aiProcess_Triangulate;
603
604 const aiScene* scene = importer.ReadFile(filePath, flags);
605 if (!scene || !scene->HasAnimations()) {
606 SLEAK_ERROR("LoadAnimationsOnly: failed to load or no animations in '{}'",
607 filePath);
608 return result;
609 }
610
611 result = ExtractAnimations(scene, skeleton);
612
613 // Rename clips to filename stem if they have generic names
614 std::string stem = std::filesystem::path(filePath).stem().string();
615 for (auto* clip : result) {
616 if (clip->name.empty() || clip->name.find("Armature") != std::string::npos
617 || clip->name.find("mixamo") != std::string::npos
618 || clip->name.find("Animation") != std::string::npos) {
619 clip->name = stem;
620 clip->BuildLookup();
621 }
622 }
623
624 SLEAK_INFO("LoadAnimationsOnly: '{}' -> {} clips", filePath, result.size());
625 return result;
626}
627
628} // namespace Sleak
#define SLEAK_ERROR(...)
Definition Logger.hpp:22
#define SLEAK_INFO(...)
Definition Logger.hpp:20
#define SLEAK_WARN(...)
Definition Logger.hpp:21
GameObject(const std::string &name="GameObject")
static GameObject * Load(const std::string &filePath, const ModelLoadOptions &options={})
Imports a model file and returns the root of the resulting GameObject hierarchy.
static std::vector< AnimationClip * > LoadAnimationsOnly(const std::string &filePath, Skeleton *skeleton)
Load only animations from an FBX, reusing an existing skeleton.
friend class RefPtr
Definition RefPtr.hpp:65
static Sleak::Texture * CreateTexture(const std::string &TexturePath)
Loads a texture via the currently registered backend factory.
static Sleak::Texture * CreateTextureFromMemory(const void *data, uint32_t width, uint32_t height, TextureFormat format, uint32_t maxMipLevels=0)
Creates a texture from raw pixel data via the currently registered backend factory.
int GetBoneCount() const
Definition Skeleton.hpp:40
Matrix< float, 4, 4 > Matrix4
Definition Matrix.hpp:413
Root namespace for everything the engine exposes.
Definition Camera.hpp:10
std::unordered_map< std::string, RefPtr<::Sleak::Texture > > TextureCache
Per-load texture cache to avoid loading the same texture file multiple times.
uint32_t IndexType
Definition MeshData.hpp:11
std::unordered_map< uint64_t, RefPtr< Material > > MaterialCache
Per-load cache so meshes sharing an Assimp material share one Material.
std::string name
Definition Skeleton.hpp:18
Math::Matrix4 offsetMatrix
Definition Skeleton.hpp:21