8#include <unordered_map>
11#include <assimp/Importer.hpp>
12#include <assimp/postprocess.h>
13#include <assimp/scene.h>
15#include <glm/gtc/quaternion.hpp>
16#include <glm/mat4x4.hpp>
17#include <glm/vec3.hpp>
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"
44constexpr u32 kMaxBonesPerVertex = 4;
45constexpr u32 kMaxBonesPerSkeleton = 128;
46constexpr f32 kDefaultTicksPerSecond = 25.0f;
48glm::mat4 ToGlm(
const aiMatrix4x4 &m)
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,
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}; }
61std::shared_ptr<ShaderProgram> &SkinnedShaderCache()
63 static std::shared_ptr<ShaderProgram> sp;
67std::shared_ptr<ShaderProgram> GetSkinnedShader()
69 auto &cache = SkinnedShaderCache();
76 auto fragmentSource = fs.LoadAssetFileText(
"shaders/skinned.frag");
77 if (vertexSource.empty() || fragmentSource.empty())
79 LOG_ERROR(
"ModelImporter could not load skinned shader sources from assets/shaders/skinned.{vert,frag}");
87void CollectBoneNodes(
const aiNode *node,
const std::unordered_map<std::string, const aiBone *> &boneByName, std::unordered_map<const aiNode *, bool> &include)
89 bool isBone = boneByName.find(node->mName.C_Str()) != boneByName.end();
90 bool anyChildIncluded =
false;
91 for (
u32 i = 0; i < node->mNumChildren; ++i)
93 CollectBoneNodes(node->mChildren[i], boneByName, include);
94 if (include[node->mChildren[i]])
96 anyChildIncluded =
true;
99 include[node] = isBone || anyChildIncluded;
103void BuildSkeletonRecursive(
const aiNode *node,
105 const std::unordered_map<std::string, const aiBone *> &boneByName,
106 const std::unordered_map<const aiNode *, bool> &include,
108 std::unordered_map<const aiNode *, i32> &nodeToBone)
110 auto incIt = include.find(node);
111 if (incIt == include.end() || !incIt->second)
114 for (
u32 i = 0; i < node->mNumChildren; ++i)
116 BuildSkeletonRecursive(node->mChildren[i], parentIndex, boneByName, include, skeleton, nodeToBone);
122 bone.name = node->mName.C_Str();
123 bone.parentIndex = parentIndex;
124 bone.localBind = ToGlm(node->mTransformation);
126 auto bIt = boneByName.find(bone.name);
127 if (bIt != boneByName.end())
129 bone.offsetMatrix = ToGlm(bIt->second->mOffsetMatrix);
132 skeleton.AddBone(bone);
133 i32 myIndex =
static_cast<i32>(skeleton.Size()) - 1;
134 nodeToBone[node] = myIndex;
136 for (
u32 i = 0; i < node->mNumChildren; ++i)
138 BuildSkeletonRecursive(node->mChildren[i], myIndex, boneByName, include, skeleton, nodeToBone);
148void InsertBoneWeight(std::array<VertexBoneSlot, kMaxBonesPerVertex> &slots,
i32 boneIndex,
f32 weight)
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)
160void NormalizeBoneSlots(std::array<VertexBoneSlot, kMaxBonesPerVertex> &slots)
163 for (
auto &s : slots)
171 for (
auto &s : slots)
177std::shared_ptr<Texture> LoadDiffuseTexture(
const aiMaterial *mat,
178 const aiScene *aiscene,
179 const std::filesystem::path &assetFolder)
181 if (mat->GetTextureCount(aiTextureType_DIFFUSE) == 0 &&
182 mat->GetTextureCount(aiTextureType_BASE_COLOR) == 0)
188 if (mat->GetTexture(aiTextureType_BASE_COLOR, 0, &aiPath) != aiReturn_SUCCESS &&
189 mat->GetTexture(aiTextureType_DIFFUSE, 0, &aiPath) != aiReturn_SUCCESS)
194 std::string raw = aiPath.C_Str();
202 if (
const aiTexture *embedded = aiscene->GetEmbeddedTexture(raw.c_str()))
204 if (embedded->mHeight == 0)
207 const auto *bytes =
reinterpret_cast<const unsigned char *
>(embedded->pcData);
208 const int numBytes =
static_cast<int>(embedded->mWidth);
213 const u32 pixelCount = embedded->mWidth * embedded->mHeight;
214 std::vector<unsigned char> rgba(pixelCount * 4);
215 for (
u32 i = 0; i < pixelCount; ++i)
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;
222 return std::make_shared<Texture>(
static_cast<int>(embedded->mWidth),
223 static_cast<int>(embedded->mHeight),
230 std::filesystem::path candidate(raw);
231 auto filename = candidate.filename();
232 auto full = assetFolder / filename;
245ImportedMesh ImportMesh(
const aiMesh *aimesh,
246 const aiScene *aiscene,
247 const std::filesystem::path &assetFolder,
248 const Skeleton &skeleton)
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();
264 el.offset = layout.stride;
265 layout.elements.push_back(el);
266 layout.stride += size * (type == GL_INT ?
sizeof(
i32) :
sizeof(
f32));
280 std::vector<std::array<VertexBoneSlot, kMaxBonesPerVertex>> boneSlots;
283 boneSlots.resize(aimesh->mNumVertices);
284 for (
u32 b = 0; b < aimesh->mNumBones; ++b)
286 const aiBone *bone = aimesh->mBones[b];
287 i32 boneIx = skeleton.FindBoneIndex(bone->mName.C_Str());
292 for (
u32 w = 0; w < bone->mNumWeights; ++w)
294 const auto &vw = bone->mWeights[w];
295 if (vw.mWeight <= 0.0f)
continue;
296 InsertBoneWeight(boneSlots[vw.mVertexId], boneIx, vw.mWeight);
299 for (
auto &slots : boneSlots)
301 NormalizeBoneSlots(slots);
308 const u32 floatsPerVertex = layout.stride /
sizeof(
f32);
309 std::vector<f32> vertices(
static_cast<size_t>(aimesh->mNumVertices) * floatsPerVertex, 0.0f);
311 for (
u32 v = 0; v < aimesh->mNumVertices; ++v)
313 f32 *vp = &vertices[v * floatsPerVertex];
314 for (
const auto &el : layout.elements)
316 f32 *dst =
reinterpret_cast<f32 *
>(
reinterpret_cast<u8 *
>(vp) + el.offset);
321 auto p = aimesh->mVertices[v];
322 dst[0] = p.x; dst[1] = p.y; dst[2] = p.z;
327 auto c = aimesh->mColors[0][v];
328 dst[0] = c.r; dst[1] = c.g; dst[2] = c.b;
333 auto t = aimesh->mTextureCoords[0][v];
334 dst[0] = t.x; dst[1] = t.y;
339 auto n = aimesh->mNormals[v];
340 dst[0] = n.x; dst[1] = n.y; dst[2] = n.z;
346 boneSlots[v][0].boneIndex, boneSlots[v][1].boneIndex,
347 boneSlots[v][2].boneIndex, boneSlots[v][3].boneIndex,
349 std::memcpy(dst, idx,
sizeof(idx));
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;
365 std::vector<u32> indices;
366 indices.reserve(
static_cast<size_t>(aimesh->mNumFaces) * 3);
367 for (
u32 f = 0; f < aimesh->mNumFaces; ++f)
369 const auto &face = aimesh->mFaces[f];
370 if (face.mNumIndices != 3)
374 indices.push_back(face.mIndices[0]);
375 indices.push_back(face.mIndices[1]);
376 indices.push_back(face.mIndices[2]);
379 out.mesh = std::make_shared<Mesh>(layout, vertices, indices);
380 out.isSkinned = hasBones;
382 auto material = std::make_shared<Material>();
385 auto skinShader = GetSkinnedShader();
388 material->SetShaderProgram(skinShader);
399 if (aimesh->mMaterialIndex < aiscene->mNumMaterials)
401 auto *aiMat = aiscene->mMaterials[aimesh->mMaterialIndex];
402 if (
auto tex = LoadDiffuseTexture(aiMat, aiscene, assetFolder))
404 material->SetParam(
"baseColorTexture", tex);
412void BuildSceneHierarchy(
const aiNode *node,
413 const aiScene *aiscene,
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 & )
422 auto *go = scene->CreateObject(node->mName.C_Str(), parent);
428 node->mTransformation.Decompose(aiScale, aiRot, aiPos);
429 go->SetPosition(ToGlm(aiPos));
430 go->SetRotation(ToGlm(aiRot));
431 go->SetScale(ToGlm(aiScale));
433 for (
u32 i = 0; i < node->mNumMeshes; ++i)
435 u32 meshIdx = node->mMeshes[i];
436 if (meshIdx >= meshes.size())
440 const auto &im = meshes[meshIdx];
441 if (im.isSkinned && skeleton)
443 auto *comp =
new SkinnedMeshComponent(im.material, im.mesh, skeleton);
444 comp->SetPaletteSource(animOwnerComp);
445 go->AddComponent(comp);
446 skinnedOut.push_back(comp);
449 go->AddComponent(
new MeshComponent(im.material, im.mesh));
453 for (
u32 i = 0; i < node->mNumChildren; ++i)
455 BuildSceneHierarchy(node->mChildren[i], aiscene, go, scene, meshes, skeleton, animOwnerComp, skinnedOut, {});
465 LOG_ERROR(
"ModelImporter::Import called with null scene (path='%s')", path.c_str());
470 auto assetFolder = std::filesystem::path(fullPath).remove_filename();
472 Assimp::Importer imp;
473 const aiScene *aiscene = imp.ReadFile(
475 aiProcess_Triangulate
476 | aiProcess_GenSmoothNormals
477 | aiProcess_LimitBoneWeights
478 | aiProcess_JoinIdenticalVertices
479 | aiProcess_ImproveCacheLocality
480 | aiProcess_GlobalScale
481 | aiProcess_PopulateArmatureData);
483 if (!aiscene || (aiscene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) != 0 || !aiscene->mRootNode)
485 LOG_ERROR(
"ModelImporter::Import failed for '%s': %s", path.c_str(), imp.GetErrorString());
490 std::unordered_map<std::string, const aiBone *> boneByName;
491 for (
u32 m = 0; m < aiscene->mNumMeshes; ++m)
493 const aiMesh *am = aiscene->mMeshes[m];
494 for (
u32 b = 0; b < am->mNumBones; ++b)
496 boneByName[am->mBones[b]->mName.C_Str()] = am->mBones[b];
500 auto skeleton = std::make_shared<Skeleton>();
501 std::unordered_map<const aiNode *, i32> nodeToBone;
502 if (!boneByName.empty())
504 std::unordered_map<const aiNode *, bool> include;
505 CollectBoneNodes(aiscene->mRootNode, boneByName, include);
506 BuildSkeletonRecursive(aiscene->mRootNode, -1, boneByName, include, *skeleton, nodeToBone);
508 if (skeleton->Size() > kMaxBonesPerSkeleton)
510 LOG_ERROR(
"ModelImporter: '%s' has %zu bones, exceeds cap of %u (skinned shader limit)",
511 path.c_str(), skeleton->Size(), kMaxBonesPerSkeleton);
514 LOG_INFO(
"ModelImporter: skeleton with %zu bones for '%s'", skeleton->Size(), path.c_str());
518 std::vector<ImportedMesh> meshes;
519 meshes.reserve(aiscene->mNumMeshes);
520 for (
u32 m = 0; m < aiscene->mNumMeshes; ++m)
522 meshes.push_back(ImportMesh(aiscene->mMeshes[m], aiscene, assetFolder, *skeleton));
526 std::vector<std::shared_ptr<SkeletalAnimationClip>> skelClips;
527 std::vector<std::shared_ptr<AnimationClip>> nodeClips;
529 for (
u32 a = 0; a < aiscene->mNumAnimations; ++a)
531 const aiAnimation *anim = aiscene->mAnimations[a];
532 const f32 tps = (anim->mTicksPerSecond > 0.0) ?
static_cast<f32>(anim->mTicksPerSecond) : kDefaultTicksPerSecond;
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;
538 auto nodeClip = std::make_shared<AnimationClip>();
539 nodeClip->name = skelClip->name;
540 nodeClip->duration = skelClip->duration;
542 for (
u32 c = 0; c < anim->mNumChannels; ++c)
544 const aiNodeAnim *ch = anim->mChannels[c];
545 std::string nodeName = ch->mNodeName.C_Str();
546 i32 boneIx = skeleton->FindBoneIndex(nodeName);
552 track.
positions.reserve(ch->mNumPositionKeys);
553 for (
u32 k = 0; k < ch->mNumPositionKeys; ++k)
555 track.
positions.push_back({
static_cast<f32>(ch->mPositionKeys[k].mTime) / tps, ToGlm(ch->mPositionKeys[k].mValue)});
557 track.
rotations.reserve(ch->mNumRotationKeys);
558 for (
u32 k = 0; k < ch->mNumRotationKeys; ++k)
560 track.
rotations.push_back({
static_cast<f32>(ch->mRotationKeys[k].mTime) / tps, ToGlm(ch->mRotationKeys[k].mValue)});
562 track.
scales.reserve(ch->mNumScalingKeys);
563 for (
u32 k = 0; k < ch->mNumScalingKeys; ++k)
565 track.
scales.push_back({
static_cast<f32>(ch->mScalingKeys[k].mTime) / tps, ToGlm(ch->mScalingKeys[k].mValue)});
567 skelClip->tracks.push_back(std::move(track));
573 track.
positions.reserve(ch->mNumPositionKeys);
574 for (
u32 k = 0; k < ch->mNumPositionKeys; ++k)
576 track.
positions.push_back({
static_cast<f32>(ch->mPositionKeys[k].mTime) / tps, ToGlm(ch->mPositionKeys[k].mValue)});
578 track.
rotations.reserve(ch->mNumRotationKeys);
579 for (
u32 k = 0; k < ch->mNumRotationKeys; ++k)
581 track.
rotations.push_back({
static_cast<f32>(ch->mRotationKeys[k].mTime) / tps, ToGlm(ch->mRotationKeys[k].mValue)});
583 track.
scales.reserve(ch->mNumScalingKeys);
584 for (
u32 k = 0; k < ch->mNumScalingKeys; ++k)
586 track.
scales.push_back({
static_cast<f32>(ch->mScalingKeys[k].mTime) / tps, ToGlm(ch->mScalingKeys[k].mValue)});
588 nodeClip->tracks.push_back(std::move(track));
592 if (!skelClip->tracks.empty())
594 skelClips.push_back(skelClip);
596 if (!nodeClip->tracks.empty())
598 nodeClips.push_back(nodeClip);
603 auto *root = scene->
CreateObject(std::filesystem::path(path).stem().string());
606 if (!skelClips.empty() || !nodeClips.empty() || skeleton->Size() > 0)
609 root->AddComponent(animComp);
611 for (
auto &c : nodeClips)
615 for (
auto &c : skelClips)
623 if (skelClips.empty() && skeleton->Size() > 0)
627 LOG_INFO(
"ModelImporter: synthesized procedural 'idle' clip for '%s' (skeleton has %zu bones, no baked anims)",
628 path.c_str(), skeleton->Size());
632 std::vector<SkinnedMeshComponent *> skinned;
633 for (
u32 i = 0; i < aiscene->mRootNode->mNumChildren; ++i)
635 BuildSceneHierarchy(aiscene->mRootNode->mChildren[i], aiscene, root, scene, meshes, skeleton, animComp, skinned, assetFolder);
638 for (
u32 i = 0; i < aiscene->mRootNode->mNumMeshes; ++i)
640 u32 meshIdx = aiscene->mRootNode->mMeshes[i];
641 if (meshIdx >= meshes.size())
645 const auto &im = meshes[meshIdx];
646 if (im.isSkinned && skeleton)
649 comp->SetPaletteSource(animComp);
650 root->AddComponent(comp);
651 skinned.push_back(comp);
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());
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,...)
#define LOG_ERROR(fmt,...)
std::shared_ptr< Material > material
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.
FileSystem & GetFileSystem()
Returns the file system helper for asset-relative path resolution.
static Engine & GetInstance()
Returns the single Engine instance (created on first call).
TextureManager & GetTextureManager()
Returns the TextureManager that caches loaded textures by path.
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.
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.
GameObject * CreateObject(const std::string &name, GameObject *parent=nullptr)
Create a plain GameObject owned by this scene.
std::shared_ptr< Texture > GetOrLoadTexture(const std::string &path)
Return a cached texture, loading it from disk on first request.
static std::shared_ptr< Texture > LoadFromMemory(const unsigned char *data, int sizeBytes)
Decode an in-memory image (PNG/JPG/...) into a GPU texture.
glm::vec3 vec3
3-component float vector (e.g. world position, RGB colour, normals).
float f32
32-bit IEEE float — the standard GL scalar type.
int32_t i32
Signed 32-bit integer — used for GL enums and sizes.
uint32_t u32
Unsigned 32-bit integer — also the GL index type.
uint8_t u8
Unsigned 8-bit integer.
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....
std::vector< KeyFrameVec3 > scales
std::vector< KeyFrameVec3 > positions
i32 boneIndex
Index into Skeleton::bones.
std::vector< KeyFrameQuat > rotations
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.