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