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