diff --git a/src/MagnumPlugins/AssimpImporter/AssimpImporter.cpp b/src/MagnumPlugins/AssimpImporter/AssimpImporter.cpp index a786f7d38..f071195bc 100644 --- a/src/MagnumPlugins/AssimpImporter/AssimpImporter.cpp +++ b/src/MagnumPlugins/AssimpImporter/AssimpImporter.cpp @@ -1617,7 +1617,8 @@ Containers::Optional AssimpImporter::doMaterial(const UnsignedInt /* +1 is null byte for the key */ if(valueSize + keyString.size() + 1 + sizeof(MaterialAttributeType) > sizeof(MaterialAttributeData)) { - Error{} << "Trade::AssimpImporter::material(): property" << keyString << "is too large with" << valueSize << "bytes, skipping"; + Warning{} << "Trade::AssimpImporter::material(): property" << keyString << + "is too large with" << valueSize + keyString.size() << "bytes, skipping"; continue; } diff --git a/src/MagnumPlugins/AssimpImporter/Test/AssimpImporterTest.cpp b/src/MagnumPlugins/AssimpImporter/Test/AssimpImporterTest.cpp index faa9adfa3..a20e8c0e9 100644 --- a/src/MagnumPlugins/AssimpImporter/Test/AssimpImporterTest.cpp +++ b/src/MagnumPlugins/AssimpImporter/Test/AssimpImporterTest.cpp @@ -892,7 +892,7 @@ void AssimpImporterTest::animationGltfTicksPerSecondPatching() { CORRADE_VERIFY(Containers::StringView{out.str()}.contains( " ticks per second is incorrect for glTF, patching to 1000\n")); } else - CORRADE_VERIFY(out.str().empty()); + CORRADE_COMPARE(out.str(), ""); } void AssimpImporterTest::animationFbxTicksPerSecondPatching() { @@ -918,7 +918,7 @@ void AssimpImporterTest::animationFbxTicksPerSecondPatching() { CORRADE_VERIFY(Containers::StringView{out.str()}.contains( " ticks per second is incorrect for FBX, patching to 1000\n")); } else - CORRADE_VERIFY(out.str().empty()); + CORRADE_COMPARE(out.str(), ""); } void AssimpImporterTest::animationDummyTracksRemovalEnabled() { @@ -978,7 +978,7 @@ void AssimpImporterTest::animationDummyTracksRemovalEnabled() { "Trade::AssimpImporter::animation(): ignoring dummy rotation track in animation 1, channel {}\n", targets[2].channel, targets[2].channel))); } else - CORRADE_VERIFY(out.str().empty()); + CORRADE_COMPARE(out.str(), ""); } void AssimpImporterTest::animationDummyTracksRemovalDisabled() { @@ -2254,7 +2254,13 @@ void AssimpImporterTest::materialRaw() { CORRADE_VERIFY(importer->openFile(Utility::Directory::join(ASSIMPIMPORTER_TEST_DIR, "material-raw.fbx"))); CORRADE_COMPARE(importer->materialCount(), 2); - Containers::Optional material = importer->material("Custom_Types"); + Containers::Optional material; + std::ostringstream out; + { + Warning redirectWarning{&out}; + material = importer->material("Custom_Types"); + } + CORRADE_VERIFY(material); CORRADE_COMPARE(material->types(), MaterialType{}); CORRADE_COMPARE(material->layerCount(), 1); @@ -2288,9 +2294,11 @@ void AssimpImporterTest::materialRaw() { CORRADE_COMPARE(material->attribute(2), 0.25f); } - if(_assimpVersion < 500) + if(_assimpVersion < 500) { CORRADE_WARN("This version of Assimp doesn't import raw FBX material properties."); - else { + + CORRADE_COMPARE(out.str(), ""); + } else { /* Raw attributes taken directly from the FBX file, prefixed with "$raw.". Seems to be the only importer that supports that. */ { @@ -2298,12 +2306,14 @@ void AssimpImporterTest::materialRaw() { CORRADE_COMPARE(material->attributeType(3), MaterialAttributeType::Vector3); CORRADE_COMPARE(material->attribute(3), (Vector3{0.1f, 0.2f, 0.3f})); } { - CORRADE_EXPECT_FAIL_IF(_assimpVersion < 500, - "This version of Assimp doesn't import raw FBX material properties."); CORRADE_COMPARE(material->attributeName(4), "$raw.SomeString"_s); CORRADE_COMPARE(material->attributeType(4), MaterialAttributeType::String); CORRADE_COMPARE(material->attribute(4), "Ministry of Finance (Turkmenistan)"); } + + CORRADE_COMPARE(out.str(), + "Trade::AssimpImporter::material(): property $raw.LongNameLongNameLongNameLongNameLongNameLongNameLongName is too large with 67 bytes, skipping\n" + "Trade::AssimpImporter::material(): property $raw.LongValue is too large with 70 bytes, skipping\n"); } if(_assimpVersion < 410) @@ -2735,7 +2745,7 @@ void AssimpImporterTest::meshSkinningAttributes() { mesh = importer->mesh(meshName); } /* No warning about glTF dropping sets of weights */ - CORRADE_VERIFY(out.str().empty()); + CORRADE_COMPARE(out.str(), ""); CORRADE_VERIFY(mesh); CORRADE_VERIFY(mesh->hasAttribute(jointsAttribute)); diff --git a/src/MagnumPlugins/AssimpImporter/Test/material-raw.fbx b/src/MagnumPlugins/AssimpImporter/Test/material-raw.fbx index 43fd1fa4a..04118aeb1 100644 --- a/src/MagnumPlugins/AssimpImporter/Test/material-raw.fbx +++ b/src/MagnumPlugins/AssimpImporter/Test/material-raw.fbx @@ -356,6 +356,8 @@ Objects: { P: "Opacity", "double", "Number", "",0.25 P: "SomeColor", "Vector3D", "", "",0.1,0.2,0.3 P: "SomeString", "KString", "", "","Ministry of Finance (Turkmenistan)" + P: "LongNameLongNameLongNameLongNameLongNameLongNameLongName", "KString", "", "","Test" + P: "LongValue", "KString", "", "","LongValueLongValueLongValueLongValueLongValueLongValue" } } } diff --git a/src/MagnumPlugins/CgltfImporter/CgltfImporter.cpp b/src/MagnumPlugins/CgltfImporter/CgltfImporter.cpp index 79a52e845..775d2c4b3 100644 --- a/src/MagnumPlugins/CgltfImporter/CgltfImporter.cpp +++ b/src/MagnumPlugins/CgltfImporter/CgltfImporter.cpp @@ -27,6 +27,7 @@ #include "CgltfImporter.h" #include /* std::stable_sort() */ +#include #include #include #include @@ -41,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -82,6 +84,9 @@ namespace { #define CGLTF_FREE(ptr) freeNoop(ptr) /* If we had a good replacement for ato(i|f|ll) we could set the corresponding CGLTF_ATOI etc. here and prevent stdlib.h from being included in cgltf.h */ +/** @todo Override CGLTF_ATOI with a parsing function that handles integers + with exponent notation: + https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#json-encoding */ #define CGLTF_IMPLEMENTATION @@ -199,7 +204,8 @@ bool isDataUri(Containers::StringView uri) { } /* Decode percent-encoded characters in URIs: - https://datatracker.ietf.org/doc/html/rfc3986#section-2.1 */ + https://datatracker.ietf.org/doc/html/rfc3986#section-2.1 + This returns std::string because we only use it with Directory::join(). */ std::string decodeUri(Containers::StringView uri) { std::string decoded = uri; const std::size_t decodedSize = cgltf_decode_uri(&decoded[0]); @@ -208,34 +214,53 @@ std::string decodeUri(Containers::StringView uri) { return decoded; } -struct JsonToken { - jsmntype_t type; - Containers::StringView str; - std::size_t size; -}; +/* Cgltf's JSON parser jsmn doesn't decode escaped characters so we do it after + parsing. If there's nothing to escape, returns an empty Optional. */ +Containers::Optional decodeString(Containers::StringView str) { + /* The input string can be UTF-8 encoded but we can use a byte search here + since all multi-byte UTF-8 characters have the high bit set and '\\' + doesn't, so this will only match single-byte ASCII characters. */ + const Containers::StringView escape = str.find('\\'); + if(escape.isEmpty()) + return Containers::NullOpt; + + /* Skip any processing until the first escape character */ + const std::size_t start = escape.data() - str.data(); + + Containers::String decoded{str}; + const std::size_t decodedSize = cgltf_decode_string(decoded.data() + start) + start; + CORRADE_INTERNAL_ASSERT(decodedSize < str.size()); + + return Containers::String{decoded.prefix(decodedSize)}; +} -Containers::Array parseJson(Containers::StringView str) { +Containers::Array parseJson(Containers::StringView str) { jsmn_parser parser{0, 0, 0}; Int numTokens = jsmn_parse(&parser, str.data(), str.size(), nullptr, 0); /* All JSON strings we're parsing come from cgltf and should already have passed jsmn parsing */ CORRADE_INTERNAL_ASSERT(numTokens >= 0); - Containers::Array jsmnTokens{std::size_t(numTokens)}; + Containers::Array tokens{std::size_t(numTokens)}; jsmn_init(&parser); - numTokens = jsmn_parse(&parser, str.data(), str.size(), jsmnTokens.data(), numTokens); - CORRADE_INTERNAL_ASSERT(std::size_t(numTokens) == jsmnTokens.size()); - - Containers::Array tokens{jsmnTokens.size()}; - for(Int i = 0; i != numTokens; ++i) { - tokens[i].type = jsmnTokens[i].type; - tokens[i].str = str.slice(jsmnTokens[i].start, jsmnTokens[i].end); - tokens[i].size = jsmnTokens[i].size; - } + numTokens = jsmn_parse(&parser, str.data(), str.size(), tokens.data(), numTokens); + CORRADE_INTERNAL_ASSERT(std::size_t(numTokens) == tokens.size()); return tokens; } +Containers::StringView tokenString(Containers::StringView json, const jsmntok_t& token) { + return json.slice(token.start, token.end); +} + +std::size_t skipJson(Containers::ArrayView tokens, std::size_t start = 0) { + const int skipped = cgltf_skip_json(tokens, int(start)); + /* Negative return value only happens for tokens with type JSMN_UNDEFINED, + which we should never get for valid JSON files */ + CORRADE_INTERNAL_ASSERT(skipped >= 0 && std::size_t(skipped) > start); + return skipped; +} + } struct CgltfImporter::Document { @@ -257,19 +282,17 @@ struct CgltfImporter::Document { so the buffer id is used as the index. */ Containers::Array> bufferData; - /* Cgltf's JSON parser jsmn doesn't decode escaped characters so we do it - after parsing. If there's nothing to escape, returns a view on the - original string. Decoded strings are cached in a map indexed by the - input view data pointer. This works because we only call this function - with views on strings from cgltf_data. + /* Decode and cache strings in a map indexed by the input view data + pointer. This works because we only call this function with views on + strings from cgltf_data. Note that parsing inside cgltf happens with unescaped strings, but we have no influence on that. In practice, this shouldn't be a problem. Old versions of the spec used to explicitly forbid non-ASCII keys/enums: https://github.com/KhronosGroup/glTF/tree/fd3ab461a1114fb0250bd76099153d2af50a7a1d/specification/2.0#json-encoding Newer spec versions changed this to "ASCII characters [...] SHOULD be - written without JSON escaping" */ - Containers::StringView decodeString(Containers::StringView str); + written without JSON escaping". */ + Containers::StringView decodeCachedString(Containers::StringView str); std::unordered_map decodedStrings; @@ -322,7 +345,7 @@ struct CgltfImporter::Document { implicitly as we can't perform Y-flip directly on the data. */ bool textureCoordinateYFlipInMaterial = false; - void materialTexture(const cgltf_texture_view& texture, Containers::Array& attributes, MaterialAttribute attribute, MaterialAttribute matrixAttribute, MaterialAttribute coordinateAttribute) const; + void materialTexture(const cgltf_texture_view& texture, Containers::Array& attributes, Containers::StringView attribute, Containers::StringView matrixAttribute, Containers::StringView coordinateAttribute) const; bool open = false; @@ -374,7 +397,7 @@ Containers::Optional> CgltfImporter::Document: storage = Containers::Array{static_cast(decoded), size}; return Containers::arrayCast(storage); } else if(importer.fileCallback()) { - const std::string fullPath = Utility::Directory::join(filePath ? *filePath : "", decodeUri(decodeString(uri))); + const std::string fullPath = Utility::Directory::join(filePath ? *filePath : "", decodeUri(decodeCachedString(uri))); Containers::Optional> view = importer.fileCallback()(fullPath, InputFileCallbackPolicy::LoadPermanent, importer.fileCallbackUserData()); if(!view) { Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "(): error opening file:" << Containers::StringView{fullPath} << ": file callback failed"; @@ -386,7 +409,7 @@ Containers::Optional> CgltfImporter::Document: Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "(): external buffers can be imported only when opening files from the filesystem or if a file callback is present"; return Containers::NullOpt; } - const std::string fullPath = Utility::Directory::join(*filePath, decodeUri(decodeString(uri))); + const std::string fullPath = Utility::Directory::join(*filePath, decodeUri(decodeCachedString(uri))); if(!Utility::Directory::exists(fullPath)) { Error{} << "Trade::CgltfImporter::" << Debug::nospace << function << Debug::nospace << "(): error opening file:" << Containers::StringView{fullPath} << ": file not found"; return Containers::NullOpt; @@ -449,32 +472,26 @@ Containers::Optional> CgltfImporter:: {std::ptrdiff_t(accessor->stride), 1}}; } -Containers::StringView CgltfImporter::Document::decodeString(Containers::StringView str) { +Containers::StringView CgltfImporter::Document::decodeCachedString(Containers::StringView str) { if(str.isEmpty()) return str; - /* String has been decoded before */ + /* StringView constructed from nullptr doesn't have this flag, but it's + caught by isEmpty() above */ + CORRADE_INTERNAL_ASSERT(str.flags() >= Containers::StringViewFlag::NullTerminated); + + /* Return cached value if the string has been decoded before */ const auto found = decodedStrings.find(str.data()); if(found != decodedStrings.end()) return found->second; - /* The input string can be UTF-8 encoded but we can use a byte search here - since all multi-byte UTF-8 characters have the high bit set and '\\' - doesn't, so this will only match single-byte ASCII characters. */ - Containers::StringView escape = str.find('\\'); - /* No escaped sequence found. If the view is null-terminated (all strings - in cgltf_data should be), this stores a non-owning String. */ - if(escape.isEmpty()) + Containers::Optional decoded = decodeString(str); + /* Nothing to escape. This creates a non-owning String with a view on the + input data. */ + if(!decoded) return decodedStrings.emplace(str.data(), Containers::String::nullTerminatedView(str)).first->second; - /* Skip any processing until the first escape character */ - const std::size_t start = escape.data() - str.data(); - - Containers::String decoded{str}; - const std::size_t decodedSize = cgltf_decode_string(decoded.data() + start) + start; - CORRADE_INTERNAL_ASSERT(decodedSize < str.size()); - - return decodedStrings.emplace(str.data(), Containers::String{decoded.prefix(decodedSize)}).first->second; + return decodedStrings.emplace(str.data(), std::move(*decoded)).first->second; } namespace { @@ -634,6 +651,8 @@ void CgltfImporter::doOpenData(Containers::Array&& data, const DataFlags d /* Check required extensions. Every extension in extensionsRequired is required to "load and/or render an asset". */ + /** @todo Allow ignoring specific extensions through a config option, e.g. + ignoreRequiredExtension=KHR_materials_volume */ const bool ignoreRequiredExtensions = configuration().value("ignoreRequiredExtensions"); constexpr Containers::StringView supportedExtensions[]{ @@ -801,7 +820,7 @@ Int CgltfImporter::doAnimationForName(const std::string& name) { _d->animationsForName.emplace(); _d->animationsForName->reserve(_d->data->animations_count); for(std::size_t i = 0; i != _d->data->animations_count; ++i) - _d->animationsForName->emplace(_d->decodeString(_d->data->animations[i].name), i); + _d->animationsForName->emplace(_d->decodeCachedString(_d->data->animations[i].name), i); } const auto found = _d->animationsForName->find(name); @@ -811,7 +830,7 @@ Int CgltfImporter::doAnimationForName(const std::string& name) { std::string CgltfImporter::doAnimationName(UnsignedInt id) { /* If the animations are merged, don't report any names */ if(configuration().value("mergeAnimationClips")) return {}; - return _d->decodeString(_d->data->animations[id].name); + return _d->decodeCachedString(_d->data->animations[id].name); } namespace { @@ -916,7 +935,7 @@ Containers::Optional CgltfImporter::doAnimation(UnsignedInt id) { /* Calculate total track count. If merging all animations together, this is the sum of all clip track counts. */ std::size_t trackCount = 0; - for(const cgltf_animation& animation : animations) { + for(const cgltf_animation& animation: animations) { for(std::size_t i = 0; i != animation.channels_count; ++i) { /* Skip animations without a target node. See comment below. */ if(animation.channels[i].target_node) @@ -928,7 +947,7 @@ Containers::Optional CgltfImporter::doAnimation(UnsignedInt id) { bool hadToRenormalize = false; std::size_t trackId = 0; Containers::Array tracks{trackCount}; - for(const cgltf_animation& animation : animations) { + for(const cgltf_animation& animation: animations) { for(std::size_t i = 0; i != animation.channels_count; ++i) { const cgltf_animation_channel& channel = animation.channels[i]; const cgltf_animation_sampler& sampler = *channel.sampler; @@ -1162,7 +1181,7 @@ Int CgltfImporter::doCameraForName(const std::string& name) { _d->camerasForName.emplace(); _d->camerasForName->reserve(_d->data->cameras_count); for(std::size_t i = 0; i != _d->data->cameras_count; ++i) - _d->camerasForName->emplace(_d->decodeString(_d->data->cameras[i].name), i); + _d->camerasForName->emplace(_d->decodeCachedString(_d->data->cameras[i].name), i); } const auto found = _d->camerasForName->find(name); @@ -1170,7 +1189,7 @@ Int CgltfImporter::doCameraForName(const std::string& name) { } std::string CgltfImporter::doCameraName(const UnsignedInt id) { - return _d->decodeString(_d->data->cameras[id].name); + return _d->decodeCachedString(_d->data->cameras[id].name); } Containers::Optional CgltfImporter::doCamera(UnsignedInt id) { @@ -1213,7 +1232,7 @@ Int CgltfImporter::doLightForName(const std::string& name) { _d->lightsForName.emplace(); _d->lightsForName->reserve(_d->data->lights_count); for(std::size_t i = 0; i != _d->data->lights_count; ++i) - _d->lightsForName->emplace(_d->decodeString(_d->data->lights[i].name), i); + _d->lightsForName->emplace(_d->decodeCachedString(_d->data->lights[i].name), i); } const auto found = _d->lightsForName->find(name); @@ -1221,7 +1240,7 @@ Int CgltfImporter::doLightForName(const std::string& name) { } std::string CgltfImporter::doLightName(const UnsignedInt id) { - return _d->decodeString(_d->data->lights[id].name); + return _d->decodeCachedString(_d->data->lights[id].name); } Containers::Optional CgltfImporter::doLight(UnsignedInt id) { @@ -1301,7 +1320,7 @@ Int CgltfImporter::doSceneForName(const std::string& name) { _d->scenesForName.emplace(); _d->scenesForName->reserve(_d->data->scenes_count); for(std::size_t i = 0; i != _d->data->scenes_count; ++i) - _d->scenesForName->emplace(_d->decodeString(_d->data->scenes[i].name), i); + _d->scenesForName->emplace(_d->decodeCachedString(_d->data->scenes[i].name), i); } const auto found = _d->scenesForName->find(name); @@ -1309,7 +1328,7 @@ Int CgltfImporter::doSceneForName(const std::string& name) { } std::string CgltfImporter::doSceneName(const UnsignedInt id) { - return _d->decodeString(_d->data->scenes[id].name); + return _d->decodeCachedString(_d->data->scenes[id].name); } Containers::Optional CgltfImporter::doScene(UnsignedInt id) { @@ -1607,7 +1626,7 @@ Long CgltfImporter::doObjectForName(const std::string& name) { _d->nodesForName.emplace(); _d->nodesForName->reserve(_d->data->nodes_count); for(std::size_t i = 0; i != _d->data->nodes_count; ++i) { - _d->nodesForName->emplace(_d->decodeString(_d->data->nodes[i].name), i); + _d->nodesForName->emplace(_d->decodeCachedString(_d->data->nodes[i].name), i); } } @@ -1616,7 +1635,7 @@ Long CgltfImporter::doObjectForName(const std::string& name) { } std::string CgltfImporter::doObjectName(UnsignedLong id) { - return _d->decodeString(_d->data->nodes[id].name); + return _d->decodeCachedString(_d->data->nodes[id].name); } UnsignedInt CgltfImporter::doSkin3DCount() const { @@ -1628,7 +1647,7 @@ Int CgltfImporter::doSkin3DForName(const std::string& name) { _d->skinsForName.emplace(); _d->skinsForName->reserve(_d->data->skins_count); for(std::size_t i = 0; i != _d->data->skins_count; ++i) - _d->skinsForName->emplace(_d->decodeString(_d->data->skins[i].name), i); + _d->skinsForName->emplace(_d->decodeCachedString(_d->data->skins[i].name), i); } const auto found = _d->skinsForName->find(name); @@ -1636,7 +1655,7 @@ Int CgltfImporter::doSkin3DForName(const std::string& name) { } std::string CgltfImporter::doSkin3DName(const UnsignedInt id) { - return _d->decodeString(_d->data->skins[id].name); + return _d->decodeCachedString(_d->data->skins[id].name); } Containers::Optional CgltfImporter::doSkin3D(const UnsignedInt id) { @@ -1695,7 +1714,7 @@ Int CgltfImporter::doMeshForName(const std::string& name) { for(std::size_t i = 0; i != _d->data->meshes_count; ++i) { /* The mesh can be duplicated for as many primitives as it has, point to the first mesh in the duplicate sequence */ - _d->meshesForName->emplace(_d->decodeString(_d->data->meshes[i].name), _d->meshSizeOffsets[i]); + _d->meshesForName->emplace(_d->decodeCachedString(_d->data->meshes[i].name), _d->meshSizeOffsets[i]); } } @@ -1705,7 +1724,7 @@ Int CgltfImporter::doMeshForName(const std::string& name) { std::string CgltfImporter::doMeshName(const UnsignedInt id) { /* This returns the same name for all multi-primitive mesh duplicates */ - return _d->decodeString(_d->data->meshes[_d->meshMap[id].first()].name); + return _d->decodeCachedString(_d->data->meshes[_d->meshMap[id].first()].name); } Containers::Optional CgltfImporter::doMesh(const UnsignedInt id, UnsignedInt) { @@ -2177,7 +2196,7 @@ Int CgltfImporter::doMaterialForName(const std::string& name) { _d->materialsForName.emplace(); _d->materialsForName->reserve(_d->data->materials_count); for(std::size_t i = 0; i != _d->data->materials_count; ++i) - _d->materialsForName->emplace(_d->decodeString(_d->data->materials[i].name), i); + _d->materialsForName->emplace(_d->decodeCachedString(_d->data->materials[i].name), i); } const auto found = _d->materialsForName->find(name); @@ -2185,10 +2204,171 @@ Int CgltfImporter::doMaterialForName(const std::string& name) { } std::string CgltfImporter::doMaterialName(const UnsignedInt id) { - return _d->decodeString(_d->data->materials[id].name); + return _d->decodeCachedString(_d->data->materials[id].name); +} + +namespace { + +bool checkMaterialAttributeSize(Containers::StringView name, MaterialAttributeType type, const void* value = nullptr) { + std::size_t valueSize; + if(type == MaterialAttributeType::String) { + CORRADE_INTERNAL_ASSERT(value); + /* +2 are null byte and size */ + valueSize = static_cast(value)->size() + 2; + } else + valueSize = materialAttributeTypeSize(type); + + /* +1 is the key null byte */ + if(valueSize + name.size() + 1 + sizeof(MaterialAttributeType) > sizeof(MaterialAttributeData)) { + Warning{} << "Trade::CgltfImporter::material(): property" << name << + "is too large with" << valueSize + name.size() << "bytes, skipping"; + return false; + } + + return true; +} + +Containers::Optional parseMaterialAttribute(Containers::StringView json, Containers::ArrayView tokens) { + std::size_t tokenIndex = 0; + + CORRADE_INTERNAL_ASSERT(tokens[tokenIndex].type == JSMN_STRING); + + Containers::StringView name = tokenString(json, tokens[tokenIndex]); + if(name.isEmpty()) { + Warning{} << "Trade::CgltfImporter::material(): property with an empty name, skipping"; + return Containers::NullOpt; + } + + ++tokenIndex; + const jsmntok_t& token = tokens[tokenIndex]; + + /* We only need temporary storage for parsing primitive (arrays) as bool/ + Float/Vector[2/3/4]. Other types/sizes are either converted or ignored, + so we know the upper limit on the data size. The alignas prevents + unaligned reads for individual floats. For strings, + MaterialAttributeData expects a pointer to StringView. */ + alignas(4) char attributeData[16]; + Containers::String attributeString; + Containers::StringView attributeStringView; + MaterialAttributeType type{}; + if(token.type == JSMN_OBJECT) { + /* Not parsing textureInfo objects here because they're only needed by + extensions but not by extras. They may also append more than one + attribute, so this is handled directly in the extension parsing + loop. */ + Warning{} << "Trade::CgltfImporter::material(): property" << name << "is an object, skipping"; + return Containers::NullOpt; + + /* A primitive is anything that's not a string, object or array. We ignore + non-primitive arrays, so we can handle both in one place. */ + } else if(token.type == JSMN_PRIMITIVE || token.type == JSMN_ARRAY) { + const UnsignedInt start = tokenIndex + UnsignedInt(token.type == JSMN_ARRAY); + /* Primitive token size is 0, but we can't use max() because + that would allow empty arrays */ + const UnsignedInt count = token.type == JSMN_PRIMITIVE ? 1 : token.size; + + /* No use importing arbitrarily-sized arrays of primitives, those are + currently not used in any glTF extension */ + if(count >= 1 && count <= 4) { + /* This still works for non-primitive array members (like objects + or nested arrays), because we instantly abort when we find one + of those, so we never need more than four tokens */ + for(const jsmntok_t& element: tokens.slice(start, start + count)) { + if(element.type != JSMN_PRIMITIVE) { + type = MaterialAttributeType{}; + break; + } + + /* Jsmn only checks the first character, which allows some + invalid values like nnn. We perform basic type detection and + invalid values result in 0. This matches cgltf behaviour. */ + const Containers::StringView value = tokenString(json, element); + if(value == "true"_s || value == "false"_s) { + /* MaterialAttributeType has no bool vectors, and + converting to a number needlessly complicates parsing + later. So far there is no glTF extension that uses bool + vectors. */ + if(count > 1) { + type = MaterialAttributeType{}; + break; + } + type = MaterialAttributeType::Bool; + } else if(value != "null"_s) { + /* Always interpret numbers as floats because the type can + be ambiguous. E.g. integer attributes may use exponent + notation and decimal points, making correct type + detection depend on glTF exporter behaviour. */ + type = MaterialAttributeType::Float; + } else { + type = MaterialAttributeType{}; + break; + } + } + } + + if(type == MaterialAttributeType{}) { + Warning{} << "Trade::CgltfImporter::material(): property" << name << "has unsupported type, skipping"; + return Containers::NullOpt; + } + + if(type == MaterialAttributeType::Float) { + constexpr MaterialAttributeType vectorType[4] { + MaterialAttributeType::Float, MaterialAttributeType::Vector2, + MaterialAttributeType::Vector3, MaterialAttributeType::Vector4 + }; + type = vectorType[count - 1]; + + Vector4& data = *reinterpret_cast(attributeData); + for(UnsignedInt i = 0; i != count; ++i) + data[i] = cgltf_json_to_float(&tokens[start + i], reinterpret_cast(json.data())); + + } else if(type == MaterialAttributeType::Bool) { + CORRADE_INTERNAL_ASSERT(count == 1); + bool& data = *reinterpret_cast(attributeData); + data = cgltf_json_to_bool(&tokens[start], reinterpret_cast(json.data())); + + } else + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + + } else if(token.type == JSMN_STRING) { + const Containers::StringView value = tokenString(json, token); + Containers::Optional decoded = decodeString(value); + if(decoded) { + attributeString = std::move(*decoded); + attributeStringView = attributeString; + } else + attributeStringView = value; + type = MaterialAttributeType::String; + + } else { + /* JSMN_UNDEFINED, should never happen for valid JSON files */ + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + } + + CORRADE_INTERNAL_ASSERT(type != MaterialAttributeType{}); + + const void* valuePointer = type == MaterialAttributeType::String ? + static_cast(&attributeStringView) : static_cast(attributeData); + if(!checkMaterialAttributeSize(name, type, valuePointer)) + return Containers::NullOpt; + + /* Uppercase attribute names are reserved. Standard glTF (extension) + attributes should all be lowercase but we don't have this guarantee for + extras attributes. Can't use String::nullTerminatedView() here because + JSON tokens are not null-terminated. */ + Containers::String nameLowercase; + if(!name.isEmpty() && std::isupper(static_cast(name.front()))) { + nameLowercase = name; + nameLowercase[0] = std::tolower(static_cast(name.front())); + name = nameLowercase; + } + + return MaterialAttributeData{name, type, valuePointer}; } -void CgltfImporter::Document::materialTexture(const cgltf_texture_view& texture, Containers::Array& attributes, const MaterialAttribute attribute, const MaterialAttribute matrixAttribute, const MaterialAttribute coordinateAttribute) const { +} + +void CgltfImporter::Document::materialTexture(const cgltf_texture_view& texture, Containers::Array& attributes, Containers::StringView attribute, Containers::StringView matrixAttribute, Containers::StringView coordinateAttribute) const { CORRADE_INTERNAL_ASSERT(texture.texture); UnsignedInt texCoord = texture.texcoord; @@ -2197,7 +2377,7 @@ void CgltfImporter::Document::materialTexture(const cgltf_texture_view& texture, unflip them back, apply the transform (which assumes origin at bottom left and Y down) and then flip the result again. Sanity of the following verified with https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/TextureTransformTest */ - if(texture.has_transform) { + if(texture.has_transform && checkMaterialAttributeSize(matrixAttribute, MaterialAttributeType::Matrix3x3)) { Matrix3 matrix; /* If material needs an Y-flip, the mesh doesn't have the texture @@ -2229,7 +2409,9 @@ void CgltfImporter::Document::materialTexture(const cgltf_texture_view& texture, /* In case the material had no texture transformation but still needs an Y-flip, put it there */ - if(!texture.has_transform && textureCoordinateYFlipInMaterial) { + if(!texture.has_transform && textureCoordinateYFlipInMaterial && + checkMaterialAttributeSize(matrixAttribute, MaterialAttributeType::Matrix3x3)) + { arrayAppend(attributes, InPlaceInit, matrixAttribute, Matrix3::translation(Vector2::yAxis(1.0f))* Matrix3::scaling(Vector2::yScale(-1.0f))); @@ -2237,13 +2419,13 @@ void CgltfImporter::Document::materialTexture(const cgltf_texture_view& texture, /* Add texture coordinate set if non-zero. The KHR_texture_transform could be modifying it, so do that after */ - if(texCoord != 0) + if(texCoord != 0 && checkMaterialAttributeSize(coordinateAttribute, MaterialAttributeType::UnsignedInt)) arrayAppend(attributes, InPlaceInit, coordinateAttribute, texCoord); /* In some cases (when dealing with packed textures), we're parsing & adding texture coordinates and matrix multiple times, but adding the packed texture ID just once. In other cases the attribute is invalid. */ - if(attribute != MaterialAttribute{}) { + if(!attribute.isEmpty() && checkMaterialAttributeSize(attribute, MaterialAttributeType::UnsignedInt)) { const UnsignedInt textureId = texture.texture - data->textures; arrayAppend(attributes, InPlaceInit, attribute, textureId); } @@ -2291,32 +2473,33 @@ Containers::Optional CgltfImporter::doMaterial(const UnsignedInt i _d->materialTexture( material.pbr_metallic_roughness.base_color_texture, attributes, - MaterialAttribute::BaseColorTexture, - MaterialAttribute::BaseColorTextureMatrix, - MaterialAttribute::BaseColorTextureCoordinates); + "BaseColorTexture"_s, + "BaseColorTextureMatrix"_s, + "BaseColorTextureCoordinates"_s); } if(material.pbr_metallic_roughness.metallic_roughness_texture.texture) { _d->materialTexture( material.pbr_metallic_roughness.metallic_roughness_texture, attributes, - MaterialAttribute::NoneRoughnessMetallicTexture, - MaterialAttribute::MetalnessTextureMatrix, - MaterialAttribute::MetalnessTextureCoordinates); + "NoneRoughnessMetallicTexture"_s, + "MetalnessTextureMatrix"_s, + "MetalnessTextureCoordinates"_s); /* Add the matrix/coordinates attributes also for the roughness texture, but skip adding the texture ID again */ _d->materialTexture( material.pbr_metallic_roughness.metallic_roughness_texture, attributes, - MaterialAttribute{}, - MaterialAttribute::RoughnessTextureMatrix, - MaterialAttribute::RoughnessTextureCoordinates); + {}, + "RoughnessTextureMatrix"_s, + "RoughnessTextureCoordinates"_s); } /** @todo Support for KHR_materials_specular? This adds an explicit F0 (texture) and a scalar factor (texture) for the entire specular - reflection to a metallic/roughness material. */ + reflection to a metallic/roughness material. Currently imported as + a custom layer below. */ } /* Specular/glossiness material */ @@ -2346,27 +2529,27 @@ Containers::Optional CgltfImporter::doMaterial(const UnsignedInt i _d->materialTexture( material.pbr_specular_glossiness.diffuse_texture, attributes, - MaterialAttribute::DiffuseTexture, - MaterialAttribute::DiffuseTextureMatrix, - MaterialAttribute::DiffuseTextureCoordinates); + "DiffuseTexture"_s, + "DiffuseTextureMatrix"_s, + "DiffuseTextureCoordinates"_s); } if(material.pbr_specular_glossiness.specular_glossiness_texture.texture) { _d->materialTexture( material.pbr_specular_glossiness.specular_glossiness_texture, attributes, - MaterialAttribute::SpecularGlossinessTexture, - MaterialAttribute::SpecularTextureMatrix, - MaterialAttribute::SpecularTextureCoordinates); + "SpecularGlossinessTexture"_s, + "SpecularTextureMatrix"_s, + "SpecularTextureCoordinates"_s); /* Add the matrix/coordinates attributes also for the glossiness texture, but skip adding the texture ID again */ _d->materialTexture( material.pbr_specular_glossiness.specular_glossiness_texture, attributes, - MaterialAttribute{}, - MaterialAttribute::GlossinessTextureMatrix, - MaterialAttribute::GlossinessTextureCoordinates); + {}, + "GlossinessTextureMatrix"_s, + "GlossinessTextureCoordinates"_s); } } @@ -2379,9 +2562,9 @@ Containers::Optional CgltfImporter::doMaterial(const UnsignedInt i _d->materialTexture( material.normal_texture, attributes, - MaterialAttribute::NormalTexture, - MaterialAttribute::NormalTextureMatrix, - MaterialAttribute::NormalTextureCoordinates); + "NormalTexture"_s, + "NormalTextureMatrix"_s, + "NormalTextureCoordinates"_s); if(material.normal_texture.scale != 1.0f) arrayAppend(attributes, InPlaceInit, @@ -2394,9 +2577,9 @@ Containers::Optional CgltfImporter::doMaterial(const UnsignedInt i _d->materialTexture( material.occlusion_texture, attributes, - MaterialAttribute::OcclusionTexture, - MaterialAttribute::OcclusionTextureMatrix, - MaterialAttribute::OcclusionTextureCoordinates); + "OcclusionTexture"_s, + "OcclusionTextureMatrix"_s, + "OcclusionTextureCoordinates"_s); /* cgltf exposes the strength multiplier as scale */ if(material.occlusion_texture.scale != 1.0f) @@ -2415,9 +2598,9 @@ Containers::Optional CgltfImporter::doMaterial(const UnsignedInt i _d->materialTexture( material.emissive_texture, attributes, - MaterialAttribute::EmissiveTexture, - MaterialAttribute::EmissiveTextureMatrix, - MaterialAttribute::EmissiveTextureCoordinates); + "EmissiveTexture"_s, + "EmissiveTextureMatrix"_s, + "EmissiveTextureCoordinates"_s); } /* Phong material fallback for backwards compatibility */ @@ -2466,6 +2649,63 @@ Containers::Optional CgltfImporter::doMaterial(const UnsignedInt i arrayAppend(attributes, InPlaceInit, MaterialAttribute::DiffuseTextureCoordinates, *diffuseTextureCoordinates); } + /* Extras -- application-specific data, added to the base layer */ + if(material.extras.start_offset) { + CORRADE_INTERNAL_ASSERT(material.extras.end_offset > material.extras.start_offset); + const Containers::StringView json{ + _d->data->json + material.extras.start_offset, + material.extras.end_offset - material.extras.start_offset}; + /* Theoretically extras can be any token type but the glTF spec + recommends objects for interoperability, makes our life easier, too: + https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-extras + Cgltf directly gives us the JSON value string instead of a token so + we can only check the type like this. */ + if(json[0] == '{') { + const auto tokens = parseJson(json); + /* This is checked by jsmn */ + CORRADE_INTERNAL_ASSERT(!tokens.empty() && tokens[0].type == JSMN_OBJECT); + + UnsignedInt numAttributes = tokens[0].size; + Containers::Array attributeTokens; + Containers::arrayReserve(attributeTokens, numAttributes); + for(UnsignedInt t = 1; t + 1 < tokens.size();) { + /* This is checked by jsmn */ + CORRADE_INTERNAL_ASSERT(tokens[t].type == JSMN_STRING && tokens[t].size == 1); + Containers::arrayAppend(attributeTokens, InPlaceInit, t); + t = skipJson(tokens, t + 1); + } + + /* Sort and mark duplicates, those will be skipped later. We don't + need to cross-check for duplicates in the base layer because + those are all internal uppercase names and we make all names + lowercase. */ + std::stable_sort(attributeTokens.begin(), attributeTokens.end(), [&](UnsignedInt a, UnsignedInt b) { + return tokenString(json, tokens[a]) < tokenString(json, tokens[b]); + }); + + for(std::size_t i = 0; i + 1 < attributeTokens.size(); ++i) { + if(tokenString(json, tokens[attributeTokens[i]]) == tokenString(json, tokens[attributeTokens[i + 1]])) { + --numAttributes; + /* We can use 0 as an invalid token to mark attributes to skip. + Token 0 is always the extras object itself. */ + attributeTokens[i] = 0u; + } + } + + Containers::arrayReserve(attributes, attributes.size() + numAttributes); + for(UnsignedInt tokenIndex: attributeTokens) { + if(tokenIndex == 0u) continue; + + const Containers::Optional parsed = parseMaterialAttribute( + json, tokens.suffix(tokenIndex)); + if(parsed) + Containers::arrayAppend(attributes, *parsed); + } + + } else + Warning{} << "Trade::CgltfImporter::material(): extras property is not an object, skipping"; + } + /* Clear coat layer -- needs to be after all base material attributes */ if(material.has_clearcoat) { types |= MaterialType::PbrClearCoat; @@ -2483,9 +2723,9 @@ Containers::Optional CgltfImporter::doMaterial(const UnsignedInt i _d->materialTexture( material.clearcoat.clearcoat_texture, attributes, - MaterialAttribute::LayerFactorTexture, - MaterialAttribute::LayerFactorTextureMatrix, - MaterialAttribute::LayerFactorTextureCoordinates); + "LayerFactorTexture"_s, + "LayerFactorTextureMatrix"_s, + "LayerFactorTextureCoordinates"_s); } arrayAppend(attributes, InPlaceInit, @@ -2496,9 +2736,9 @@ Containers::Optional CgltfImporter::doMaterial(const UnsignedInt i _d->materialTexture( material.clearcoat.clearcoat_roughness_texture, attributes, - MaterialAttribute::RoughnessTexture, - MaterialAttribute::RoughnessTextureMatrix, - MaterialAttribute::RoughnessTextureCoordinates); + "RoughnessTexture"_s, + "RoughnessTextureMatrix"_s, + "RoughnessTextureCoordinates"_s); /* The extension description doesn't mention it, but the schema says the clearcoat roughness is actually in the G channel: @@ -2512,9 +2752,9 @@ Containers::Optional CgltfImporter::doMaterial(const UnsignedInt i _d->materialTexture( material.clearcoat.clearcoat_normal_texture, attributes, - MaterialAttribute::NormalTexture, - MaterialAttribute::NormalTextureMatrix, - MaterialAttribute::NormalTextureCoordinates); + "NormalTexture"_s, + "NormalTextureMatrix"_s, + "NormalTextureCoordinates"_s); if(material.clearcoat.clearcoat_normal_texture.scale != 1.0f) arrayAppend(attributes, InPlaceInit, @@ -2523,6 +2763,306 @@ Containers::Optional CgltfImporter::doMaterial(const UnsignedInt i } } + /* Import extensions with non-standard layer/attribute types that are + already parsed by cgltf and hence don't appear in the extension list + anymore. We use the original attribute names as found in the extension + specifications. To imitate actual unknown extension import below, all + Int and UnsignedInt attributes must be converted to Float. */ + /** @todo If these are turned into standard layer types they will have to + be duplicated for backwards compatibility, with the old names and type + conversion. */ + if(material.has_ior) { + arrayAppend(layers, UnsignedInt(attributes.size())); + arrayAppend(attributes, InPlaceInit, MaterialAttribute::LayerName, "#KHR_materials_ior"_s); + + arrayAppend(attributes, InPlaceInit, "ior", material.ior.ior); + } + + if(material.has_specular) { + arrayAppend(layers, UnsignedInt(attributes.size())); + arrayAppend(attributes, InPlaceInit, MaterialAttribute::LayerName, "#KHR_materials_specular"_s); + + arrayAppend(attributes, InPlaceInit, + "specularFactor"_s, + material.specular.specular_factor); + + if(material.specular.specular_texture.texture) { + _d->materialTexture( + material.specular.specular_texture, + attributes, + "specularTexture"_s, + "specularTextureMatrix"_s, + "specularTextureCoordinates"_s); + + /* Specular strength is stored in the alpha channel */ + arrayAppend(attributes, InPlaceInit, + "specularTextureSwizzle"_s, + MaterialTextureSwizzle::A); + } + + const Vector3 specularColorFactor = Vector3::from(material.specular.specular_color_factor); + arrayAppend(attributes, InPlaceInit, + "specularColorFactor"_s, + specularColorFactor); + + if(material.specular.specular_color_texture.texture) + _d->materialTexture( + material.specular.specular_color_texture, + attributes, + "specularColorTexture"_s, + "specularColorTextureMatrix"_s, + "specularColorTextureCoordinates"_s); + } + + if(material.has_transmission) { + arrayAppend(layers, UnsignedInt(attributes.size())); + arrayAppend(attributes, InPlaceInit, MaterialAttribute::LayerName, "#KHR_materials_transmission"_s); + + arrayAppend(attributes, InPlaceInit, + "transmissionFactor"_s, + material.transmission.transmission_factor); + + if(material.transmission.transmission_texture.texture) + _d->materialTexture( + material.transmission.transmission_texture, + attributes, + "transmissionTexture"_s, + "transmissionTextureMatrix"_s, + "transmissionTextureCoordinates"_s); + } + + if(material.has_volume) { + arrayAppend(layers, UnsignedInt(attributes.size())); + arrayAppend(attributes, InPlaceInit, MaterialAttribute::LayerName, "#KHR_materials_volume"_s); + + arrayAppend(attributes, InPlaceInit, + "thicknessFactor"_s, + material.volume.thickness_factor); + + if(material.volume.thickness_texture.texture) { + _d->materialTexture( + material.volume.thickness_texture, + attributes, + "thicknessTexture"_s, + "thicknessTextureMatrix"_s, + "thicknessTextureCoordinates"_s); + + /* Thickness is stored in the green channel */ + arrayAppend(attributes, InPlaceInit, + "thicknessTextureSwizzle"_s, + MaterialTextureSwizzle::G); + } + + /* Default spec value is infinity but cgltf uses FLT_MAX, fix it */ + const Float attenuationDistance = material.volume.attenuation_distance == std::numeric_limits::max() ? + Constants::inf() : material.volume.attenuation_distance; + arrayAppend(attributes, InPlaceInit, + "attenuationDistance"_s, + attenuationDistance); + + const Vector3 attenuationColor = Vector3::from(material.volume.attenuation_color); + arrayAppend(attributes, InPlaceInit, + "attenuationColor"_s, + attenuationColor); + } + + if(material.has_sheen) { + arrayAppend(layers, UnsignedInt(attributes.size())); + arrayAppend(attributes, InPlaceInit, MaterialAttribute::LayerName, "#KHR_materials_sheen"_s); + + const Vector3 sheenColorFactor = Vector3::from(material.sheen.sheen_color_factor); + arrayAppend(attributes, InPlaceInit, + "sheenColorFactor"_s, + sheenColorFactor); + + if(material.sheen.sheen_color_texture.texture) + _d->materialTexture( + material.sheen.sheen_color_texture, + attributes, + "sheenColorTexture"_s, + "sheenColorTextureMatrix"_s, + "sheenColorTextureCoordinates"_s); + + arrayAppend(attributes, InPlaceInit, + "sheenRoughnessFactor"_s, + material.sheen.sheen_roughness_factor); + + if(material.sheen.sheen_roughness_texture.texture) { + _d->materialTexture( + material.sheen.sheen_roughness_texture, + attributes, + "sheenRoughnessTexture"_s, + "sheenRoughnessTextureMatrix"_s, + "sheenRoughnessTextureCoordinates"_s); + + /* Sheen roughness is stored in the alpha channel */ + arrayAppend(attributes, InPlaceInit, + "sheenRoughnessTextureSwizzle"_s, + MaterialTextureSwizzle::A); + } + } + + /* Stable-sort extensions by name so that we can easily find duplicates and + overwrite lexically preceding extensions. This matches cgltf parsing + behaviour. */ + Containers::Array extensionOrder{material.extensions_count}; + for(UnsignedInt i = 0; i < extensionOrder.size(); ++i) + extensionOrder[i] = i; + + std::stable_sort(extensionOrder.begin(), extensionOrder.end(), [&](UnsignedInt a, UnsignedInt b) { + return std::strcmp(material.extensions[a].name, material.extensions[b].name) < 0; + }); + + /* Mark duplicates, those will be skipped later */ + for(std::size_t i = 0; i + 1 < extensionOrder.size(); ++i) { + const cgltf_extension& current = material.extensions[extensionOrder[i]]; + const cgltf_extension& next = material.extensions[extensionOrder[i + 1]]; + if(std::strcmp(current.name, next.name) == 0) + extensionOrder[i] = ~0u; + } + + /* Import unrecognized extension attributes as custom attributes, one + layer per extension */ + for(UnsignedInt e: extensionOrder) { + if(e == ~0u) continue; + + const cgltf_extension& extension = material.extensions[e]; + + const Containers::StringView extensionName = extension.name; + if(extensionName.isEmpty()) { + Warning{} << "Trade::CgltfImporter::material(): extension with an empty name, skipping"; + continue; + } + + /* +1 is the key null byte. +3 are the '#' layer prefix, the layer null + byte and the length. */ + if(" LayerName"_s.size() + 1 + extensionName.size() + 3 + sizeof(MaterialAttributeType) > sizeof(MaterialAttributeData)) { + Warning{} << "Trade::CgltfImporter::material(): extension name" << extensionName << + "is too long with" << extensionName.size() << "characters, skipping"; + continue; + } + + const Containers::StringView json = extension.data; + const auto tokens = parseJson(json); + /* First token is the extension object. This is checked by cgltf. If + the object is empty, tokens.size() is 1. */ + CORRADE_INTERNAL_ASSERT(!tokens.empty() && tokens[0].type == JSMN_OBJECT); + + UnsignedInt numAttributes = tokens[0].size; + Containers::Array attributeTokens; + Containers::arrayReserve(attributeTokens, numAttributes); + for(UnsignedInt t = 1; t + 1 < tokens.size();) { + /* This is checked by jsmn */ + CORRADE_INTERNAL_ASSERT(tokens[t].type == JSMN_STRING && tokens[t].size == 1); + Containers::arrayAppend(attributeTokens, InPlaceInit, t); + t = skipJson(tokens, t + 1); + } + + /* Sort and mark duplicates, those will be skipped later */ + std::stable_sort(attributeTokens.begin(), attributeTokens.end(), [&](UnsignedInt a, UnsignedInt b) { + return tokenString(json, tokens[a]) < tokenString(json, tokens[b]); + }); + + for(std::size_t i = 0; i + 1 < attributeTokens.size(); ++i) { + if(tokenString(json, tokens[attributeTokens[i]]) == tokenString(json, tokens[attributeTokens[i + 1]])) { + --numAttributes; + /* We can use 0 as an invalid token to mark attributes to skip. + Token 0 is always the extension object itself. */ + attributeTokens[i] = 0u; + } + } + + Containers::Array extensionAttributes; + Containers::arrayReserve(extensionAttributes, numAttributes); + for(UnsignedInt tokenIndex: attributeTokens) { + if(tokenIndex == 0u) continue; + + const Containers::StringView name = tokenString(json, tokens[tokenIndex]); + if(name.isEmpty()) { + Warning{} << "Trade::CgltfImporter::material(): property with an empty name, skipping"; + continue; + } + + if(tokens[tokenIndex + 1].type == JSMN_OBJECT) { + /* Parse glTF textureInfo objects. Any objects without the + correct suffix and type are ignored. */ + if(name.size() < 8 || !name.hasSuffix("Texture")) { + Warning{} << "Trade::CgltfImporter::material(): property" << name << "has non-texture object type, skipping"; + continue; + } + + cgltf_texture_view textureView{}; + const bool valid = cgltf_parse_json_texture_view(&_d->options, tokens, tokenIndex + 1, + reinterpret_cast(json.data()), &textureView) >= 0; + /* Free memory allocated by cgltf. We're only interested in + KHR_texture_transform and that's already parsed into + textureView.transform. */ + cgltf_free_extensions(_d->data, textureView.extensions, textureView.extensions_count); + + /* cgltf_parse_json_texture_view() casts and saves index + 1 as + cgltf_texture*. 0 indicates there was no index property. + It's mandatory, so we check for it. */ + if(!valid || !textureView.texture) { + Warning{} << "Trade::CgltfImporter::material(): property" << name << "has invalid texture object type, skipping"; + continue; + } + + const std::size_t index = std::size_t(textureView.texture) - 1; + if(index >= _d->data->images_count) { + Error{} << "Trade::CgltfImporter::material():" << name << "index" << index << "out of bounds for" << _d->data->textures_count << "textures"; + return Containers::NullOpt; + } + + /* materialTexture() expects a fixed up texture pointer in + cgltf_texture_view, normally done by cgltf */ + textureView.texture = &_d->data->textures[index]; + + Containers::String nameBuffer{NoInit, name.size()*2 + 6 + 11}; + Utility::formatInto(nameBuffer, "{}Matrix{}Coordinates", name, name); + _d->materialTexture( + textureView, + extensionAttributes, + name, + nameBuffer.prefix(name.size() + 6), + nameBuffer.suffix(name.size() + 6)); + + /** @todo If there are ever extensions that reference texture + types other than textureInfo and normalTextureInfo, we + might have to be a bit smarter here, e.g. detect + occlusionTextureInfo and suffix with "Strength" instead. + cgltf parses both "strength" and "scale" into the same + variable. */ + if(textureView.scale != 1.0f) { + Utility::formatInto(nameBuffer, "{}Scale", name); + const Containers::StringView scaleName = nameBuffer.prefix(name.size() + 5); + if(checkMaterialAttributeSize(scaleName, MaterialAttributeType::Float)) + arrayAppend(extensionAttributes, InPlaceInit, + scaleName, textureView.scale); + } + + } else { + /* All other attribute types: bool, numbers, strings */ + const Containers::Optional parsed = parseMaterialAttribute( + json, tokens.suffix(tokenIndex)); + if(parsed) + Containers::arrayAppend(extensionAttributes, *parsed); + } + } + + /* Only add a layer if the extension contains any attributes */ + if(!extensionAttributes.empty()) { + /* Uppercase layer names are reserved. Since all extension names + start with an uppercase vendor identifier, making the first + character lowercase seems silly, so we use a unique prefix. */ + Containers::String layerName{NoInit, extensionName.size() + 1}; + Utility::formatInto(layerName, "#{}", extensionName); + + arrayAppend(layers, UnsignedInt(attributes.size())); + arrayAppend(attributes, InPlaceInit, MaterialAttribute::LayerName, layerName); + arrayAppend(attributes, Containers::arrayView(extensionAttributes)); + } + } + /* If there's any layer, add the final attribute count */ arrayAppend(layers, UnsignedInt(attributes.size())); @@ -2542,7 +3082,7 @@ Int CgltfImporter::doTextureForName(const std::string& name) { _d->texturesForName.emplace(); _d->texturesForName->reserve(_d->data->textures_count); for(std::size_t i = 0; i != _d->data->textures_count; ++i) - _d->texturesForName->emplace(_d->decodeString(_d->data->textures[i].name), i); + _d->texturesForName->emplace(_d->decodeCachedString(_d->data->textures[i].name), i); } const auto found = _d->texturesForName->find(name); @@ -2550,7 +3090,7 @@ Int CgltfImporter::doTextureForName(const std::string& name) { } std::string CgltfImporter::doTextureName(const UnsignedInt id) { - return _d->decodeString(_d->data->textures[id].name); + return _d->decodeCachedString(_d->data->textures[id].name); } Containers::Optional CgltfImporter::doTexture(const UnsignedInt id) { @@ -2583,19 +3123,37 @@ Containers::Optional CgltfImporter::doTexture(const UnsignedInt id) - image importers available via manager()->aliasList()? - are there even files out there with more than one extension? */ for(std::size_t i = 0; i != tex.extensions_count && imageId == ~0u; ++i) { - for(const auto& ext: extensions) { - if(tex.extensions[i].name == ext) { + for(std::size_t j = 0; j != Containers::arraySize(extensions) && imageId == ~0u; ++j) { + if(tex.extensions[i].name == extensions[j]) { + const Containers::StringView json = tex.extensions[i].data; const auto tokens = parseJson(tex.extensions[i].data); - if(tokens.size() == 3 && tokens[0].type == JSMN_OBJECT && tokens[1].type == JSMN_STRING && tokens[1].str == "source" && tokens[2].type == JSMN_PRIMITIVE) { - std::size_t parsed = 0; - const Int source = std::stoi(tokens[2].str, &parsed); - if(parsed != tokens[2].str.size() || source < 0 || UnsignedInt(source) >= _d->data->images_count) { - Error{} << "Trade::CgltfImporter::texture():" << ext << "image" << source << "out of bounds for" << _d->data->images_count << "images"; + /* This is checked by cgltf */ + CORRADE_INTERNAL_ASSERT(!tokens.empty() && tokens[0].type == JSMN_OBJECT); + + Containers::Optional source; + std::size_t t = 1; + while(t + 1 < tokens.size()) { + /* This is checked by jsmn */ + CORRADE_INTERNAL_ASSERT(tokens[t].type == JSMN_STRING && tokens[t].size == 1); + + if(tokenString(json, tokens[t]) == "source" && tokens[t + 1].type == JSMN_PRIMITIVE) { + source = cgltf_json_to_int(&tokens[t + 1], reinterpret_cast(json.data())); + t += 2; + } else + t = skipJson(tokens, t + 1); + } + + /* Only check the index here because there can be multiple + JSON "source" keys and the last one wins. This matches + cgltf behaviour. */ + if(source) { + if(*source < 0 || std::size_t(*source) >= _d->data->images_count) { + Error{} << "Trade::CgltfImporter::texture():" << extensions[j] << "image" << + *source << "out of bounds for" << _d->data->images_count << "images"; return Containers::NullOpt; } - imageId = source; + imageId = *source; } - break; } } } @@ -2725,7 +3283,7 @@ Int CgltfImporter::doImage2DForName(const std::string& name) { _d->imagesForName.emplace(); _d->imagesForName->reserve(_d->data->images_count); for(std::size_t i = 0; i != _d->data->images_count; ++i) - _d->imagesForName->emplace(_d->decodeString(_d->data->images[i].name), i); + _d->imagesForName->emplace(_d->decodeCachedString(_d->data->images[i].name), i); } const auto found = _d->imagesForName->find(name); @@ -2733,7 +3291,7 @@ Int CgltfImporter::doImage2DForName(const std::string& name) { } std::string CgltfImporter::doImage2DName(const UnsignedInt id) { - return _d->decodeString(_d->data->images[id].name); + return _d->decodeCachedString(_d->data->images[id].name); } AbstractImporter* CgltfImporter::setupOrReuseImporterForImage(const UnsignedInt id, const char* const function) { @@ -2792,7 +3350,7 @@ AbstractImporter* CgltfImporter::setupOrReuseImporterForImage(const UnsignedInt return nullptr; } - if(!importer.openFile(Utility::Directory::join(_d->filePath ? *_d->filePath : "", decodeUri(_d->decodeString(image.uri))))) + if(!importer.openFile(Utility::Directory::join(_d->filePath ? *_d->filePath : "", decodeUri(_d->decodeCachedString(image.uri))))) return nullptr; return &_d->imageImporter.emplace(std::move(importer)); } diff --git a/src/MagnumPlugins/CgltfImporter/CgltfImporter.h b/src/MagnumPlugins/CgltfImporter/CgltfImporter.h index c5b524b07..fe647de3f 100644 --- a/src/MagnumPlugins/CgltfImporter/CgltfImporter.h +++ b/src/MagnumPlugins/CgltfImporter/CgltfImporter.h @@ -259,7 +259,7 @@ Import of morph data is not supported at the moment. @ref VertexFormat::Vector2sNormalized (which includes the additional types specified by [KHR_mesh_quantization](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_mesh_quantization/README.md)). The data are by default Y-flipped on import unless - @cb{.conf} textureCoordinateYFlipInMaterial @ce is either explicitly + @cb{.ini} textureCoordinateYFlipInMaterial @ce is either explicitly enabled, or if the file contains non-normalized integer or normalized signed integer texture coordinates (which can't easily be flipped). In that case texture coordinate data are kept as-is and materials provide a texture @@ -314,6 +314,35 @@ fail. types, and a new layer with clearcoat properties is added - Custom texture coordinate sets as well as [KHR_texture_transform](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_texture_transform/README.md) properties are imported on all textures. +- Unrecognized material extensions are imported as custom layers with a `#` + prefix. Extension properties are imported with their raw names and types, + the following of which are supported: + - @ref MaterialAttributeType::String + - @ref MaterialAttributeType::Bool + - All numbers as @ref MaterialAttributeType::Float to avoid inconsistency + with different glTF exporters. The only exception are texture indices + and coordinate sets inside [textureInfo](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-textureinfo) + objects. + - Number arrays as @ref MaterialAttributeType::Vector2/ + @ref MaterialAttributeType::Vector3/ + @ref MaterialAttributeType::Vector4. + Empty arrays, arrays of size 5 or higher as well as arrays containing + anything that isn't a number are ignored. + - [textureInfo](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-textureinfo) + objects, including all attributes handled for regular textures. Texture + attributes are prefixed by the name of the object: e.g. if an extension + has a `someTexture` property, the texture index, matrix, coordinate set + and scale would be imported as `someTexture`, `someTextureMatrix`, + `someTextureCoordinates` and `someTextureScale`. Non-texture object + types are ignored. + If you handle any of these custom material extensions, it may make sense + to enable the @cb{.ini} ignoreRequiredExtensions @ce + @ref Trade-CgltfImporter-configuration "configuration option". +- [Extras](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-extras) + metadata is imported into the base material layer. The `extras` attribute + must be an object. Type support is the same as for unrecognized material + extensions, except for [textureInfo](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-textureinfo) + objects which are ignored. - If the on-by-default @cb{.ini} phongMaterialFallback @ce @ref Trade-CgltfImporter-configuration "configuration option" is enabled, the importer provides a Phong fallback for backwards diff --git a/src/MagnumPlugins/CgltfImporter/Test/CMakeLists.txt b/src/MagnumPlugins/CgltfImporter/Test/CMakeLists.txt index 283e4428d..850a50e6b 100644 --- a/src/MagnumPlugins/CgltfImporter/Test/CMakeLists.txt +++ b/src/MagnumPlugins/CgltfImporter/Test/CMakeLists.txt @@ -121,6 +121,7 @@ corrade_add_test(CgltfImporterTest light-missing-spot.gltf material-common.gltf material-clearcoat.gltf + material-extras.gltf material-invalid-alpha-mode.gltf material-invalid-clearcoat-factor-oob.gltf material-invalid-clearcoat-normal-oob.gltf @@ -132,13 +133,19 @@ corrade_add_test(CgltfImporterTest material-invalid-pbr-diffuse-oob.gltf material-invalid-pbr-metallic-roughness-oob.gltf material-invalid-pbr-specular-oob.gltf + material-ior.gltf material-metallicroughness.gltf material-phong-fallback.gltf + material-raw.gltf + material-sheen.gltf + material-specular.gltf material-specularglossiness.gltf material-texcoord-flip.bin material-texcoord-flip.gltf material-texcoord-flip-unnormalized.gltf + material-transmission.gltf material-unlit.gltf + material-volume.gltf mesh.gltf mesh.bin mesh.glb diff --git a/src/MagnumPlugins/CgltfImporter/Test/CgltfImporterTest.cpp b/src/MagnumPlugins/CgltfImporter/Test/CgltfImporterTest.cpp index 75cea04d8..05b2be9e7 100644 --- a/src/MagnumPlugins/CgltfImporter/Test/CgltfImporterTest.cpp +++ b/src/MagnumPlugins/CgltfImporter/Test/CgltfImporterTest.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -148,8 +149,16 @@ struct CgltfImporterTest: TestSuite::Tester { void materialPbrSpecularGlossiness(); void materialCommon(); void materialUnlit(); + void materialExtras(); void materialClearCoat(); void materialPhongFallback(); + void materialRaw(); + void materialRawIor(); + void materialRawSpecular(); + void materialRawTransmission(); + void materialRawVolume(); + void materialRawSheen(); + void materialRawOutOfBounds(); void materialOutOfBounds(); void materialInvalidAlphaMode(); @@ -768,8 +777,16 @@ CgltfImporterTest::CgltfImporterTest() { &CgltfImporterTest::materialPbrSpecularGlossiness, &CgltfImporterTest::materialCommon, &CgltfImporterTest::materialUnlit, + &CgltfImporterTest::materialExtras, &CgltfImporterTest::materialClearCoat, - &CgltfImporterTest::materialPhongFallback}); + &CgltfImporterTest::materialPhongFallback, + &CgltfImporterTest::materialRaw, + &CgltfImporterTest::materialRawIor, + &CgltfImporterTest::materialRawSpecular, + &CgltfImporterTest::materialRawTransmission, + &CgltfImporterTest::materialRawVolume, + &CgltfImporterTest::materialRawSheen, + &CgltfImporterTest::materialRawOutOfBounds}); addInstancedTests({&CgltfImporterTest::materialOutOfBounds}, Containers::arraySize(MaterialOutOfBoundsData)); @@ -3477,7 +3494,8 @@ void CgltfImporterTest::materialPbrSpecularGlossiness() { CORRADE_COMPARE(pbr.diffuseTexture(), 0); CORRADE_COMPARE(pbr.specularColor(), (Color4{0.4f, 0.5f, 0.6f, 0.0f})); CORRADE_VERIFY(pbr.hasSpecularGlossinessTexture()); - CORRADE_COMPARE(pbr.specularTexture(), 1); CORRADE_COMPARE(pbr.glossiness(), 0.9f); + CORRADE_COMPARE(pbr.specularTexture(), 1); + CORRADE_COMPARE(pbr.glossiness(), 0.9f); } { const char* name = "identity texture transform"; auto material = importer->material(name); @@ -3684,6 +3702,143 @@ void CgltfImporterTest::materialUnlit() { CORRADE_COMPARE(flat.texture(), 1); } +void compareMaterials(const MaterialData& actual, const MaterialData& expected) { + CORRADE_COMPARE(actual.types(), expected.types()); + CORRADE_COMPARE(actual.layerCount(), expected.layerCount()); + + for(UnsignedInt layer = 0; layer != expected.layerCount(); ++layer) { + CORRADE_ITERATION(expected.layerName(layer)); + CORRADE_COMPARE(actual.layerName(layer), expected.layerName(layer)); + CORRADE_COMPARE(actual.attributeCount(layer), expected.attributeCount(layer)); + for(UnsignedInt i = 0; i != expected.attributeCount(layer); ++i) { + const Containers::StringView name = expected.attributeName(layer, i); + CORRADE_ITERATION(name); + CORRADE_VERIFY(actual.hasAttribute(layer, name)); + const MaterialAttributeType type = expected.attributeType(layer, name); + CORRADE_COMPARE(actual.attributeType(layer, name), type); + switch(type) { + #define _v(type, valueType) case MaterialAttributeType::type: \ + CORRADE_COMPARE(actual.attribute(layer, name), expected.attribute(layer, name)); \ + break; + #define _c(type) _v(type, type) + _c(UnsignedInt) + _c(Float) + _c(Vector2) + _c(Vector3) + _c(Vector4) + _c(Matrix3x3) + _v(Bool, bool) + _v(String, Containers::StringView) + _v(TextureSwizzle, MaterialTextureSwizzle) + #undef _c + #undef _v + default: + CORRADE_FAIL_IF(true, "Unexpected attribute type" << type); + } + } + } +} + +void CgltfImporterTest::materialExtras() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + importer->configuration().setValue("phongMaterialFallback", false); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "material-extras.gltf"))); + + { + for(const char* name: {"primitive", "string", "array"}) { + CORRADE_ITERATION(name); + Containers::Optional material; + std::ostringstream out; + { + Warning redirectWarning{&out}; + material = importer->material(name); + } + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 0); + + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::material(): extras property is not an object, skipping\n"); + } + } { + const char* name = "empty"; + CORRADE_ITERATION(name); + const auto material = importer->material(name); + CORRADE_VERIFY(material); + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 0); + } { + const char* name = "invalid"; + CORRADE_ITERATION(name); + Containers::Optional material; + std::ostringstream out; + { + Warning redirectWarning{&out}; + material = importer->material(name); + } + CORRADE_VERIFY(material); + /* All attributes are invalid and ignored */ + CORRADE_COMPARE(material->layerCount(), 1); + CORRADE_COMPARE(material->attributeCount(), 0); + + CORRADE_COMPARE(out.str(), + "Trade::CgltfImporter::material(): property with an empty name, skipping\n" + "Trade::CgltfImporter::material(): property aValueThatWontFit is too large with 84 bytes, skipping\n" + "Trade::CgltfImporter::material(): property anIncrediblyLongNameThatSadlyWontFitPaddingPaddingPadding!! is too large with 63 bytes, skipping\n" + "Trade::CgltfImporter::material(): property boolArray has unsupported type, skipping\n" + "Trade::CgltfImporter::material(): property emptyArray has unsupported type, skipping\n" + "Trade::CgltfImporter::material(): property mixedBoolArray has unsupported type, skipping\n" + "Trade::CgltfImporter::material(): property mixedObjectArray has unsupported type, skipping\n" + "Trade::CgltfImporter::material(): property mixedStringArray has unsupported type, skipping\n" + "Trade::CgltfImporter::material(): property nestedObject is an object, skipping\n" + "Trade::CgltfImporter::material(): property nestedObjectTexture is an object, skipping\n" + "Trade::CgltfImporter::material(): property null has unsupported type, skipping\n" + "Trade::CgltfImporter::material(): property oversizedArray has unsupported type, skipping\n" + "Trade::CgltfImporter::material(): property stringArray has unsupported type, skipping\n"); + } { + const char* name = "extras"; + CORRADE_ITERATION(name); + Containers::Optional material; + std::ostringstream out; + { + Warning redirectWarning{&out}; + material = importer->material(name); + } + CORRADE_VERIFY(material); + + const MaterialData expected{MaterialType::PbrMetallicRoughness|MaterialType::PbrClearCoat, { + {MaterialAttribute::BaseColor, Color4{0.8f, 0.2f, 0.4f, 0.3f}}, + {MaterialAttribute::BaseColorTexture, 0u}, + {MaterialAttribute::DoubleSided, true}, + + /* Extras are in the base layer */ + {"boolTrue"_s, true}, + {"boolFalse"_s, false}, + {"int"_s, -7992835.0f}, + {"unsignedInt"_s, 109835761.0f}, + {"float"_s, 4.321f}, + {"string"_s, "Ribbit -- ribbit"_s}, + {"encodedString"_s, "마이크 체크"_s}, + {"emptyString"_s, ""_s}, + {"doubleSided"_s, false}, + {"vec1"_s, 91.2f}, + {"vec2"_s, Vector2{9.0f, 8.0f}}, + {"vec3"_s, Vector3{9.0f, 0.08f, 7.3141f}}, + {"vec4"_s, Vector4{-9.0f, 8.0f, 7.0f, -6.0f}}, + {"duplicate"_s, true}, + + {Trade::MaterialAttribute::LayerName, "ClearCoat"_s}, + {MaterialAttribute::LayerFactor, 0.5f}, + {MaterialAttribute::Roughness, 0.0f} + }, {17, 20}}; + + compareMaterials(*material, expected); + + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::material(): property invalid has unsupported type, skipping\n"); + } +} + void CgltfImporterTest::materialClearCoat() { Containers::Pointer importer = _manager.instantiate("CgltfImporter"); @@ -3814,7 +3969,7 @@ void CgltfImporterTest::materialPhongFallback() { Containers::Pointer importer = _manager.instantiate("CgltfImporter"); /* phongMaterialFallback should be on by default */ - //importer->configuration().setValue("phongMaterialFallback", true); + CORRADE_VERIFY(importer->configuration().value("phongMaterialFallback")); CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, "material-phong-fallback.gltf"))); @@ -3931,6 +4086,421 @@ void CgltfImporterTest::materialPhongFallback() { } } +void CgltfImporterTest::materialRaw() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + importer->configuration().setValue("phongMaterialFallback", false); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "material-raw.gltf"))); + + Containers::Optional material; + std::ostringstream out; + { + Warning redirectWarning{&out}; + material = importer->material("raw"); + CORRADE_VERIFY(material); + } + + const MaterialData expected{MaterialType::PbrMetallicRoughness|MaterialType::PbrClearCoat, { + /* Standard layer import still works */ + {MaterialAttribute::BaseColor, Color4{0.8f, 0.2f, 0.4f, 0.3f}}, + {MaterialAttribute::BaseColorTexture, 0u}, + {MaterialAttribute::DoubleSided, true}, + + /* Known extension layer import still works */ + {Trade::MaterialAttribute::LayerName, "ClearCoat"_s}, + {MaterialAttribute::LayerFactor, 0.5f}, + {MaterialAttribute::Roughness, 0.0f}, + + /* Unknown extension with a textureInfo object */ + {Trade::MaterialAttribute::LayerName, "#MAGNUM_material_snake"_s}, + {"snakeFactor"_s, 6.6f}, + {"snakeTexture"_s, 1u}, + {"snakeTextureMatrix"_s, Matrix3{ + {0.33f, 0.0f, 0.0f}, + {0.0f, 0.44f, 0.0f}, + {0.5f, 1.06f, 1.0f} + }}, + {"snakeTextureCoordinates"_s, 3u}, + {"snakeTextureScale"_s, 0.2f}, + {"defaultScaleTexture"_s, 1u}, + /* No defaultScaleTextureScale because scale is 1.0 */ + + /* Unknown extension with all other supported types */ + {Trade::MaterialAttribute::LayerName, "#MAGNUM_material_type_zoo"_s}, + {"boolTrue"_s, true}, + {"boolFalse"_s, false}, + {"int"_s, -7992835.0f}, + {"unsignedInt"_s, 109835761.0f}, + {"float"_s, 4.321f}, + {"string"_s, "Ribbit -- ribbit"_s}, + {"encodedString"_s, "마이크 체크"_s}, + {"emptyString"_s, ""_s}, + {"uppercaseName"_s, true}, + {"vec1"_s, 91.2f}, + {"vec2"_s, Vector2{9.0f, 8.0f}}, + {"vec3"_s, Vector3{9.0f, 0.08f, 7.3141f}}, + {"vec4"_s, Vector4{-9.0f, 8.0f, 7.0f, -6.0f}}, + {"duplicate"_s, true} + + /* No layer for MAGNUM_material_forbidden_types because all attributes + have invalid types or are too large, and hence are skipped */ + }, {3, 6, 13, 28}}; + + compareMaterials(*material, expected); + + + CORRADE_COMPARE(out.str(), + /* MAGNUM_material_forbidden_types. Attributes are sorted by name. */ + "Trade::CgltfImporter::material(): extension with an empty name, skipping\n" + "Trade::CgltfImporter::material(): property with an empty name, skipping\n" + "Trade::CgltfImporter::material(): property Texture has non-texture object type, skipping\n" + "Trade::CgltfImporter::material(): property aValueThatWontFit is too large with 84 bytes, skipping\n" + /* These are not sorted because they're not JSON attributes, and added + in this order by materialTexture() */ + "Trade::CgltfImporter::material(): property alsoTestingThisWithAnOverlyElongatedNameButThisTimeForATextureMatrix is too large with 104 bytes, skipping\n" + "Trade::CgltfImporter::material(): property alsoTestingThisWithAnOverlyElongatedNameButThisTimeForATextureCoordinates is too large with 77 bytes, skipping\n" + "Trade::CgltfImporter::material(): property alsoTestingThisWithAnOverlyElongatedNameButThisTimeForATexture is too large with 66 bytes, skipping\n" + "Trade::CgltfImporter::material(): property alsoTestingThisWithAnOverlyElongatedNameButThisTimeForATextureScale is too large with 71 bytes, skipping\n" + "Trade::CgltfImporter::material(): property anIncrediblyLongNameThatSadlyWontFitPaddingPaddingPadding!! is too large with 63 bytes, skipping\n" + "Trade::CgltfImporter::material(): property boolArray has unsupported type, skipping\n" + "Trade::CgltfImporter::material(): property emptyArray has unsupported type, skipping\n" + "Trade::CgltfImporter::material(): property invalidTexture has invalid texture object type, skipping\n" + "Trade::CgltfImporter::material(): property mixedBoolArray has unsupported type, skipping\n" + "Trade::CgltfImporter::material(): property mixedObjectArray has unsupported type, skipping\n" + "Trade::CgltfImporter::material(): property mixedStringArray has unsupported type, skipping\n" + "Trade::CgltfImporter::material(): property nonTextureObject has non-texture object type, skipping\n" + "Trade::CgltfImporter::material(): property null has unsupported type, skipping\n" + "Trade::CgltfImporter::material(): property oversizedArray has unsupported type, skipping\n" + "Trade::CgltfImporter::material(): property stringArray has unsupported type, skipping\n" + /* MAGNUM_material_type_zoo */ + "Trade::CgltfImporter::material(): property invalid has unsupported type, skipping\n" + "Trade::CgltfImporter::material(): extension name VENDOR_material_thisnameiswaytoolongforalayername! is too long with 50 characters, skipping\n"); +} + +void CgltfImporterTest::materialRawIor() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* Disable Phong material fallback (enabled by default for compatibility), + testing that separately in materialPhongFallback() */ + importer->configuration().setValue("phongMaterialFallback", false); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "material-ior.gltf"))); + + constexpr Containers::StringView layer = "#KHR_materials_ior"_s; + + const Containers::Pair materials[]{ + {"defaults"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + /* IOR is always imported, set to glTF default if missing */ + {"ior"_s, 1.5f} + }, {0, 2}}}, + {"factors"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"ior"_s, 1.25f} + }, {0, 2}}} + }; + + CORRADE_COMPARE(importer->materialCount(), Containers::arraySize(materials)); + + for(const auto& expected: materials) { + auto material = importer->material(expected.first()); + CORRADE_ITERATION(expected.first()); + CORRADE_VERIFY(material); + compareMaterials(*material, expected.second()); + } +} + +void CgltfImporterTest::materialRawSpecular() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* Disable Phong material fallback (enabled by default for compatibility), + testing that separately in materialPhongFallback() */ + importer->configuration().setValue("phongMaterialFallback", false); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "material-specular.gltf"))); + + constexpr Containers::StringView layer = "#KHR_materials_specular"_s; + + const Containers::Pair materials[]{ + {"defaults"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + /* Factors are always imported, set to glTF defaults if missing */ + {"specularFactor"_s, 1.0f}, + {"specularColorFactor"_s, Vector3{1.0f, 1.0f, 1.0f}}, + }, {0, 3}}}, + {"factors"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"specularFactor"_s, 0.67f}, + {"specularColorFactor"_s, Vector3{0.2f, 0.4f, 0.6f}}, + }, {0, 3}}}, + {"textures"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"specularFactor"_s, 0.7f}, + {"specularColorFactor"_s, Vector3{0.3f, 0.4f, 0.5f}}, + {"specularTexture"_s, 2u}, + {"specularTextureSwizzle"_s, MaterialTextureSwizzle::A}, + {"specularColorTexture"_s, 1u} + }, {0, 6}}}, + {"texture identity transform"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"specularFactor"_s, 1.0f}, + {"specularColorFactor"_s, Vector3{1.0f, 1.0f, 1.0f}}, + {"specularTexture"_s, 1u}, + {"specularTextureMatrix"_s, Matrix3{}}, + {"specularTextureSwizzle"_s, MaterialTextureSwizzle::A}, + {"specularColorTexture"_s, 0u}, + {"specularColorTextureMatrix"_s, Matrix3{}} + }, {0, 8}}}, + {"texture transform + coordinate set"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"specularFactor"_s, 1.0f}, + {"specularColorFactor"_s, Vector3{1.0f, 1.0f, 1.0f}}, + {"specularTexture"_s, 2u}, + {"specularTextureCoordinates"_s, 4u}, + {"specularTextureMatrix"_s, Matrix3{ + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.0f, -1.0f, 1.0f} + }}, + {"specularTextureSwizzle"_s, MaterialTextureSwizzle::A}, + {"specularColorTexture"_s, 1u}, + {"specularColorTextureCoordinates"_s, 1u}, + {"specularColorTextureMatrix"_s, Matrix3{ + {0.5f, 0.0f, 0.0f}, + {0.0f, 0.5f, 0.0f}, + {0.0f, 0.5f, 1.0f} + }} + }, {0, 10}}} + }; + + CORRADE_COMPARE(importer->materialCount(), Containers::arraySize(materials)); + + for(const auto& expected: materials) { + auto material = importer->material(expected.first()); + CORRADE_ITERATION(expected.first()); + CORRADE_VERIFY(material); + compareMaterials(*material, expected.second()); + } +} + +void CgltfImporterTest::materialRawTransmission() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* Disable Phong material fallback (enabled by default for compatibility), + testing that separately in materialPhongFallback() */ + importer->configuration().setValue("phongMaterialFallback", false); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "material-transmission.gltf"))); + + constexpr Containers::StringView layer = "#KHR_materials_transmission"_s; + + const Containers::Pair materials[]{ + {"defaults"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + /* transmissionFactor is always imported, set to glTF default + if missing */ + {"transmissionFactor"_s, 0.0f} + }, {0, 2}}}, + {"factors"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"transmissionFactor"_s, 0.67f} + }, {0, 2}}}, + {"textures"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"transmissionFactor"_s, 0.7f}, + {"transmissionTexture"_s, 1u} + }, {0, 3}}}, + {"texture identity transform"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"transmissionFactor"_s, 0.0f}, + {"transmissionTexture"_s, 0u}, + {"transmissionTextureMatrix"_s, Matrix3{}} + }, {0, 4}}}, + {"texture transform + coordinate set"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"transmissionFactor"_s, 0.0f}, + {"transmissionTexture"_s, 1u}, + {"transmissionTextureCoordinates"_s, 3u}, + {"transmissionTextureMatrix"_s, Matrix3{ + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.0f, -1.0f, 1.0f} + }} + }, {0, 5}}} + }; + + CORRADE_COMPARE(importer->materialCount(), Containers::arraySize(materials)); + + for(const auto& expected: materials) { + auto material = importer->material(expected.first()); + CORRADE_ITERATION(expected.first()); + CORRADE_VERIFY(material); + compareMaterials(*material, expected.second()); + } +} + +void CgltfImporterTest::materialRawVolume() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* Disable Phong material fallback (enabled by default for compatibility), + testing that separately in materialPhongFallback() */ + importer->configuration().setValue("phongMaterialFallback", false); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "material-volume.gltf"))); + + constexpr Containers::StringView layer = "#KHR_materials_volume"_s; + + const Containers::Pair materials[]{ + {"defaults"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + /* Factors are always imported, set to glTF defaults if missing */ + {"thicknessFactor"_s, 0.0f}, + {"attenuationDistance"_s, Constants::inf()}, + {"attenuationColor"_s, Vector3{1.0f, 1.0f, 1.0f}} + }, {0, 4}}}, + {"factors"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"thicknessFactor"_s, 0.67f}, + {"attenuationDistance"_s, 15.3f}, + {"attenuationColor"_s, Vector3{0.7f, 0.1f, 1.0f}} + }, {0, 4}}}, + {"textures"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"thicknessFactor"_s, 0.7f}, + {"attenuationDistance"_s, 1.12f}, + {"attenuationColor"_s, Vector3{0.1f, 0.05f, 0.0f}}, + {"thicknessTexture"_s, 1u}, + {"thicknessTextureSwizzle"_s, MaterialTextureSwizzle::G} + }, {0, 6}}}, + {"texture identity transform"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"thicknessFactor"_s, 0.0f}, + {"attenuationDistance"_s, Constants::inf()}, + {"attenuationColor"_s, Vector3{1.0f, 1.0f, 1.0f}}, + {"thicknessTexture"_s, 0u}, + {"thicknessTextureMatrix"_s, Matrix3{}}, + {"thicknessTextureSwizzle"_s, MaterialTextureSwizzle::G} + }, {0, 7}}}, + {"texture transform + coordinate set"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"thicknessFactor"_s, 0.0f}, + {"attenuationDistance"_s, Constants::inf()}, + {"attenuationColor"_s, Vector3{1.0f, 1.0f, 1.0f}}, + {"thicknessTexture"_s, 1u}, + {"thicknessTextureCoordinates"_s, 3u}, + {"thicknessTextureMatrix"_s, Matrix3{ + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.0f, -1.0f, 1.0f} + }}, + {"thicknessTextureSwizzle"_s, MaterialTextureSwizzle::G} + }, {0, 8}}} + }; + + CORRADE_COMPARE(importer->materialCount(), Containers::arraySize(materials)); + + for(const auto& expected: materials) { + auto material = importer->material(expected.first()); + CORRADE_ITERATION(expected.first()); + CORRADE_VERIFY(material); + compareMaterials(*material, expected.second()); + } +} + +void CgltfImporterTest::materialRawSheen() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + /* Disable Phong material fallback (enabled by default for compatibility), + testing that separately in materialPhongFallback() */ + importer->configuration().setValue("phongMaterialFallback", false); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "material-sheen.gltf"))); + + constexpr Containers::StringView layer = "#KHR_materials_sheen"_s; + + const Containers::Pair materials[]{ + {"defaults"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + /* Factors are always imported, set to glTF defaults if missing */ + {"sheenColorFactor"_s, Vector3{0.0f, 0.0f, 0.0f}}, + {"sheenRoughnessFactor"_s, 0.0f} + }, {0, 3}}}, + {"factors"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"sheenColorFactor"_s, Vector3{0.2f, 0.4f, 0.6f}}, + {"sheenRoughnessFactor"_s, 0.67f} + }, {0, 3}}}, + {"textures"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"sheenColorFactor"_s, Vector3{0.3f, 0.4f, 0.5f}}, + {"sheenRoughnessFactor"_s, 0.7f}, + {"sheenColorTexture"_s, 1u}, + {"sheenRoughnessTexture"_s, 2u}, + {"sheenRoughnessTextureSwizzle"_s, MaterialTextureSwizzle::A} + }, {0, 6}}}, + {"texture identity transform"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"sheenColorFactor"_s, Vector3{0.0f, 0.0f, 0.0f}}, + {"sheenRoughnessFactor"_s, 0.0f}, + {"sheenColorTexture"_s, 1u}, + {"sheenColorTextureMatrix"_s, Matrix3x3{}}, + {"sheenRoughnessTexture"_s, 0u}, + /* sheenRoughnessTextureMatrix is too large and skipped */ + {"sheenRoughnessTextureSwizzle"_s, MaterialTextureSwizzle::A} + }, {0, 7}}}, + {"texture transform + coordinate set"_s, MaterialData{MaterialType{}, { + {Trade::MaterialAttribute::LayerName, layer}, + {"sheenColorFactor"_s, Vector3{0.0f, 0.0f, 0.0f}}, + {"sheenRoughnessFactor"_s, 0.0f}, + {"sheenColorTexture"_s, 2u}, + {"sheenColorTextureCoordinates"_s, 4u}, + {"sheenColorTextureMatrix"_s, Matrix3{ + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.0f, -1.0f, 1.0f} + }}, + {"sheenRoughnessTexture"_s, 1u}, + {"sheenRoughnessTextureCoordinates"_s, 1u}, + /* sheenRoughnessTextureMatrix is too large and skipped */ + {"sheenRoughnessTextureSwizzle"_s, MaterialTextureSwizzle::A} + }, {0, 9}}} + }; + + CORRADE_COMPARE(importer->materialCount(), Containers::arraySize(materials)); + + std::ostringstream out; + Warning redirectWarning{&out}; + + for(const auto& expected: materials) { + auto material = importer->material(expected.first()); + CORRADE_ITERATION(expected.first()); + CORRADE_VERIFY(material); + compareMaterials(*material, expected.second()); + } + + CORRADE_COMPARE(out.str(), + "Trade::CgltfImporter::material(): property sheenRoughnessTextureMatrix is too large with 63 bytes, skipping\n" + "Trade::CgltfImporter::material(): property sheenRoughnessTextureMatrix is too large with 63 bytes, skipping\n"); +} + +void CgltfImporterTest::materialRawOutOfBounds() { + Containers::Pointer importer = _manager.instantiate("CgltfImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, + "material-raw.gltf"))); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->material("raw out-of-bounds")); + CORRADE_COMPARE(out.str(), "Trade::CgltfImporter::material(): snakeTexture index 2 out of bounds for 2 textures\n"); +} + void CgltfImporterTest::materialOutOfBounds() { auto&& data = MaterialOutOfBoundsData[testCaseInstanceId()]; setTestCaseDescription(data.name); diff --git a/src/MagnumPlugins/CgltfImporter/Test/material-extras.gltf b/src/MagnumPlugins/CgltfImporter/Test/material-extras.gltf new file mode 100644 index 000000000..3d88a9ec5 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/material-extras.gltf @@ -0,0 +1,99 @@ +{ + "asset": { + "version": "2.0" + }, + "materials": [ + { + "name": "primitive", + "extras": 0.5 + }, + { + "name": "string", + "extras": "Ignored" + }, + { + "name": "array", + "extras": [ 1.2 ] + }, + { + "name": "empty", + "extras": {} + }, + { + "name": "invalid", + "extras": { + "nestedObject": { + "this": "should be ignored" + }, + "nestedObjectTexture": { + "this": "should also be ignored", + "index": 0 + }, + "null": null, + "emptyArray": [], + "oversizedArray": [9.1, 8.2, 7.3, 6.4, 5.5], + "stringArray": ["one", "two", "", "four"], + "boolArray": [true, false], + "mixedStringArray": ["1", "2", 3, "4"], + "mixedBoolArray": [1, 2, 3, false], + "mixedObjectArray": [1, 2, 3, {"4": "5", "6": "7"}], + "anIncrediblyLongNameThatSadlyWontFitPaddingPaddingPadding!!": 0, + "aValueThatWontFit": "1 pound ground turkey, 1 medium-sized onion, 1 cup celery (diced)", + "": "Empty attributes are ignored" + } + }, + { + "name": "extras", + "pbrMetallicRoughness": { + "baseColorFactor": [0.8, 0.2, 0.4, 0.3], + "baseColorTexture": { + "index": 0 + } + }, + "doubleSided": true, + "extensions": { + "KHR_materials_clearcoat": { + "clearcoatFactor": 0.5 + } + }, + "extras": { + "duplicate": "overwritten by later values", + "boolTrue": true, + "boolFalse": false, + "int": -7992835, + "unsignedInt": 109835761, + "float": 4.321, + "string": "Ribbit -- ribbit", + "encodedString": "\uB9C8\uC774\uD06C \uCCB4\uD06C", + "emptyString": "", + "DoubleSided": false, + "invalid": ["just checking if it keeps parsing after a warning"], + "vec1": [9.12e+1], + "vec2": [ + 9 , + 8 ], + "vec3": [9, 8E-2, 7.3141], + "vec4": [-9, 8, 7, -6], + "duplicate": 0.2, + "duplicate": true + } + } + ], + "samplers": [ + {} + ], + "textures": [ + { + "sampler": 0, + "source": 0 + } + ], + "images": [ + { + "uri": "nonexistent.png" + } + ], + "extensionsUsed": [ + "KHR_materials_clearcoat" + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/material-ior.gltf b/src/MagnumPlugins/CgltfImporter/Test/material-ior.gltf new file mode 100644 index 000000000..8309d1e04 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/material-ior.gltf @@ -0,0 +1,24 @@ +{ + "asset": { + "version": "2.0" + }, + "materials": [ + { + "name": "defaults", + "extensions": { + "KHR_materials_ior": {} + } + }, + { + "name": "factors", + "extensions": { + "KHR_materials_ior": { + "ior": 1.25 + } + } + } + ], + "extensionsUsed": [ + "KHR_materials_ior" + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/material-raw.gltf b/src/MagnumPlugins/CgltfImporter/Test/material-raw.gltf new file mode 100644 index 000000000..94b8275a4 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/material-raw.gltf @@ -0,0 +1,152 @@ +{ + "asset": { + "version": "2.0" + }, + "images": [ + { + "uri": "basecolor.png" + }, + { + "uri": "snake.snk" + } + ], + "materials": [ + { + "name": "raw out-of-bounds", + "extensions": { + "MAGNUM_material_snake": { + "snakeTexture": { + "index": 2 + } + } + } + }, + { + "name": "raw", + "pbrMetallicRoughness": { + "baseColorFactor": [ 0.8, 0.2, 0.4, 0.3 ], + "baseColorTexture": { + "index": 0 + } + }, + "doubleSided": true, + "extensions": { + "MAGNUM_material_snake": { + "note": "should be overwritten by later duplicate extension", + "snakeFactor": 3.3, + "thisWillDisappear": true + }, + "KHR_materials_clearcoat": { + "note": "should be imported as a predefined layer", + "clearcoatFactor": 0.5 + }, + "MAGNUM_material_snake": { + "snakeFactor": 6.6, + "snakeTexture": { + "index": 1, + "texCoord": 3, + "snakeCoord": "ignored, not parsed by cgltf", + "scale": 0.2, + "extensions": { + "KHR_texture_transform": { + "offset": [0.5, -0.5], + "scale": [0.33, 0.44] + } + } + }, + "defaultScaleTexture": { + "index": 1, + "scale": 1.0 + } + }, + "MAGNUM_material_type_zoo": { + "duplicate": "overwritten by later values", + "boolTrue": true, + "boolFalse": false, + "int": -7992835, + "unsignedInt": 109835761, + "float": 4.321, + "string": "Ribbit -- ribbit", + "encodedString": "\uB9C8\uC774\uD06C \uCCB4\uD06C", + "emptyString": "", + "UppercaseName": true , + "invalid": ["just checking if it keeps parsing after a warning"], + "vec1": [9.12e+1], + "vec2": [ + 9 , + 8 ], + "vec3": [9, 8E-2, 7.3141], + "vec4": [-9, 8, 7, -6], + "duplicate": 0.2, + "duplicate": true + }, + "MAGNUM_material_forbidden_types": { + "null": null, + "emptyArray": [], + "oversizedArray": [9.1, 8.2, 7.3, 6.4, 5.5], + "stringArray": ["one", "two", "", "four"], + "boolArray": [true, false], + "mixedStringArray": ["1", "2", 3, "4"], + "mixedBoolArray": [1, 2, 3, false], + "mixedObjectArray": [1, 2, 3, {"4": "5", "6": "7"}], + "nonTextureObject": { + "these": {"should": "all"}, + "be": ["ignored"] + }, + "Texture": { + "note": "this should be ignored", + "index": 1 + }, + "invalidTexture": { + "note": "index is mandatory", + "indeks": 0 + }, + "anIncrediblyLongNameThatSadlyWontFitPaddingPaddingPadding!!": 0, + "aValueThatWontFit": "1 pound ground turkey, 1 medium-sized onion, 1 cup celery (diced)", + "alsoTestingThisWithAnOverlyElongatedNameButThisTimeForATexture": { + "index": 1, + "texCoord": 3, + "scale": 0.2, + "extensions": { + "KHR_texture_transform": { + "offset": [0.5, -0.5], + "scale": [0.33, 0.44] + } + } + }, + "": "Empty attributes are ignored" + }, + "VENDOR_empty_extension_object": {}, + "VENDOR_material_thisnameiswaytoolongforalayername!": { + "wow": ["so", "long"] + }, + "": { + "note": "Empty extension names are ignored" + } + } + } + ], + "samplers": [ + {} + ], + "textures": [ + { + "sampler": 0, + "source": 0 + }, + { + "sampler": 0, + "source": 1 + } + ], + "extensionsUsed": [ + "KHR_materials_clearcoat", + "MAGNUM_material_snake", + "MAGNUM_material_type_zoo", + "MAGNUM_material_forbidden_types", + "VENDOR_empty_extension_object", + "VENDOR_material_thisnameiswaytoolongforalayername!", + "", + "KHR_texture_transform" + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/material-sheen.gltf b/src/MagnumPlugins/CgltfImporter/Test/material-sheen.gltf new file mode 100644 index 000000000..7cc48aa7a --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/material-sheen.gltf @@ -0,0 +1,107 @@ +{ + "asset": { + "version": "2.0" + }, + "images": [ + { + "uri": "texture.png" + } + ], + "materials": [ + { + "name": "defaults", + "extensions": { + "KHR_materials_sheen": {} + } + }, + { + "name": "factors", + "extensions": { + "KHR_materials_sheen": { + "sheenColorFactor": [0.2, 0.4, 0.6], + "sheenRoughnessFactor": 0.67 + } + } + }, + { + "name": "textures", + "extensions": { + "KHR_materials_sheen": { + "sheenColorFactor": [0.3, 0.4, 0.5], + "sheenColorTexture": { + "index": 1 + }, + "sheenRoughnessFactor": 0.7, + "sheenRoughnessTexture": { + "index": 2 + } + } + } + }, + { + "name": "texture identity transform", + "extensions": { + "KHR_materials_sheen": { + "sheenColorTexture": { + "index": 1, + "extensions": { + "KHR_texture_transform": {} + } + }, + "sheenRoughnessTexture": { + "index": 0, + "extensions": { + "KHR_texture_transform": {} + } + } + } + } + }, + { + "name": "texture transform + coordinate set", + "extensions": { + "KHR_materials_sheen": { + "sheenColorTexture": { + "index": 2, + "extensions": { + "KHR_texture_transform": { + "offset": [0.0, 1.0], + "texCoord": 4 + } + } + }, + "sheenRoughnessTexture": { + "index": 1, + "extensions": { + "KHR_texture_transform": { + "scale": [0.5, 0.5] + } + }, + "texCoord": 1 + } + } + } + } + ], + "samplers": [ + {} + ], + "textures": [ + { + "sampler": 0, + "source": 0 + }, + { + "sampler": 0, + "source": 0 + }, + { + "sampler": 0, + "source": 0 + } + ], + "extensionsUsed": [ + "KHR_materials_sheen", + "KHR_texture_transform" + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/material-specular.gltf b/src/MagnumPlugins/CgltfImporter/Test/material-specular.gltf new file mode 100644 index 000000000..246d7f891 --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/material-specular.gltf @@ -0,0 +1,107 @@ +{ + "asset": { + "version": "2.0" + }, + "images": [ + { + "uri": "texture.png" + } + ], + "materials": [ + { + "name": "defaults", + "extensions": { + "KHR_materials_specular": {} + } + }, + { + "name": "factors", + "extensions": { + "KHR_materials_specular": { + "specularFactor": 0.67, + "specularColorFactor": [0.2, 0.4, 0.6] + } + } + }, + { + "name": "textures", + "extensions": { + "KHR_materials_specular": { + "specularFactor": 0.7, + "specularTexture": { + "index": 2 + }, + "specularColorFactor": [0.3, 0.4, 0.5], + "specularColorTexture": { + "index": 1 + } + } + } + }, + { + "name": "texture identity transform", + "extensions": { + "KHR_materials_specular": { + "specularTexture": { + "index": 1, + "extensions": { + "KHR_texture_transform": {} + } + }, + "specularColorTexture": { + "index": 0, + "extensions": { + "KHR_texture_transform": {} + } + } + } + } + }, + { + "name": "texture transform + coordinate set", + "extensions": { + "KHR_materials_specular": { + "specularTexture": { + "index": 2, + "extensions": { + "KHR_texture_transform": { + "offset": [0.0, 1.0], + "texCoord": 4 + } + } + }, + "specularColorTexture": { + "index": 1, + "extensions": { + "KHR_texture_transform": { + "scale": [0.5, 0.5] + } + }, + "texCoord": 1 + } + } + } + } + ], + "samplers": [ + {} + ], + "textures": [ + { + "sampler": 0, + "source": 0 + }, + { + "sampler": 0, + "source": 0 + }, + { + "sampler": 0, + "source": 0 + } + ], + "extensionsUsed": [ + "KHR_materials_specular", + "KHR_texture_transform" + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/material-transmission.gltf b/src/MagnumPlugins/CgltfImporter/Test/material-transmission.gltf new file mode 100644 index 000000000..107955afd --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/material-transmission.gltf @@ -0,0 +1,84 @@ +{ + "asset": { + "version": "2.0" + }, + "images": [ + { + "uri": "texture.png" + } + ], + "materials": [ + { + "name": "defaults", + "extensions": { + "KHR_materials_transmission": {} + } + }, + { + "name": "factors", + "extensions": { + "KHR_materials_transmission": { + "transmissionFactor": 0.67 + } + } + }, + { + "name": "textures", + "extensions": { + "KHR_materials_transmission": { + "transmissionFactor": 0.7, + "transmissionTexture": { + "index": 1 + } + } + } + }, + { + "name": "texture identity transform", + "extensions": { + "KHR_materials_transmission": { + "transmissionTexture": { + "index": 0, + "extensions": { + "KHR_texture_transform": {} + } + } + } + } + }, + { + "name": "texture transform + coordinate set", + "extensions": { + "KHR_materials_transmission": { + "transmissionTexture": { + "index": 1, + "extensions": { + "KHR_texture_transform": { + "offset": [0.0, 1.0], + "texCoord": 3 + } + }, + "texCoord": 1 + } + } + } + } + ], + "samplers": [ + {} + ], + "textures": [ + { + "sampler": 0, + "source": 0 + }, + { + "sampler": 0, + "source": 0 + } + ], + "extensionsUsed": [ + "KHR_materials_transmission", + "KHR_texture_transform" + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/material-unlit.gltf b/src/MagnumPlugins/CgltfImporter/Test/material-unlit.gltf index 99f4997c1..c49ff208f 100644 --- a/src/MagnumPlugins/CgltfImporter/Test/material-unlit.gltf +++ b/src/MagnumPlugins/CgltfImporter/Test/material-unlit.gltf @@ -15,9 +15,10 @@ "index": 1 } }, - "note": "to verify Type::PbrSpecularGlossiness gets removed too", - "KHR_materials_pbrSpecularGlossiness": {}, "extensions": { + "KHR_materials_pbrSpecularGlossiness": { + "note": "to verify Type::PbrSpecularGlossiness gets removed too" + }, "KHR_materials_unlit": {} } } @@ -36,6 +37,7 @@ } ], "extensionsUsed": [ + "KHR_materials_pbrSpecularGlossiness", "KHR_materials_unlit" ] } diff --git a/src/MagnumPlugins/CgltfImporter/Test/material-volume.gltf b/src/MagnumPlugins/CgltfImporter/Test/material-volume.gltf new file mode 100644 index 000000000..ae182545d --- /dev/null +++ b/src/MagnumPlugins/CgltfImporter/Test/material-volume.gltf @@ -0,0 +1,88 @@ +{ + "asset": { + "version": "2.0" + }, + "images": [ + { + "uri": "texture.png" + } + ], + "materials": [ + { + "name": "defaults", + "extensions": { + "KHR_materials_volume": {} + } + }, + { + "name": "factors", + "extensions": { + "KHR_materials_volume": { + "thicknessFactor": 0.67, + "attenuationDistance": 15.3, + "attenuationColor": [0.7, 0.1, 1] + } + } + }, + { + "name": "textures", + "extensions": { + "KHR_materials_volume": { + "thicknessFactor": 0.7, + "attenuationDistance": 1.12, + "attenuationColor": [0.1, 0.05, 0], + "thicknessTexture": { + "index": 1 + } + } + } + }, + { + "name": "texture identity transform", + "extensions": { + "KHR_materials_volume": { + "thicknessTexture": { + "index": 0, + "extensions": { + "KHR_texture_transform": {} + } + } + } + } + }, + { + "name": "texture transform + coordinate set", + "extensions": { + "KHR_materials_volume": { + "thicknessTexture": { + "index": 1, + "extensions": { + "KHR_texture_transform": { + "offset": [0.0, 1.0], + "texCoord": 3 + } + }, + "texCoord": 1 + } + } + } + } + ], + "samplers": [ + {} + ], + "textures": [ + { + "sampler": 0, + "source": 0 + }, + { + "sampler": 0, + "source": 0 + } + ], + "extensionsUsed": [ + "KHR_materials_volume", + "KHR_texture_transform" + ] +} diff --git a/src/MagnumPlugins/CgltfImporter/Test/texture-extensions-invalid.gltf b/src/MagnumPlugins/CgltfImporter/Test/texture-extensions-invalid.gltf index fd96dfdf5..07fbc9723 100644 --- a/src/MagnumPlugins/CgltfImporter/Test/texture-extensions-invalid.gltf +++ b/src/MagnumPlugins/CgltfImporter/Test/texture-extensions-invalid.gltf @@ -22,6 +22,8 @@ "source" : 1, "extensions": { "GOOGLE_texture_basis": { + "note": "lexically preceding keys should be overwritten, so parsing should not stop after the first source", + "source": 2, "source": 3 } } @@ -29,14 +31,14 @@ { "name": "unknown extension, no fallback", "extensions": { - "MGNM_fake_extension": { + "MAGNUM_fake_extension": { "source": 2 } } } ], "extensionsUsed": [ - "MGNM_fake_extension", + "MAGNUM_fake_extension", "GOOGLE_texture_basis" ] } diff --git a/src/MagnumPlugins/CgltfImporter/Test/texture-extensions.gltf b/src/MagnumPlugins/CgltfImporter/Test/texture-extensions.gltf index e98dd81ac..35cb80fae 100644 --- a/src/MagnumPlugins/CgltfImporter/Test/texture-extensions.gltf +++ b/src/MagnumPlugins/CgltfImporter/Test/texture-extensions.gltf @@ -30,6 +30,11 @@ "source": 0, "extensions": { "GOOGLE_texture_basis": { + "note": "other attributes should be ignored, and duplicate source attributes handled (last one wins)", + "source": -3, + "jetsam": { "ignore": "me" }, + "source": 777, + "flotsam": [1, 2, [3, 4], { "5": "6" }], "source": 1 } } @@ -80,7 +85,7 @@ "name": "unknown extension", "source": 0, "extensions": { - "MGNM_fake_extension": { + "MAGNUM_fake_extension": { "source": 4 } } @@ -89,7 +94,7 @@ "name": "GOOGLE_texture_basis and unknown", "source": 0, "extensions": { - "MGNM_fake_extension": { + "MAGNUM_fake_extension": { "source": 4 }, "GOOGLE_texture_basis": { @@ -102,6 +107,6 @@ "GOOGLE_texture_basis", "KHR_texture_basisu", "MSFT_texture_dds", - "MGNM_fake_extension" + "MAGNUM_fake_extension" ] } diff --git a/src/MagnumPlugins/TinyGltfImporter/Test/TinyGltfImporterTest.cpp b/src/MagnumPlugins/TinyGltfImporter/Test/TinyGltfImporterTest.cpp index c525d1745..569382769 100644 --- a/src/MagnumPlugins/TinyGltfImporter/Test/TinyGltfImporterTest.cpp +++ b/src/MagnumPlugins/TinyGltfImporter/Test/TinyGltfImporterTest.cpp @@ -3451,7 +3451,7 @@ void TinyGltfImporterTest::materialPhongFallback() { Containers::Pointer importer = _manager.instantiate("TinyGltfImporter"); /* phongMaterialFallback should be on by default */ - //importer->configuration().setValue("phongMaterialFallback", true); + CORRADE_VERIFY(importer->configuration().value("phongMaterialFallback")); CORRADE_VERIFY(importer->openFile(Utility::Directory::join(CGLTFIMPORTER_TEST_DIR, "material-phong-fallback.gltf"))); diff --git a/src/MagnumPlugins/TinyGltfImporter/TinyGltfImporter.h b/src/MagnumPlugins/TinyGltfImporter/TinyGltfImporter.h index 4a0887bd0..52912a441 100644 --- a/src/MagnumPlugins/TinyGltfImporter/TinyGltfImporter.h +++ b/src/MagnumPlugins/TinyGltfImporter/TinyGltfImporter.h @@ -267,7 +267,7 @@ Import of morph data is not supported at the moment. @ref VertexFormat::Vector2sNormalized (which includes the additional types specified by [KHR_mesh_quantization](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_mesh_quantization/README.md)). The data are by default Y-flipped on import unless - @cb{.conf} textureCoordinateYFlipInMaterial @ce is either explicitly + @cb{.ini} textureCoordinateYFlipInMaterial @ce is either explicitly enabled, or if the file contains non-normalized integer or normalized signed integer texture coordinates (which can't easily be flipped). In that case texture coordinate data are kept as-is and materials provide a texture