20#include <assimp/Importer.hpp>
21#include <assimp/scene.h>
22#include <assimp/postprocess.h>
30Math::Matrix4 ModelLoader::ConvertMatrix(
const void* aiMatPtr) {
31 const auto& m = *
static_cast<const aiMatrix4x4*
>(
32 static_cast<const void*
>(aiMatPtr));
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;
46 Assimp::Importer importer;
49 unsigned int baseFlags = aiProcess_Triangulate
50 | aiProcess_GenSmoothNormals
51 | aiProcess_CalcTangentSpace
52 | aiProcess_JoinIdenticalVertices
53 | aiProcess_OptimizeMeshes;
56 baseFlags |= aiProcess_FlipUVs;
58 const aiScene* scene = importer.ReadFile(filePath, baseFlags);
60 if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
61 SLEAK_ERROR(
"Assimp: Failed to load model '{}': {}", filePath,
62 importer.GetErrorString());
66 bool hasAnimations = scene->HasAnimations();
68 SLEAK_INFO(
"Model loaded: {} ({} meshes, {} materials, {} textures, {} animations)",
69 filePath, scene->mNumMeshes, scene->mNumMaterials,
70 scene->mNumTextures, scene->mNumAnimations);
72 std::string directory = std::filesystem::path(filePath).parent_path().string();
73 auto* root =
new GameObject(std::filesystem::path(filePath).stem().
string());
80 unsigned int staticFlags = baseFlags | aiProcess_PreTransformVertices;
81 scene = importer.ReadFile(filePath, staticFlags);
83 if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
84 SLEAK_ERROR(
"Assimp: Failed to reimport model '{}': {}", filePath,
85 importer.GetErrorString());
90 ProcessNode(scene->mRootNode, scene, root, directory, options,
91 textureCache, materialCache);
94 Skeleton* skeleton = ExtractSkeleton(scene);
95 std::vector<AnimationClip*> clips = ExtractAnimations(scene, skeleton);
97 SLEAK_INFO(
" Skeleton: {} bones, {} animation clips",
100 ProcessNodeAnimated(scene->mRootNode, scene, root, directory, options,
101 textureCache, materialCache, skeleton, clips);
108Skeleton* ModelLoader::ExtractSkeleton(
const aiScene* scene) {
112 for (
unsigned int m = 0; m < scene->mNumMeshes; ++m) {
113 aiMesh* mesh = scene->mMeshes[m];
114 if (!mesh->HasBones())
continue;
116 for (
unsigned int b = 0; b < mesh->mNumBones; ++b) {
117 aiBone* bone = mesh->mBones[b];
118 std::string boneName = bone->mName.C_Str();
120 if (skeleton->FindBoneId(boneName) == -1) {
122 newBone.
name = boneName;
123 newBone.
offsetMatrix = ConvertMatrix(&bone->mOffsetMatrix);
124 skeleton->AddBone(newBone);
130 BuildBoneHierarchy(scene->mRootNode, skeleton, -1);
133 BuildNodeTree(scene->mRootNode, skeleton);
136 aiMatrix4x4 rootTransform = scene->mRootNode->mTransformation;
137 rootTransform.Inverse();
138 skeleton->SetGlobalInverseTransform(ConvertMatrix(&rootTransform));
143int ModelLoader::BuildNodeTree(
const aiNode* node,
Skeleton* skeleton) {
145 nodeData.name = node->mName.C_Str();
146 nodeData.defaultTransform = ConvertMatrix(&node->mTransformation);
147 nodeData.boneIndex = skeleton->FindBoneId(nodeData.name);
149 int nodeIdx = skeleton->AddNode(nodeData);
151 for (
unsigned int i = 0; i < node->mNumChildren; ++i) {
152 int childIdx = BuildNodeTree(node->mChildren[i], skeleton);
153 skeleton->AddNodeChild(nodeIdx, childIdx);
159void ModelLoader::BuildBoneHierarchy(
const aiNode* node,
Skeleton* skeleton,
161 std::string nodeName = node->mName.C_Str();
162 int boneId = skeleton->FindBoneId(nodeName);
165 int nextParent = parentId;
167 skeleton->SetBoneParent(boneId, parentId);
171 for (
unsigned int i = 0; i < node->mNumChildren; ++i) {
172 BuildBoneHierarchy(node->mChildren[i], skeleton, nextParent);
176std::vector<AnimationClip*> ModelLoader::ExtractAnimations(
177 const aiScene* scene,
Skeleton* skeleton) {
178 std::vector<AnimationClip*> clips;
180 for (
unsigned int i = 0; i < scene->mNumAnimations; ++i) {
181 aiAnimation* anim = scene->mAnimations[i];
182 auto* clip =
new AnimationClip();
184 clip->name = anim->mName.C_Str();
185 if (clip->name.empty())
186 clip->name =
"Animation_" + std::to_string(i);
188 clip->duration =
static_cast<float>(anim->mDuration);
189 clip->ticksPerSecond = anim->mTicksPerSecond > 0
190 ?
static_cast<float>(anim->mTicksPerSecond)
193 for (
unsigned int c = 0; c < anim->mNumChannels; ++c) {
194 aiNodeAnim* nodeAnim = anim->mChannels[c];
195 AnimationChannel channel;
197 channel.boneName = nodeAnim->mNodeName.C_Str();
198 channel.boneId = skeleton->FindBoneId(channel.boneName);
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)
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)
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)
228 clip->channels.push_back(std::move(channel));
233 SLEAK_INFO(
" Animation '{}': {} channels, {:.1f}s",
234 clip->name, clip->channels.size(),
235 clip->GetDurationInSeconds());
237 clips.push_back(clip);
243void ModelLoader::ProcessNode(aiNode* node,
const aiScene* scene,
244 GameObject* parent,
const std::string& directory,
248 std::string nodeName = node->mName.C_Str();
249 if (nodeName.empty()) nodeName =
"Node";
251 for (
unsigned int i = 0; i < node->mNumMeshes; ++i) {
252 aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
254 std::string meshName = mesh->mName.C_Str();
255 if (meshName.empty()) meshName = nodeName +
"_Mesh" + std::to_string(i);
259 float sf = options.scaleFactor;
260 meshObj->AddComponent<TransformComponent>(
263 Math::Vector3D(sf, sf, sf));
265 if (mesh->mMaterialIndex < scene->mNumMaterials) {
266 auto& material = materialCache[uint64_t(mesh->mMaterialIndex) << 1];
269 ProcessMaterial(scene->mMaterials[mesh->mMaterialIndex],
270 scene, directory, textureCache);
271 meshObj->AddComponent<MaterialComponent>(material);
274 MeshData meshData = ProcessMesh(mesh, options);
277 meshObj->AddComponent<MeshComponent>(std::move(meshData));
279 meshObj->SetParent(parent);
282 for (
unsigned int i = 0; i < node->mNumChildren; ++i) {
283 ProcessNode(node->mChildren[i], scene, parent, directory, options,
284 textureCache, materialCache);
288void ModelLoader::ProcessNodeAnimated(
289 aiNode* node,
const aiScene* scene,
GameObject* parent,
292 Skeleton* skeleton, std::vector<AnimationClip*>& clips) {
293 std::string nodeName = node->mName.C_Str();
294 if (nodeName.empty()) nodeName =
"Node";
296 for (
unsigned int i = 0; i < node->mNumMeshes; ++i) {
297 aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
299 std::string meshName = mesh->mName.C_Str();
300 if (meshName.empty()) meshName = nodeName +
"_Mesh" + std::to_string(i);
304 float sf = options.scaleFactor;
305 meshObj->AddComponent<TransformComponent>(
308 Math::Vector3D(sf, sf, sf));
311 bool hasBones = mesh->HasBones();
312 if (mesh->mMaterialIndex < scene->mNumMaterials) {
314 (uint64_t(mesh->mMaterialIndex) << 1) | (hasBones ? 1u : 0u);
315 auto& material = materialCache[key];
318 ProcessMaterial(scene->mMaterials[mesh->mMaterialIndex],
319 scene, directory, textureCache, hasBones);
320 meshObj->AddComponent<MaterialComponent>(material);
324 MeshData meshData = ProcessMesh(mesh, options, skeleton);
327 meshObj->AddComponent<MeshComponent>(std::move(meshData));
330 if (hasBones && skeleton->GetBoneCount() > 0 && !clips.empty()) {
331 meshObj->AddComponent<AnimatorComponent>(skeleton, clips);
334 meshObj->SetParent(parent);
337 for (
unsigned int i = 0; i < node->mNumChildren; ++i) {
338 ProcessNodeAnimated(node->mChildren[i], scene, parent, directory,
339 options, textureCache, materialCache, skeleton,
347 float nSign = options.flipNormals ? -1.0f : 1.0f;
350 for (
unsigned int i = 0; i < mesh->mNumVertices; ++i) {
353 v.px = mesh->mVertices[i].x;
354 v.py = mesh->mVertices[i].y;
355 v.pz = mesh->mVertices[i].z;
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;
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;
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;
376 v.r = 1.0f; v.g = 1.0f; v.b = 1.0f; v.a = 1.0f;
379 if (mesh->HasTextureCoords(0)) {
380 v.u = mesh->mTextureCoords[0][i].x;
381 v.v = mesh->mTextureCoords[0][i].y;
385 data.vertices.AddVertex(v);
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);
398 newBone.name = boneName;
399 newBone.offsetMatrix = ConvertMatrix(&bone->mOffsetMatrix);
400 boneId = skeleton->AddBone(newBone);
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;
407 if (vertexId >= data.vertices.GetSize())
continue;
409 Vertex* vPtr = data.vertices.GetMutableData() + vertexId;
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;
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]));
431 for (
unsigned int j = 0; j < face.mNumIndices; ++j) {
432 data.indices.add(
static_cast<IndexType>(face.mIndices[j]));
437 SLEAK_INFO(
" Mesh '{}': {} vertices, {} indices{}",
438 mesh->mName.C_Str(), mesh->mNumVertices,
439 data.indices.GetSize(),
440 mesh->HasBones() ?
" (skinned)" :
"");
446 const aiScene* scene,
447 const std::string& directory,
451 material->SetShader(skinned ?
"assets/shaders/skinned_shader.hlsl"
452 :
"assets/shaders/default_shader.hlsl");
455 if (mat->Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS) {
456 material->SetDiffuseColor(color.r, color.g, color.b, color.a);
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)));
464 if (mat->Get(AI_MATKEY_COLOR_EMISSIVE, color) == AI_SUCCESS) {
465 material->SetEmissiveColor(color.r, color.g, color.b);
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);
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);
484 tex = LoadMaterialTexture(mat, aiTextureType_NORMALS, scene, directory, textureCache);
485 if (tex) material->SetNormalTexture(tex);
487 tex = LoadMaterialTexture(mat, aiTextureType_SPECULAR, scene, directory, textureCache);
488 if (tex) material->SetSpecularTexture(tex);
490 tex = LoadMaterialTexture(mat, aiTextureType_EMISSIVE, scene, directory, textureCache);
491 if (tex) material->SetEmissiveTexture(tex);
493 tex = LoadMaterialTexture(mat, aiTextureType_METALNESS, scene, directory, textureCache);
494 if (tex) material->SetMetallicTexture(tex);
496 tex = LoadMaterialTexture(mat, aiTextureType_DIFFUSE_ROUGHNESS, scene, directory, textureCache);
497 if (tex) material->SetRoughnessTexture(tex);
503 aiMaterial* mat,
int type,
const aiScene* scene,
504 const std::string& directory,
TextureCache& textureCache) {
505 auto texType =
static_cast<aiTextureType
>(type);
510 mat->GetTexture(texType, 0, &aiPath);
511 std::string texPath = aiPath.C_Str();
514 std::string cacheKey = texPath +
":" + std::to_string(type);
515 auto it = textureCache.find(cacheKey);
516 if (it != textureCache.end())
520 const aiTexture* embedded = scene->GetEmbeddedTexture(texPath.c_str());
522 ::Sleak::Texture* tex =
nullptr;
523 if (embedded->mHeight == 0) {
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);
532 SLEAK_ERROR(
" Failed to decode embedded texture: {}", texPath);
537 pixels,
static_cast<uint32_t
>(w),
static_cast<uint32_t
>(h),
540 stbi_image_free(pixels);
543 SLEAK_INFO(
" Loaded embedded texture: {} ({}x{})", texPath, w, h);
547 uint32_t w = embedded->mWidth;
548 uint32_t h = embedded->mHeight;
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];
555 rgba[i * 4 + 1] = src[i * 4 + 2];
556 rgba[i * 4 + 2] = src[i * 4 + 3];
557 rgba[i * 4 + 3] = src[i * 4 + 0];
564 SLEAK_INFO(
" Loaded embedded raw texture: {} ({}x{})", texPath, w, h);
568 RefPtr<Texture> owned(tex);
569 if (owned) textureCache[cacheKey] = owned;
574 std::string fullPath = directory +
"/" + texPath;
577 std::replace(fullPath.begin(), fullPath.end(),
'\\',
'/');
579 if (!std::filesystem::exists(fullPath)) {
580 SLEAK_WARN(
" Texture file not found: {}", fullPath);
587 textureCache[cacheKey] = tex;
593 const std::string& filePath,
Skeleton* skeleton) {
594 std::vector<AnimationClip*> result;
601 Assimp::Importer importer;
602 unsigned int flags = aiProcess_Triangulate;
604 const aiScene* scene = importer.ReadFile(filePath, flags);
605 if (!scene || !scene->HasAnimations()) {
606 SLEAK_ERROR(
"LoadAnimationsOnly: failed to load or no animations in '{}'",
611 result = ExtractAnimations(scene, skeleton);
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) {
624 SLEAK_INFO(
"LoadAnimationsOnly: '{}' -> {} clips", filePath, result.size());
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.
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.
Matrix< float, 4, 4 > Matrix4
Root namespace for everything the engine exposes.
std::unordered_map< std::string, RefPtr<::Sleak::Texture > > TextureCache
Per-load texture cache to avoid loading the same texture file multiple times.
std::unordered_map< uint64_t, RefPtr< Material > > MaterialCache
Per-load cache so meshes sharing an Assimp material share one Material.
Math::Matrix4 offsetMatrix