20#include <assimp/Importer.hpp>
21#include <assimp/scene.h>
22#include <assimp/postprocess.h>
33 for (
size_t i = 0; i < obj->
GetChildren().GetSize(); ++i) {
38Math::Matrix4 ModelLoader::ConvertMatrix(
const void* aiMatPtr) {
39 const auto& m = *
static_cast<const aiMatrix4x4*
>(
40 static_cast<const void*
>(aiMatPtr));
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;
54 Assimp::Importer importer;
57 unsigned int baseFlags = aiProcess_Triangulate
58 | aiProcess_GenSmoothNormals
59 | aiProcess_CalcTangentSpace
60 | aiProcess_JoinIdenticalVertices
61 | aiProcess_OptimizeMeshes;
64 baseFlags |= aiProcess_FlipUVs;
66 const aiScene* scene = importer.ReadFile(filePath, baseFlags);
68 if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
69 SLEAK_ERROR(
"Assimp: Failed to load model '{}': {}", filePath,
70 importer.GetErrorString());
74 bool hasAnimations = scene->HasAnimations();
76 SLEAK_INFO(
"Model loaded: {} ({} meshes, {} materials, {} textures, {} animations)",
77 filePath, scene->mNumMeshes, scene->mNumMaterials,
78 scene->mNumTextures, scene->mNumAnimations);
80 std::string directory = std::filesystem::path(filePath).parent_path().string();
81 auto* root =
new GameObject(std::filesystem::path(filePath).stem().
string());
87 unsigned int staticFlags = baseFlags | aiProcess_PreTransformVertices;
88 scene = importer.ReadFile(filePath, staticFlags);
90 if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
91 SLEAK_ERROR(
"Assimp: Failed to reimport model '{}': {}", filePath,
92 importer.GetErrorString());
97 ProcessNode(scene->mRootNode, scene, root, directory, options, textureCache);
100 Skeleton* skeleton = ExtractSkeleton(scene);
101 std::vector<AnimationClip*> clips = ExtractAnimations(scene, skeleton);
103 SLEAK_INFO(
" Skeleton: {} bones, {} animation clips",
106 ProcessNodeAnimated(scene->mRootNode, scene, root, directory,
107 options, textureCache, skeleton, clips);
114Skeleton* ModelLoader::ExtractSkeleton(
const aiScene* scene) {
118 for (
unsigned int m = 0; m < scene->mNumMeshes; ++m) {
119 aiMesh* mesh = scene->mMeshes[m];
120 if (!mesh->HasBones())
continue;
122 for (
unsigned int b = 0; b < mesh->mNumBones; ++b) {
123 aiBone* bone = mesh->mBones[b];
124 std::string boneName = bone->mName.C_Str();
126 if (skeleton->FindBoneId(boneName) == -1) {
128 newBone.
name = boneName;
129 newBone.
offsetMatrix = ConvertMatrix(&bone->mOffsetMatrix);
130 skeleton->AddBone(newBone);
136 BuildBoneHierarchy(scene->mRootNode, skeleton, -1);
139 BuildNodeTree(scene->mRootNode, skeleton);
142 aiMatrix4x4 rootTransform = scene->mRootNode->mTransformation;
143 rootTransform.Inverse();
144 skeleton->SetGlobalInverseTransform(ConvertMatrix(&rootTransform));
149int ModelLoader::BuildNodeTree(
const aiNode* node,
Skeleton* skeleton) {
151 nodeData.name = node->mName.C_Str();
152 nodeData.defaultTransform = ConvertMatrix(&node->mTransformation);
153 nodeData.boneIndex = skeleton->FindBoneId(nodeData.name);
155 int nodeIdx = skeleton->AddNode(nodeData);
157 for (
unsigned int i = 0; i < node->mNumChildren; ++i) {
158 int childIdx = BuildNodeTree(node->mChildren[i], skeleton);
159 skeleton->AddNodeChild(nodeIdx, childIdx);
165void ModelLoader::BuildBoneHierarchy(
const aiNode* node,
Skeleton* skeleton,
167 std::string nodeName = node->mName.C_Str();
168 int boneId = skeleton->FindBoneId(nodeName);
171 int nextParent = parentId;
173 skeleton->SetBoneParent(boneId, parentId);
177 for (
unsigned int i = 0; i < node->mNumChildren; ++i) {
178 BuildBoneHierarchy(node->mChildren[i], skeleton, nextParent);
182std::vector<AnimationClip*> ModelLoader::ExtractAnimations(
183 const aiScene* scene,
Skeleton* skeleton) {
184 std::vector<AnimationClip*> clips;
186 for (
unsigned int i = 0; i < scene->mNumAnimations; ++i) {
187 aiAnimation* anim = scene->mAnimations[i];
188 auto* clip =
new AnimationClip();
190 clip->name = anim->mName.C_Str();
191 if (clip->name.empty())
192 clip->name =
"Animation_" + std::to_string(i);
194 clip->duration =
static_cast<float>(anim->mDuration);
195 clip->ticksPerSecond = anim->mTicksPerSecond > 0
196 ?
static_cast<float>(anim->mTicksPerSecond)
199 for (
unsigned int c = 0; c < anim->mNumChannels; ++c) {
200 aiNodeAnim* nodeAnim = anim->mChannels[c];
201 AnimationChannel channel;
203 channel.boneName = nodeAnim->mNodeName.C_Str();
204 channel.boneId = skeleton->FindBoneId(channel.boneName);
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)
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)
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)
234 clip->channels.push_back(std::move(channel));
239 SLEAK_INFO(
" Animation '{}': {} channels, {:.1f}s",
240 clip->name, clip->channels.size(),
241 clip->GetDurationInSeconds());
243 clips.push_back(clip);
249void ModelLoader::ProcessNode(aiNode* node,
const aiScene* scene,
250 GameObject* parent,
const std::string& directory,
253 std::string nodeName = node->mName.C_Str();
254 if (nodeName.empty()) nodeName =
"Node";
256 for (
unsigned int i = 0; i < node->mNumMeshes; ++i) {
257 aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
259 std::string meshName = mesh->mName.C_Str();
260 if (meshName.empty()) meshName = nodeName +
"_Mesh" + std::to_string(i);
262 auto* meshObj =
new GameObject(meshName);
264 float sf = options.scaleFactor;
265 meshObj->AddComponent<TransformComponent>(
268 Math::Vector3D(sf, sf, sf));
270 if (mesh->mMaterialIndex < scene->mNumMaterials) {
271 auto material = ProcessMaterial(
272 scene->mMaterials[mesh->mMaterialIndex], scene, directory, textureCache);
273 meshObj->AddComponent<MaterialComponent>(material);
276 MeshData meshData = ProcessMesh(mesh, options);
279 meshObj->AddComponent<MeshComponent>(std::move(meshData));
281 meshObj->SetParent(parent);
284 for (
unsigned int i = 0; i < node->mNumChildren; ++i) {
285 ProcessNode(node->mChildren[i], scene, parent, directory, options, textureCache);
289void ModelLoader::ProcessNodeAnimated(aiNode* node,
const aiScene* scene,
290 GameObject* parent,
const std::string& directory,
294 std::vector<AnimationClip*>& clips) {
295 std::string nodeName = node->mName.C_Str();
296 if (nodeName.empty()) nodeName =
"Node";
298 for (
unsigned int i = 0; i < node->mNumMeshes; ++i) {
299 aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
301 std::string meshName = mesh->mName.C_Str();
302 if (meshName.empty()) meshName = nodeName +
"_Mesh" + std::to_string(i);
304 auto* meshObj =
new GameObject(meshName);
306 float sf = options.scaleFactor;
307 meshObj->AddComponent<TransformComponent>(
310 Math::Vector3D(sf, sf, sf));
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);
322 MeshData meshData = ProcessMesh(mesh, options, skeleton);
325 meshObj->AddComponent<MeshComponent>(std::move(meshData));
328 if (hasBones && skeleton->GetBoneCount() > 0 && !clips.empty()) {
329 meshObj->AddComponent<AnimatorComponent>(skeleton, clips);
332 meshObj->SetParent(parent);
335 for (
unsigned int i = 0; i < node->mNumChildren; ++i) {
336 ProcessNodeAnimated(node->mChildren[i], scene, parent, directory,
337 options, textureCache, skeleton, clips);
344 float nSign = options.flipNormals ? -1.0f : 1.0f;
347 for (
unsigned int i = 0; i < mesh->mNumVertices; ++i) {
350 v.px = mesh->mVertices[i].x;
351 v.py = mesh->mVertices[i].y;
352 v.pz = mesh->mVertices[i].z;
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;
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;
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;
373 v.r = 1.0f; v.g = 1.0f; v.b = 1.0f; v.a = 1.0f;
376 if (mesh->HasTextureCoords(0)) {
377 v.u = mesh->mTextureCoords[0][i].x;
378 v.v = mesh->mTextureCoords[0][i].y;
382 data.vertices.AddVertex(v);
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);
395 newBone.name = boneName;
396 newBone.offsetMatrix = ConvertMatrix(&bone->mOffsetMatrix);
397 boneId = skeleton->AddBone(newBone);
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;
404 if (vertexId >= data.vertices.GetSize())
continue;
406 Vertex* vPtr = data.vertices.GetMutableData() + vertexId;
410 if (vPtr->boneIDs[s] < 0) {
411 vPtr->boneIDs[s] = boneId;
412 vPtr->boneWeights[s] = weight;
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]));
428 for (
unsigned int j = 0; j < face.mNumIndices; ++j) {
429 data.indices.add(
static_cast<IndexType>(face.mIndices[j]));
434 SLEAK_INFO(
" Mesh '{}': {} vertices, {} indices{}",
435 mesh->mName.C_Str(), mesh->mNumVertices,
436 data.indices.GetSize(),
437 mesh->HasBones() ?
" (skinned)" :
"");
443 const aiScene* scene,
444 const std::string& directory,
448 material->SetShader(skinned ?
"assets/shaders/skinned_shader.hlsl"
449 :
"assets/shaders/default_shader.hlsl");
452 if (mat->Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS) {
453 material->SetDiffuseColor(color.r, color.g, color.b, color.a);
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)));
461 if (mat->Get(AI_MATKEY_COLOR_EMISSIVE, color) == AI_SUCCESS) {
462 material->SetEmissiveColor(color.r, color.g, color.b);
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);
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);
481 tex = LoadMaterialTexture(mat, aiTextureType_NORMALS, scene, directory, textureCache);
482 if (tex) material->SetNormalTexture(tex);
484 tex = LoadMaterialTexture(mat, aiTextureType_SPECULAR, scene, directory, textureCache);
485 if (tex) material->SetSpecularTexture(tex);
487 tex = LoadMaterialTexture(mat, aiTextureType_EMISSIVE, scene, directory, textureCache);
488 if (tex) material->SetEmissiveTexture(tex);
490 tex = LoadMaterialTexture(mat, aiTextureType_METALNESS, scene, directory, textureCache);
491 if (tex) material->SetMetallicTexture(tex);
493 tex = LoadMaterialTexture(mat, aiTextureType_DIFFUSE_ROUGHNESS, scene, directory, textureCache);
494 if (tex) material->SetRoughnessTexture(tex);
496 return RefPtr<Material>(material);
499::Sleak::Texture* ModelLoader::LoadMaterialTexture(aiMaterial* mat,
int type,
500 const aiScene* scene,
501 const std::string& directory,
503 auto texType =
static_cast<aiTextureType
>(type);
505 if (mat->GetTextureCount(texType) == 0)
509 mat->GetTexture(texType, 0, &aiPath);
510 std::string texPath = aiPath.C_Str();
513 std::string cacheKey = texPath +
":" + std::to_string(type);
514 auto it = textureCache.find(cacheKey);
515 if (it != textureCache.end())
519 const aiTexture* embedded = scene->GetEmbeddedTexture(texPath.c_str());
521 ::Sleak::Texture* tex =
nullptr;
522 if (embedded->mHeight == 0) {
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);
531 SLEAK_ERROR(
" Failed to decode embedded texture: {}", texPath);
536 pixels,
static_cast<uint32_t
>(w),
static_cast<uint32_t
>(h),
539 stbi_image_free(pixels);
542 SLEAK_INFO(
" Loaded embedded texture: {} ({}x{})", texPath, w, h);
546 uint32_t w = embedded->mWidth;
547 uint32_t h = embedded->mHeight;
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];
554 rgba[i * 4 + 1] = src[i * 4 + 2];
555 rgba[i * 4 + 2] = src[i * 4 + 3];
556 rgba[i * 4 + 3] = src[i * 4 + 0];
563 SLEAK_INFO(
" Loaded embedded raw texture: {} ({}x{})", texPath, w, h);
567 if (tex) textureCache[cacheKey] = tex;
572 std::string fullPath = directory +
"/" + texPath;
575 std::replace(fullPath.begin(), fullPath.end(),
'\\',
'/');
577 if (!std::filesystem::exists(fullPath)) {
578 SLEAK_WARN(
" Texture file not found: {}", fullPath);
585 textureCache[cacheKey] = tex;
591 const std::string& filePath,
Skeleton* skeleton) {
592 std::vector<AnimationClip*> result;
599 Assimp::Importer importer;
600 unsigned int flags = aiProcess_Triangulate;
602 const aiScene* scene = importer.ReadFile(filePath, flags);
603 if (!scene || !scene->HasAnimations()) {
604 SLEAK_ERROR(
"LoadAnimationsOnly: failed to load or no animations in '{}'",
609 result = ExtractAnimations(scene, skeleton);
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) {
622 SLEAK_INFO(
"LoadAnimationsOnly: '{}' -> {} clips", filePath, result.size());
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.
Matrix< float, 4, 4 > Matrix4
Root namespace for everything the engine exposes.
static void InitializeRecursive(GameObject *obj)
Recursively initialize a GameObject and all its children.
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
Math::Matrix4 offsetMatrix