diff --git a/src/Magnum/MaterialTools/CMakeLists.txt b/src/Magnum/MaterialTools/CMakeLists.txt index 2f9d648a0..d3f6c3fcf 100644 --- a/src/Magnum/MaterialTools/CMakeLists.txt +++ b/src/Magnum/MaterialTools/CMakeLists.txt @@ -35,13 +35,15 @@ set(MagnumMaterialTools_SRCS # Files compiled with different flags for main library and unit test library set(MagnumMaterialTools_GracefulAssert_SRCS Filter.cpp - Merge.cpp) + Merge.cpp + RemoveDuplicates.cpp) set(MagnumMaterialTools_HEADERS Copy.h Filter.h Merge.h PhongToPbrMetallicRoughness.h + RemoveDuplicates.h visibility.h) diff --git a/src/Magnum/MaterialTools/RemoveDuplicates.cpp b/src/Magnum/MaterialTools/RemoveDuplicates.cpp new file mode 100644 index 000000000..14c5a0200 --- /dev/null +++ b/src/Magnum/MaterialTools/RemoveDuplicates.cpp @@ -0,0 +1,165 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "RemoveDuplicates.h" + +#include +#include +#include + +#include "Magnum/MaterialTools/Implementation/attributesEqual.h" +#include "Magnum/Trade/MaterialData.h" + +namespace Magnum { namespace MaterialTools { + +namespace { + +bool materialEqual(const Trade::MaterialData& a, const Trade::MaterialData& b) { + /* Check if types match */ + if(a.types() != b.types()) + return false; + + /* If one has layer data implicit and the other has just one layer, they're + equivalent */ + if(!(a.layerData().isEmpty() && b.layerData().size() == 1 && a.attributeData().size() == b.layerData()[0]) && + !(b.layerData().isEmpty() && a.layerData().size() == 1 && b.attributeData().size() == a.layerData()[0])) + { + /* Otherwise, check if layer count matches */ + if(a.layerData().size() != b.layerData().size()) + return false; + + /* And if layer data match */ + if(const Containers::ArrayView layerData = a.layerData()) { + for(UnsignedInt layer = 0; layer != layerData.size(); ++layer) { + if(b.layerData()[layer] != layerData[layer]) + return false; + } + } + } + + /* Check if attribute count matches */ + if(a.attributeData().size() != b.attributeData().size()) + return false; + + /* Check if attribute data match */ + for(UnsignedInt attribute = 0; attribute != a.attributeData().size(); ++attribute) { + if(a.attributeData()[attribute].name() != b.attributeData()[attribute].name() || + a.attributeData()[attribute].type() != b.attributeData()[attribute].type() || + !Implementation::attributesEqual(a.attributeData()[attribute], b.attributeData()[attribute])) + return false; + } + + return true; +} + +} + +std::size_t removeDuplicatesInPlaceInto(const Containers::Iterable& materials, const Containers::StridedArrayView1D& mapping) { + CORRADE_ASSERT(mapping.size() == materials.size(), + "MaterialTools::removeDuplicatesInPlaceInto(): bad output size, expected" << materials.size() << "but got" << mapping.size(), {}); + + /* O(n^2). As there's a lot of early returns, should be fine for a moderate + count of materials that differ in a significant way. Won't work well for + materials that are all the same except one attribute value. */ + std::size_t uniqueCount = 0; + for(std::size_t i = 0; i != materials.size(); ++i) { + /* Find a material that's already in the unique set */ + Containers::Optional found; + for(std::size_t j = 0; j != uniqueCount; ++j) { + if(materialEqual(materials[i], materials[j])) { + found = j; + break; + } + } + + /* Material found, reference its ID */ + if(found) { + mapping[i] = *found; + + /* Move the material into its new location, unless it's the same + index, and increase the number of unique materials */ + } else { + if(uniqueCount != i) + materials[uniqueCount] = Utility::move(materials[i]); + mapping[i] = uniqueCount++; + } + } + + return uniqueCount; +} + +Containers::Pair, std::size_t> removeDuplicatesInPlace(const Containers::Iterable& materials) { + Containers::Array out{NoInit, materials.size()}; + const std::size_t uniqueCount = removeDuplicatesInPlaceInto(materials, out); + return {Utility::move(out), uniqueCount}; +} + +std::size_t removeDuplicatesInto(const Containers::Iterable& materials, const Containers::StridedArrayView1D& mapping) { + CORRADE_ASSERT(mapping.size() == materials.size(), + "MaterialTools::removeDuplicatesInto(): bad output size, expected" << materials.size() << "but got" << mapping.size(), {}); + + /* O(n^2). Like removeDuplicatesInPlaceInto(), but as the input material + list is immutable, it has to go through the already-processed prefix + and compare only against materials that are unique, which may add some + extra overhead. Another option would be to allocate a temporary array + with (contiguous) references to the material data, but so far I think + the prefix iteration is efficient enough to not need that. */ + std::size_t uniqueCount = 0; + for(std::size_t i = 0; i != materials.size(); ++i) { + /* Find a material that's already in the unique set by going through + the already-processed prefix and comparing only against materials + that are unique, i.e. for which the output index is the same as the + input index. */ + Containers::Optional found; + for(std::size_t j = 0; j != i; ++j) { + if(mapping[j] == j && materialEqual(materials[i], materials[j])) { + found = j; + break; + } + } + + /* Material found, reference its ID */ + if(found) { + mapping[i] = *found; + + /* Otherwise the output index the same as the input index. Also + increase the number of unique materials which isn't used for + anything here except the return value. */ + } else { + mapping[i] = i; + uniqueCount++; + } + } + + return uniqueCount; +} + +Containers::Pair, std::size_t> removeDuplicates(const Containers::Iterable& materials) { + Containers::Array out{NoInit, materials.size()}; + const std::size_t uniqueCount = removeDuplicatesInto(materials, out); + return {Utility::move(out), uniqueCount}; +} + +}} diff --git a/src/Magnum/MaterialTools/RemoveDuplicates.h b/src/Magnum/MaterialTools/RemoveDuplicates.h new file mode 100644 index 000000000..c001823a0 --- /dev/null +++ b/src/Magnum/MaterialTools/RemoveDuplicates.h @@ -0,0 +1,124 @@ +#ifndef Magnum_MaterialTools_RemoveDuplicates_h +#define Magnum_MaterialTools_RemoveDuplicates_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/** + * @file + * @brief Function @ref Magnum::MaterialTools::removeDuplicatesInPlace(), @ref Magnum::MaterialTools::removeDuplicatesInPlaceInto(), @ref Magnum::MaterialTools::removeDuplicates(), @ref Magnum::MaterialTools::removeDuplicatesInto() + * @m_since_latest + */ + +#include + +#include "Magnum/MaterialTools/visibility.h" +#include "Magnum/Trade/Trade.h" + +namespace Magnum { namespace MaterialTools { + +/** +@brief Remove duplicate materials from a list in-place +@param[in,out] materials List of materials +@return Index array to map the original material indices to the output indices + and size of the unique prefix in the cleaned up @p materials array +@m_since_latest + +Removes duplicate materials from the input by comparing material types, +attribute names, types and values and layer offsets. Floating-point attribute +values are compared using fuzzy comparison. Importer state and data flags +aren't considered when comparing the materials. Unique materials are shifted to +the front with order preserved, the returned mapping array has the same size as +the @p materials list and maps from the original indices to prefix of the +output. See @ref removeDuplicates() for a variant that doesn't modify the input +list in any way but instead returns a mapping array pointing to original data +locations. + +The operation is done in an @f$ \mathcal{O}(n^2 m) @f$ complexity with +@f$ n @f$ being the material list size and @f$ m @f$ the per-material attribute +count --- every material in the list is compared to all unique materials +collected so far. As attributes are sorted in @ref Trade::MaterialData, +material comparison is just a linear operation. The function doesn't allocate +any temporary memory. +@see @ref removeDuplicatesInPlaceInto() +*/ +MAGNUM_MATERIALTOOLS_EXPORT Containers::Pair, std::size_t> removeDuplicatesInPlace(const Containers::Iterable& materials); + +/** +@brief Remove duplicate materials from a list in-place and put mapping into given output array +@param[in,out] materials List of materials +@param[out] mapping Where to put the resulting mapping array +@return Size of the unique prefix in the cleaned up @p materials array +@m_since_latest + +Like @ref removeDuplicatesInPlace() but puts the mapping indices into +@p mapping instead of allocating a new array. Expects that @p mapping has the +same size as @p materials. +@see @ref removeDuplicatesInto() +*/ +MAGNUM_MATERIALTOOLS_EXPORT std::size_t removeDuplicatesInPlaceInto(const Containers::Iterable& materials, const Containers::StridedArrayView1D& mapping); + +/** +@brief Remove duplicate materials from a list +@param[in] materials List of materials +@return Array to map the original material indices to unique materials and size + of the unique prefix in the cleaned up @p materials array +@m_since_latest + +Removes duplicate materials from the input by comparing material types, +attribute names, types and values and layer offsets. Floating-point attribute +values are compared using fuzzy comparison. Importer state and data flags +aren't considered when comparing the materials. The returned mapping array has +the same size as the @p materials list and maps from the original indices to +only unique materials in the input array. See @ref removeDuplicatesInPlace() +for a variant that also shifts the unique materials to the front of the list. + +The operation is done in an @f$ \mathcal{O}(n^2 m) @f$ complexity with +@f$ n @f$ being the material list size and @f$ m @f$ the per-material attribute +count --- every material in the list is compared to all unique materials +collected so far, by iterating the filled prefix of the output index list and +considering only index for which the index value is the same as the index. As +attributes are sorted in @ref Trade::MaterialData, material comparison is just +a linear operation. The function doesn't allocate any temporary memory. +@see @ref removeDuplicatesInto() +*/ +MAGNUM_MATERIALTOOLS_EXPORT Containers::Pair, std::size_t> removeDuplicates(const Containers::Iterable& materials); + +/** +@brief Remove duplicate materials from a list in-place and put mapping into given output array +@param[in,out] materials List of materials +@param[out] mapping Where to put the resulting mapping array +@return Size of the unique prefix in the cleaned up @p materials array +@m_since_latest + +Like @ref removeDuplicates() but puts the mapping indices into @p mapping +instead of allocating a new array. Expects that @p mapping has the same size as +@p materials. +@see @ref removeDuplicatesInPlaceInto() +*/ +MAGNUM_MATERIALTOOLS_EXPORT std::size_t removeDuplicatesInto(const Containers::Iterable& materials, const Containers::StridedArrayView1D& mapping); + +}} + +#endif diff --git a/src/Magnum/MaterialTools/Test/CMakeLists.txt b/src/Magnum/MaterialTools/Test/CMakeLists.txt index 22cb2308f..8caa55990 100644 --- a/src/Magnum/MaterialTools/Test/CMakeLists.txt +++ b/src/Magnum/MaterialTools/Test/CMakeLists.txt @@ -30,4 +30,5 @@ set(CMAKE_FOLDER "Magnum/MaterialTools/Test") corrade_add_test(MaterialToolsCopyTest CopyTest.cpp LIBRARIES MagnumMaterialTools) corrade_add_test(MaterialToolsFilterTest FilterTest.cpp LIBRARIES MagnumDebugTools MagnumMaterialToolsTestLib) corrade_add_test(MaterialToolsMergeTest MergeTest.cpp LIBRARIES MagnumDebugTools MagnumMaterialToolsTestLib) +corrade_add_test(MaterialToolsRemoveDuplicatesTest RemoveDuplicatesTest.cpp LIBRARIES MagnumDebugTools MagnumMaterialToolsTestLib) corrade_add_test(MaterialToolsPhongToPbrMetall___Test PhongToPbrMetallicRoughnessTest.cpp LIBRARIES MagnumDebugTools MagnumMaterialTools) diff --git a/src/Magnum/MaterialTools/Test/RemoveDuplicatesTest.cpp b/src/Magnum/MaterialTools/Test/RemoveDuplicatesTest.cpp new file mode 100644 index 000000000..0f019f9b4 --- /dev/null +++ b/src/Magnum/MaterialTools/Test/RemoveDuplicatesTest.cpp @@ -0,0 +1,661 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include /** @todo remove once Debug is stream-free */ +#include +#include +#include +#include +#include /** @todo remove once Debug is stream-free */ + +#include "Magnum/DebugTools/CompareMaterial.h" +#include "Magnum/Math/Color.h" +#include "Magnum/Math/Matrix3.h" +#include "Magnum/MaterialTools/RemoveDuplicates.h" +#include "Magnum/Trade/MaterialData.h" + +namespace Magnum { namespace MaterialTools { namespace Test { namespace { + +struct RemoveDuplicatesTest: TestSuite::Tester { + explicit RemoveDuplicatesTest(); + + void empty(); + + void emptyMaterial(); + void emptyMaterialLayers(); + + void differentAttributeName(); + void differentAttributeType(); + void differentAttributeValue(); + void differentAttributeValueFuzzy(); + void extraAttributes(); + + void implicitBaseLayerSize(); + void multipleLayersSameContents(); + void multipleLayersDifferentContents(); + + /* All cases above test the removeDuplicatesInto() variant, these the + remaining 3 */ + void asArray(); + void inPlace(); + void inPlaceAsArray(); + + void invalidSize(); +}; + +using namespace Math::Literals; + +constexpr Int A = 3, B = 4; +constexpr const Int *PointerA = &A, *PointerB = &B; + +const struct { + const char* name; + Trade::MaterialAttributeData attribute, different; +} DifferentAttributeValueData[]{ + {"bool", + {Trade::MaterialAttribute::AlphaBlend, true}, + {Trade::MaterialAttribute::AlphaBlend, false}}, + {"scalar", + {Trade::MaterialAttribute::BaseColorTexture, 2u}, + {Trade::MaterialAttribute::BaseColorTexture, 1u}}, + {"vector", + {"objectIds", Vector3ui{3, 7, 9}}, + {"objectIds", Vector3ui{3, 6, 9}}}, + /* Matrices are only floating-point, tested in + DifferentAttributeValueFuzzyData instead */ + {"pointer", + /* Takes a pointer to a pointer, not the pointer itself */ + {"objectPtr", &PointerA}, + {"objectPtr", &PointerB}}, + {"mutable pointer", + /* The pointed-to-locations aren't actually mutable, but as the value + isn't used anywhere it should be okay */ + {"objectPtr", Trade::MaterialAttributeType::MutablePointer, &PointerA}, + {"objectPtr", Trade::MaterialAttributeType::MutablePointer, &PointerB}}, + {"string", + {"name", "hellO"}, + {"name", "hell0"}}, + {"buffer", + {"data", Containers::ArrayView{"\x0a\x0b\x0c"}}, + {"data", Containers::ArrayView{"\x0a\x0c\x0b"}}}, +}; + +const struct { + const char* name; + Trade::MaterialAttributeData attribute, same, different; +} DifferentAttributeValueFuzzyData[]{ + {"scalar", + {Trade::MaterialAttribute::Roughness, 0.7f}, + {Trade::MaterialAttribute::Roughness, 0.7f + Math::TypeTraits::epsilon()*0.5f}, + {Trade::MaterialAttribute::Roughness, 0.7f + Math::TypeTraits::epsilon()*2.0f}}, + {"vector", + {Trade::MaterialAttribute::BaseColor, Vector4{0.5f, 0.9f, 0.7f, 0.9f}}, + {Trade::MaterialAttribute::BaseColor, Vector4{0.5f, 0.9f, 0.7f + Math::TypeTraits::epsilon()*0.5f, 0.9f}}, + {Trade::MaterialAttribute::BaseColor, Vector4{0.5f, 0.9f, 0.7f + Math::TypeTraits::epsilon()*2.0f, 0.9f}}}, + {"matrix", + {Trade::MaterialAttribute::TextureMatrix, + Matrix3::translation({5.0f, 9.0f})}, + {Trade::MaterialAttribute::TextureMatrix, + Matrix3::translation({5.0f, 9.0f + Math::TypeTraits::epsilon()*5.0f})}, + {Trade::MaterialAttribute::TextureMatrix, + Matrix3::translation({5.0f, 9.0f + Math::TypeTraits::epsilon()*20.0f})}}, +}; + +RemoveDuplicatesTest::RemoveDuplicatesTest() { + addTests({&RemoveDuplicatesTest::empty, + + &RemoveDuplicatesTest::emptyMaterial, + &RemoveDuplicatesTest::emptyMaterialLayers, + + &RemoveDuplicatesTest::differentAttributeName, + &RemoveDuplicatesTest::differentAttributeType}); + + addInstancedTests({&RemoveDuplicatesTest::differentAttributeValue}, + Containers::arraySize(DifferentAttributeValueData)); + + addInstancedTests({&RemoveDuplicatesTest::differentAttributeValueFuzzy}, + Containers::arraySize(DifferentAttributeValueFuzzyData)); + + addTests({&RemoveDuplicatesTest::extraAttributes, + + &RemoveDuplicatesTest::implicitBaseLayerSize, + &RemoveDuplicatesTest::multipleLayersSameContents, + &RemoveDuplicatesTest::multipleLayersDifferentContents, + + &RemoveDuplicatesTest::asArray, + &RemoveDuplicatesTest::inPlace, + &RemoveDuplicatesTest::inPlaceAsArray, + + &RemoveDuplicatesTest::invalidSize}); +} + +void RemoveDuplicatesTest::empty() { + CORRADE_COMPARE(removeDuplicatesInPlaceInto({}, {}), 0); +} + +void RemoveDuplicatesTest::emptyMaterial() { + const Trade::MaterialData materials[]{ + Trade::MaterialData{Trade::MaterialType::PbrClearCoat, {}}, + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness|Trade::MaterialType::PbrClearCoat, {}}, + Trade::MaterialData{Trade::MaterialType::PbrClearCoat, {}}, + Trade::MaterialData{{}, {}}, + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness|Trade::MaterialType::PbrClearCoat, {}}, + /* This one has an importer state compared to the first. It's ignored + so it should also be treated as the same. */ + Trade::MaterialData{Trade::MaterialType::PbrClearCoat, {}, &A}, + }; + + UnsignedInt mapping[6]; + CORRADE_COMPARE(removeDuplicatesInto(materials, mapping), 3); + CORRADE_COMPARE_AS(Containers::arrayView(mapping), Containers::arrayView({ + 0u, 1u, 0u, 3u, 1u, 0u + }), TestSuite::Compare::Container); +} + +void RemoveDuplicatesTest::emptyMaterialLayers() { + const Trade::MaterialData materials[]{ + Trade::MaterialData{Trade::MaterialType::PbrClearCoat, {}, {0, 0}}, + Trade::MaterialData{Trade::MaterialType::PbrClearCoat, {}, {0}}, + Trade::MaterialData{Trade::MaterialType::PbrClearCoat, {}, {0, 0}}, + /* This one has the same prefix as the first but different count */ + Trade::MaterialData{Trade::MaterialType::PbrClearCoat, {}, {0, 0, 0}}, + /* This one is the same as second but has different type so it + shouldn't match */ + Trade::MaterialData{{}, {}, {0}}, + /* This one is the same as the second, it just has the base layer size + implicit */ + Trade::MaterialData{Trade::MaterialType::PbrClearCoat, {}}, + }; + + UnsignedInt mapping[6]; + CORRADE_COMPARE(removeDuplicatesInto(materials, mapping), 4); + CORRADE_COMPARE_AS(Containers::arrayView(mapping), Containers::arrayView({ + 0u, 1u, 0u, 3u, 4u, 1u + }), TestSuite::Compare::Container); +} + +void RemoveDuplicatesTest::differentAttributeName() { + const Trade::MaterialData materials[]{ + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + /* This one has the same attribute value and type at the same position + but different attribute name, should be treated as different. Both + instances of it are the same tho, so they should be treated as + same. */ + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Roughness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Roughness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + /* This one should be treated as equivalent to the first */ + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + /* This one has everything the same as the first but has a different + type, should be treated different also */ + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + }; + + UnsignedInt mapping[5]; + CORRADE_COMPARE(removeDuplicatesInto(materials, mapping), 3); + CORRADE_COMPARE_AS(Containers::arrayView(mapping), Containers::arrayView({ + 0u, 1u, 1u, 0u, 4u + }), TestSuite::Compare::Container); +} + +void RemoveDuplicatesTest::differentAttributeType() { + const Trade::MaterialData materials[]{ + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.0f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + /* This one has the same attribute name and bit-exact value at the same + position but different attribute name, should be treated as + different */ + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::MetalnessTexture, 0u}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + /* This one should be treated as equivalent to the first */ + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.0f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + /* This one has everything the same as the third first but has a + different type for the last attribute, should be treated different + also */ + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.0f}, + /* Different type allowed only with a string name, not with enum */ + {"SpecularColor", "brown"}, + }}, + }; + + UnsignedInt mapping[4]; + CORRADE_COMPARE(removeDuplicatesInto(materials, mapping), 3); + CORRADE_COMPARE_AS(Containers::arrayView(mapping), Containers::arrayView({ + 0u, 1u, 0u, 3u + }), TestSuite::Compare::Container); +} + +void RemoveDuplicatesTest::differentAttributeValue() { + auto&& data = DifferentAttributeValueData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + const Trade::MaterialData materials[]{ + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::BaseColorTextureCoordinates, 3u}, + {Trade::MaterialAttribute::Glossiness, 3.7f}, + data.attribute + }}, + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::BaseColorTextureCoordinates, 3u}, + {Trade::MaterialAttribute::Glossiness, 3.7f}, + data.different + }}, + /* It's sorted on construction, so this should compare equal */ + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::BaseColorTextureCoordinates, 3u}, + data.attribute, + {Trade::MaterialAttribute::Glossiness, 3.7f}, + }}, + }; + + UnsignedInt mapping[3]; + CORRADE_COMPARE(removeDuplicatesInto(materials, mapping), 2); + CORRADE_COMPARE_AS(Containers::arrayView(mapping), Containers::arrayView({ + 0u, 1u, 0u + }), TestSuite::Compare::Container); +} + +void RemoveDuplicatesTest::differentAttributeValueFuzzy() { + auto&& data = DifferentAttributeValueFuzzyData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + const Trade::MaterialData materials[]{ + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::TextureCoordinates, 3u}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + data.attribute + }}, + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::TextureCoordinates, 3u}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + data.different + }}, + /* Not bit-exact but should be treated as the same */ + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::TextureCoordinates, 3u}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + data.same + }}, + }; + + UnsignedInt mapping[3]; + CORRADE_COMPARE(removeDuplicatesInto(materials, mapping), 2); + CORRADE_COMPARE_AS(Containers::arrayView(mapping), Containers::arrayView({ + 0u, 1u, 0u + }), TestSuite::Compare::Container); +} + +void RemoveDuplicatesTest::extraAttributes() { + const Trade::MaterialData materials[]{ + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + /* This one has the same attribute prefix as the first but one more + attribute after, should be treated as different */ + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + {Trade::MaterialAttribute::TextureLayer, 0u}, + }}, + /* This one has the same attribute prefix as the second, but one + attribute less, should be treated as different */ + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Roughness, 0.3f} + }}, + /* This one is the same again, just with (ignored) importer state */ + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }, {}, &B}, + }; + + UnsignedInt mapping[4]; + CORRADE_COMPARE(removeDuplicatesInto(materials, mapping), 3); + CORRADE_COMPARE_AS(Containers::arrayView(mapping), Containers::arrayView({ + 0u, 1u, 2u, 0u + }), TestSuite::Compare::Container); +} + +void RemoveDuplicatesTest::implicitBaseLayerSize() { + const Trade::MaterialData materials[]{ + /* Implicit layer size after explicit, should be treated the same. Not + the one at the end though, which has a different attribute value. */ + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.0f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }, {3}}, + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.0f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + Trade::MaterialData{Trade::MaterialType::Flat, { + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + /* Explicit layer size after implicit, should be treated the same. Not + the one in the middle though, which has a different attribute + value. */ + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::TextureCoordinates, 3u}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + }}, + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::TextureCoordinates, 4u}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + }, {2}}, + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::TextureCoordinates, 3u}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + }, {2}}, + }; + + UnsignedInt mapping[6]; + CORRADE_COMPARE(removeDuplicatesInto(materials, mapping), 4); + CORRADE_COMPARE_AS(Containers::arrayView(mapping), Containers::arrayView({ + 0u, 0u, 2u, 3u, 4u, 3u + }), TestSuite::Compare::Container); +} + +void RemoveDuplicatesTest::multipleLayersSameContents() { + const Trade::MaterialAttributeData attributes[]{ + {Trade::MaterialAttribute::AlphaBlend, false}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + {Trade::MaterialAttribute::TextureCoordinates, 3u}, + }; + const UnsignedInt layers[]{3, 6}; + + const Trade::MaterialData materials[]{ + /* The attributes are deliberately ordered alphabetically to ensure + they retain the same order even if different layers */ + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::AlphaBlend, false}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + {Trade::MaterialAttribute::TextureCoordinates, 3u}, + }, {2, 6}}, + /* The first layer has 3 elements instead of 2, should be different */ + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::AlphaBlend, false}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + {Trade::MaterialAttribute::TextureCoordinates, 3u}, + }, {3, 6}}, + /* There's an empty base layer before, should be different */ + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::AlphaBlend, false}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + {Trade::MaterialAttribute::TextureCoordinates, 3u}, + }, {0, 2, 6}}, + /* There's an empty layer at the end, should be different */ + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::AlphaBlend, false}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + {Trade::MaterialAttribute::TextureCoordinates, 3u}, + }, {2, 6, 6}}, + /* Same as the second, just with externally owned data */ + Trade::MaterialData{{}, {}, attributes, {}, layers}, + /* Everything in one layer, should be different */ + Trade::MaterialData{{}, {}, attributes}, + }; + + UnsignedInt mapping[6]; + CORRADE_COMPARE(removeDuplicatesInto(materials, mapping), 5); + CORRADE_COMPARE_AS(Containers::arrayView(mapping), Containers::arrayView({ + 0u, 1u, 2u, 3u, 1u, 5u + }), TestSuite::Compare::Container); +} + +void RemoveDuplicatesTest::multipleLayersDifferentContents() { + const Trade::MaterialData materials[]{ + /* Same thing, twice */ + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::AlphaBlend, false}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + {Trade::MaterialAttribute::TextureCoordinates, 3u}, + }, {3, 4, 6}}, + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::AlphaBlend, false}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + {Trade::MaterialAttribute::TextureCoordinates, 3u}, + }, {3, 4, 6}}, + /* Same layer order, different value in one layer. Should be treated + as different. */ + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::AlphaBlend, false}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + {Trade::MaterialAttribute::BaseColor, 0xff3366aa_rgbaf}, + {Trade::MaterialAttribute::Metalness, 0.4f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + {Trade::MaterialAttribute::TextureCoordinates, 3u}, + }, {3, 4, 6}}, + }; + + UnsignedInt mapping[3]; + CORRADE_COMPARE(removeDuplicatesInto(materials, mapping), 2); + CORRADE_COMPARE_AS(Containers::arrayView(mapping), Containers::arrayView({ + 0u, 0u, 2u + }), TestSuite::Compare::Container); +} + +void RemoveDuplicatesTest::asArray() { + const Trade::MaterialData materials[]{ + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::BaseColorTexture, 2u}, + }}, + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::Roughness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::BaseColorTexture, 2u}, + }}, + Trade::MaterialData{Trade::MaterialType::Flat, {}}, + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::Roughness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + Trade::MaterialData{Trade::MaterialType::Flat, {}}, + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::AlphaBlend, false}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + }} + }; + + Containers::Pair, std::size_t> out = removeDuplicates(materials); + CORRADE_COMPARE(out.second(), 4); + CORRADE_COMPARE_AS(out.first(), Containers::arrayView({ + 0u, 1u, 0u, 3u, 1u, 3u, 6u + }), TestSuite::Compare::Container); +} + +void RemoveDuplicatesTest::inPlace() { + Trade::MaterialData materials[]{ + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::BaseColorTexture, 2u}, + }}, + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::Roughness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::BaseColorTexture, 2u}, + }}, + Trade::MaterialData{Trade::MaterialType::Flat, {}}, + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::Roughness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + Trade::MaterialData{Trade::MaterialType::Flat, {}}, + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::AlphaBlend, false}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + }} + }; + + UnsignedInt mapping[7]; + CORRADE_COMPARE(removeDuplicatesInPlaceInto(materials, mapping), 4); + CORRADE_COMPARE_AS(Containers::arrayView(mapping), Containers::arrayView({ + 0u, 1u, 0u, 2u, 1u, 2u, 3u + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(Containers::arrayView(materials)[0], (Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::BaseColorTexture, 2u}, + }}), DebugTools::CompareMaterial); + CORRADE_COMPARE_AS(Containers::arrayView(materials)[1], (Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::Roughness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}), DebugTools::CompareMaterial); + CORRADE_COMPARE_AS(Containers::arrayView(materials)[2], + (Trade::MaterialData{Trade::MaterialType::Flat, {}}), + DebugTools::CompareMaterial); + CORRADE_COMPARE_AS(Containers::arrayView(materials)[3], (Trade::MaterialData{{}, { + {Trade::MaterialAttribute::AlphaBlend, false}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + }}), DebugTools::CompareMaterial); +} + +void RemoveDuplicatesTest::inPlaceAsArray() { + Trade::MaterialData materials[]{ + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::BaseColorTexture, 2u}, + }}, + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::Roughness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::BaseColorTexture, 2u}, + }}, + Trade::MaterialData{Trade::MaterialType::Flat, {}}, + Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::Roughness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}, + Trade::MaterialData{Trade::MaterialType::Flat, {}}, + Trade::MaterialData{{}, { + {Trade::MaterialAttribute::AlphaBlend, false}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + }} + }; + + Containers::Pair, std::size_t> out = removeDuplicatesInPlace(materials); + CORRADE_COMPARE(out.second(), 4); + CORRADE_COMPARE_AS(out.first(), Containers::arrayView({ + 0u, 1u, 0u, 2u, 1u, 2u, 3u + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(Containers::arrayView(materials)[0], (Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::BaseColorTexture, 2u}, + }}), DebugTools::CompareMaterial); + CORRADE_COMPARE_AS(Containers::arrayView(materials)[1], (Trade::MaterialData{Trade::MaterialType::PbrMetallicRoughness, { + {Trade::MaterialAttribute::Roughness, 0.3f}, + {Trade::MaterialAttribute::SpecularColor, 0x66779900_rgbaf}, + }}), DebugTools::CompareMaterial); + CORRADE_COMPARE_AS(Containers::arrayView(materials)[2], + (Trade::MaterialData{Trade::MaterialType::Flat, {}}), + DebugTools::CompareMaterial); + CORRADE_COMPARE_AS(Containers::arrayView(materials)[3], (Trade::MaterialData{{}, { + {Trade::MaterialAttribute::AlphaBlend, false}, + {Trade::MaterialAttribute::AlphaMask, 0.7f}, + }}), DebugTools::CompareMaterial); +} + +void RemoveDuplicatesTest::invalidSize() { + CORRADE_SKIP_IF_NO_ASSERT(); + + Trade::MaterialData data[]{ + Trade::MaterialData{{}, {}}, + Trade::MaterialData{{}, {}}, + }; + UnsignedInt mapping[3]; + + std::ostringstream out; + Error redirectError{&out}; + removeDuplicatesInto(data, mapping); + removeDuplicatesInPlaceInto(data, mapping); + CORRADE_COMPARE(out.str(), + "MaterialTools::removeDuplicatesInto(): bad output size, expected 2 but got 3\n" + "MaterialTools::removeDuplicatesInPlaceInto(): bad output size, expected 2 but got 3\n"); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::MaterialTools::Test::RemoveDuplicatesTest)