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 © 2018 Tobias Stein <stein.tobi@t-online.de>
7     Copyright © 2018 Jonathan Hale <squareys@googlemail.com>
8 
9     Permission is hereby granted, free of charge, to any person obtaining a
10     copy of this software and associated documentation files (the "Software"),
11     to deal in the Software without restriction, including without limitation
12     the rights to use, copy, modify, merge, publish, distribute, sublicense,
13     and/or sell copies of the Software, and to permit persons to whom the
14     Software is furnished to do so, subject to the following conditions:
15 
16     The above copyright notice and this permission notice shall be included
17     in all copies or substantial portions of the Software.
18 
19     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
22     THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24     FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25     DEALINGS IN THE SOFTWARE.
26 */
27 
28 #include "TinyGltfImporter.h"
29 
30 #include <algorithm>
31 #include <limits>
32 #include <unordered_map>
33 #include <Corrade/Containers/ArrayView.h>
34 #include <Corrade/Containers/Optional.h>
35 #include <Corrade/Utility/ConfigurationGroup.h>
36 #include <Corrade/Utility/DebugStl.h>
37 #include <Corrade/Utility/Directory.h>
38 #include <Corrade/Utility/String.h>
39 #include <Magnum/FileCallback.h>
40 #include <Magnum/Mesh.h>
41 #include <Magnum/PixelFormat.h>
42 #include <Magnum/Math/CubicHermite.h>
43 #include <Magnum/Math/Matrix4.h>
44 #include <Magnum/Math/Quaternion.h>
45 #include <Magnum/Trade/AnimationData.h>
46 #include <Magnum/Trade/CameraData.h>
47 #include <Magnum/Trade/LightData.h>
48 #include <Magnum/Trade/SceneData.h>
49 #include <Magnum/Trade/PhongMaterialData.h>
50 #include <Magnum/Trade/TextureData.h>
51 #include <Magnum/Trade/ImageData.h>
52 #include <Magnum/Trade/MeshData3D.h>
53 #include <Magnum/Trade/MeshObjectData3D.h>
54 
55 #include "MagnumPlugins/AnyImageImporter/AnyImageImporter.h"
56 
57 #define TINYGLTF_IMPLEMENTATION
58 /* Opt out of tinygltf stb_image dependency */
59 #define TINYGLTF_NO_STB_IMAGE
60 #define TINYGLTF_NO_STB_IMAGE_WRITE
61 /* Opt out of loading external images */
62 #define TINYGLTF_NO_EXTERNAL_IMAGE
63 /* Opt out of filesystem access, as we handle it ourselves. However that makes
64    it fail to compile as std::ofstream is not define, so we do that here (and
65    newer versions don't seem to fix that either). Enabling filesystem access
66    makes the compilation fail on WinRT 2015 because for some reason
67    ExpandEnvironmentStringsA() is not defined there. Not sure what is that
68    related to since windows.h is included, I assume it's related to the MSVC
69    2015 bug where a duplicated templated alias causes the compiler to forget
70    random symbols, but I couldn't find anything like that in the codebase.
71    Also, this didn't happen before when plugins were built with GL enabled. */
72 #define TINYGLTF_NO_FS
73 #include <fstream>
74 
75 #ifdef CORRADE_TARGET_WINDOWS
76 /* Tinygltf includes some windows headers, avoid including more than ncessary
77    to speed up compilation. WIN32_LEAN_AND_MEAN and NOMINMAX is already defined
78    by CMake. */
79 #define VC_EXTRALEAN
80 #endif
81 
82 /* Include like this instead of "MagnumExternal/TinyGltf/tiny_gltf.h" so we can
83    include it as a system header and suppress warnings */
84 #include "tiny_gltf.h"
85 #ifdef CORRADE_TARGET_WINDOWS
86 #undef near
87 #undef far
88 #endif
89 
90 namespace Magnum { namespace Trade {
91 
92 using namespace Magnum::Math::Literals;
93 
94 namespace {
95 
loadImageData(tinygltf::Image * image,std::string *,std::string *,int,int,const unsigned char * data,int size,void *)96 bool loadImageData(tinygltf::Image* image, std::string*, std::string*, int, int, const unsigned char* data, int size, void*) {
97     /* In case the image is an embedded URI, copy its decoded value to the data
98        buffer. In all other cases we'll access the referenced buffer or
99        external file directly from the doImage2D() implementation. */
100     if(image->bufferView == -1 && image->uri.empty())
101         image->image.assign(data, data + size);
102 
103     return true;
104 }
105 
elementSize(const tinygltf::Accessor & accessor)106 std::size_t elementSize(const tinygltf::Accessor& accessor) {
107     /* GetTypeSizeInBytes() is totally bogus and misleading name, it should
108        have been called GetTypeComponentCount but who am I to judge. */
109     return tinygltf::GetComponentSizeInBytes(accessor.componentType)*tinygltf::GetTypeSizeInBytes(accessor.type);
110 }
111 
bufferView(const tinygltf::Model & model,const tinygltf::Accessor & accessor)112 Containers::ArrayView<const char> bufferView(const tinygltf::Model& model, const tinygltf::Accessor& accessor) {
113     const std::size_t bufferElementSize = elementSize(accessor);
114     CORRADE_INTERNAL_ASSERT(std::size_t(accessor.bufferView) < model.bufferViews.size());
115     const tinygltf::BufferView& bufferView = model.bufferViews[accessor.bufferView];
116     CORRADE_INTERNAL_ASSERT(std::size_t(bufferView.buffer) < model.buffers.size());
117     const tinygltf::Buffer& buffer = model.buffers[bufferView.buffer];
118 
119     CORRADE_INTERNAL_ASSERT(bufferView.byteStride == 0 || bufferView.byteStride == bufferElementSize);
120     return {reinterpret_cast<const char*>(buffer.data.data()) + bufferView.byteOffset + accessor.byteOffset, accessor.count*bufferElementSize};
121 }
122 
bufferView(const tinygltf::Model & model,const tinygltf::Accessor & accessor)123 template<class T> Containers::ArrayView<const T> bufferView(const tinygltf::Model& model, const tinygltf::Accessor& accessor) {
124     CORRADE_INTERNAL_ASSERT(elementSize(accessor) == sizeof(T));
125     return Containers::arrayCast<const T>(bufferView(model, accessor));
126 }
127 
128 }
129 
130 struct TinyGltfImporter::Document {
131     Containers::Optional<std::string> filePath;
132 
133     tinygltf::Model model;
134 
135     Containers::Optional<std::unordered_map<std::string, Int>>
136         animationsForName,
137         camerasForName,
138         lightsForName,
139         scenesForName,
140         nodesForName,
141         meshesForName,
142         materialsForName,
143         imagesForName,
144         texturesForName;
145 
146     /* Mapping for multi-primitive meshes:
147 
148         -   meshMap.size() is the count of meshes reported to the user
149         -   meshSizeOffsets.size() is the count of original meshes in the file
150         -   meshMap[id] is a pair of (original mesh ID, primitive ID)
151         -   meshSizeOffsets[j] points to the first item in meshMap for
152             original mesh ID `j` -- which also translates the original ID to
153             reported ID
154         -   meshSizeOffsets[j + 1] - meshSizeOffsets[j] is count of meshes for
155             original mesh ID `j` (or number of primitives in given mesh)
156     */
157     std::vector<std::pair<std::size_t, std::size_t>> meshMap;
158     std::vector<std::size_t> meshSizeOffsets;
159 
160     /* Mapping for nodes having multi-primitive nodes. The same as above, but
161        for nodes. Hierarchy-wise, the subsequent nodes are direct children of
162        the first, have no transformation or other children and point to the
163        subsequent meshes. */
164     std::vector<std::pair<std::size_t, std::size_t>> nodeMap;
165     std::vector<std::size_t> nodeSizeOffsets;
166 
167     bool open = false;
168 };
169 
170 namespace {
171 
fillDefaultConfiguration(Utility::ConfigurationGroup & conf)172 void fillDefaultConfiguration(Utility::ConfigurationGroup& conf) {
173     /** @todo horrible workaround, fix this properly */
174     conf.setValue("optimizeQuaternionShortestPath", true);
175     conf.setValue("normalizeQuaternions", true);
176     conf.setValue("mergeAnimationClips", false);
177 }
178 
179 }
180 
TinyGltfImporter()181 TinyGltfImporter::TinyGltfImporter() {
182     /** @todo horrible workaround, fix this properly */
183     fillDefaultConfiguration(configuration());
184 }
185 
TinyGltfImporter(PluginManager::AbstractManager & manager,const std::string & plugin)186 TinyGltfImporter::TinyGltfImporter(PluginManager::AbstractManager& manager, const std::string& plugin): AbstractImporter{manager, plugin} {}
187 
TinyGltfImporter(PluginManager::Manager<AbstractImporter> & manager)188 TinyGltfImporter::TinyGltfImporter(PluginManager::Manager<AbstractImporter>& manager): AbstractImporter{manager} {
189     /** @todo horrible workaround, fix this properly */
190     fillDefaultConfiguration(configuration());
191 }
192 
193 TinyGltfImporter::~TinyGltfImporter() = default;
194 
doFeatures() const195 auto TinyGltfImporter::doFeatures() const -> Features { return Feature::OpenData|Feature::FileCallback; }
196 
doIsOpened() const197 bool TinyGltfImporter::doIsOpened() const { return !!_d && _d->open; }
198 
doClose()199 void TinyGltfImporter::doClose() { _d = nullptr; }
200 
doOpenFile(const std::string & filename)201 void TinyGltfImporter::doOpenFile(const std::string& filename) {
202     _d.reset(new Document);
203     _d->filePath = Utility::Directory::path(filename);
204     AbstractImporter::doOpenFile(filename);
205 }
206 
doOpenData(const Containers::ArrayView<const char> data)207 void TinyGltfImporter::doOpenData(const Containers::ArrayView<const char> data) {
208     tinygltf::TinyGLTF loader;
209     std::string err;
210 
211     if(!_d) _d.reset(new Document);
212 
213     /* Set up file callbacks */
214     tinygltf::FsCallbacks callbacks;
215     callbacks.user_data = this;
216     /* We don't need any expansion of environment variables in file paths.
217        That should be done in a completely different place and is not something
218        the importer should care about. Further, FileExists and ExpandFilePath
219        is used to search for files in a few different locations. That's also
220        totally useless, since location of dependent files is *clearly* and
221        uniquely defined. Also, tinygltf's path joining is STUPID and so
222        /foo/bar/ + /file.dat gets joined to /foo/bar//file.dat. So we supply
223        an empty path there and handle it here correctly. */
224     callbacks.FileExists = [](const std::string&, void*) { return true; };
225     callbacks.ExpandFilePath = [](const std::string& path, void*) {
226         return path;
227     };
228     if(fileCallback()) callbacks.ReadWholeFile = [](std::vector<unsigned char>* out, std::string* err, const std::string& filename, void* userData) {
229             auto& self = *static_cast<TinyGltfImporter*>(userData);
230             const std::string fullPath = Utility::Directory::join(self._d->filePath ? *self._d->filePath : "", filename);
231             Containers::Optional<Containers::ArrayView<const char>> data = self.fileCallback()(fullPath, InputFileCallbackPolicy::LoadTemporary, self.fileCallbackUserData());
232             if(!data) {
233                 *err = "file callback failed";
234                 return false;
235             }
236             out->assign(data->begin(), data->end());
237             return true;
238         };
239     else callbacks.ReadWholeFile = [](std::vector<unsigned char>* out, std::string* err, const std::string& filename, void* userData) {
240             auto& self = *static_cast<TinyGltfImporter*>(userData);
241             if(!self._d->filePath) {
242                 *err = "external buffers can be imported only when opening files from the filesystem or if a file callback is present";
243                 return false;
244             }
245             const std::string fullPath = Utility::Directory::join(*self._d->filePath, filename);
246             if(!Utility::Directory::exists(fullPath)) {
247                 *err = "file not found";
248                 return false;
249             }
250             Containers::Array<char> data = Utility::Directory::read(fullPath);
251             out->assign(data.begin(), data.end());
252             return true;
253         };
254     loader.SetFsCallbacks(callbacks);
255 
256     loader.SetImageLoader(&loadImageData, nullptr);
257 
258     _d->open = true;
259     if(data.size() >= 4 && strncmp(data.data(), "glTF", 4) == 0) {
260         _d->open = loader.LoadBinaryFromMemory(&_d->model, &err, nullptr, reinterpret_cast<const unsigned char*>(data.data()), data.size(), "", tinygltf::SectionCheck::NO_REQUIRE);
261     } else {
262         _d->open = loader.LoadASCIIFromString(&_d->model, &err, nullptr, data.data(), data.size(), "", tinygltf::SectionCheck::NO_REQUIRE);
263     }
264 
265     if(!_d->open) {
266         Utility::String::rtrimInPlace(err);
267         Error{} << "Trade::TinyGltfImporter::openData(): error opening file:" << err;
268         doClose();
269         return;
270     }
271 
272     /* Treat meshes with multiple primitives as separate meshes. Each mesh gets
273        duplicated as many times as is the size of the primitives array. */
274     _d->meshSizeOffsets.emplace_back(0);
275     for(std::size_t i = 0; i != _d->model.meshes.size(); ++i) {
276         CORRADE_INTERNAL_ASSERT(!_d->model.meshes[i].primitives.empty());
277         for(std::size_t j = 0; j != _d->model.meshes[i].primitives.size(); ++j)
278             _d->meshMap.emplace_back(i, j);
279 
280         _d->meshSizeOffsets.emplace_back(_d->meshMap.size());
281     }
282 
283     /* In order to support multi-primitive meshes, we need to duplicate the
284        nodes as well */
285     _d->nodeSizeOffsets.emplace_back(0);
286     for(std::size_t i = 0; i != _d->model.nodes.size(); ++i) {
287         _d->nodeMap.emplace_back(i, 0);
288 
289         const Int mesh = _d->model.nodes[i].mesh;
290         if(mesh != -1) {
291             /* If a node has a mesh with multiple primitives, add nested nodes
292                containing the other primitives after it */
293             const std::size_t count = _d->model.meshes[mesh].primitives.size();
294             for(std::size_t j = 1; j < count; ++j)
295                 _d->nodeMap.emplace_back(i, j);
296         }
297 
298         _d->nodeSizeOffsets.emplace_back(_d->nodeMap.size());
299     }
300 
301     /* Name maps are lazy-loaded because these might not be needed every time */
302 }
303 
doCameraCount() const304 UnsignedInt TinyGltfImporter::doCameraCount() const {
305     return _d->model.cameras.size();
306 }
307 
doCameraForName(const std::string & name)308 Int TinyGltfImporter::doCameraForName(const std::string& name) {
309     if(!_d->camerasForName) {
310         _d->camerasForName.emplace();
311         _d->camerasForName->reserve(_d->model.cameras.size());
312         for(std::size_t i = 0; i != _d->model.cameras.size(); ++i)
313             _d->camerasForName->emplace(_d->model.cameras[i].name, i);
314     }
315 
316     const auto found = _d->camerasForName->find(name);
317     return found == _d->camerasForName->end() ? -1 : found->second;
318 }
319 
doCameraName(const UnsignedInt id)320 std::string TinyGltfImporter::doCameraName(const UnsignedInt id) {
321     return _d->model.cameras[id].name;
322 }
323 
doAnimationCount() const324 UnsignedInt TinyGltfImporter::doAnimationCount() const {
325     /* If the animations are merged, there's at most one */
326     if(configuration().value<bool>("mergeAnimationClips"))
327         return _d->model.animations.empty() ? 0 : 1;
328 
329     return _d->model.animations.size();
330 }
331 
doAnimationForName(const std::string & name)332 Int TinyGltfImporter::doAnimationForName(const std::string& name) {
333     /* If the animations are merged, don't report any names */
334     if(configuration().value<bool>("mergeAnimationClips")) return -1;
335 
336     if(!_d->animationsForName) {
337         _d->animationsForName.emplace();
338         _d->animationsForName->reserve(_d->model.animations.size());
339         for(std::size_t i = 0; i != _d->model.animations.size(); ++i)
340             _d->animationsForName->emplace(_d->model.animations[i].name, i);
341     }
342 
343     const auto found = _d->animationsForName->find(name);
344     return found == _d->animationsForName->end() ? -1 : found->second;
345 }
346 
doAnimationName(UnsignedInt id)347 std::string TinyGltfImporter::doAnimationName(UnsignedInt id) {
348     /* If the animations are merged, don't report any names */
349     if(configuration().value<bool>("mergeAnimationClips")) return {};
350     return _d->model.animations[id].name;
351 }
352 
353 namespace {
354 
postprocessSplineTrack(const std::size_t timeTrackUsed,const Containers::ArrayView<const Float> keys,const Containers::ArrayView<Math::CubicHermite<V>> values)355 template<class V> void postprocessSplineTrack(const std::size_t timeTrackUsed, const Containers::ArrayView<const Float> keys, const Containers::ArrayView<Math::CubicHermite<V>> values) {
356     /* Already processed, don't do that again */
357     if(timeTrackUsed != ~std::size_t{}) return;
358 
359     CORRADE_INTERNAL_ASSERT(keys.size() == values.size());
360     if(keys.size() < 2) return;
361 
362     /* Convert the `a` values to `n` and the `b` values to `m` as described in
363        https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#appendix-c-spline-interpolation
364        Unfortunately I was not able to find any concrete name for this, so it's
365        not part of the CubicHermite implementation but is kept here locally. */
366     for(std::size_t i = 0; i < keys.size() - 1; ++i) {
367         const Float timeDifference = keys[i + 1] - keys[i];
368         values[i].outTangent() *= timeDifference;
369         values[i + 1].inTangent() *= timeDifference;
370     }
371 }
372 
373 }
374 
doAnimation(UnsignedInt id)375 Containers::Optional<AnimationData> TinyGltfImporter::doAnimation(UnsignedInt id) {
376     /* Import either a single animation or all of them together. At the moment,
377        Blender doesn't really support cinematic animations (affecting multiple
378        objects): https://blender.stackexchange.com/q/5689. And since
379        https://github.com/KhronosGroup/glTF-Blender-Exporter/pull/166, these
380        are exported as a set of object-specific clips, which may not be wanted,
381        so we give the users an option to merge them all together. */
382     const std::size_t animationBegin =
383         configuration().value<bool>("mergeAnimationClips") ? 0 : id;
384     const std::size_t animationEnd =
385         configuration().value<bool>("mergeAnimationClips") ? _d->model.animations.size() : id + 1;
386 
387     /* First gather the input and output data ranges. Key is unique accessor ID
388        so we don't duplicate shared data, value is range in the input buffer,
389        offset in the output data and ID of the corresponding key track in case
390        given track is a spline interpolation. The key ID is initialized to ~0
391        and will be used later to check that a spline track was not used with
392        more than one time track, as it needs to be postprocessed for given time
393        track. */
394     std::unordered_map<int, std::tuple<Containers::ArrayView<const char>, std::size_t, std::size_t>> samplerData;
395     std::size_t dataSize = 0;
396     for(std::size_t a = animationBegin; a != animationEnd; ++a) {
397         const tinygltf::Animation& animation = _d->model.animations[a];
398         for(std::size_t i = 0; i != animation.samplers.size(); ++i) {
399             const tinygltf::AnimationSampler& sampler = animation.samplers[i];
400             const tinygltf::Accessor& input = _d->model.accessors[sampler.input];
401             const tinygltf::Accessor& output = _d->model.accessors[sampler.output];
402 
403             /** @todo handle alignment once we do more than just four-byte types */
404 
405             /* If the input view is not yet present in the output data buffer, add
406             it */
407             if(samplerData.find(sampler.input) == samplerData.end()) {
408                 Containers::ArrayView<const char> view = bufferView(_d->model, input);
409                 samplerData.emplace(sampler.input, std::make_tuple(view, dataSize, ~std::size_t{}));
410                 dataSize += view.size();
411             }
412 
413             /* If the output view is not yet present in the output data buffer, add
414             it */
415             if(samplerData.find(sampler.output) == samplerData.end()) {
416                 Containers::ArrayView<const char> view = bufferView(_d->model, output);
417                 samplerData.emplace(sampler.output, std::make_tuple(view, dataSize, ~std::size_t{}));
418                 dataSize += view.size();
419             }
420         }
421     }
422 
423     /* Populate the data array */
424     /**
425      * @todo Once memory-mapped files are supported, this can all go away
426      *      except when spline tracks are present -- in that case we need to
427      *      postprocess them and can't just use the memory directly.
428      */
429     Containers::Array<char> data{dataSize};
430     for(const std::pair<int, std::tuple<Containers::ArrayView<const char>, std::size_t, std::size_t>>& view: samplerData) {
431         Containers::ArrayView<const char> input;
432         std::size_t outputOffset;
433         std::tie(input, outputOffset, std::ignore) = view.second;
434 
435         CORRADE_INTERNAL_ASSERT(outputOffset + input.size() <= data.size());
436         std::copy(input.begin(), input.end(), data.begin() + outputOffset);
437     }
438 
439     /* Calculate total track count. If merging all animations together, this is
440        the sum of all clip track counts. */
441     std::size_t trackCount = 0;
442     for(std::size_t a = animationBegin; a != animationEnd; ++a)
443         trackCount += _d->model.animations[a].channels.size();
444 
445     /* Import all tracks */
446     bool hadToRenormalize = false;
447     std::size_t trackId = 0;
448     Containers::Array<Trade::AnimationTrackData> tracks{trackCount};
449     for(std::size_t a = animationBegin; a != animationEnd; ++a) {
450         const tinygltf::Animation& animation = _d->model.animations[a];
451         for(std::size_t i = 0; i != animation.channels.size(); ++i) {
452             const tinygltf::AnimationChannel& channel = animation.channels[i];
453             const tinygltf::AnimationSampler& sampler = animation.samplers[channel.sampler];
454 
455             /* Key properties -- always float time */
456             const tinygltf::Accessor& input = _d->model.accessors[sampler.input];
457             if(input.type != TINYGLTF_TYPE_SCALAR || input.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) {
458                 Error{} << "Trade::TinyGltfImporter::animation(): time track has unexpected type" << input.type << Debug::nospace << "/" << Debug::nospace << input.componentType;
459                 return Containers::NullOpt;
460             }
461 
462             /* View on the key data */
463             const auto inputDataFound = samplerData.find(sampler.input);
464             CORRADE_INTERNAL_ASSERT(inputDataFound != samplerData.end());
465             const auto keys = Containers::arrayCast<const Float>(data.suffix(std::get<1>(inputDataFound->second)).prefix(std::get<0>(inputDataFound->second).size()));
466 
467             /* Interpolation mode */
468             Animation::Interpolation interpolation;
469             if(sampler.interpolation == "LINEAR") {
470                 interpolation = Animation::Interpolation::Linear;
471             } else if(sampler.interpolation == "CUBICSPLINE") {
472                 interpolation = Animation::Interpolation::Spline;
473             } else if(sampler.interpolation == "STEP") {
474                 interpolation = Animation::Interpolation::Constant;
475             } else {
476                 Error{} << "Trade::TinyGltfImporter::animation(): unsupported interpolation" << sampler.interpolation;
477                 return Containers::NullOpt;
478             }
479 
480             /* Decide on value properties */
481             const tinygltf::Accessor& output = _d->model.accessors[sampler.output];
482             AnimationTrackTargetType target;
483             AnimationTrackType type, resultType;
484             Animation::TrackViewStorage<Float> track;
485             const auto outputDataFound = samplerData.find(sampler.output);
486             CORRADE_INTERNAL_ASSERT(outputDataFound != samplerData.end());
487             const auto outputData = data.suffix(std::get<1>(outputDataFound->second)).prefix(std::get<0>(outputDataFound->second).size());
488             std::size_t& timeTrackUsed = std::get<2>(outputDataFound->second);
489 
490             /* Translation */
491             if(channel.target_path == "translation") {
492                 if(output.type != TINYGLTF_TYPE_VEC3 || output.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) {
493                     Error{} << "Trade::TinyGltfImporter::animation(): translation track has unexpected type" << output.type << Debug::nospace << "/" << Debug::nospace << output.componentType;
494                     return Containers::NullOpt;
495                 }
496 
497                 /* View on the value data */
498                 target = AnimationTrackTargetType::Translation3D;
499                 resultType = AnimationTrackType::Vector3;
500                 if(interpolation == Animation::Interpolation::Spline) {
501                     /* Postprocess the spline track. This can be done only once for
502                     every track -- postprocessSplineTrack() checks that. */
503                     const auto values = Containers::arrayCast<CubicHermite3D>(outputData);
504                     postprocessSplineTrack(timeTrackUsed, keys, values);
505 
506                     type = AnimationTrackType::CubicHermite3D;
507                     track = Animation::TrackView<Float, CubicHermite3D>{
508                         keys, values, interpolation,
509                         animationInterpolatorFor<CubicHermite3D>(interpolation),
510                         Animation::Extrapolation::Constant};
511                 } else {
512                     type = AnimationTrackType::Vector3;
513                     track = Animation::TrackView<Float, Vector3>{keys,
514                         Containers::arrayCast<const Vector3>(outputData),
515                         interpolation,
516                         animationInterpolatorFor<Vector3>(interpolation),
517                         Animation::Extrapolation::Constant};
518                 }
519 
520             /* Rotation */
521             } else if(channel.target_path == "rotation") {
522                 /** @todo rotation can be also normalized (?!) to a vector of 8/16/32bit (signed?!) integers */
523 
524                 if(output.type != TINYGLTF_TYPE_VEC4 || output.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) {
525                     Error{} << "Trade::TinyGltfImporter::animation(): rotation track has unexpected type" << output.type << Debug::nospace << "/" << Debug::nospace << output.componentType;
526                     return Containers::NullOpt;
527                 }
528 
529                 /* View on the value data */
530                 target = AnimationTrackTargetType::Rotation3D;
531                 resultType = AnimationTrackType::Quaternion;
532                 if(interpolation == Animation::Interpolation::Spline) {
533                     /* Postprocess the spline track. This can be done only once for
534                     every track -- postprocessSplineTrack() checks that. */
535                     const auto values = Containers::arrayCast<CubicHermiteQuaternion>(outputData);
536                     postprocessSplineTrack(timeTrackUsed, keys, values);
537 
538                     type = AnimationTrackType::CubicHermiteQuaternion;
539                     track = Animation::TrackView<Float, CubicHermiteQuaternion>{
540                         keys, values, interpolation,
541                         animationInterpolatorFor<CubicHermiteQuaternion>(interpolation),
542                         Animation::Extrapolation::Constant};
543                 } else {
544                     /* Ensure shortest path is always chosen. Not doing this for
545                     spline interpolation, there it would cause war and famine. */
546                     const auto values = Containers::arrayCast<Quaternion>(outputData);
547                     if(configuration().value<bool>("optimizeQuaternionShortestPath")) {
548                         Float flip = 1.0f;
549                         for(std::size_t i = 0; i != values.size() - 1; ++i) {
550                             if(Math::dot(values[i], values[i + 1]*flip) < 0) flip = -flip;
551                             values[i + 1] *= flip;
552                         }
553                     }
554 
555                     /* Normalize the quaternions if not already. Don't attempt to
556                     normalize every time to avoid tiny differences, only when
557                     the quaternion looks to be off. Again, not doing this for
558                     splines as it would cause things to go haywire. */
559                     if(configuration().value<bool>("normalizeQuaternions")) {
560                         for(auto& i: values) if(!i.isNormalized()) {
561                             i = i.normalized();
562                             hadToRenormalize = true;
563                         }
564                     }
565 
566                     type = AnimationTrackType::Quaternion;
567                     track = Animation::TrackView<Float, Quaternion>{
568                         keys, values, interpolation,
569                         animationInterpolatorFor<Quaternion>(interpolation),
570                         Animation::Extrapolation::Constant};
571                 }
572 
573             /* Scale */
574             } else if(channel.target_path == "scale") {
575                 if(output.type != TINYGLTF_TYPE_VEC3 || output.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) {
576                     Error{} << "Trade::TinyGltfImporter::animation(): scaling track has unexpected type" << output.type << Debug::nospace << "/" << Debug::nospace << output.componentType;
577                     return Containers::NullOpt;
578                 }
579 
580                 /* View on the value data */
581                 target = AnimationTrackTargetType::Scaling3D;
582                 resultType = AnimationTrackType::Vector3;
583                 if(interpolation == Animation::Interpolation::Spline) {
584                     /* Postprocess the spline track. This can be done only once for
585                     every track -- postprocessSplineTrack() checks that. */
586                     const auto values = Containers::arrayCast<CubicHermite3D>(outputData);
587                     postprocessSplineTrack(timeTrackUsed, keys, values);
588 
589                     type = AnimationTrackType::CubicHermite3D;
590                     track = Animation::TrackView<Float, CubicHermite3D>{
591                         keys, values, interpolation,
592                         animationInterpolatorFor<CubicHermite3D>(interpolation),
593                         Animation::Extrapolation::Constant};
594                 } else {
595                     type = AnimationTrackType::Vector3;
596                     track = Animation::TrackView<Float, Vector3>{keys,
597                         Containers::arrayCast<const Vector3>(outputData),
598                         interpolation,
599                         animationInterpolatorFor<Vector3>(interpolation),
600                         Animation::Extrapolation::Constant};
601                 }
602 
603             } else {
604                 Error{} << "Trade::TinyGltfImporter::animation(): unsupported track target" << channel.target_path;
605                 return Containers::NullOpt;
606             }
607 
608             /* Splines were postprocessed using the corresponding time track. If a
609             spline is not yet marked as postprocessed, mark it. Otherwise check
610             that the spline track is always used with the same time track. */
611             if(interpolation == Animation::Interpolation::Spline) {
612                 if(timeTrackUsed == ~std::size_t{})
613                     timeTrackUsed = sampler.input;
614                 else if(timeTrackUsed != std::size_t(sampler.input)) {
615                     Error{} << "Trade::TinyGltfImporter::animation(): spline track is shared with different time tracks, we don't support that, sorry";
616                     return Containers::NullOpt;
617                 }
618             }
619 
620             tracks[trackId++] = AnimationTrackData{type, resultType, target,
621                 /* In cases where multi-primitive mesh nodes are split into multipe
622                 objects, the animation should affect the first node -- the other
623                 nodes are direct children of it and so they get affected too */
624                 UnsignedInt(_d->nodeSizeOffsets[channel.target_node]),
625                 track};
626         }
627     }
628 
629     if(hadToRenormalize)
630         Warning{} << "Trade::TinyGltfImporter::animation(): quaternions in some rotation tracks were renormalized";
631 
632     return AnimationData{std::move(data), std::move(tracks),
633         configuration().value<bool>("mergeAnimationClips") ? nullptr :
634         &_d->model.animations[id]};
635 }
636 
doCamera(UnsignedInt id)637 Containers::Optional<CameraData> TinyGltfImporter::doCamera(UnsignedInt id) {
638     const tinygltf::Camera& camera = _d->model.cameras[id];
639 
640     /* https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#projection-matrices */
641 
642     /* Perspective camera. glTF uses vertical FoV and X/Y aspect ratio, so to
643        avoid accidental bugs we will directly calculate the near plane size and
644        use that to create the camera data (instead of passing it the horizontal
645        FoV). Also tinygltf is stupid and uses 0 to denote infinite far plane
646        (wat). */
647     if(camera.type == "perspective") {
648         const Vector2 size = 2.0f*camera.perspective.znear*Math::tan(camera.perspective.yfov*0.5_radf)*Vector2::xScale(camera.perspective.aspectRatio);
649         const Float far = camera.perspective.zfar == 0.0f ? Constants::inf() :
650             camera.perspective.zfar;
651         return CameraData{CameraType::Perspective3D, size, camera.perspective.znear, far, &camera};
652     }
653 
654     /* Orthographic camera. glTF uses a "scale" instead of "size", which means
655        we have to double. */
656     if(camera.type == "orthographic")
657         return CameraData{CameraType::Orthographic3D,
658             Vector2{camera.orthographic.xmag, camera.orthographic.ymag}*2.0f,
659             camera.orthographic.znear, camera.orthographic.zfar, &camera};
660 
661     std::abort(); /* LCOV_EXCL_LINE */
662 }
663 
doLightCount() const664 UnsignedInt TinyGltfImporter::doLightCount() const {
665     return _d->model.lights.size();
666 }
667 
doLightForName(const std::string & name)668 Int TinyGltfImporter::doLightForName(const std::string& name) {
669     if(!_d->lightsForName) {
670         _d->lightsForName.emplace();
671         _d->lightsForName->reserve(_d->model.lights.size());
672         for(std::size_t i = 0; i != _d->model.lights.size(); ++i)
673             _d->lightsForName->emplace(_d->model.lights[i].name, i);
674     }
675 
676     const auto found = _d->lightsForName->find(name);
677     return found == _d->lightsForName->end() ? -1 : found->second;
678 }
679 
doLightName(const UnsignedInt id)680 std::string TinyGltfImporter::doLightName(const UnsignedInt id) {
681     return _d->model.lights[id].name;
682 }
683 
doLight(UnsignedInt id)684 Containers::Optional<LightData> TinyGltfImporter::doLight(UnsignedInt id) {
685     const tinygltf::Light& light = _d->model.lights[id];
686 
687     Color3 lightColor{float(light.color[0]), float(light.color[1]), float(light.color[2])};
688     Float lightIntensity{1.0f}; /* not supported by tinygltf */
689 
690     LightData::Type lightType;
691 
692     if(light.type == "point") {
693         lightType = LightData::Type::Point;
694     } else if(light.type == "spot") {
695         lightType = LightData::Type::Spot;
696     } else if(light.type == "directional") {
697         lightType = LightData::Type::Infinite;
698     } else if(light.type == "ambient") {
699         Error() << "Trade::TinyGltfImporter::light(): unsupported value for light type:" << light.type;
700         return Containers::NullOpt;
701     /* LCOV_EXCL_START */
702     } else {
703         Error() << "Trade::TinyGltfImporter::light(): invalid value for light type:" << light.type;
704         return Containers::NullOpt;
705     }
706     /* LCOV_EXCL_STOP */
707 
708     return LightData{lightType, lightColor, lightIntensity, &light};
709 }
710 
doDefaultScene()711 Int TinyGltfImporter::doDefaultScene() {
712     /* While https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes
713        says that "When scene is undefined, runtime is not required to render
714        anything at load time.", several official sample glTF models (e.g. the
715        AnimatedTriangle) have no "scene" property, so that's a bit stupid
716        behavior to have. As per discussion at https://github.com/KhronosGroup/glTF/issues/815#issuecomment-274286889,
717        if a default scene isn't defined and there is at least one scene, just
718        use the first one. */
719     if(_d->model.defaultScene == -1 && !_d->model.scenes.empty())
720         return 0;
721 
722     return _d->model.defaultScene;
723 }
724 
doSceneCount() const725 UnsignedInt TinyGltfImporter::doSceneCount() const { return _d->model.scenes.size(); }
726 
doSceneForName(const std::string & name)727 Int TinyGltfImporter::doSceneForName(const std::string& name) {
728     if(!_d->scenesForName) {
729         _d->scenesForName.emplace();
730         _d->scenesForName->reserve(_d->model.scenes.size());
731         for(std::size_t i = 0; i != _d->model.scenes.size(); ++i)
732             _d->scenesForName->emplace(_d->model.scenes[i].name, i);
733     }
734 
735     const auto found = _d->scenesForName->find(name);
736     return found == _d->scenesForName->end() ? -1 : found->second;
737 }
738 
doSceneName(const UnsignedInt id)739 std::string TinyGltfImporter::doSceneName(const UnsignedInt id) {
740     return _d->model.scenes[id].name;
741 }
742 
doScene(UnsignedInt id)743 Containers::Optional<SceneData> TinyGltfImporter::doScene(UnsignedInt id) {
744     const tinygltf::Scene& scene = _d->model.scenes[id];
745 
746     /* The scene contains always the top-level nodes, all multi-primitive mesh
747        nodes are children of them */
748     std::vector<UnsignedInt> children;
749     children.reserve(scene.nodes.size());
750     for(const std::size_t i: scene.nodes)
751         children.push_back(_d->nodeSizeOffsets[i]);
752 
753     return SceneData{{}, std::move(children), &scene};
754 }
755 
doObject3DCount() const756 UnsignedInt TinyGltfImporter::doObject3DCount() const {
757     return _d->nodeMap.size();
758 }
759 
doObject3DForName(const std::string & name)760 Int TinyGltfImporter::doObject3DForName(const std::string& name) {
761     if(!_d->nodesForName) {
762         _d->nodesForName.emplace();
763         _d->nodesForName->reserve(_d->model.nodes.size());
764         for(std::size_t i = 0; i != _d->model.nodes.size(); ++i) {
765             /* A mesh node can be duplicated for as many primitives as the mesh
766                has, point to the first node in the duplicate sequence */
767             _d->nodesForName->emplace(_d->model.nodes[i].name, _d->nodeSizeOffsets[i]);
768         }
769     }
770 
771     const auto found = _d->nodesForName->find(name);
772     return found == _d->nodesForName->end() ? -1 : found->second;
773 }
774 
doObject3DName(UnsignedInt id)775 std::string TinyGltfImporter::doObject3DName(UnsignedInt id) {
776     /* This returns the same name for all multi-primitive mesh node duplicates */
777     return _d->model.nodes[_d->nodeMap[id].first].name;
778 }
779 
doObject3D(UnsignedInt id)780 Containers::Pointer<ObjectData3D> TinyGltfImporter::doObject3D(UnsignedInt id) {
781     const std::size_t originalNodeId = _d->nodeMap[id].first;
782     const tinygltf::Node& node = _d->model.nodes[originalNodeId];
783 
784     /* This is an extra node added for multi-primitive meshes -- return it with
785        no children, identity transformation and just a link to the particular
786        mesh & material combo */
787     const std::size_t nodePrimitiveId = _d->nodeMap[id].second;
788     if(nodePrimitiveId) {
789         const UnsignedInt meshId = _d->meshSizeOffsets[node.mesh] + nodePrimitiveId;
790         const Int materialId = _d->model.meshes[node.mesh].primitives[nodePrimitiveId].material;
791         return Containers::pointer(new MeshObjectData3D{{}, {}, {}, Vector3{1.0f}, meshId, materialId, &node});
792     }
793 
794     CORRADE_INTERNAL_ASSERT(node.rotation.size() == 0 || node.rotation.size() == 4);
795     CORRADE_INTERNAL_ASSERT(node.translation.size() == 0 || node.translation.size() == 3);
796     CORRADE_INTERNAL_ASSERT(node.scale.size() == 0 || node.scale.size() == 3);
797     /* Ensure we have either a matrix or T-R-S */
798     CORRADE_INTERNAL_ASSERT(node.matrix.size() == 0 ||
799         (node.matrix.size() == 16 && node.translation.size() == 0 && node.rotation.size() == 0 && node.scale.size() == 0));
800 
801     /* Node children: first add extra nodes caused by multi-primitive meshes,
802        after that the usual children. */
803     std::vector<UnsignedInt> children;
804     const std::size_t extraChildrenCount = _d->nodeSizeOffsets[originalNodeId + 1] - _d->nodeSizeOffsets[originalNodeId] - 1;
805     children.reserve(extraChildrenCount + node.children.size());
806     for(std::size_t i = 0; i != extraChildrenCount; ++i) {
807         /** @todo the test should fail with children.push_back(originalNodeId + i + 1); */
808         children.push_back(_d->nodeSizeOffsets[originalNodeId] + i + 1);
809     }
810     for(const std::size_t i: node.children)
811         children.push_back(_d->nodeSizeOffsets[i]);
812 
813     /* According to the spec, order is T-R-S: first scale, then rotate, then
814        translate (or translate*rotate*scale multiplication of matrices). Makes
815        most sense, since non-uniform scaling of rotated object is unwanted in
816        99% cases, similarly with rotating or scaling a translated object. Also
817        independently verified by exporting a model with translation, rotation
818        *and* scaling of hierarchic objects. */
819     ObjectFlags3D flags;
820     Matrix4 transformation;
821     Vector3 translation;
822     Quaternion rotation;
823     Vector3 scaling{1.0f};
824     if(node.matrix.size() == 16) {
825         transformation = Matrix4(Matrix4d::from(node.matrix.data()));
826     } else {
827         /* Having TRS is a better property than not having it, so we set this
828            flag even when there is no transformation at all. */
829         flags |= ObjectFlag3D::HasTranslationRotationScaling;
830         if(node.translation.size() == 3)
831             translation = Vector3{Vector3d::from(node.translation.data())};
832         if(node.rotation.size() == 4) {
833             rotation = Quaternion{Vector3{Vector3d::from(node.rotation.data())}, Float(node.rotation[3])};
834             if(!rotation.isNormalized() && configuration().value<bool>("normalizeQuaternions")) {
835                 rotation = rotation.normalized();
836                 Warning{} << "Trade::TinyGltfImporter::object3D(): rotation quaternion was renormalized";
837             }
838         }
839         if(node.scale.size() == 3)
840             scaling = Vector3{Vector3d::from(node.scale.data())};
841     }
842 
843     /* Node is a mesh */
844     if(node.mesh >= 0) {
845         /* Multi-primitive nodes are handled above */
846         CORRADE_INTERNAL_ASSERT(_d->nodeMap[id].second == 0);
847         CORRADE_INTERNAL_ASSERT(!_d->model.meshes[node.mesh].primitives.empty());
848 
849         const UnsignedInt meshId = _d->meshSizeOffsets[node.mesh];
850         const Int materialId = _d->model.meshes[node.mesh].primitives[0].material;
851         return Containers::pointer(flags & ObjectFlag3D::HasTranslationRotationScaling ?
852             new MeshObjectData3D{std::move(children), translation, rotation, scaling, meshId, materialId, &node} :
853             new MeshObjectData3D{std::move(children), transformation, meshId, materialId, &node});
854     }
855 
856     /* Unknown nodes are treated as Empty */
857     ObjectInstanceType3D instanceType = ObjectInstanceType3D::Empty;
858     UnsignedInt instanceId = ~UnsignedInt{}; /* -1 */
859 
860     /* Node is a camera */
861     if(node.camera >= 0) {
862         instanceType = ObjectInstanceType3D::Camera;
863         instanceId = node.camera;
864 
865     /* Node is a light */
866     } else if(node.extensions.find("KHR_lights_cmn") != node.extensions.end()) {
867         instanceType = ObjectInstanceType3D::Light;
868         instanceId = UnsignedInt(node.extensions.at("KHR_lights_cmn").Get("light").Get<int>());
869     }
870 
871     return Containers::pointer(flags & ObjectFlag3D::HasTranslationRotationScaling ?
872         new ObjectData3D{std::move(children), translation, rotation, scaling, instanceType, instanceId, &node} :
873         new ObjectData3D{std::move(children), transformation, instanceType, instanceId, &node});
874 }
875 
doMesh3DCount() const876 UnsignedInt TinyGltfImporter::doMesh3DCount() const {
877     return _d->meshMap.size();
878 }
879 
doMesh3DForName(const std::string & name)880 Int TinyGltfImporter::doMesh3DForName(const std::string& name) {
881     if(!_d->meshesForName) {
882         _d->meshesForName.emplace();
883         _d->meshesForName->reserve(_d->model.meshes.size());
884         for(std::size_t i = 0; i != _d->model.meshes.size(); ++i) {
885             /* The mesh can be duplicated for as many primitives as it has,
886                point to the first mesh in the duplicate sequence */
887             _d->meshesForName->emplace(_d->model.meshes[i].name, _d->meshSizeOffsets[i]);
888         }
889     }
890 
891     const auto found = _d->meshesForName->find(name);
892     return found == _d->meshesForName->end() ? -1 : found->second;
893 }
894 
doMesh3DName(const UnsignedInt id)895 std::string TinyGltfImporter::doMesh3DName(const UnsignedInt id) {
896     /* This returns the same name for all multi-primitive mesh duplicates */
897     return _d->model.meshes[_d->meshMap[id].first].name;
898 }
899 
doMesh3D(const UnsignedInt id)900 Containers::Optional<MeshData3D> TinyGltfImporter::doMesh3D(const UnsignedInt id) {
901     const tinygltf::Mesh& mesh = _d->model.meshes[_d->meshMap[id].first];
902     const tinygltf::Primitive& primitive = mesh.primitives[_d->meshMap[id].second];
903 
904     MeshPrimitive meshPrimitive{};
905     if(primitive.mode == TINYGLTF_MODE_POINTS) {
906         meshPrimitive = MeshPrimitive::Points;
907     } else if(primitive.mode == TINYGLTF_MODE_LINE) {
908         meshPrimitive = MeshPrimitive::Lines;
909     } else if(primitive.mode == TINYGLTF_MODE_LINE_LOOP) {
910         meshPrimitive = MeshPrimitive::LineLoop;
911     } else if(primitive.mode == 3) {
912         /* For some reason tiny_gltf doesn't have a define for this */
913         meshPrimitive = MeshPrimitive::LineStrip;
914     } else if(primitive.mode == TINYGLTF_MODE_TRIANGLES) {
915         meshPrimitive = MeshPrimitive::Triangles;
916     } else if(primitive.mode == TINYGLTF_MODE_TRIANGLE_FAN) {
917         meshPrimitive = MeshPrimitive::TriangleFan;
918     } else if(primitive.mode == TINYGLTF_MODE_TRIANGLE_STRIP) {
919         meshPrimitive = MeshPrimitive::TriangleStrip;
920     /* LCOV_EXCL_START */
921     } else {
922         Error{} << "Trade::TinyGltfImporter::mesh3D(): unrecognized primitive" << primitive.mode;
923         return Containers::NullOpt;
924     }
925     /* LCOV_EXCL_STOP */
926 
927     std::vector<Vector3> positions;
928     std::vector<std::vector<Vector3>> normalArrays;
929     std::vector<std::vector<Vector2>> textureCoordinateArrays;
930     std::vector<std::vector<Color4>> colorArrays;
931     for(auto& attribute: primitive.attributes) {
932         const tinygltf::Accessor& accessor = _d->model.accessors[attribute.second];
933         const tinygltf::BufferView& view = _d->model.bufferViews[accessor.bufferView];
934 
935         /* Some of the Khronos sample models have explicitly specified stride
936            (without interleaving), don't fail on that */
937         if(view.byteStride != 0 && view.byteStride != elementSize(accessor)) {
938             Error() << "Trade::TinyGltfImporter::mesh3D(): interleaved buffer views are not supported";
939             return  Containers::NullOpt;
940         }
941 
942         /* At the moment all vertex attributes should have float underlying
943            type */
944         if(accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) {
945             Error() << "Trade::TinyGltfImporter::mesh3D(): vertex attribute" << attribute.first << "has unexpected type" << accessor.componentType;
946             return Containers::NullOpt;
947         }
948 
949         if(attribute.first == "POSITION") {
950             if(accessor.type != TINYGLTF_TYPE_VEC3) {
951                 Error() << "Trade::TinyGltfImporter::mesh3D(): expected type of" << attribute.first << "is VEC3";
952                 return Containers::NullOpt;
953             }
954 
955             positions.reserve(accessor.count);
956             const auto buffer = bufferView<Vector3>(_d->model, accessor);
957             std::copy(buffer.begin(), buffer.end(), std::back_inserter(positions));
958 
959         } else if(attribute.first == "NORMAL") {
960             if(accessor.type != TINYGLTF_TYPE_VEC3) {
961                 Error() << "Trade::TinyGltfImporter::mesh3D(): expected type of" << attribute.first << "is VEC3";
962                 return Containers::NullOpt;
963             }
964 
965             normalArrays.emplace_back();
966             std::vector<Vector3>& normals = normalArrays.back();
967             normals.reserve(accessor.count);
968             const auto buffer = bufferView<Vector3>(_d->model, accessor);
969             std::copy(buffer.begin(), buffer.end(), std::back_inserter(normals));
970 
971         /* Texture coordinate attribute ends with _0, _1 ... */
972         } else if(Utility::String::beginsWith(attribute.first, "TEXCOORD")) {
973             if(accessor.type != TINYGLTF_TYPE_VEC2) {
974                 Error() << "Trade::TinyGltfImporter::mesh3D(): expected type of" << attribute.first << "is VEC2";
975                 return Containers::NullOpt;
976             }
977 
978             textureCoordinateArrays.emplace_back();
979             std::vector<Vector2>& textureCoordinates = textureCoordinateArrays.back();
980             textureCoordinates.reserve(accessor.count);
981             const auto buffer = bufferView<Vector2>(_d->model, accessor);
982             std::copy(buffer.begin(), buffer.end(), std::back_inserter(textureCoordinates));
983 
984         /* Color attribute ends with _0, _1 ... */
985         } else if(Utility::String::beginsWith(attribute.first, "COLOR")) {
986             colorArrays.emplace_back();
987             std::vector<Color4>& colors = colorArrays.back();
988             colors.reserve(accessor.count);
989 
990             if(accessor.type == TINYGLTF_TYPE_VEC3) {
991                 colors.reserve(accessor.count);
992                 const auto buffer = bufferView<Color3>(_d->model, accessor);
993                 std::copy(buffer.begin(), buffer.end(), std::back_inserter(colors));
994 
995             } else if(accessor.type == TINYGLTF_TYPE_VEC4) {
996                 colors.reserve(accessor.count);
997                 const auto buffer = bufferView<Color4>(_d->model, accessor);
998                 std::copy(buffer.begin(), buffer.end(), std::back_inserter(colors));
999 
1000             } else {
1001                 Error() << "Trade::TinyGltfImporter::mesh3D(): expected type of" << attribute.first << "is VEC3 or VEC4";
1002                 return Containers::NullOpt;
1003             }
1004 
1005         } else {
1006             Warning() << "Trade::TinyGltfImporter::mesh3D(): unsupported mesh vertex attribute" << attribute.first;
1007             continue;
1008         }
1009     }
1010 
1011     /* Indices */
1012     std::vector<UnsignedInt> indices;
1013     if(primitive.indices != -1) {
1014         const tinygltf::Accessor& accessor = _d->model.accessors[primitive.indices];
1015 
1016         if(accessor.type != TINYGLTF_TYPE_SCALAR) {
1017             Error() << "Trade::TinyGltfImporter::mesh3D(): expected type of index is SCALAR";
1018             return Containers::NullOpt;
1019         }
1020 
1021         indices.reserve(accessor.count);
1022         if(accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) {
1023             const auto buffer = bufferView<UnsignedByte>(_d->model, accessor);
1024             std::copy(buffer.begin(), buffer.end(), std::back_inserter(indices));
1025         } else if(accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) {
1026             const auto buffer = bufferView<UnsignedShort>(_d->model, accessor);
1027             std::copy(buffer.begin(), buffer.end(), std::back_inserter(indices));
1028         } else if(accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) {
1029             const auto buffer = bufferView<UnsignedInt>(_d->model, accessor);
1030             std::copy(buffer.begin(), buffer.end(), std::back_inserter(indices));
1031         } else std::abort(); /* LCOV_EXCL_LINE */
1032     }
1033 
1034     /* Flip Y axis of texture coordinates */
1035     for(std::vector<Vector2>& layer: textureCoordinateArrays)
1036         for(Vector2& c: layer) c.y() = 1.0f - c.y();
1037 
1038     return MeshData3D(meshPrimitive, std::move(indices), {std::move(positions)}, std::move(normalArrays), std::move(textureCoordinateArrays), std::move(colorArrays), &mesh);
1039 }
1040 
doMaterialCount() const1041 UnsignedInt TinyGltfImporter::doMaterialCount() const {
1042     return _d->model.materials.size();
1043 }
1044 
doMaterialForName(const std::string & name)1045 Int TinyGltfImporter::doMaterialForName(const std::string& name) {
1046     if(!_d->materialsForName) {
1047         _d->materialsForName.emplace();
1048         _d->materialsForName->reserve(_d->model.materials.size());
1049         for(std::size_t i = 0; i != _d->model.materials.size(); ++i)
1050             _d->materialsForName->emplace(_d->model.materials[i].name, i);
1051     }
1052 
1053     const auto found = _d->materialsForName->find(name);
1054     return found == _d->materialsForName->end() ? -1 : found->second;
1055 }
1056 
doMaterialName(const UnsignedInt id)1057 std::string TinyGltfImporter::doMaterialName(const UnsignedInt id) {
1058     return _d->model.materials[id].name;
1059 }
1060 
doMaterial(const UnsignedInt id)1061 Containers::Pointer<AbstractMaterialData> TinyGltfImporter::doMaterial(const UnsignedInt id) {
1062     const tinygltf::Material& material = _d->model.materials[id];
1063 
1064     /* Alpha mode and mask, double sided */
1065     PhongMaterialData::Flags flags;
1066     MaterialAlphaMode alphaMode = MaterialAlphaMode::Opaque;
1067     Float alphaMask = 0.5f;
1068     {
1069         auto found = material.additionalValues.find("alphaCutoff");
1070         if(found != material.additionalValues.end())
1071             alphaMask = found->second.Factor();
1072     } {
1073         auto found = material.additionalValues.find("alphaMode");
1074         if(found != material.additionalValues.end()) {
1075             if(found->second.string_value == "OPAQUE")
1076                 alphaMode = MaterialAlphaMode::Opaque;
1077             else if(found->second.string_value == "BLEND")
1078                 alphaMode = MaterialAlphaMode::Blend;
1079             else if(found->second.string_value == "MASK")
1080                 alphaMode = MaterialAlphaMode::Mask;
1081             else {
1082                 Error{} << "Trade::TinyGltfImporter::material(): unknown alpha mode" << found->second.string_value;
1083                 return nullptr;
1084             }
1085         }
1086     } {
1087         auto found = material.additionalValues.find("doubleSided");
1088         if(found != material.additionalValues.end() && found->second.bool_value)
1089             flags |= PhongMaterialData::Flag::DoubleSided;
1090     }
1091 
1092     /* Textures */
1093     UnsignedInt diffuseTexture{}, specularTexture{};
1094     Color4 diffuseColor{1.0f};
1095     Color3 specularColor{1.0f};
1096     Float shininess{1.0f};
1097 
1098     /* Make Blinn/Phong a priority, because there we can import most properties */
1099     if(material.extensions.find("KHR_materials_cmnBlinnPhong") != material.extensions.end()) {
1100         tinygltf::Value cmnBlinnPhongExt = material.extensions.at("KHR_materials_cmnBlinnPhong");
1101 
1102         auto diffuseTextureValue = cmnBlinnPhongExt.Get("diffuseTexture");
1103         if(diffuseTextureValue.Type() != tinygltf::NULL_TYPE) {
1104             diffuseTexture = UnsignedInt(diffuseTextureValue.Get("index").Get<int>());
1105             flags |= PhongMaterialData::Flag::DiffuseTexture;
1106         }
1107 
1108         auto specularTextureValue = cmnBlinnPhongExt.Get("specularShininessTexture");
1109         if(specularTextureValue.Type() != tinygltf::NULL_TYPE) {
1110             specularTexture = UnsignedInt(specularTextureValue.Get("index").Get<int>());
1111             flags |= PhongMaterialData::Flag::SpecularTexture;
1112         }
1113 
1114         /* Colors */
1115         auto diffuseFactorValue = cmnBlinnPhongExt.Get("diffuseFactor");
1116         if(diffuseFactorValue.Type() != tinygltf::NULL_TYPE) {
1117             diffuseColor = Vector4{Vector4d{
1118                 diffuseFactorValue.Get(0).Get<double>(),
1119                 diffuseFactorValue.Get(1).Get<double>(),
1120                 diffuseFactorValue.Get(2).Get<double>(),
1121                 diffuseFactorValue.Get(3).Get<double>()}};
1122         }
1123 
1124         auto specularColorValue = cmnBlinnPhongExt.Get("specularFactor");
1125         if(specularColorValue.Type() != tinygltf::NULL_TYPE) {
1126             specularColor = Vector3{Vector3d{
1127                 specularColorValue.Get(0).Get<double>(),
1128                 specularColorValue.Get(1).Get<double>(),
1129                 specularColorValue.Get(2).Get<double>()}};
1130         }
1131 
1132         /* Parameters */
1133         auto shininessFactorValue = cmnBlinnPhongExt.Get("shininessFactor");
1134         if(shininessFactorValue.Type() != tinygltf::NULL_TYPE) {
1135             shininess = float(shininessFactorValue.Get<double>());
1136         }
1137 
1138     /* After that there is the PBR Specular/Glosiness */
1139     } else if(material.extensions.find("KHR_materials_pbrSpecularGlossiness") != material.extensions.end()) {
1140         tinygltf::Value pbrSpecularGlossiness = material.extensions.at("KHR_materials_pbrSpecularGlossiness");
1141 
1142         auto diffuseTextureValue = pbrSpecularGlossiness.Get("diffuseTexture");
1143         if(diffuseTextureValue.Type() != tinygltf::NULL_TYPE) {
1144             diffuseTexture = UnsignedInt(diffuseTextureValue.Get("index").Get<int>());
1145             flags |= PhongMaterialData::Flag::DiffuseTexture;
1146         }
1147 
1148         auto specularTextureValue = pbrSpecularGlossiness.Get("specularGlossinessTexture");
1149         if(specularTextureValue.Type() != tinygltf::NULL_TYPE) {
1150             specularTexture = UnsignedInt(specularTextureValue.Get("index").Get<int>());
1151             flags |= PhongMaterialData::Flag::SpecularTexture;
1152         }
1153 
1154         /* Colors */
1155         auto diffuseFactorValue = pbrSpecularGlossiness.Get("diffuseFactor");
1156         if(diffuseFactorValue.Type() != tinygltf::NULL_TYPE) {
1157             diffuseColor = Vector4{Vector4d{
1158                 diffuseFactorValue.Get(0).Get<double>(),
1159                 diffuseFactorValue.Get(1).Get<double>(),
1160                 diffuseFactorValue.Get(2).Get<double>(),
1161                 diffuseFactorValue.Get(3).Get<double>()}};
1162         }
1163 
1164         auto specularColorValue = pbrSpecularGlossiness.Get("specularFactor");
1165         if(specularColorValue.Type() != tinygltf::NULL_TYPE) {
1166             specularColor = Vector3{Vector3d{
1167                 specularColorValue.Get(0).Get<double>(),
1168                 specularColorValue.Get(1).Get<double>(),
1169                 specularColorValue.Get(2).Get<double>()}};
1170         }
1171 
1172     /* From the core Metallic/Roughness we get just the base color / texture */
1173     } else {
1174         auto dt = material.values.find("baseColorTexture");
1175         if(dt != material.values.end()) {
1176             diffuseTexture = dt->second.TextureIndex();
1177             flags |= PhongMaterialData::Flag::DiffuseTexture;
1178         }
1179 
1180         auto baseColorFactorValue = material.values.find("baseColorFactor");
1181         if(baseColorFactorValue != material.values.end()) {
1182             tinygltf::ColorValue color = baseColorFactorValue->second.ColorFactor();
1183             diffuseColor = Vector4{Vector4d::from(color.data())};
1184         }
1185     }
1186 
1187     /* Put things together */
1188     Containers::Pointer<PhongMaterialData> data{Containers::InPlaceInit, flags, alphaMode, alphaMask, shininess, &material};
1189     if(flags & PhongMaterialData::Flag::DiffuseTexture)
1190         data->diffuseTexture() = diffuseTexture;
1191     else data->diffuseColor() = diffuseColor;
1192     if(flags & PhongMaterialData::Flag::SpecularTexture)
1193         data->specularTexture() = specularTexture;
1194     else data->specularColor() = specularColor;
1195 
1196     /* Needs to be explicit on GCC 4.8 and Clang 3.8 so it can properly upcast
1197        the pointer. Just std::move() works as well, but that gives a warning
1198        on GCC 9. */
1199     return Containers::Pointer<AbstractMaterialData>{std::move(data)};
1200 }
1201 
doTextureCount() const1202 UnsignedInt TinyGltfImporter::doTextureCount() const {
1203     return _d->model.textures.size();
1204 }
1205 
doTextureForName(const std::string & name)1206 Int TinyGltfImporter::doTextureForName(const std::string& name) {
1207     if(!_d->texturesForName) {
1208         _d->texturesForName.emplace();
1209         _d->texturesForName->reserve(_d->model.textures.size());
1210         for(std::size_t i = 0; i != _d->model.textures.size(); ++i)
1211             _d->texturesForName->emplace(_d->model.textures[i].name, i);
1212     }
1213 
1214     const auto found = _d->texturesForName->find(name);
1215     return found == _d->texturesForName->end() ? -1 : found->second;
1216 }
1217 
doTextureName(const UnsignedInt id)1218 std::string TinyGltfImporter::doTextureName(const UnsignedInt id) {
1219     return _d->model.textures[id].name;
1220 }
1221 
doTexture(const UnsignedInt id)1222 Containers::Optional<TextureData> TinyGltfImporter::doTexture(const UnsignedInt id) {
1223     const tinygltf::Texture& tex = _d->model.textures[id];
1224 
1225     /* Image ID. Try various extensions first. */
1226     UnsignedInt imageId;
1227 
1228     /* Basis textures. This extension is nonstandard and in case of embedded
1229        images there's no standardized MIME type either. Fortunately we
1230        don't care as we detect the file type based on magic, unfortunately we
1231        *have to* use data:application/octet-stream there because TinyGLTF has a
1232        whitelist for MIME types:
1233        https://github.com/syoyo/tinygltf/blob/7e009041e35b999fd1e47c0f0e42cadcf8f5c31c/tiny_gltf.h#L2706
1234        This will all get solved once KTX2 materializes (but then it becomes
1235        more complex as well). For reference:
1236        https://github.com/BabylonJS/Babylon.js/issues/6636
1237        https://github.com/BinomialLLC/basis_universal/issues/52 */
1238     if(tex.extensions.find("GOOGLE_texture_basis") != tex.extensions.end()) {
1239         /** @todo check for "extensionsRequired" as well? currently not doing
1240             that, because I don't see why */
1241         tinygltf::Value basis = tex.extensions.at("GOOGLE_texture_basis");
1242         imageId = basis.Get("source").Get<int>();
1243 
1244     /* Image source */
1245     } else if(tex.source != -1) {
1246         imageId = UnsignedInt(tex.source);
1247 
1248     /* Well. */
1249     } else {
1250         Error{} << "Trade::TinyGltfImporter::texture(): no image source found";
1251         return Containers::NullOpt;
1252     }
1253 
1254     /* Sampler */
1255     if(tex.sampler < 0) {
1256         /* The specification instructs to use "auto sampling", i.e. it is left
1257            to the implementor to decide on the default values... */
1258         return TextureData{TextureData::Type::Texture2D, SamplerFilter::Linear, SamplerFilter::Linear,
1259             SamplerMipmap::Linear, {SamplerWrapping::Repeat, SamplerWrapping::Repeat, SamplerWrapping::Repeat}, imageId, &tex};
1260     }
1261     const tinygltf::Sampler& s = _d->model.samplers[tex.sampler];
1262 
1263     SamplerFilter minFilter;
1264     SamplerMipmap mipmap;
1265     switch(s.minFilter) {
1266         case TINYGLTF_TEXTURE_FILTER_NEAREST:
1267             minFilter = SamplerFilter::Nearest;
1268             mipmap = SamplerMipmap::Base;
1269             break;
1270         case TINYGLTF_TEXTURE_FILTER_LINEAR:
1271             minFilter = SamplerFilter::Linear;
1272             mipmap = SamplerMipmap::Base;
1273             break;
1274         case TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_NEAREST:
1275             minFilter = SamplerFilter::Nearest;
1276             mipmap = SamplerMipmap::Nearest;
1277             break;
1278         case TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_LINEAR:
1279             minFilter = SamplerFilter::Nearest;
1280             mipmap = SamplerMipmap::Linear;
1281             break;
1282         case TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_NEAREST:
1283             minFilter = SamplerFilter::Linear;
1284             mipmap = SamplerMipmap::Nearest;
1285             break;
1286         case TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_LINEAR:
1287             minFilter = SamplerFilter::Linear;
1288             mipmap = SamplerMipmap::Linear;
1289             break;
1290         default: std::abort(); /* LCOV_EXCL_LINE */
1291     }
1292 
1293     SamplerFilter magFilter;
1294     switch(s.magFilter) {
1295         case TINYGLTF_TEXTURE_FILTER_NEAREST:
1296             magFilter = SamplerFilter::Nearest;
1297             break;
1298         case TINYGLTF_TEXTURE_FILTER_LINEAR:
1299             magFilter = SamplerFilter::Linear;
1300             break;
1301         default: std::abort(); /* LCOV_EXCL_LINE */
1302     }
1303 
1304     /* There's wrapR that is a tiny_gltf extension and is set to zero. Ignoring
1305        that one and hardcoding it to Repeat. */
1306     Array3D<SamplerWrapping> wrapping;
1307     wrapping.z() = SamplerWrapping::Repeat;
1308     for(auto&& wrap: std::initializer_list<std::pair<int, int>>{
1309         {s.wrapS, 0}, {s.wrapT, 1}})
1310     {
1311         switch(wrap.first) {
1312             case TINYGLTF_TEXTURE_WRAP_REPEAT:
1313                 wrapping[wrap.second] = SamplerWrapping::Repeat;
1314                 break;
1315             case TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE:
1316                 wrapping[wrap.second] = SamplerWrapping::ClampToEdge;
1317                 break;
1318             case TINYGLTF_TEXTURE_WRAP_MIRRORED_REPEAT:
1319                 wrapping[wrap.second] = SamplerWrapping::MirroredRepeat;
1320                 break;
1321             default: std::abort(); /* LCOV_EXCL_LINE */
1322         }
1323     }
1324 
1325     /* glTF supports only 2D textures */
1326     return TextureData{TextureData::Type::Texture2D, minFilter, magFilter,
1327         mipmap, wrapping, imageId, &tex};
1328 }
1329 
doImage2DCount() const1330 UnsignedInt TinyGltfImporter::doImage2DCount() const {
1331     return _d->model.images.size();
1332 }
1333 
doImage2DForName(const std::string & name)1334 Int TinyGltfImporter::doImage2DForName(const std::string& name) {
1335     if(!_d->imagesForName) {
1336         _d->imagesForName.emplace();
1337         _d->imagesForName->reserve(_d->model.images.size());
1338         for(std::size_t i = 0; i != _d->model.images.size(); ++i)
1339             _d->imagesForName->emplace(_d->model.images[i].name, i);
1340     }
1341 
1342     const auto found = _d->imagesForName->find(name);
1343     return found == _d->imagesForName->end() ? -1 : found->second;
1344 }
1345 
doImage2DName(const UnsignedInt id)1346 std::string TinyGltfImporter::doImage2DName(const UnsignedInt id) {
1347     return _d->model.images[id].name;
1348 }
1349 
doImage2D(const UnsignedInt id)1350 Containers::Optional<ImageData2D> TinyGltfImporter::doImage2D(const UnsignedInt id) {
1351     CORRADE_ASSERT(manager(), "Trade::TinyGltfImporter::image2D(): the plugin must be instantiated with access to plugin manager in order to load images", {});
1352 
1353     /* Because we specified an empty callback for loading image data,
1354        Image.image, Image.width, Image.height and Image.component will not be
1355        valid and should not be accessed. */
1356 
1357     const tinygltf::Image& image = _d->model.images[id];
1358 
1359     AnyImageImporter imageImporter{*manager()};
1360     if(fileCallback()) imageImporter.setFileCallback(fileCallback(), fileCallbackUserData());
1361 
1362     /* Load embedded image */
1363     if(image.uri.empty()) {
1364         Containers::ArrayView<const char> data;
1365 
1366         /* The image data are stored in a buffer */
1367         if(image.bufferView != -1) {
1368             const tinygltf::BufferView& bufferView = _d->model.bufferViews[image.bufferView];
1369             const tinygltf::Buffer& buffer = _d->model.buffers[bufferView.buffer];
1370 
1371             data = Containers::arrayCast<const char>(Containers::arrayView(&buffer.data[bufferView.byteOffset], bufferView.byteLength));
1372 
1373         /* Image data were a data URI, the loadImageData() callback copied them
1374            without decoding to the internal data vector */
1375         } else {
1376             data = Containers::arrayCast<const char>(Containers::arrayView(image.image.data(), image.image.size()));
1377         }
1378 
1379         Containers::Optional<ImageData2D> imageData;
1380         if(!imageImporter.openData(data) || !(imageData = imageImporter.image2D(0)))
1381             return Containers::NullOpt;
1382 
1383         return ImageData2D{std::move(*imageData), &image};
1384 
1385     /* Load external image */
1386     } else {
1387         if(!_d->filePath && !fileCallback()) {
1388             Error{} << "Trade::TinyGltfImporter::image2D(): external images can be imported only when opening files from the filesystem or if a file callback is present";
1389             return {};
1390         }
1391 
1392         Containers::Optional<ImageData2D> imageData;
1393         if(!imageImporter.openFile(Utility::Directory::join(_d->filePath ? *_d->filePath : "", image.uri)) || !(imageData = imageImporter.image2D(0)))
1394             return Containers::NullOpt;
1395 
1396         return ImageData2D{std::move(*imageData), &image};
1397     }
1398 }
1399 
doImporterState() const1400 const void* TinyGltfImporter::doImporterState() const {
1401     return &_d->model;
1402 }
1403 
1404 }}
1405 
1406 CORRADE_PLUGIN_REGISTER(TinyGltfImporter, Magnum::Trade::TinyGltfImporter,
1407     "cz.mosra.magnum.Trade.AbstractImporter/0.3")
1408