1 /*
2     This file is part of Magnum.
3 
4     Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019
5               Vladimír Vondruš <mosra@centrum.cz>
6     Copyright © 2017 Jonathan Hale <squareys@googlemail.com>
7 
8     Permission is hereby granted, free of charge, to any person obtaining a
9     copy of this software and associated documentation files (the "Software"),
10     to deal in the Software without restriction, including without limitation
11     the rights to use, copy, modify, merge, publish, distribute, sublicense,
12     and/or sell copies of the Software, and to permit persons to whom the
13     Software is furnished to do so, subject to the following conditions:
14 
15     The above copyright notice and this permission notice shall be included
16     in all copies or substantial portions of the Software.
17 
18     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21     THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23     FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24     DEALINGS IN THE SOFTWARE.
25 */
26 
27 #include "AssimpImporter.h"
28 
29 #include <cstring>
30 #include <algorithm>
31 #include <unordered_map>
32 #include <Corrade/Containers/ArrayView.h>
33 #include <Corrade/Containers/Optional.h>
34 #include <Corrade/Utility/ConfigurationGroup.h>
35 #include <Corrade/Utility/DebugStl.h>
36 #include <Corrade/Utility/Directory.h>
37 #include <Corrade/Utility/String.h>
38 #include <Magnum/FileCallback.h>
39 #include <Magnum/Mesh.h>
40 #include <Magnum/Math/Vector.h>
41 #include <Magnum/PixelFormat.h>
42 #include <Magnum/PixelStorage.h>
43 #include <Magnum/Trade/CameraData.h>
44 #include <Magnum/Trade/ImageData.h>
45 #include <Magnum/Trade/LightData.h>
46 #include <Magnum/Trade/MeshData3D.h>
47 #include <Magnum/Trade/MeshObjectData3D.h>
48 #include <Magnum/Trade/PhongMaterialData.h>
49 #include <Magnum/Trade/SceneData.h>
50 #include <Magnum/Trade/TextureData.h>
51 
52 #include <MagnumPlugins/AnyImageImporter/AnyImageImporter.h>
53 
54 #include <assimp/postprocess.h>
55 #include <assimp/Importer.hpp>
56 #include <assimp/IOStream.hpp>
57 #include <assimp/IOSystem.hpp>
58 #include <assimp/scene.h>
59 
60 namespace Magnum { namespace Math { namespace Implementation {
61 
62 template<> struct VectorConverter<3, Float, aiColor3D> {
fromMagnum::Math::Implementation::VectorConverter63     static Vector<3, Float> from(const aiColor3D& other) {
64         return {other.r, other.g, other.b};
65     }
66 };
67 
68 }}}
69 
70 namespace Magnum { namespace Trade {
71 
72 struct AssimpImporter::File {
73     Containers::Optional<std::string> filePath;
74     const aiScene* scene = nullptr;
75     std::vector<aiNode*> nodes;
76     std::vector<std::tuple<const aiMaterial*, aiTextureType, UnsignedInt>> textures;
77     std::vector<std::pair<const aiMaterial*, aiTextureType>> images;
78 
79     std::unordered_map<const aiNode*, UnsignedInt> nodeIndices;
80     std::unordered_map<const aiNode*, std::pair<Trade::ObjectInstanceType3D, UnsignedInt>> nodeInstances;
81     std::unordered_map<std::string, UnsignedInt> materialIndicesForName;
82     std::unordered_map<const aiMaterial*, UnsignedInt> textureIndices;
83 };
84 
85 namespace {
86 
fillDefaultConfiguration(Utility::ConfigurationGroup & conf)87 void fillDefaultConfiguration(Utility::ConfigurationGroup& conf) {
88     /** @todo horrible workaround, fix this properly */
89     Utility::ConfigurationGroup& postprocess = *conf.addGroup("postprocess");
90     postprocess.setValue("JoinIdenticalVertices", true);
91     postprocess.setValue("Triangulate", true);
92     postprocess.setValue("SortByPType", true);
93 }
94 
95 }
96 
AssimpImporter()97 AssimpImporter::AssimpImporter(): _importer{new Assimp::Importer} {
98     /** @todo horrible workaround, fix this properly */
99     fillDefaultConfiguration(configuration());
100 }
101 
AssimpImporter(PluginManager::Manager<AbstractImporter> & manager)102 AssimpImporter::AssimpImporter(PluginManager::Manager<AbstractImporter>& manager): AbstractImporter(manager), _importer{new Assimp::Importer} {
103     /** @todo horrible workaround, fix this properly */
104     fillDefaultConfiguration(configuration());
105 }
106 
AssimpImporter(PluginManager::AbstractManager & manager,const std::string & plugin)107 AssimpImporter::AssimpImporter(PluginManager::AbstractManager& manager, const std::string& plugin): AbstractImporter(manager, plugin), _importer{new Assimp::Importer}  {}
108 
109 AssimpImporter::~AssimpImporter() = default;
110 
doFeatures() const111 auto AssimpImporter::doFeatures() const -> Features { return Feature::OpenData|Feature::OpenState|Feature::FileCallback; }
112 
doIsOpened() const113 bool AssimpImporter::doIsOpened() const { return _f && _f->scene; }
114 
115 namespace {
116 
117 struct IoStream: Assimp::IOStream {
IoStreamMagnum::Trade::__anonced598630211::IoStream118     explicit IoStream(std::string filename, Containers::ArrayView<const char> data): filename{std::move(filename)}, _data{data}, _pos{} {}
119 
120     /* mimics fread() / fseek() */
ReadMagnum::Trade::__anonced598630211::IoStream121     std::size_t Read(void* buffer, std::size_t size, std::size_t count) override {
122         const Containers::ArrayView<const char> slice = _data.suffix(_pos);
123         const std::size_t maxCount = Math::min(slice.size()/size, count);
124         std::memcpy(buffer, slice.begin(), size*maxCount);
125         _pos += size*maxCount;
126         return maxCount;
127     }
SeekMagnum::Trade::__anonced598630211::IoStream128     aiReturn Seek(std::size_t offset, aiOrigin origin) override {
129         if(origin == aiOrigin_SET && offset < _data.size())
130             _pos = offset;
131         else if(origin == aiOrigin_CUR && _pos + offset < _data.size())
132             _pos += offset;
133         else if(origin == aiOrigin_END && _data.size() + std::ptrdiff_t(offset) < _data.size())
134             _pos = _data.size() + std::ptrdiff_t(offset);
135         else return aiReturn_FAILURE;
136         return aiReturn_SUCCESS;
137     }
TellMagnum::Trade::__anonced598630211::IoStream138     std::size_t Tell() const override { return _pos; }
FileSizeMagnum::Trade::__anonced598630211::IoStream139     std::size_t FileSize() const override { return _data.size(); }
140 
141     /* We are just a reader */
142     /* LCOV_EXCL_START */
WriteMagnum::Trade::__anonced598630211::IoStream143     std::size_t Write(const void*, std::size_t, std::size_t) override {
144         std::abort();
145     }
FlushMagnum::Trade::__anonced598630211::IoStream146     void Flush() override {
147         std::abort();
148     }
149     /* LCOV_EXCL_STOP */
150 
151     std::string filename; /* needed for closing properly on the user side */
152     private:
153         Containers::ArrayView<const char> _data;
154         std::size_t _pos;
155 };
156 
157 struct IoSystem: Assimp::IOSystem {
IoSystemMagnum::Trade::__anonced598630211::IoSystem158     explicit IoSystem(Containers::Optional<Containers::ArrayView<const char>>(*callback)(const std::string&, InputFileCallbackPolicy, void*), void* userData): _callback{callback}, _userData{userData} {}
159 
160     /* What else? I can't know. */
ExistsMagnum::Trade::__anonced598630211::IoSystem161     bool Exists(const char*) const override { return true; }
162 
163     /* The file callback on user side has to deal with this */
getOsSeparatorMagnum::Trade::__anonced598630211::IoSystem164     char getOsSeparator() const override { return '/'; }
165 
OpenMagnum::Trade::__anonced598630211::IoSystem166     Assimp::IOStream* Open(const char* file, const char* mode) override {
167         CORRADE_INTERNAL_ASSERT(mode == std::string{"rb"});
168         const Containers::Optional<Containers::ArrayView<const char>> data = _callback(file, InputFileCallbackPolicy::LoadTemporary, _userData);
169         if(!data) return {};
170         return new IoStream{file, *data};
171     }
172 
CloseMagnum::Trade::__anonced598630211::IoSystem173     void Close(Assimp::IOStream* file) override {
174         _callback(static_cast<IoStream*>(file)->filename, InputFileCallbackPolicy::Close, _userData);
175         delete file;
176     }
177 
178     Containers::Optional<Containers::ArrayView<const char>>(*_callback)(const std::string&, InputFileCallbackPolicy, void*);
179     void* _userData;
180 };
181 
182 }
183 
doSetFileCallback(Containers::Optional<Containers::ArrayView<const char>> (* callback)(const std::string &,InputFileCallbackPolicy,void *),void * userData)184 void AssimpImporter::doSetFileCallback(Containers::Optional<Containers::ArrayView<const char>>(*callback)(const std::string&, InputFileCallbackPolicy, void*), void* userData) {
185     if(callback) {
186         _importer->SetIOHandler(_ourFileCallback = new IoSystem{callback, userData});
187 
188     /* Passing nullptr to Assimp will deliberately leak the previous IOSystem
189        instance (whereas passing a non-null instance will delete the previous
190        one). That means we need to keep track of our file callbacks and delete
191        them manually. */
192     } else if(_importer->GetIOHandler() == _ourFileCallback) {
193         delete _ourFileCallback;
194         _importer->SetIOHandler(nullptr);
195         _ourFileCallback = nullptr;
196     }
197 }
198 
199 namespace {
200 
flagsFromConfiguration(Utility::ConfigurationGroup & conf)201 UnsignedInt flagsFromConfiguration(Utility::ConfigurationGroup& conf) {
202     UnsignedInt flags = 0;
203     const Utility::ConfigurationGroup& postprocess = *conf.group("postprocess");
204     #define _c(val) if(postprocess.value<bool>(#val)) flags |= aiProcess_ ## val;
205     /* Without aiProcess_JoinIdenticalVertices all meshes are deindexed (wtf?) */
206     _c(JoinIdenticalVertices) /* enabled by default */
207     _c(Triangulate) /* enabled by default */
208     _c(GenNormals)
209     _c(GenSmoothNormals)
210     _c(SplitLargeMeshes)
211     _c(PreTransformVertices)
212     _c(ValidateDataStructure)
213     _c(ImproveCacheLocality)
214     _c(RemoveRedundantMaterials)
215     _c(FixInfacingNormals)
216     _c(SortByPType) /* enabled by default */
217     _c(FindDegenerates)
218     _c(FindInvalidData)
219     _c(GenUVCoords)
220     _c(TransformUVCoords)
221     _c(FindInstances)
222     _c(OptimizeMeshes)
223     _c(OptimizeGraph)
224     _c(FlipUVs)
225     _c(FlipWindingOrder)
226     #undef _c
227     return flags;
228 }
229 
230 }
231 
doOpenData(const Containers::ArrayView<const char> data)232 void AssimpImporter::doOpenData(const Containers::ArrayView<const char> data) {
233     if(!_f) {
234         _f.reset(new File);
235         /* File callbacks are set up in doSetFileCallbacks() */
236         if(!(_f->scene = _importer->ReadFileFromMemory(data.data(), data.size(), flagsFromConfiguration(configuration())))) {
237             Error{} << "Trade::AssimpImporter::openData(): loading failed:" << _importer->GetErrorString();
238             return;
239         }
240     }
241 
242     CORRADE_INTERNAL_ASSERT(_f->scene);
243 
244     /* Fill hashmaps for index lookup for materials/textures/meshes/nodes */
245     _f->materialIndicesForName.reserve(_f->scene->mNumMaterials);
246 
247     aiString matName;
248     aiString texturePath;
249     Int textureIndex = 0;
250     std::unordered_map<std::string, UnsignedInt> uniqueImages;
251     for(std::size_t i = 0; i < _f->scene->mNumMaterials; ++i) {
252         const aiMaterial* mat = _f->scene->mMaterials[i];
253 
254         if(mat->Get(AI_MATKEY_NAME, matName) == AI_SUCCESS) {
255             std::string name = matName.C_Str();
256             _f->materialIndicesForName[name] = i;
257         }
258 
259         /* Store first possible texture index for this material, next textures
260            use successive indices. For images ensure we have an unique set so
261            each file isn't imported more than once. */
262         _f->textureIndices[mat] = textureIndex;
263         for(auto type: {aiTextureType_AMBIENT, aiTextureType_DIFFUSE, aiTextureType_SPECULAR}) {
264             if(mat->Get(AI_MATKEY_TEXTURE(type, 0), texturePath) == AI_SUCCESS) {
265                 auto uniqueImage = uniqueImages.emplace(texturePath.C_Str(), _f->images.size());
266                 if(uniqueImage.second) _f->images.emplace_back(mat, type);
267 
268                 _f->textures.emplace_back(mat, type, uniqueImage.first->second);
269                 ++textureIndex;
270             }
271         }
272     }
273 
274     /* For some formats (such as COLLADA) Assimp fails to open the scene if
275        there are no nodes, so there this is always non-null. For other formats
276        (such as glTF) Assimp happily provides a null root node, even thought
277        that's not the documented behavior. */
278     aiNode* const root = _f->scene->mRootNode;
279     if(root) {
280         /* I would assert here on !root->mNumMeshes to verify I didn't miss
281            anything in the root node, but at least for COLLADA, if the file has
282            no meshes, it adds some bogus one, thinking it's a skeleton-only
283            file and trying to be helpful. Ugh.
284            https://github.com/assimp/assimp/blob/92078bc47c462d5b643aab3742a8864802263700/code/ColladaLoader.cpp#L225 */
285 
286         /* If there is more than just a root node, extract children of the root
287            node, as we treat the root node as the scene here and it has no
288            transformation or anything attached. */
289         if(root->mNumChildren) {
290             _f->nodes.reserve(root->mNumChildren);
291             _f->nodes.insert(_f->nodes.end(), root->mChildren, root->mChildren + root->mNumChildren);
292             _f->nodeIndices.reserve(root->mNumChildren);
293 
294         /* In some pathological cases there's just one root node --- for
295            example the DART integration depends on that. Import it as a single
296            node. */
297         } else {
298             _f->nodes.push_back(root);
299             _f->nodeIndices.reserve(1);
300         }
301 
302         /* Insert may invalidate iterators, so we use indices here. */
303         for(std::size_t i = 0; i < _f->nodes.size(); ++i) {
304             aiNode* node = _f->nodes[i];
305             _f->nodeIndices[node] = UnsignedInt(i);
306 
307             _f->nodes.insert(_f->nodes.end(), node->mChildren, node->mChildren + node->mNumChildren);
308 
309             if(node->mNumMeshes > 0) {
310                 /** @todo: Support multiple meshes per node */
311                 _f->nodeInstances[node] = {ObjectInstanceType3D::Mesh, node->mMeshes[0]};
312             }
313         }
314 
315         for(std::size_t i = 0; i < _f->scene->mNumCameras; ++i) {
316             const aiNode* cameraNode = _f->scene->mRootNode->FindNode(_f->scene->mCameras[i]->mName);
317             if(cameraNode) {
318                 _f->nodeInstances[cameraNode] = {ObjectInstanceType3D::Camera, i};
319             }
320         }
321 
322         for(std::size_t i = 0; i < _f->scene->mNumLights; ++i) {
323             const aiNode* lightNode = _f->scene->mRootNode->FindNode(_f->scene->mLights[i]->mName);
324             if(lightNode) {
325                 _f->nodeInstances[lightNode] = {ObjectInstanceType3D::Light, i};
326             }
327         }
328     }
329 }
330 
doOpenState(const void * state,const std::string & filePath)331 void AssimpImporter::doOpenState(const void* state, const std::string& filePath) {
332     _f.reset(new File);
333     _f->scene = static_cast<const aiScene*>(state);
334     _f->filePath = filePath;
335 
336     doOpenData({});
337 }
338 
doOpenFile(const std::string & filename)339 void AssimpImporter::doOpenFile(const std::string& filename) {
340     _f.reset(new File);
341     _f->filePath = Utility::Directory::path(filename);
342 
343     /* File callbacks are set up in doSetFileCallbacks() */
344     if(!(_f->scene = _importer->ReadFile(filename, flagsFromConfiguration(configuration())))) {
345         Error{} << "Trade::AssimpImporter::openFile(): failed to open" << filename << Debug::nospace << ":" << _importer->GetErrorString();
346         return;
347     }
348 
349     doOpenData({});
350 }
351 
doClose()352 void AssimpImporter::doClose() {
353     _importer->FreeScene();
354     _f.reset();
355 }
356 
doDefaultScene()357 Int AssimpImporter::doDefaultScene() { return _f->scene->mRootNode ? 0 : -1; }
358 
doSceneCount() const359 UnsignedInt AssimpImporter::doSceneCount() const { return _f->scene->mRootNode ? 1 : 0; }
360 
doScene(UnsignedInt)361 Containers::Optional<SceneData> AssimpImporter::doScene(UnsignedInt) {
362     const aiNode* root = _f->scene->mRootNode;
363 
364     std::vector<UnsignedInt> children;
365     /* In consistency with the distinction in doOpenData(), if the root node
366        has children, add them directly (and treat the root node as the scene) */
367     if(root->mNumChildren) {
368         children.reserve(root->mNumChildren);
369         for(std::size_t i = 0; i < root->mNumChildren; ++i)
370             children.push_back(_f->nodeIndices[root->mChildren[i]]);
371 
372     /* Otherwise there's just the root node, which is at index 0 */
373     } else children.push_back(0);
374 
375     return SceneData{{}, std::move(children), root};
376 }
377 
doCameraCount() const378 UnsignedInt AssimpImporter::doCameraCount() const {
379     return _f->scene->mNumCameras;
380 }
381 
doCamera(UnsignedInt id)382 Containers::Optional<CameraData> AssimpImporter::doCamera(UnsignedInt id) {
383     const aiCamera* cam = _f->scene->mCameras[id];
384     /** @todo aspect and up vector are not used... */
385     return CameraData{CameraType::Perspective3D, Rad(cam->mHorizontalFOV), 1.0f, cam->mClipPlaneNear, cam->mClipPlaneFar, cam};
386 }
387 
doObject3DCount() const388 UnsignedInt AssimpImporter::doObject3DCount() const {
389     return _f->nodes.size();
390 }
391 
doObject3DForName(const std::string & name)392 Int AssimpImporter::doObject3DForName(const std::string& name) {
393     const aiNode* found = _f->scene->mRootNode->FindNode(aiString(name));
394     return found ? _f->nodeIndices[found] : -1;
395 }
396 
doObject3DName(const UnsignedInt id)397 std::string AssimpImporter::doObject3DName(const UnsignedInt id) {
398     return _f->nodes[id]->mName.C_Str();
399 }
400 
doObject3D(const UnsignedInt id)401 Containers::Pointer<ObjectData3D> AssimpImporter::doObject3D(const UnsignedInt id) {
402     /** @todo support for bone nodes */
403     const aiNode* node = _f->nodes[id];
404 
405     /* Gather child indices */
406     std::vector<UnsignedInt> children;
407     children.reserve(node->mNumChildren);
408     for(auto child: Containers::arrayView(node->mChildren, node->mNumChildren))
409         children.push_back(_f->nodeIndices[child]);
410 
411     /* aiMatrix4x4 is always row-major, transpose */
412     const Matrix4 transformation = Matrix4::from(reinterpret_cast<const float*>(&node->mTransformation)).transposed();
413 
414     auto instance = _f->nodeInstances.find(node);
415     if(instance != _f->nodeInstances.end()) {
416         const ObjectInstanceType3D type = (*instance).second.first;
417         const int index = (*instance).second.second;
418         if(type == ObjectInstanceType3D::Mesh) {
419             const aiMesh* mesh = _f->scene->mMeshes[index];
420             return Containers::pointer(new MeshObjectData3D(children, transformation, index, mesh->mMaterialIndex, node));
421         }
422 
423         return Containers::pointer(new ObjectData3D(children, transformation, type, index, node));
424     }
425 
426     return Containers::pointer(new ObjectData3D(children, transformation, node));
427 }
428 
doLightCount() const429 UnsignedInt AssimpImporter::doLightCount() const {
430     return _f->scene->mNumLights;
431 }
432 
doLight(UnsignedInt id)433 Containers::Optional<LightData> AssimpImporter::doLight(UnsignedInt id) {
434     const aiLight* l = _f->scene->mLights[id];
435 
436     LightData::Type lightType;
437     if(l->mType == aiLightSource_DIRECTIONAL) {
438         lightType = LightData::Type::Infinite;
439     } else if(l->mType == aiLightSource_POINT) {
440         lightType = LightData::Type::Point;
441     } else if(l->mType == aiLightSource_SPOT) {
442         lightType = LightData::Type::Spot;
443     } else {
444         Error() << "Trade::AssimpImporter::light(): light type" << l->mType << "is not supported";
445         return {};
446     }
447 
448     Color3 ambientColor = Color3(l->mColorDiffuse);
449 
450     /** @todo angle inner/outer cone, linear/quadratic/constant attenuation, ambient/specular color are not used */
451     return LightData(lightType, ambientColor, 1.0f, l);
452 }
453 
doMesh3DCount() const454 UnsignedInt AssimpImporter::doMesh3DCount() const {
455     return _f->scene->mNumMeshes;
456 }
457 
doMesh3D(const UnsignedInt id)458 Containers::Optional<MeshData3D> AssimpImporter::doMesh3D(const UnsignedInt id) {
459     const aiMesh* mesh = _f->scene->mMeshes[id];
460 
461     /* Primitive */
462     MeshPrimitive primitive;
463     if(mesh->mPrimitiveTypes == aiPrimitiveType_POINT) {
464         primitive = MeshPrimitive::Points;
465     } else if(mesh->mPrimitiveTypes == aiPrimitiveType_LINE) {
466         primitive = MeshPrimitive::Lines;
467     } else if(mesh->mPrimitiveTypes == aiPrimitiveType_TRIANGLE) {
468         primitive = MeshPrimitive::Triangles;
469     } else {
470         Error() << "Trade::AssimpImporter::mesh3D(): unsupported aiPrimitiveType" << mesh->mPrimitiveTypes;
471         return Containers::NullOpt;
472     }
473 
474     /* Mesh data */
475     std::vector<std::vector<Vector3>> positions{{}};
476     std::vector<std::vector<Vector2>> textureCoordinates;
477     std::vector<std::vector<Vector3>> normals;
478     std::vector<std::vector<Color4>> colors;
479 
480     /* Import positions */
481     auto vertexArray = Containers::arrayCast<Vector3>(Containers::arrayView(mesh->mVertices, mesh->mNumVertices));
482     positions.front().assign(vertexArray.begin(), vertexArray.end());
483 
484     if(mesh->HasNormals()) {
485         normals.emplace_back();
486         auto normalArray = Containers::arrayCast<Vector3>(Containers::arrayView(mesh->mNormals, mesh->mNumVertices));
487         normals.front().assign(normalArray.begin(), normalArray.end());
488     }
489 
490     /** @todo only first uv layer (or "channel") supported) */
491     textureCoordinates.reserve(mesh->GetNumUVChannels());
492     for(std::size_t layer = 0; layer < mesh->GetNumUVChannels(); ++layer) {
493         if(mesh->mNumUVComponents[layer] != 2) {
494             /** @todo Only 2 dimensional texture coordinates supported in MeshData3D */
495             Warning() << "Trade::AssimpImporter::mesh3D(): skipping texture coordinate layer" << layer << "which has" << mesh->mNumUVComponents[layer] << "components per coordinate. Only two dimensional texture coordinates are supported.";
496             continue;
497         }
498 
499         textureCoordinates.emplace_back();
500         auto& texCoords = textureCoordinates[layer];
501         texCoords.reserve(mesh->mNumVertices);
502         for(std::size_t i = 0; i < mesh->mNumVertices; ++i) {
503             /* GCC 4.7 has a problem with .x/.y here */
504             texCoords.emplace_back(mesh->mTextureCoords[layer][i][0], mesh->mTextureCoords[layer][i][1]);
505         }
506     }
507 
508     colors.reserve(mesh->GetNumColorChannels());
509     for(std::size_t layer = 0; layer < mesh->GetNumColorChannels(); ++layer) {
510         colors.emplace_back();
511         auto colorArray = Containers::arrayCast<Color4>(Containers::arrayView(mesh->mColors[layer], mesh->mNumVertices));
512         colors[layer].assign(colorArray.begin(), colorArray.end());
513     }
514 
515     /* Import indices */
516     std::vector<UnsignedInt> indices;
517     indices.reserve(mesh->mNumFaces*3);
518 
519     for(std::size_t faceIndex = 0; faceIndex < mesh->mNumFaces; ++faceIndex) {
520         const aiFace& face = mesh->mFaces[faceIndex];
521 
522         CORRADE_ASSERT(face.mNumIndices <= 3, "Trade::AssimpImporter::mesh3D(): triangulation while loading should have ensured <= 3 vertices per primitive", {});
523         for(std::size_t i = 0; i < face.mNumIndices; ++i) {
524             indices.push_back(face.mIndices[i]);
525         }
526     }
527 
528     return MeshData3D(primitive, std::move(indices), std::move(positions), std::move(normals), std::move(textureCoordinates), std::move(colors), mesh);
529 }
530 
doMaterialCount() const531 UnsignedInt AssimpImporter::doMaterialCount() const { return _f->scene->mNumMaterials; }
532 
doMaterialForName(const std::string & name)533 Int AssimpImporter::doMaterialForName(const std::string& name) {
534     auto found = _f->materialIndicesForName.find(name);
535     return found != _f->materialIndicesForName.end() ? found->second : -1;
536 }
537 
doMaterialName(const UnsignedInt id)538 std::string AssimpImporter::doMaterialName(const UnsignedInt id) {
539     const aiMaterial* mat = _f->scene->mMaterials[id];
540     aiString name;
541     mat->Get(AI_MATKEY_NAME, name);
542 
543     return name.C_Str();
544 }
545 
doMaterial(const UnsignedInt id)546 Containers::Pointer<AbstractMaterialData> AssimpImporter::doMaterial(const UnsignedInt id) {
547     /* Put things together */
548     const aiMaterial* mat = _f->scene->mMaterials[id];
549 
550     /* Verify that shading mode is either unknown or Phong (not supporting
551        anything else ATM) */
552     aiShadingMode shadingMode;
553     if(mat->Get(AI_MATKEY_SHADING_MODEL, shadingMode) == AI_SUCCESS && shadingMode != aiShadingMode_Phong) {
554         Error() << "Trade::AssimpImporter::material(): unsupported shading mode" << shadingMode;
555         return {};
556     }
557 
558     PhongMaterialData::Flags flags;
559     aiString texturePath;
560     aiColor3D color;
561 
562     if(mat->Get(AI_MATKEY_TEXTURE(aiTextureType_AMBIENT, 0), texturePath) == AI_SUCCESS)
563         flags |= PhongMaterialData::Flag::AmbientTexture;
564     if(mat->Get(AI_MATKEY_TEXTURE(aiTextureType_DIFFUSE, 0), texturePath) == AI_SUCCESS)
565         flags |= PhongMaterialData::Flag::DiffuseTexture;
566     if(mat->Get(AI_MATKEY_TEXTURE(aiTextureType_SPECULAR, 0), texturePath) == AI_SUCCESS)
567         flags |= PhongMaterialData::Flag::SpecularTexture;
568     /** @todo many more types supported in assimp */
569 
570     /* Shininess is *not* always present (for example in STL models), default
571        to 0 */
572     Float shininess = 0.0f;
573     mat->Get(AI_MATKEY_SHININESS, shininess);
574 
575     Containers::Pointer<PhongMaterialData> data{Containers::InPlaceInit, flags, MaterialAlphaMode::Opaque, 0.5f, shininess, mat};
576 
577     /* Key always present, default black */
578     mat->Get(AI_MATKEY_COLOR_AMBIENT, color);
579     UnsignedInt firstTextureIndex = _f->textureIndices[mat];
580     if(flags & PhongMaterialData::Flag::AmbientTexture) {
581         data->ambientTexture() = firstTextureIndex++;
582     } else {
583         data->ambientColor() = Color3(color);
584 
585         /* Assimp 4.1 forces ambient color to white for STL models. That's just
586            plain wrong, so we force it back to black (and emit a warning, so in
587            the very rare case when someone would actually want white ambient,
588            they'll know it got overriden). Fixed by
589            https://github.com/assimp/assimp/pull/2563, not released yet. */
590         if(data->ambientColor() == Color4{1.0f}) {
591             Warning{} << "Trade::AssimpImporter::material(): white ambient detected, forcing back to black";
592             data->ambientColor() = Color3{0.0f};
593         }
594     }
595 
596     /* Key always present, default black */
597     mat->Get(AI_MATKEY_COLOR_DIFFUSE, color);
598     if(flags & PhongMaterialData::Flag::DiffuseTexture)
599         data->diffuseTexture() = firstTextureIndex++;
600     else
601         data->diffuseColor() = Color3(color);
602 
603     /* Key always present, default black */
604     mat->Get(AI_MATKEY_COLOR_SPECULAR, color);
605     if(flags & PhongMaterialData::Flag::SpecularTexture)
606         data->specularTexture() = firstTextureIndex++;
607     else
608         data->specularColor() = Color3(color);
609 
610     /* Needs to be explicit on GCC 4.8 and Clang 3.8 so it can properly upcast
611        the pointer. Just std::move() works as well, but that gives a warning
612        on GCC 9. */
613     return Containers::Pointer<AbstractMaterialData>{std::move(data)};
614 }
615 
doTextureCount() const616 UnsignedInt AssimpImporter::doTextureCount() const { return _f->textures.size(); }
617 
doTexture(const UnsignedInt id)618 Containers::Optional<TextureData> AssimpImporter::doTexture(const UnsignedInt id) {
619     auto toWrapping = [](aiTextureMapMode mapMode) {
620         switch (mapMode) {
621             case aiTextureMapMode_Wrap:
622                 return SamplerWrapping::Repeat;
623             case aiTextureMapMode_Decal:
624                 Warning() << "Trade::AssimpImporter::texture(): no wrapping "
625                     "enum to match aiTextureMapMode_Decal, using "
626                     "SamplerWrapping::ClampToEdge";
627                 return SamplerWrapping::ClampToEdge;
628             case aiTextureMapMode_Clamp:
629                 return SamplerWrapping::ClampToEdge;
630             case aiTextureMapMode_Mirror:
631                 return SamplerWrapping::MirroredRepeat;
632             default:
633                 Warning() << "Trade::AssimpImporter::texture(): unknown "
634                     "aiTextureMapMode" << mapMode << Debug::nospace << ", "
635                     "using SamplerWrapping::ClampToEdge";
636                 return SamplerWrapping::ClampToEdge;
637         }
638     };
639 
640     aiTextureMapMode mapMode;
641     const aiMaterial* mat = std::get<0>(_f->textures[id]);
642     const aiTextureType type = std::get<1>(_f->textures[id]);
643     SamplerWrapping wrappingU = SamplerWrapping::ClampToEdge;
644     SamplerWrapping wrappingV = SamplerWrapping::ClampToEdge;
645     if(mat->Get(AI_MATKEY_MAPPINGMODE_U(type, 0), mapMode) == AI_SUCCESS)
646         wrappingU = toWrapping(mapMode);
647     if(mat->Get(AI_MATKEY_MAPPINGMODE_V(type, 0), mapMode) == AI_SUCCESS)
648         wrappingV = toWrapping(mapMode);
649 
650     return TextureData{TextureData::Type::Texture2D,
651         SamplerFilter::Linear, SamplerFilter::Linear, SamplerMipmap::Linear,
652         {wrappingU, wrappingV, SamplerWrapping::ClampToEdge}, std::get<2>(_f->textures[id]), &_f->textures[id]};
653 }
654 
doImage2DCount() const655 UnsignedInt AssimpImporter::doImage2DCount() const { return _f->images.size(); }
656 
doImage2D(const UnsignedInt id)657 Containers::Optional<ImageData2D> AssimpImporter::doImage2D(const UnsignedInt id) {
658     CORRADE_ASSERT(manager(), "Trade::AssimpImporter::image2D(): the plugin must be instantiated with access to plugin manager in order to open image files", {});
659 
660     const aiMaterial* mat;
661     aiTextureType type;
662     std::tie(mat, type) = _f->images[id];
663 
664     aiString texturePath;
665     if(mat->Get(AI_MATKEY_TEXTURE(type, 0), texturePath) != AI_SUCCESS) {
666         Error() << "Trade::AssimpImporter::image2D(): error getting path for texture" << id;
667         return Containers::NullOpt;
668     }
669 
670     std::string path = texturePath.C_Str();
671     /* If path is prefixed with '*', load embedded texture */
672     if(path[0] == '*') {
673         char* err;
674         const std::string indexStr = path.substr(1);
675         const char* str = indexStr.c_str();
676 
677         const Int index = Int(std::strtol(str, &err, 10));
678         if(err == nullptr || err == str) {
679             Error() << "Trade::AssimpImporter::image2D(): embedded texture path did not contain a valid integer string";
680             return Containers::NullOpt;
681         }
682 
683         const aiTexture* texture = _f->scene->mTextures[index];
684         if(texture->mHeight == 0) {
685             /* Compressed image data */
686             auto textureData = Containers::ArrayView<const char>(reinterpret_cast<const char*>(texture->pcData), texture->mWidth);
687 
688             AnyImageImporter importer{*manager()};
689             if(!importer.openData(textureData))
690                 return Containers::NullOpt;
691             return importer.image2D(0);
692 
693         /* Uncompressed image data */
694         } else {
695             Error() << "Trade::AssimpImporter::image2D(): uncompressed embedded image data is not supported";
696             return Containers::NullOpt;
697         }
698 
699     /* Load external texture */
700     } else {
701         if(!_f->filePath && !fileCallback()) {
702             Error{} << "Trade::AssimpImporter::image2D(): external images can be imported only when opening files from the filesystem or if a file callback is present";
703             return {};
704         }
705 
706         AnyImageImporter importer{*manager()};
707         if(fileCallback()) importer.setFileCallback(fileCallback(), fileCallbackUserData());
708         /* Assimp doesn't trim spaces from the end of image paths in OBJ
709            materials so we have to. See the image-filename-space.mtl test. */
710         if(!importer.openFile(Utility::String::trim(Utility::Directory::join(_f->filePath ? *_f->filePath : "", path))))
711             return Containers::NullOpt;
712         return importer.image2D(0);
713     }
714 }
715 
doImporterState() const716 const void* AssimpImporter::doImporterState() const {
717     return _f->scene;
718 }
719 
720 }}
721 
722 CORRADE_PLUGIN_REGISTER(AssimpImporter, Magnum::Trade::AssimpImporter,
723     "cz.mosra.magnum.Trade.AbstractImporter/0.3")
724