Monad Engine da02fba2
> Undercity Codex_
Loading...
Searching...
No Matches
ModelImporter.cpp
Go to the documentation of this file.
1#include "io/ModelImporter.h"
2
3#include <algorithm>
4#include <array>
5#include <cstring>
6#include <filesystem>
7#include <memory>
8#include <unordered_map>
9#include <vector>
10
11#include <assimp/Importer.hpp>
12#include <assimp/postprocess.h>
13#include <assimp/scene.h>
14
15#include <glm/gtc/quaternion.hpp>
16#include <glm/mat4x4.hpp>
17#include <glm/vec3.hpp>
18
19#include "Engine.h"
20#include "Log.h"
21#include "animation/IdleClip.h"
22#include "animation/Pose.h"
23#include "animation/Skeleton.h"
25#include "graphics/GraphicsAPI.h"
26#include "graphics/ShaderProgram.h"
27#include "graphics/Texture.h"
28#include "graphics/VertexLayout.h"
29#include "io/FileSystem.h"
30#include "render/Material.h"
31#include "render/Mesh.h"
32#include "scene/GameObject.h"
33#include "scene/Scene.h"
34#include "scene/components/AnimationComponent.h"
35#include "scene/components/MeshComponent.h"
37
38namespace mnd
39{
40
41namespace
42{
43
44constexpr u32 kMaxBonesPerVertex = 4;
45constexpr u32 kMaxBonesPerSkeleton = 128;
46constexpr f32 kDefaultTicksPerSecond = 25.0f;
47
48glm::mat4 ToGlm(const aiMatrix4x4 &m)
49{
50 return {
51 m.a1, m.b1, m.c1, m.d1,
52 m.a2, m.b2, m.c2, m.d2,
53 m.a3, m.b3, m.c3, m.d3,
54 m.a4, m.b4, m.c4, m.d4,
55 };
56}
57
58glm::vec3 ToGlm(const aiVector3D &v) { return {v.x, v.y, v.z}; }
59glm::quat ToGlm(const aiQuaternion &q) { return {q.w, q.x, q.y, q.z}; }
60
61std::shared_ptr<ShaderProgram> &SkinnedShaderCache()
62{
63 static std::shared_ptr<ShaderProgram> sp;
64 return sp;
65}
66
67std::shared_ptr<ShaderProgram> GetSkinnedShader()
68{
69 auto &cache = SkinnedShaderCache();
70 if (cache)
71 {
72 return cache;
73 }
75 auto vertexSource = fs.LoadAssetFileText("shaders/skinned.vert");
76 auto fragmentSource = fs.LoadAssetFileText("shaders/skinned.frag");
77 if (vertexSource.empty() || fragmentSource.empty())
78 {
79 LOG_ERROR("ModelImporter could not load skinned shader sources from assets/shaders/skinned.{vert,frag}");
80 return nullptr;
81 }
82 cache = Engine::GetInstance().GetGraphicsAPI().CreateShaderProgram(vertexSource, fragmentSource);
83 return cache;
84}
85
86/// Pre-pass: collect the set of nodes that participate in any skeleton.
87void CollectBoneNodes(const aiNode *node, const std::unordered_map<std::string, const aiBone *> &boneByName, std::unordered_map<const aiNode *, bool> &include)
88{
89 bool isBone = boneByName.find(node->mName.C_Str()) != boneByName.end();
90 bool anyChildIncluded = false;
91 for (u32 i = 0; i < node->mNumChildren; ++i)
92 {
93 CollectBoneNodes(node->mChildren[i], boneByName, include);
94 if (include[node->mChildren[i]])
95 {
96 anyChildIncluded = true;
97 }
98 }
99 include[node] = isBone || anyChildIncluded;
100}
101
102/// Build a flat skeleton in DFS order so parents always precede children.
103void BuildSkeletonRecursive(const aiNode *node,
104 i32 parentIndex,
105 const std::unordered_map<std::string, const aiBone *> &boneByName,
106 const std::unordered_map<const aiNode *, bool> &include,
107 Skeleton &skeleton,
108 std::unordered_map<const aiNode *, i32> &nodeToBone)
109{
110 auto incIt = include.find(node);
111 if (incIt == include.end() || !incIt->second)
112 {
113 // Subtree contains no bones; keep walking but don't promote nodes.
114 for (u32 i = 0; i < node->mNumChildren; ++i)
115 {
116 BuildSkeletonRecursive(node->mChildren[i], parentIndex, boneByName, include, skeleton, nodeToBone);
117 }
118 return;
119 }
120
121 Bone bone;
122 bone.name = node->mName.C_Str();
123 bone.parentIndex = parentIndex;
124 bone.localBind = ToGlm(node->mTransformation);
125
126 auto bIt = boneByName.find(bone.name);
127 if (bIt != boneByName.end())
128 {
129 bone.offsetMatrix = ToGlm(bIt->second->mOffsetMatrix);
130 }
131
132 skeleton.AddBone(bone);
133 i32 myIndex = static_cast<i32>(skeleton.Size()) - 1;
134 nodeToBone[node] = myIndex;
135
136 for (u32 i = 0; i < node->mNumChildren; ++i)
137 {
138 BuildSkeletonRecursive(node->mChildren[i], myIndex, boneByName, include, skeleton, nodeToBone);
139 }
140}
141
142struct VertexBoneSlot
143{
145 f32 weight = 0.0f;
146};
147
148void InsertBoneWeight(std::array<VertexBoneSlot, kMaxBonesPerVertex> &slots, i32 boneIndex, f32 weight)
149{
150 // Replace the smallest existing weight if the incoming one is bigger.
151 auto minIt = std::min_element(slots.begin(), slots.end(),
152 [](const VertexBoneSlot &a, const VertexBoneSlot &b) { return a.weight < b.weight; });
153 if (weight > minIt->weight)
154 {
155 minIt->boneIndex = boneIndex;
156 minIt->weight = weight;
157 }
158}
159
160void NormalizeBoneSlots(std::array<VertexBoneSlot, kMaxBonesPerVertex> &slots)
161{
162 f32 sum = 0.0f;
163 for (auto &s : slots)
164 {
165 sum += s.weight;
166 }
167 if (sum <= 0.0f)
168 {
169 return;
170 }
171 for (auto &s : slots)
172 {
173 s.weight /= sum;
174 }
175}
176
177std::shared_ptr<Texture> LoadDiffuseTexture(const aiMaterial *mat,
178 const aiScene *aiscene,
179 const std::filesystem::path &assetFolder)
180{
181 if (mat->GetTextureCount(aiTextureType_DIFFUSE) == 0 &&
182 mat->GetTextureCount(aiTextureType_BASE_COLOR) == 0)
183 {
184 return nullptr;
185 }
186
187 aiString aiPath;
188 if (mat->GetTexture(aiTextureType_BASE_COLOR, 0, &aiPath) != aiReturn_SUCCESS &&
189 mat->GetTexture(aiTextureType_DIFFUSE, 0, &aiPath) != aiReturn_SUCCESS)
190 {
191 return nullptr;
192 }
193
194 std::string raw = aiPath.C_Str();
195 if (raw.empty())
196 {
197 return nullptr;
198 }
199
200 // Embedded texture: GLB and FBX-with-media reference media as "*N" or by
201 // name; Assimp resolves both via GetEmbeddedTexture.
202 if (const aiTexture *embedded = aiscene->GetEmbeddedTexture(raw.c_str()))
203 {
204 if (embedded->mHeight == 0)
205 {
206 // Compressed bytes (PNG/JPG) — decode via stb.
207 const auto *bytes = reinterpret_cast<const unsigned char *>(embedded->pcData);
208 const int numBytes = static_cast<int>(embedded->mWidth);
209 return Texture::LoadFromMemory(bytes, numBytes);
210 }
211 // Uncompressed BGRA pixel array. Repack to RGBA so the GL upload sees
212 // the channels in the order the rest of the engine assumes.
213 const u32 pixelCount = embedded->mWidth * embedded->mHeight;
214 std::vector<unsigned char> rgba(pixelCount * 4);
215 for (u32 i = 0; i < pixelCount; ++i)
216 {
217 rgba[i * 4 + 0] = embedded->pcData[i].r;
218 rgba[i * 4 + 1] = embedded->pcData[i].g;
219 rgba[i * 4 + 2] = embedded->pcData[i].b;
220 rgba[i * 4 + 3] = embedded->pcData[i].a;
221 }
222 return std::make_shared<Texture>(static_cast<int>(embedded->mWidth),
223 static_cast<int>(embedded->mHeight),
224 4,
225 rgba.data());
226 }
227
228 // FBX exporters often emit absolute Windows paths. Reduce to filename
229 // and resolve against the model's folder.
230 std::filesystem::path candidate(raw);
231 auto filename = candidate.filename();
232 auto full = assetFolder / filename;
233
235}
236
237/// Pack one mesh into engine GPU resources. Optionally maps bone names → skeleton indices.
238struct ImportedMesh
239{
240 std::shared_ptr<Mesh> mesh;
241 std::shared_ptr<Material> material;
242 bool isSkinned = false;
243};
244
245ImportedMesh ImportMesh(const aiMesh *aimesh,
246 const aiScene *aiscene,
247 const std::filesystem::path &assetFolder,
248 const Skeleton &skeleton)
249{
250 ImportedMesh out;
251
252 const bool hasNormals = aimesh->HasNormals();
253 const bool hasUV = aimesh->HasTextureCoords(0);
254 const bool hasColor = aimesh->HasVertexColors(0);
255 const bool hasBones = aimesh->HasBones();
256
257 VertexLayout layout;
258 auto AddElem = [&](u32 index, u32 size, GLuint type)
259 {
260 VertexElement el;
261 el.index = index;
262 el.size = size;
263 el.type = type;
264 el.offset = layout.stride;
265 layout.elements.push_back(el);
266 layout.stride += size * (type == GL_INT ? sizeof(i32) : sizeof(f32));
267 };
268
269 AddElem(VertexElement::PositionIndex, 3, GL_FLOAT);
270 if (hasColor) AddElem(VertexElement::ColorIndex, 3, GL_FLOAT);
271 if (hasUV) AddElem(VertexElement::UVIndex, 2, GL_FLOAT);
272 if (hasNormals) AddElem(VertexElement::NormalIndex, 3, GL_FLOAT);
273 if (hasBones)
274 {
275 AddElem(VertexElement::BoneIndicesIndex, 4, GL_INT);
276 AddElem(VertexElement::BoneWeightsIndex, 4, GL_FLOAT);
277 }
278
279 // Pre-aggregate bone weights per vertex.
280 std::vector<std::array<VertexBoneSlot, kMaxBonesPerVertex>> boneSlots;
281 if (hasBones)
282 {
283 boneSlots.resize(aimesh->mNumVertices);
284 for (u32 b = 0; b < aimesh->mNumBones; ++b)
285 {
286 const aiBone *bone = aimesh->mBones[b];
287 i32 boneIx = skeleton.FindBoneIndex(bone->mName.C_Str());
288 if (boneIx < 0)
289 {
290 continue;
291 }
292 for (u32 w = 0; w < bone->mNumWeights; ++w)
293 {
294 const auto &vw = bone->mWeights[w];
295 if (vw.mWeight <= 0.0f) continue;
296 InsertBoneWeight(boneSlots[vw.mVertexId], boneIx, vw.mWeight);
297 }
298 }
299 for (auto &slots : boneSlots)
300 {
301 NormalizeBoneSlots(slots);
302 }
303 }
304
305 // Pack interleaved vertex buffer. Bone indices are packed as i32 bytes
306 // into f32 slots via memcpy; Mesh.cpp routes integer-typed elements
307 // through glVertexAttribIPointer so the shader sees ivec4.
308 const u32 floatsPerVertex = layout.stride / sizeof(f32);
309 std::vector<f32> vertices(static_cast<size_t>(aimesh->mNumVertices) * floatsPerVertex, 0.0f);
310
311 for (u32 v = 0; v < aimesh->mNumVertices; ++v)
312 {
313 f32 *vp = &vertices[v * floatsPerVertex];
314 for (const auto &el : layout.elements)
315 {
316 f32 *dst = reinterpret_cast<f32 *>(reinterpret_cast<u8 *>(vp) + el.offset);
317 switch (el.index)
318 {
320 {
321 auto p = aimesh->mVertices[v];
322 dst[0] = p.x; dst[1] = p.y; dst[2] = p.z;
323 break;
324 }
326 {
327 auto c = aimesh->mColors[0][v];
328 dst[0] = c.r; dst[1] = c.g; dst[2] = c.b;
329 break;
330 }
332 {
333 auto t = aimesh->mTextureCoords[0][v];
334 dst[0] = t.x; dst[1] = t.y;
335 break;
336 }
338 {
339 auto n = aimesh->mNormals[v];
340 dst[0] = n.x; dst[1] = n.y; dst[2] = n.z;
341 break;
342 }
344 {
345 i32 idx[4] = {
346 boneSlots[v][0].boneIndex, boneSlots[v][1].boneIndex,
347 boneSlots[v][2].boneIndex, boneSlots[v][3].boneIndex,
348 };
349 std::memcpy(dst, idx, sizeof(idx));
350 break;
351 }
353 {
354 dst[0] = boneSlots[v][0].weight;
355 dst[1] = boneSlots[v][1].weight;
356 dst[2] = boneSlots[v][2].weight;
357 dst[3] = boneSlots[v][3].weight;
358 break;
359 }
360 default: break;
361 }
362 }
363 }
364
365 std::vector<u32> indices;
366 indices.reserve(static_cast<size_t>(aimesh->mNumFaces) * 3);
367 for (u32 f = 0; f < aimesh->mNumFaces; ++f)
368 {
369 const auto &face = aimesh->mFaces[f];
370 if (face.mNumIndices != 3)
371 {
372 continue;
373 }
374 indices.push_back(face.mIndices[0]);
375 indices.push_back(face.mIndices[1]);
376 indices.push_back(face.mIndices[2]);
377 }
378
379 out.mesh = std::make_shared<Mesh>(layout, vertices, indices);
380 out.isSkinned = hasBones;
381
382 auto material = std::make_shared<Material>();
383 if (hasBones)
384 {
385 auto skinShader = GetSkinnedShader();
386 if (skinShader)
387 {
388 material->SetShaderProgram(skinShader);
389 } else
390 {
391 material->SetShaderProgram(Engine::GetInstance().GetGraphicsAPI().GetDefaultShaderProgram());
392 }
393 } else
394 {
395 material->SetShaderProgram(Engine::GetInstance().GetGraphicsAPI().GetDefaultShaderProgram());
396 }
397 material->SetParam("color", vec3(1.0f, 1.0f, 1.0f));
398
399 if (aimesh->mMaterialIndex < aiscene->mNumMaterials)
400 {
401 auto *aiMat = aiscene->mMaterials[aimesh->mMaterialIndex];
402 if (auto tex = LoadDiffuseTexture(aiMat, aiscene, assetFolder))
403 {
404 material->SetParam("baseColorTexture", tex);
405 }
406 }
407
408 out.material = material;
409 return out;
410}
411
412void BuildSceneHierarchy(const aiNode *node,
413 const aiScene *aiscene,
414 GameObject *parent,
415 Scene *scene,
416 const std::vector<ImportedMesh> &meshes,
417 const std::shared_ptr<Skeleton> &skeleton,
418 AnimationComponent *animOwnerComp,
419 std::vector<SkinnedMeshComponent *> &skinnedOut,
420 const std::filesystem::path & /*assetFolder*/)
421{
422 auto *go = scene->CreateObject(node->mName.C_Str(), parent);
423
424 // Decompose node->mTransformation into TRS and write to GameObject.
425 aiVector3D aiPos;
426 aiQuaternion aiRot;
427 aiVector3D aiScale;
428 node->mTransformation.Decompose(aiScale, aiRot, aiPos);
429 go->SetPosition(ToGlm(aiPos));
430 go->SetRotation(ToGlm(aiRot));
431 go->SetScale(ToGlm(aiScale));
432
433 for (u32 i = 0; i < node->mNumMeshes; ++i)
434 {
435 u32 meshIdx = node->mMeshes[i];
436 if (meshIdx >= meshes.size())
437 {
438 continue;
439 }
440 const auto &im = meshes[meshIdx];
441 if (im.isSkinned && skeleton)
442 {
443 auto *comp = new SkinnedMeshComponent(im.material, im.mesh, skeleton);
444 comp->SetPaletteSource(animOwnerComp);
445 go->AddComponent(comp);
446 skinnedOut.push_back(comp);
447 } else
448 {
449 go->AddComponent(new MeshComponent(im.material, im.mesh));
450 }
451 }
452
453 for (u32 i = 0; i < node->mNumChildren; ++i)
454 {
455 BuildSceneHierarchy(node->mChildren[i], aiscene, go, scene, meshes, skeleton, animOwnerComp, skinnedOut, /*assetFolder*/ {});
456 }
457}
458
459} // namespace
460
461GameObject *ModelImporter::Import(const std::string &path, Scene *scene)
462{
463 if (!scene)
464 {
465 LOG_ERROR("ModelImporter::Import called with null scene (path='%s')", path.c_str());
466 return nullptr;
467 }
468
469 auto fullPath = Engine::GetInstance().GetFileSystem().GetAssetsFolder() / path;
470 auto assetFolder = std::filesystem::path(fullPath).remove_filename();
471
472 Assimp::Importer imp;
473 const aiScene *aiscene = imp.ReadFile(
474 fullPath.string(),
475 aiProcess_Triangulate
476 | aiProcess_GenSmoothNormals
477 | aiProcess_LimitBoneWeights
478 | aiProcess_JoinIdenticalVertices
479 | aiProcess_ImproveCacheLocality
480 | aiProcess_GlobalScale
481 | aiProcess_PopulateArmatureData);
482
483 if (!aiscene || (aiscene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) != 0 || !aiscene->mRootNode)
484 {
485 LOG_ERROR("ModelImporter::Import failed for '%s': %s", path.c_str(), imp.GetErrorString());
486 return nullptr;
487 }
488
489 // -- Skeleton ----------------------------------------------------------
490 std::unordered_map<std::string, const aiBone *> boneByName;
491 for (u32 m = 0; m < aiscene->mNumMeshes; ++m)
492 {
493 const aiMesh *am = aiscene->mMeshes[m];
494 for (u32 b = 0; b < am->mNumBones; ++b)
495 {
496 boneByName[am->mBones[b]->mName.C_Str()] = am->mBones[b];
497 }
498 }
499
500 auto skeleton = std::make_shared<Skeleton>();
501 std::unordered_map<const aiNode *, i32> nodeToBone;
502 if (!boneByName.empty())
503 {
504 std::unordered_map<const aiNode *, bool> include;
505 CollectBoneNodes(aiscene->mRootNode, boneByName, include);
506 BuildSkeletonRecursive(aiscene->mRootNode, -1, boneByName, include, *skeleton, nodeToBone);
507
508 if (skeleton->Size() > kMaxBonesPerSkeleton)
509 {
510 LOG_ERROR("ModelImporter: '%s' has %zu bones, exceeds cap of %u (skinned shader limit)",
511 path.c_str(), skeleton->Size(), kMaxBonesPerSkeleton);
512 return nullptr;
513 }
514 LOG_INFO("ModelImporter: skeleton with %zu bones for '%s'", skeleton->Size(), path.c_str());
515 }
516
517 // -- Meshes ------------------------------------------------------------
518 std::vector<ImportedMesh> meshes;
519 meshes.reserve(aiscene->mNumMeshes);
520 for (u32 m = 0; m < aiscene->mNumMeshes; ++m)
521 {
522 meshes.push_back(ImportMesh(aiscene->mMeshes[m], aiscene, assetFolder, *skeleton));
523 }
524
525 // -- Animations --------------------------------------------------------
526 std::vector<std::shared_ptr<SkeletalAnimationClip>> skelClips;
527 std::vector<std::shared_ptr<AnimationClip>> nodeClips;
528
529 for (u32 a = 0; a < aiscene->mNumAnimations; ++a)
530 {
531 const aiAnimation *anim = aiscene->mAnimations[a];
532 const f32 tps = (anim->mTicksPerSecond > 0.0) ? static_cast<f32>(anim->mTicksPerSecond) : kDefaultTicksPerSecond;
533
534 auto skelClip = std::make_shared<SkeletalAnimationClip>();
535 skelClip->name = anim->mName.length > 0 ? anim->mName.C_Str() : "noname";
536 skelClip->duration = static_cast<f32>(anim->mDuration) / tps;
537
538 auto nodeClip = std::make_shared<AnimationClip>();
539 nodeClip->name = skelClip->name;
540 nodeClip->duration = skelClip->duration;
541
542 for (u32 c = 0; c < anim->mNumChannels; ++c)
543 {
544 const aiNodeAnim *ch = anim->mChannels[c];
545 std::string nodeName = ch->mNodeName.C_Str();
546 i32 boneIx = skeleton->FindBoneIndex(nodeName);
547
548 if (boneIx >= 0)
549 {
550 BoneTrack track;
551 track.boneIndex = boneIx;
552 track.positions.reserve(ch->mNumPositionKeys);
553 for (u32 k = 0; k < ch->mNumPositionKeys; ++k)
554 {
555 track.positions.push_back({static_cast<f32>(ch->mPositionKeys[k].mTime) / tps, ToGlm(ch->mPositionKeys[k].mValue)});
556 }
557 track.rotations.reserve(ch->mNumRotationKeys);
558 for (u32 k = 0; k < ch->mNumRotationKeys; ++k)
559 {
560 track.rotations.push_back({static_cast<f32>(ch->mRotationKeys[k].mTime) / tps, ToGlm(ch->mRotationKeys[k].mValue)});
561 }
562 track.scales.reserve(ch->mNumScalingKeys);
563 for (u32 k = 0; k < ch->mNumScalingKeys; ++k)
564 {
565 track.scales.push_back({static_cast<f32>(ch->mScalingKeys[k].mTime) / tps, ToGlm(ch->mScalingKeys[k].mValue)});
566 }
567 skelClip->tracks.push_back(std::move(track));
568 } else
569 {
570 // Non-bone channel — drives a plain GameObject by name.
571 TransformTrack track;
572 track.targetName = nodeName;
573 track.positions.reserve(ch->mNumPositionKeys);
574 for (u32 k = 0; k < ch->mNumPositionKeys; ++k)
575 {
576 track.positions.push_back({static_cast<f32>(ch->mPositionKeys[k].mTime) / tps, ToGlm(ch->mPositionKeys[k].mValue)});
577 }
578 track.rotations.reserve(ch->mNumRotationKeys);
579 for (u32 k = 0; k < ch->mNumRotationKeys; ++k)
580 {
581 track.rotations.push_back({static_cast<f32>(ch->mRotationKeys[k].mTime) / tps, ToGlm(ch->mRotationKeys[k].mValue)});
582 }
583 track.scales.reserve(ch->mNumScalingKeys);
584 for (u32 k = 0; k < ch->mNumScalingKeys; ++k)
585 {
586 track.scales.push_back({static_cast<f32>(ch->mScalingKeys[k].mTime) / tps, ToGlm(ch->mScalingKeys[k].mValue)});
587 }
588 nodeClip->tracks.push_back(std::move(track));
589 }
590 }
591
592 if (!skelClip->tracks.empty())
593 {
594 skelClips.push_back(skelClip);
595 }
596 if (!nodeClip->tracks.empty())
597 {
598 nodeClips.push_back(nodeClip);
599 }
600 }
601
602 // -- Hierarchy & components -------------------------------------------
603 auto *root = scene->CreateObject(std::filesystem::path(path).stem().string());
604
605 AnimationComponent *animComp = nullptr;
606 if (!skelClips.empty() || !nodeClips.empty() || skeleton->Size() > 0)
607 {
608 animComp = new AnimationComponent();
609 root->AddComponent(animComp);
610 animComp->SetSkeleton(skeleton);
611 for (auto &c : nodeClips)
612 {
613 animComp->RegisterClip(c->name, c);
614 }
615 for (auto &c : skelClips)
616 {
617 animComp->RegisterSkeletalClip(c->name, c);
618 }
619
620 // If the asset shipped rigged but with no skeletal clips, generate a
621 // procedural idle so the model is alive by default. Game code can
622 // still override by registering its own clips and calling Play().
623 if (skelClips.empty() && skeleton->Size() > 0)
624 {
625 auto idle = MakeBreathingIdleClip(*skeleton);
626 animComp->RegisterSkeletalClip(idle->name, idle);
627 LOG_INFO("ModelImporter: synthesized procedural 'idle' clip for '%s' (skeleton has %zu bones, no baked anims)",
628 path.c_str(), skeleton->Size());
629 }
630 }
631
632 std::vector<SkinnedMeshComponent *> skinned;
633 for (u32 i = 0; i < aiscene->mRootNode->mNumChildren; ++i)
634 {
635 BuildSceneHierarchy(aiscene->mRootNode->mChildren[i], aiscene, root, scene, meshes, skeleton, animComp, skinned, assetFolder);
636 }
637 // If the root has its own meshes, attach them to root directly.
638 for (u32 i = 0; i < aiscene->mRootNode->mNumMeshes; ++i)
639 {
640 u32 meshIdx = aiscene->mRootNode->mMeshes[i];
641 if (meshIdx >= meshes.size())
642 {
643 continue;
644 }
645 const auto &im = meshes[meshIdx];
646 if (im.isSkinned && skeleton)
647 {
648 auto *comp = new SkinnedMeshComponent(im.material, im.mesh, skeleton);
649 comp->SetPaletteSource(animComp);
650 root->AddComponent(comp);
651 skinned.push_back(comp);
652 } else
653 {
654 root->AddComponent(new MeshComponent(im.material, im.mesh));
655 }
656 }
657
658 LOG_INFO("ModelImporter: '%s' loaded — meshes=%u skinned=%zu skelClips=%zu nodeClips=%zu",
659 path.c_str(), aiscene->mNumMeshes, skinned.size(), skelClips.size(), nodeClips.size());
660
661 return root;
662}
663
664} // namespace mnd
unsigned int GLuint
Definition GLForward.h:11
Procedural skeletal idle clip — used when an asset ships rigged but without baked animations....
Console logging macros for all engine and game code.
#define LOG_INFO(fmt,...)
Definition Log.h:79
#define LOG_ERROR(fmt,...)
Definition Log.h:81
std::shared_ptr< Material > material
bool isSkinned
f32 weight
i32 boneIndex
std::shared_ptr< Mesh > mesh
Assimp-backed model loader. Handles FBX / glTF / OBJ.
Per-frame skeletal pose: local bone transforms + final GPU palette.
Per-bone TRS keyframe tracks driving a Skeleton.
Bone hierarchy + bind/offset matrices for GPU-skinned meshes.
Renderable component for GPU-skinned meshes driven by a Skeleton.
Drives transform animation on a hierarchy of GameObjects.
void SetSkeleton(const std::shared_ptr< Skeleton > &skeleton)
Bind a skeleton to the component so skeletal clips can be evaluated.
void RegisterSkeletalClip(const std::string &name, const std::shared_ptr< SkeletalAnimationClip > &clip)
Register a skeletal clip (per-bone TRS tracks) — used by SkinnedMeshComponent.
void RegisterClip(const std::string &name, const std::shared_ptr< AnimationClip > &clip)
Register a named clip so it can be started via Play().
GraphicsAPI & GetGraphicsAPI()
Returns the low-level OpenGL wrapper used for all draw calls.
Definition Engine.cpp:418
FileSystem & GetFileSystem()
Returns the file system helper for asset-relative path resolution.
Definition Engine.cpp:453
static Engine & GetInstance()
Returns the single Engine instance (created on first call).
Definition Engine.cpp:59
TextureManager & GetTextureManager()
Returns the TextureManager that caches loaded textures by path.
Definition Engine.cpp:433
std::string LoadAssetFileText(const std::string &relativePath)
Load a text file from the assets directory into a string.
std::filesystem::path GetAssetsFolder() const
Return the runtime assets directory.
Node in the scene graph: a named transform that owns components and children.
Definition GameObject.h:91
std::shared_ptr< ShaderProgram > CreateShaderProgram(const std::string &vertexSource, const std::string &fragmentSource)
Compile vertex + fragment GLSL source and link into a program.
Makes a GameObject renderable by submitting its mesh each frame.
static GameObject * Import(const std::string &path, Scene *scene)
Load path (relative to assets folder) and build a GameObject hierarchy under scene....
Container for the entire game object graph.
Definition Scene.h:59
GameObject * CreateObject(const std::string &name, GameObject *parent=nullptr)
Create a plain GameObject owned by this scene.
Definition Scene.cpp:132
std::shared_ptr< Texture > GetOrLoadTexture(const std::string &path)
Return a cached texture, loading it from disk on first request.
Definition Texture.cpp:58
static std::shared_ptr< Texture > LoadFromMemory(const unsigned char *data, int sizeBytes)
Decode an in-memory image (PNG/JPG/...) into a GPU texture.
Definition Texture.cpp:104
glm::vec3 vec3
3-component float vector (e.g. world position, RGB colour, normals).
Definition Types.h:51
float f32
32-bit IEEE float — the standard GL scalar type.
Definition Types.h:40
int32_t i32
Signed 32-bit integer — used for GL enums and sizes.
Definition Types.h:37
uint32_t u32
Unsigned 32-bit integer — also the GL index type.
Definition Types.h:32
uint8_t u8
Unsigned 8-bit integer.
Definition Types.h:30
std::shared_ptr< SkeletalAnimationClip > MakeBreathingIdleClip(const Skeleton &skeleton, f32 duration, f32 ampScale)
Build a looping breathing/sway clip targeting whichever of the canonical arm bones exist in skeleton....
Definition IdleClip.cpp:50
std::vector< KeyFrameVec3 > scales
std::vector< KeyFrameVec3 > positions
i32 boneIndex
Index into Skeleton::bones.
std::vector< KeyFrameQuat > rotations
Animation data for one GLTF node: position, rotation, and scale tracks.
std::vector< KeyFrameVec3 > scales
Sorted scale keyframes.
std::string targetName
Name of the child GameObject to animate.
std::vector< KeyFrameQuat > rotations
Sorted rotation keyframes (slerp-interpolated).
std::vector< KeyFrameVec3 > positions
Sorted position keyframes.
static constexpr int NormalIndex
layout(location = 3) vec3 aNormal
static constexpr int BoneWeightsIndex
layout(location = 5) vec4 aBoneWeights
static constexpr int BoneIndicesIndex
layout(location = 4) ivec4 aBoneIndices
static constexpr int ColorIndex
layout(location = 1) vec3 aColor
static constexpr int UVIndex
layout(location = 2) vec2 aUV
static constexpr int PositionIndex
Canonical attribute slot indices — must match the GLSL layout locations.