Cogs.Core
AssetImporterLoader.cpp
1#include "AssetImporterLoader.h"
2
3#include "Context.h"
4
5#include <assimp/Importer.hpp>
6#include <assimp/IOSystem.hpp>
7#include <assimp/IOStream.hpp>
8#include <assimp/postprocess.h>
9#include <assimp/mesh.h>
10#include <assimp/scene.h>
11
12#include "Resources/MeshManager.h"
13#include "Resources/ModelManager.h"
14#include "Resources/MaterialManager.h"
15#include "Resources/TextureManager.h"
16#include "Resources/DefaultMaterial.h"
17#include "Resources/AnimationManager.h"
18#include "Resources/ResourceStore.h"
19#include "Resources/Skeleton.h"
20#include "Resources/Animation.h"
21
22#include "Rendering/IGraphicsDevice.h"
23#include "Rendering/ITextures.h"
24
25#include "Foundation/Logging/Logger.h"
26#include "Foundation/Platform/IO.h"
27#include "Foundation/Platform/Timer.h"
28
29#include <glm/gtx/matrix_decompose.hpp>
30
31#include <stdio.h>
32#include <sys/stat.h>
33#include <sys/types.h>
34
35#include "Services/Variables.h"
36
37using namespace Cogs::Core;
38using namespace Cogs;
39
40namespace
41{
42 Cogs::Logging::Log logger = Cogs::Logging::getLogger("AssetImporterLoader");
43
44 glm::mat4 toGlm(aiMatrix4x4 matrix)
45 {
46 matrix.Transpose();
47
48 return glm::make_mat4((float *)&(matrix));
49 }
50
51 glm::vec3 toGlm(aiVector3D v)
52 {
53 return glm::make_vec3((float *)&(v));
54 }
55
56 glm::quat toGlm(aiQuaternion q)
57 {
58 return glm::quat(q.w, q.x, q.y, q.z);
59 }
60
61 class InputIOStream : public Assimp::IOStream
62 {
63 public:
64 InputIOStream(const Cogs::Core::ResourceBuffer & resourceBuffer) :
65 resourceBuffer(resourceBuffer),
66 buffer(resourceBuffer.data()),
67 position(resourceBuffer.data()),
68 size(resourceBuffer.size())
69 {
70
71 }
72
73 size_t Read(void * destBuffer, size_t pSize, size_t pCount) override
74 {
75 const size_t bytesToRead = pSize * pCount;
76 const size_t offset = position - buffer;
77
78 if (offset < size) {
79 auto bytesLeft = size - offset;
80
81 auto bytesRead = std::min(bytesToRead, bytesLeft);
82
83 if (bytesRead != 0) {
84 ::memcpy(destBuffer, position, bytesRead);
85 position += bytesRead;
86 }
87
88 return bytesRead;
89 }
90
91 return 0;
92 }
93
94 size_t Write(const void * /*pvBuffer*/, size_t /*pSize*/, size_t /*pCount*/) override
95 {
96 // Write not supported for now.
97 return 0;
98 }
99
100#ifdef _WIN32
101#pragma warning(push)
102#pragma warning(disable: 26812) // prefer enum class over enum
103#endif
104 aiReturn Seek(size_t pOffset, aiOrigin pOrigin) override
105 {
106 switch (pOrigin) {
107 case aiOrigin_SET:
108 position = buffer + pOffset;
109 break;
110 case aiOrigin_CUR:
111 position += pOffset;
112 break;
113 case aiOrigin_END:
114 position = buffer + size + pOffset;
115 break;
116 default:
117 break;
118 }
119
120 return aiReturn_SUCCESS;
121 }
122#ifdef _WIN32
123#pragma warning(pop)
124#endif
125
126 size_t Tell() const override
127 {
128 return position - buffer;
129 }
130
131 size_t FileSize() const override
132 {
133 return size;
134 }
135
136 void Flush() override
137 {
138 return;
139 }
140
141 private:
142 Cogs::Core::ResourceBuffer resourceBuffer;
143 const uint8_t * buffer;
144 const uint8_t * position;
145 size_t size;
146 };
147
148 class MmapIOStream : public Assimp::IOStream
149 {
150 public:
151 MmapIOStream(const Cogs::StringView & path) :
152 path(path)
153 {
154 file = IO::openFile(path, FileHandle::OpenMode::OpenAlways, FileHandle::AccessMode::Read);
155 size = IO::fileSize(file);
156 buffer = (const uint8_t *)IO::mapFile(file, FileHandle::ProtectionMode::ReadOnly, 0, size);
157 position = buffer;
158 }
159
160 size_t Read(void * destBuffer, size_t pSize, size_t pCount) override
161 {
162 const size_t bytesToRead = pSize * pCount;
163 const size_t offset = position - buffer;
164
165 if (offset < size) {
166 auto bytesLeft = size - offset;
167
168 auto bytesRead = std::min(bytesToRead, bytesLeft);
169
170 if (bytesRead != 0) {
171 ::memcpy(destBuffer, position, bytesRead);
172 position += bytesRead;
173 }
174
175 return bytesRead;
176 }
177
178 return 0;
179 }
180
181 size_t Write(const void * /*pvBuffer*/, size_t /*pSize*/, size_t /*pCount*/) override
182 {
183 // Write not supported for now.
184 return 0;
185 }
186
187 aiReturn Seek(size_t pOffset, aiOrigin pOrigin) override
188 {
189 switch (pOrigin) {
190 case aiOrigin_SET:
191 position = buffer + pOffset;
192 break;
193 case aiOrigin_CUR:
194 position += pOffset;
195 break;
196 case aiOrigin_END:
197 position = buffer + size + pOffset;
198 break;
199 default:
200 break;
201 }
202
203 return aiReturn_SUCCESS;
204 }
205
206 size_t Tell() const override
207 {
208 return position - buffer;
209 }
210
211 size_t FileSize() const override
212 {
213 return size;
214 }
215
216 void Flush() override
217 {
218 return;
219 }
220
221 private:
222 Cogs::StringView path;
223 Cogs::FileHandle::Ptr file;
224 const uint8_t * buffer = nullptr;
225 const uint8_t * position = nullptr;
226 size_t size = 0;
227 };
228
229 class IOSystem : public Assimp::IOSystem
230 {
231 public:
232 IOSystem(Context * context) : context(context)
233 {
234 useMmap = context->variables->get("resources.useMemoryMappedIO", false);
235 }
236
237 bool Exists(const char * pFile) const override
238 {
239 if (useMmap) {
240 auto path = context->resourceStore->getResourcePath(pFile, ResourceStoreFlags::NoCachedContent);
241 return IO::exists(path);
242 } else {
243 return context->resourceStore->getResourceContents(pFile).size() != 0;
244 }
245 }
246
247 char getOsSeparator() const override
248 {
249 return IO::pathSeparator();
250 }
251
252 Assimp::IOStream * Open(const char* pFile, const char* /*pMode = "rb"*/) override
253 {
254 if (useMmap) {
255 auto name = context->resourceStore->getResourcePath(pFile, ResourceStoreFlags::NoCachedContent);
256
257 return new MmapIOStream(name.c_str());
258 } else {
259 auto content = context->resourceStore->getResourceContents(pFile);
260
261 if (!content.size()) return nullptr;
262
263 return new InputIOStream(content);
264 }
265 }
266
267 void Close(Assimp::IOStream * pFile) override
268 {
269 delete pFile;
270 }
271
272 private:
273 Context * context = nullptr;
274 bool useMmap = true;
275 };
276
277 glm::vec3 xformPosition(const glm::mat4 & transform, const glm::vec3 & pos)
278 {
279 glm::vec4 c = transform * glm::vec4(pos.x, pos.y, pos.z, 1);
280 if (c.w == 0) {
281 c = glm::vec4(0, 0, 0, 1.0f);
282 } else {
283 c /= c.w;
284 }
285
286 return glm::vec3(c);
287 }
288
289 glm::vec3 xformVector(const glm::mat4 & transform, const glm::vec3 & n)
290 {
291 return xformPosition(transform, n) - xformPosition(transform, glm::vec3(0, 0, 0));
292 }
293
294 glm::vec3 calcNormal(const glm::mat4 & transform, const glm::vec3 & p1, const glm::vec3 & p2, const glm::vec3 & p3)
295 {
296 return glm::normalize(glm::cross(xformVector(transform, p2 - p1), xformVector(transform, p3 - p2)));
297 }
298
304 bool needInvertNormals(const glm::mat4 & transform,
305 const std::vector<uint32_t> &indexes,
306 const glm::vec3 * points, const glm::vec3 * normals,
307 const uint32_t count)
308 {
309 if (!indexes.empty()) {
310 for (size_t p = 1; p < indexes.size() - 1; p += 3) {
311 const auto n = calcNormal(transform, points[indexes[p - 1]], points[indexes[p]], points[indexes[p + 1]]);
312 float dotProduct = glm::dot(n, glm::normalize(xformVector(transform, normals[indexes[p]])));
313 if (dotProduct > 0.5f)
314 return false;
315 else if (dotProduct < -0.5f)
316 return true;
317 }
318 } else {
319 for (uint32_t p = 1; p < count - 1; p += 3) {
320 const auto n = calcNormal(transform, points[p - 1], points[p], points[p + 1]);
321 float dotProduct = glm::dot(n, glm::normalize(xformVector(transform, normals[p])));
322 if (dotProduct > 0.5f)
323 return false;
324 else if (dotProduct < -0.5f)
325 return true;
326 }
327 }
328
329 return false;
330 }
331
332}
333
334void Cogs::Core::AssetImporterLoader::handleBones(const aiMesh * mesh, Model & root, const glm::mat4 & globalTransform, Mesh * meshData)
335{
336 const size_t numBones = mesh->mNumBones;
337
338 std::vector<int> numBoneWeights(mesh->mNumVertices);
339 std::vector<glm::ivec4> boneIndexes(mesh->mNumVertices);
340 std::vector<glm::vec4> boneWeights(mesh->mNumVertices);
341
342 bool weightsOutOfBounds = false;
343
344 auto & skeleton = root.skeleton;
345 skeleton.bindPose = glm::inverse(globalTransform);
346
347 auto poseIndexes = meshData->mapPoseIndexes((uint32_t)numBones);
348
349 if (numBones > kMaxBones) {
350 LOG_WARNING(logger, "Mesh bone count %zd exceeds max limit %zd.", numBones, kMaxBones);
351 }
352
353 for (size_t i = 0; i < numBones; ++i) {
354 auto aBone = mesh->mBones[i];
355
356 std::string name = aBone->mName.C_Str();
357
358 auto fIt = skeleton.bonesByName.find(name);
359 if (fIt == skeleton.bonesByName.end()) continue;
360
361 auto boneIndex = fIt->second;
362 auto & bone = skeleton.bones[boneIndex];
363
364 poseIndexes[i] = (uint32_t)boneIndex;
365
366 bone.used = true;
367
368 if (!bone.hasOffset) {
369 bone.inverseBindPose = toGlm(aBone->mOffsetMatrix);
370 bone.hasOffset = true;
371 }
372
373 auto numWeights = aBone->mNumWeights;
374
375 for (size_t j = 0; j < numWeights; ++j) {
376 auto & weight = aBone->mWeights[j];
377
378 const int weightIndex = numBoneWeights[weight.mVertexId]++;
379
380 if (weightIndex > 3) {
381 if (!weightsOutOfBounds) {
382 LOG_WARNING(logger, "Too many bone weights for vertex %d, extra weights will be dropped.", weight.mVertexId);
383 weightsOutOfBounds = true;
384 }
385 continue;
386 }
387
388 glm::value_ptr(boneIndexes[weight.mVertexId])[weightIndex] = static_cast<int>(i);
389 glm::value_ptr(boneWeights[weight.mVertexId])[weightIndex] = weight.mWeight;
390 }
391 }
392
393 //TODO: If weights are out of bounds, we may renormalize the weights to avoid strange artifacts.
394
395 meshData->set(VertexDataType::Colors2, VertexFormats::BoneIndex4i, boneIndexes);
396 meshData->set(VertexDataType::Colors3, VertexFormats::BoneWeight4f, boneWeights);
397
398 meshData->setMeshFlag(MeshFlags::Skinned);
399}
400
401void Cogs::Core::AssetImporterLoader::handleAssImpMesh(Context * context, Model & root, ModelPart & modelPart, const aiMesh * mesh, const glm::mat4 & localTransform, const glm::mat4 & globalTransform)
402{
403 // Decompose matrix, check scaling. Only invert normals if negative scaling.
404 // Should work for all cases, but...
405 glm::vec3 scale;
406 glm::quat rotation;
407 glm::vec3 translation;
408 glm::vec3 skew;
409 glm::vec4 perspective;
410 glm::decompose(localTransform, scale, rotation, translation, skew, perspective);
411 const bool positiveScale = scale.x >= 0 && scale.y >= 0 && scale.z >= 0;
412
413 root.setPartTransform(modelPart, localTransform);
414
415 auto meshData = context->meshManager->createLocked();
416
417 modelPart.meshIndex = (uint32_t)root.meshes.size();
418 root.meshes.emplace_back(meshData.getHandle());
419
420 if (mesh->mName.length) {
421 meshData->setName(mesh->mName.data);
422 }
423
424 std::vector<uint32_t> indexes;
425
426 if (mesh->mPrimitiveTypes == aiPrimitiveType_LINE || mesh->mPrimitiveTypes == aiPrimitiveType_TRIANGLE) {
427 size_t numIndices = 0;
428 for (uint32_t f = 0; f < mesh->mNumFaces; f++) {
429 const aiFace & face = mesh->mFaces[f];
430 numIndices += face.mNumIndices;
431 }
432
433 indexes.reserve(numIndices);
434 for (uint32_t f = 0; f < mesh->mNumFaces; f++) {
435 const aiFace & face = mesh->mFaces[f];
436
437 assert((mesh->mPrimitiveTypes == aiPrimitiveType_LINE) || face.mNumIndices == 3);
438
439 for (uint32_t i = 0; i < face.mNumIndices; i++) {
440 indexes.push_back(face.mIndices[i]);
441 }
442 }
443 } else {
444 LOG_WARNING(logger, "Skipped geometry of type %x.", mesh->mPrimitiveTypes);
445 }
446
447 auto pPositions = reinterpret_cast<const glm::vec3 *>(mesh->mVertices);
448 meshData->setPositions(pPositions, pPositions + mesh->mNumVertices);
449
450 if (mesh->mNormals != nullptr) {
451 auto pNormals = reinterpret_cast<const glm::vec3 *>(mesh->mNormals);
452
453 bool needInvert = false;
454 if (!positiveScale && mesh->mPrimitiveTypes == aiPrimitiveType_TRIANGLE) {
455 needInvert = needInvertNormals(localTransform, indexes, pPositions, pNormals, mesh->mNumVertices);
456 }
457
458 if (needInvert) {
460 }
461
462 meshData->setNormals(pNormals, pNormals + mesh->mNumVertices);
463 }
464
465 meshData->setIndexes(std::move(indexes));
466
467 if (mesh->mTangents) {
468 auto pTangents = reinterpret_cast<glm::vec3 *>(mesh->mTangents);
469 meshData->setTangents(pTangents, pTangents + mesh->mNumVertices);
470 }
471
472 if (mesh->mTextureCoords[0] != nullptr) {
473 std::vector<glm::vec2> texCoords(mesh->mNumVertices);
474
475 for (size_t i = 0; i < mesh->mNumVertices; ++i) {
476 texCoords[i] = glm::vec2(mesh->mTextureCoords[0][i].x, mesh->mTextureCoords[0][i].y);
477 }
478
479 meshData->setTexCoords(std::move(texCoords));
480 }
481
482 if (mesh->HasBones()) {
483 handleBones(mesh, root, globalTransform, meshData.operator->());
484 }
485
487 meshData->primitiveType = (mesh->mPrimitiveTypes == aiPrimitiveType_LINE) ? Cogs::PrimitiveType::LineList : Cogs::PrimitiveType::TriangleList;
488
489 Geometry::BoundingBox bounds = calculateBounds(&*meshData);
490 meshData->setBounds(bounds);
491
492 modelPart.boundsIndex = static_cast<uint32_t>(root.bounds.size());
493 root.bounds.push_back(bounds);
494
495 modelPart.materialIndex = mesh->mMaterialIndex;
496}
497
498void Cogs::Core::AssetImporterLoader::handleAssImpNode(Context * context, Model & root, size_t parent, const aiScene * scene, const aiNode * node, const glm::mat4 & parentTransform)
499{
500 const glm::mat4 localTransform = toGlm(node->mTransformation);
501 const glm::mat4 globalTransform = parentTransform * localTransform;
502
503 const size_t index = root.parts.size();
504 root.parts.emplace_back();
505 auto & modelNode = root.parts.back();
506 root.setPartTransform(modelNode, localTransform);
507 root.setPartName(modelNode, node->mName.C_Str());
508 modelNode.parentIndex = (uint32_t)parent;
509
510 if (node->mNumMeshes == 1) {
511 handleAssImpMesh(context, root, modelNode, scene->mMeshes[node->mMeshes[0]], localTransform, globalTransform);
512 } else {
513 for (uint32_t m = 0; m < node->mNumMeshes; m++) {
514 aiMesh * mesh = scene->mMeshes[node->mMeshes[m]];
515
516 root.parts.emplace_back();
517 auto & modelPart = root.parts.back();
518 modelPart.parentIndex = (uint32_t)index;
519 handleAssImpMesh(context, root, modelPart, mesh, localTransform, globalTransform);
520 }
521 }
522
523 for (uint32_t c = 0; c < node->mNumChildren; c++) {
524 aiNode * child = node->mChildren[c];
525
526 handleAssImpNode(context, root, index, scene, child, globalTransform);
527 }
528}
529
530Cogs::SamplerState::AddressMode getAddressMode(aiTextureMapMode aiMode)
531{
532 switch (aiMode) {
533 case aiTextureMapMode_Wrap:
535 case aiTextureMapMode_Clamp:
537 case aiTextureMapMode_Decal:
539 case aiTextureMapMode_Mirror:
541 default:
543 }
544}
545
546bool loadTexture(Cogs::Core::Context * context,
547 const aiScene * scene,
548 const aiMaterial * aiMaterial,
550 std::string texturePath,
551 aiTextureType textureType,
552 const Cogs::Core::VariableKey key)
553{
554 aiString textureresourceName;
555 aiTextureMapMode mode[2] = {}; //separate modes for U and V axis
556 if (aiMaterial->GetTexture(textureType, 0, &textureresourceName, 0, 0, 0, 0, mode) == aiReturn_SUCCESS) {
557
558#ifdef _WIN32
559 std::string textureFullPath = texturePath == "#" ?
560 //If texturePath is # it means we have a .fbm folder and all textures should be fetched from there:
561 Cogs::IO::fileName(textureresourceName.C_Str()) :
562 Cogs::IO::combine(texturePath, textureresourceName.C_Str());
563#else
564 std::string textureresourceNameStr(textureresourceName.C_Str());
565 std::replace(textureresourceNameStr.begin(), textureresourceNameStr.end(), '\\', '/');
566 std::string textureFullPath = texturePath == "#" ?
567 Cogs::IO::fileName(textureresourceNameStr) :
568 Cogs::IO::combine(texturePath, textureresourceNameStr);
569#endif
570
571 auto textureFilename = IO::fileName(textureFullPath);
572
573 auto flags = (textureType == aiTextureType_DIFFUSE) ? TextureLoadFlags::None : TextureLoadFlags::LinearColorSpace;
574
575 Cogs::Core::TextureHandle textureHandle;
576 if (textureFilename.size() > 0 && textureFilename[0] == '*') {
577 unsigned int textureIndex;
578#ifdef _WIN32
579 if (sscanf_s(textureFilename.c_str(), "*%u", &textureIndex) != 1) {
580#else
581 if (sscanf(textureFilename.c_str(), "*%u", &textureIndex) != 1) {
582#endif
583 LOG_WARNING(logger, "Invalid embedded texture name '%s'.", textureFilename.c_str());
584 return false;
585 }
586
587 if (textureIndex >= scene->mNumTextures) {
588 LOG_WARNING(logger, "Texture index to big: %u out of %u.", textureIndex, scene->mNumTextures);
589 return false;
590 }
591
592 auto textureData = scene->mTextures[textureIndex];
593 if (textureData->mHeight == 0) {
594 static size_t counter = 0;
595 std::string resourceName = "Assimp_embedded_resource_" + std::to_string(counter++) + "." + textureData->achFormatHint;
596
597 LOG_DEBUG(logger, "Loading embedded, compressed texture. Name: '%s'", resourceName.c_str());
598
599 context->resourceStore->addResource(resourceName, std::string(reinterpret_cast<const char*>(textureData->pcData), textureData->mWidth));
600
601 textureHandle = context->textureManager->loadTexture(resourceName, NoResourceId, flags);
602 } else {
603 LOG_DEBUG(logger, "Loading embedded, un-compressed texture. Hint: '%s'", textureData->achFormatHint);
604 // TODO: Hint can in theory specify different formats other than RGBA8. May need fixing.
605 textureHandle = context->textureManager->loadTexture2D(textureData->pcData, textureData->mWidth, textureData->mHeight, Cogs::TextureFormat::R8G8B8A8_UNORM, 0, NoResourceId, flags);
606 }
607 } else {
608 textureHandle = context->textureManager->loadTexture(textureFullPath, NoResourceId, flags);
609 }
610
611 if (key != Cogs::Core::NoProperty) {
612 material.setTextureProperty(key, textureHandle);
613 material.setTextureAddressMode(key, getAddressMode(mode[0]));
614 }
615
616 return true;
617 }
618
619 return false;
620}
621
622void Cogs::Core::AssetImporterLoader::handleAssImpMaterial(Context * context, ModelLoadFlags flags, const aiScene * scene, const aiMaterial * aiMaterial, MaterialInstance & material, std::string texturePath)
623{
624 aiString materialName;
625 aiMaterial->Get(AI_MATKEY_NAME, materialName);
626
627 if (materialName.length) {
628 material.setName(materialName.C_Str());
629 }
630
631 float opacity = 1;
632 if (aiMaterial->Get(AI_MATKEY_OPACITY, opacity) != aiReturn_SUCCESS) {
633 opacity = 1;
634 }
635
636 float specularPower;
637 if (aiMaterial->Get(AI_MATKEY_SHININESS, specularPower) == aiReturn_SUCCESS && specularPower != 0) {
638 material.setFloatProperty(DefaultMaterial::SpecularPower, specularPower);
639 }
640
641 int twoSided = 0;
642 if ((aiMaterial->Get(AI_MATKEY_TWOSIDED, twoSided) == aiReturn_SUCCESS && twoSided != 0) ||
645 }
646
647 int lightModel = 0;
648 if (aiMaterial->Get(AI_MATKEY_SHADING_MODEL, lightModel) == aiReturn_SUCCESS && lightModel == aiShadingMode_NoShading) {
649 material.setVariant("LightModel", "BaseColor");
650 }
651 if (opacity < 1) {
652 material.setTransparent();
653 } else if ((flags & ModelLoadFlags::AutomaticTransparency) != 0) {
654 material.options.transparencyMode = TransparencyMode::Auto;
655 } else {
656 material.options.transparencyMode = TransparencyMode::Off;
657 }
658
659 auto checkColor = [&](const char * key, int type, int index, const VariableKey & name, float opacity, bool is4Component = false)
660 {
661 aiColor3D checkedColor;
662 if (aiMaterial->Get(key, type, index, checkedColor) == aiReturn_SUCCESS) {
663 // We only want to register the property if it is actually present, if not fall back to default.
664 if (is4Component) {
665 material.setVec4Property(name, glm::vec4(checkedColor.r, checkedColor.g, checkedColor.b, opacity));
666 } else {
667 material.setVec3Property(name, glm::vec3(checkedColor.r, checkedColor.g, checkedColor.b));
668 }
669 }
670 };
671
672 checkColor(AI_MATKEY_COLOR_DIFFUSE, DefaultMaterial::DiffuseColor, opacity, true);
673 checkColor(AI_MATKEY_COLOR_SPECULAR, DefaultMaterial::SpecularColor, 1);
674 checkColor(AI_MATKEY_COLOR_EMISSIVE, DefaultMaterial::EmissiveColor, 1);
675
676 bool hasDiffuse = loadTexture(context, scene, aiMaterial, material, texturePath, aiTextureType_DIFFUSE, DefaultMaterial::DiffuseMap);
677 bool hasSpecular = loadTexture(context, scene, aiMaterial, material, texturePath, aiTextureType_SPECULAR, DefaultMaterial::SpecularMap);
678 bool hasNormal = loadTexture(context, scene, aiMaterial, material, texturePath, aiTextureType_NORMALS, DefaultMaterial::NormalMap);
679 bool hasOpacity = loadTexture(context, scene, aiMaterial, material, texturePath, aiTextureType_OPACITY, DefaultMaterial::OpacityMap);
680
681 if (hasOpacity && hasDiffuse) {
682 material.options.transparencyMode = TransparencyMode::Alpha;
683 material.setVariant("AlphaTest", "Discard");
684 }
685
686 material.setPermutation("Default");
687 material.setVariant("EnableLighting", "true");
688
689 if (hasSpecular || hasNormal || hasDiffuse) {
690 material.setVariant("Textured", "true");
691
692 if (hasNormal) {
693 material.setVariant("TangentSpaceNormalMap", "true");
694 }
695
696 if (hasSpecular) {
697 material.setVariant("SpecularMap", "true");
698 }
699 }
700
701 if (hasBones(scene, scene->mRootNode)) {
702 material.setVariant("Skinned", true);
703 }
704}
705
706bool Cogs::Core::AssetImporterLoader::canLoad(Context * /*context*/, const ModelLoadInfo & loadInfo)
707{
708 auto extension = IO::extension(loadInfo.resourcePath);
709#ifdef COGS_EXTENSIONS_TINYOBJLOADER
710 if (extension == ".obj") return false;
711#endif
712 if (extension == ".gltf") return false;
713 if (extension == ".glb") return false;
714 if (extension == ".rvm") return false;
715 if (extension == ".rvmdump") return false;
716
717 //TODO: This is probably a little slow. Work around using a static thread local
718 // instance or smth.
719 Assimp::Importer importer;
720 return importer.IsExtensionSupported(extension);
721}
722
723void addBones(Skeleton & skeleton, const aiNode * node, size_t parentIndex)
724{
725 auto localTransform = toGlm(node->mTransformation);
726
727 std::string name = node->mName.C_Str();
728
729 auto bone = Bone{ name, parentIndex, glm::vec3(0.0), glm::vec3(1.0), glm::quat(), localTransform, glm::mat4(), glm::mat4(), false, false, false };
730
731 skeleton.bones.emplace_back(bone);
732
733 const uint32_t index = static_cast<uint32_t>(skeleton.bones.size()) - 1;
734
735 skeleton.bonesByName[name] = index;
736
737 for (size_t i = 0; i < node->mNumChildren; ++i) {
738 addBones(skeleton, node->mChildren[i], index);
739 }
740}
741
742void buildSkeleton(Model & model, const aiNode * node)
743{
744 auto & skeleton = model.skeleton;
745
746 addBones(skeleton, node, (size_t)-1);
747}
748
749void updateSkeleton(Skeleton & skeleton)
750{
751 if (skeleton.bones.empty()) return;
752
753 for (auto & bone : skeleton.bones) {
754 bool isRoot = bone.parentBone == (size_t)-1;
755
756 if (isRoot) {
757 bone.absolute = bone.relative;
758 } else {
759 auto & parent = skeleton.bones[bone.parentBone];
760
761 bone.absolute = parent.absolute * bone.relative;
762 }
763 }
764}
765
766void handleAnimations(Context * context, Model & model, const aiScene * scene)
767{
768 model.animation = context->animationManager->create();
769
770 auto & animation = *model.animation.resolve();
771
772 auto & skeleton = model.skeleton;
773
774 auto numAnims = scene->mNumAnimations;
775
776 for (size_t i = 0; i < numAnims; ++i) {
777 auto & aAnim = scene->mAnimations[i];
778
779 animation.clips.emplace_back();
780 auto & clip = animation.clips.back();
781
782 clip.name = aAnim->mName.C_Str();
783 clip.duration = static_cast<float>(aAnim->mDuration);
784 clip.resolution = static_cast<float>(aAnim->mTicksPerSecond);
785
786 auto numChannels = aAnim->mNumChannels;
787
788 clip.tracks.resize(numChannels);
789
790 for (size_t j = 0; j < numChannels; ++j) {
791 auto & aChannel = aAnim->mChannels[j];
792 std::string channelName = aChannel->mNodeName.C_Str();
793
794 auto & track = clip.tracks[j];
795 track.boneIndex = (uint32_t)-1;
796
797 if (skeleton.bonesByName.find(channelName) == model.skeleton.bonesByName.end()) {
798
799 } else {
800 auto boneIndex = model.skeleton.bonesByName[channelName];
801
802 track.boneIndex = boneIndex;
803 model.skeleton.bones[boneIndex].animated = true;
804
805 for (size_t k = 0; k < aChannel->mNumPositionKeys; ++k) {
806 const float time = static_cast<float>(aChannel->mPositionKeys[k].mTime);
807 const glm::vec3 translation = toGlm(aChannel->mPositionKeys[k].mValue);
808
809 track.translations.emplace_back(TranslationKey{ time, translation });
810 }
811
812 for (size_t k = 0; k < aChannel->mNumRotationKeys; ++k) {
813 const float time = static_cast<float>(aChannel->mRotationKeys[k].mTime);
814 const glm::quat rotation = toGlm(aChannel->mRotationKeys[k].mValue);
815
816 track.rotations.emplace_back(RotationKey{ time, rotation });
817 }
818
819 for (size_t k = 0; k < aChannel->mNumScalingKeys; ++k) {
820 const float time = static_cast<float>(aChannel->mScalingKeys[k].mTime);
821 const glm::vec3 scale = toGlm(aChannel->mScalingKeys[k].mValue);
822
823 track.scales.emplace_back(ScaleKey{ time, scale });
824 }
825 }
826 }
827 }
828}
829
830inline bool isFolder(std::string path)
831{
832 struct stat sb;
833 if (stat(path.c_str(), &sb) != 0) return false;
834
835 auto dirMode = sb.st_mode & S_IFDIR;
836 return dirMode != 0;
837}
838
839inline std::string toLower(std::string const & str)
840{
841 std::string ret(str);
842 for (auto& c : ret) {
843 c = static_cast<decltype(ret)::value_type>(std::tolower(c));
844 }
845 return ret;
846}
847
848inline bool endsWith(std::string const & value, std::string const & ending)
849{
850 if (ending.size() > value.size()) return false;
851 return std::equal(ending.rbegin(), ending.rend(), value.rbegin());
852}
853
854bool Cogs::Core::AssetImporterLoader::load(Context * context, const ModelLoadInfo & loadInfo)
855{
856 auto timer = Timer::startNew();
857
858 LOG_DEBUG(logger, "Loading model: %s", loadInfo.resourceName.c_str());
859
860 auto & modelManager = *context->modelManager;
861 auto & materialManager = *context->materialManager;
862 auto & materialInstanceManager = *context->materialInstanceManager;
863
864 if (!loadInfo.resourcePath.size() && !loadInfo.resourceData.size()) return false;
865
866 Assimp::Importer importer;
867
868 importer.SetIOHandler(new IOSystem(context));
869
870 const bool optimize = (loadInfo.modelFlags & ModelLoadFlags::DisableOptimization) == ModelLoadFlags::None;
871 const bool tangents = (loadInfo.modelFlags & ModelLoadFlags::NeedsTangents) != ModelLoadFlags::None;
872
873 int flags =
874 aiProcess_RemoveRedundantMaterials |
875 aiProcess_GenNormals |
876 aiProcess_FlipWindingOrder |
877 aiProcess_Triangulate;
878
879
880 if ((loadInfo.modelFlags & ModelLoadFlags::NoFlipTexcoords) == ModelLoadFlags::None) {
881 flags |= aiProcess_FlipUVs;
882 }
883
884 if (optimize) {
885 flags |= aiProcess_ImproveCacheLocality |
886 aiProcess_SortByPType |
887 aiProcess_GenUVCoords |
888 aiProcess_OptimizeMeshes;
889 }
890
891 if (tangents) {
892 flags |= aiProcess_CalcTangentSpace;
893 }
894
895 const aiScene * scene = nullptr;
896 bool hasMatLib = false;
897
898 if (loadInfo.resourceData.size()) {
899 scene = importer.ReadFileFromMemory(
900 loadInfo.resourceData.data(),
901 loadInfo.resourceData.size(),
902 flags,
903 loadInfo.resourcePath.size() ? loadInfo.resourcePath.c_str() : "");
904 if (!scene)
905 LOG_ERROR(logger, "Data load size: %zu", loadInfo.resourceData.size());
906 } else {
907
908 //check if FBX...
909 if (endsWith(toLower(loadInfo.resourcePath), ".fbx")) {
910 //Check if matching .fbm folder exists...
911 auto path = context->resourceStore->getResourcePath(loadInfo.resourcePath);
912 auto pos = path.find_last_of(".");
913 auto base = path.substr(0, pos); //path up to .fbx
914 auto materialFolder = base + ".fbm";
915 if (isFolder(materialFolder)) {
916 context->resourceStore->addSearchPath(materialFolder + IO::pathSeparator());
917 hasMatLib = true;
918 }
919 }
920
921 scene = importer.ReadFile(loadInfo.resourcePath, flags);
922 if (!scene) {
923 auto data = context->resourceStore->getResourceContents(loadInfo.resourcePath);
924 if (data.size()) {
925 scene = importer.ReadFileFromMemory(data.data(),
926 data.size(),
927 flags,
928 loadInfo.resourcePath.size() ? loadInfo.resourcePath.c_str() : "");
929 }
930 }
931 }
932
933 if (!scene) {
934 LOG_ERROR(logger, "Error loading model: %s", importer.GetErrorString());
935 return false;
936 }
937
938 auto model = modelManager.lock(loadInfo.handle);
939
940 bool needsTangents = false;
941
942 //FIXME: Should use a cleaner way of indicating that we should strip path from texture filenames
943 const std::string texturePath = hasMatLib ? "#" : IO::parentPath(loadInfo.resourcePath);
944
945 for (uint32_t m = 0; m < scene->mNumMaterials; m++) {
946 const aiMaterial * aiMat = scene->mMaterials[m];
947
948 MaterialInstanceHandle materialHandle = materialInstanceManager.createMaterialInstance(materialManager.getDefaultMaterial());
949 MaterialInstance* material = materialInstanceManager.get(materialHandle);
950
951 handleAssImpMaterial(context, loadInfo.modelFlags, scene, aiMat, *material, texturePath);
952
953
954 if (material->getVariant("TangentSpaceNormalMap") == "true") {
955 needsTangents = true;
956 }
957
958 model->materials.push_back(materialHandle);
959 }
960
961 if (needsTangents || (!tangents && optimize)) {
962 importer.ApplyPostProcessing(aiProcess_CalcTangentSpace);
963 }
964
965 if (hasBones(scene, scene->mRootNode)) {
966 buildSkeleton(*model, scene->mRootNode);
967 updateSkeleton(model->skeleton);
968 }
969
970 if (scene->HasAnimations()) {
971 handleAnimations(context, *model, scene);
972 }
973
974 glm::mat4 rootTransform(1.0f);
975 handleAssImpNode(context, *model, NoParentPart, scene, scene->mRootNode, rootTransform);
976
977 if (model->animation) {
978 model->animation->skeleton = model->skeleton;
979 }
980
981 LOG_TRACE(logger, "Assimp elapsed: %f.", timer.elapsedSeconds());
982
983 return true;
984}
985
986// Check recursively for any mesh with bones:
987bool Cogs::Core::AssetImporterLoader::hasBones(const aiScene * scene, const aiNode * node)
988{
989 for (uint32_t m = 0; m < node->mNumMeshes; m++) {
990 aiMesh * mesh = scene->mMeshes[node->mMeshes[m]];
991 if (mesh->HasBones()) {
992 return true;
993 }
994 }
995
996 for (uint32_t c = 0; c < node->mNumChildren; c++) {
997 aiNode * child = node->mChildren[c];
998 if (hasBones(scene, child))
999 return true;
1000 }
1001
1002 return false;
1003}
A Context instance contains all the services, systems and runtime components needed to use Cogs.
Definition: Context.h:83
std::unique_ptr< class Variables > variables
Variables service instance.
Definition: Context.h:180
std::unique_ptr< class ResourceStore > resourceStore
ResourceStore service instance.
Definition: Context.h:210
Log implementation class.
Definition: LogManager.h:140
Provides a weakly referenced view over the contents of a string.
Definition: StringView.h:50
Contains the Engine, Renderer, resource managers and other systems needed to run Cogs....
ModelLoadFlags
Model loading flags. May be combined with resource loading flags.
@ NeedsTangents
The importer should generate tangent data if not present.
@ NoFlipTexcoords
Do not flip texture coordinates.
@ LinearColorSpace
For textures with RGBA format without color space information, mark the data as being in linear color...
Cogs::Geometry::BoundingBox COGSCORE_DLL_API calculateBounds(Mesh *mesh)
Calculate a bounding box for the given mesh.
Definition: MeshHelper.cpp:283
@ None
No primitive culling performed.
uint16_t VariableKey
Used to lookup material properties.
Definition: Resources.h:46
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:181
Contains all Cogs related functionality.
Definition: FieldSetter.h:23
@ LineList
List of lines.
@ TriangleList
List of triangles.
Material instances represent a specialized Material combined with state for all its buffers and prope...
void setFloatProperty(const VariableKey key, float value)
Set the float property with the given key to value.
void setTransparent()
Set the material instance to transparent, indicating to the renderer that blending should be enabled ...
void setVec3Property(const VariableKey key, glm::vec3 value)
Set the vec3 property with the given key to value.
void setTextureAddressMode(const StringView &key, const StringView &addressMode)
Set texture address mode with textual name.
void setTextureProperty(const StringView &key, TextureHandle value)
Set the texture property with the given key to the texture resource held by value.
MaterialOptions options
Material rendering options used by this instance.
void setVec4Property(const VariableKey key, glm::vec4 value)
Set the vec4 property with the given key to value.
TransparencyMode transparencyMode
Transparency mode.
CullMode cullMode
Primitive culling mode to use.
@ ClockwiseWinding
The mesh uses clockwise winding order for it's triangles as opposed to the counter-clockwise default.
Definition: Mesh.h:63
Meshes contain streams of vertex data in addition to index data and options defining geometry used fo...
Definition: Mesh.h:265
void setTangents(std::span< const glm::vec3 > tangents)
Set the tangent data of the Mesh.
Definition: Mesh.h:448
void setIndexes(std::span< const uint32_t > collection)
Set the index data to the collection given.
Definition: Mesh.h:596
void setTexCoords(std::span< const glm::vec2 > texCoords)
Set the texture coordinate data of the Mesh.
Definition: Mesh.h:504
void setBounds(Geometry::BoundingBox box)
Set custom bounds for the mesh.
Definition: Mesh.h:298
void set(const VertexDataType::EVertexDataType type, VertexFormatHandle format, Element *begin, Element *end)
Set the data of the vertex stream indexed by type.
Definition: Mesh.h:792
void setNormals(std::span< const glm::vec3 > normals)
Set the normal data of the Mesh.
Definition: Mesh.h:392
void setMeshFlag(MeshFlags::EMeshFlags flag)
Set the given mesh flag.
Definition: Mesh.h:659
void setPositions(std::span< const glm::vec3 > positions)
Set the position data of the Mesh.
Definition: Mesh.h:315
Model resources define a template for a set of connected entities, with resources such as meshes,...
Definition: Model.h:56
void setName(const StringView &name)
Set the user friendly name of the resource.
Definition: ResourceBase.h:298
ResourceType * resolve() const
Resolve the handle, returning a pointer to the actual resource.
std::string resourcePath
Resource path. Used to locate resource.
std::string resourceName
Desired resource name. If no name is given, a default name will be chosen.
ResourceHandleBase handle
Handle to resource structure for holding actual resource data.
std::vector< uint8_t > resourceData
Resource load data.
AddressMode
Addressing modes to use when sampling textures.
Definition: SamplerState.h:15
@ Clamp
Texture coordinates are clamped to the [0, 1] range.
Definition: SamplerState.h:17
@ Border
Texture color is set to the border color when outside [0, 1] range.
Definition: SamplerState.h:23
@ Wrap
Texture coordinates automatically wrap around to [0, 1] range.
Definition: SamplerState.h:19
@ Mirror
Texture coordinates are mirrored when outside [0, 1] range.
Definition: SamplerState.h:21