Browse Source

Trade: add a type enum *set* to MaterialData.

Compared to previous AbstractMaterialData, which was always just a
single type, the new data can describe several different materials at
once. This is the case for example with glTF, where a material can be
metallic/roughness but also have an alternative description using
specular/glossiness.

Currently the usage is undocumented, but when everything is in place, if
a material advertises given type, it can be then cast to one of
its convenience subclasses.
pull/459/head
Vladimír Vondruš 6 years ago
parent
commit
f250e7a4c9
  1. 14
      src/Magnum/Trade/AbstractMaterialData.cpp
  2. 17
      src/Magnum/Trade/AbstractMaterialData.h
  3. 27
      src/Magnum/Trade/MaterialData.cpp
  4. 45
      src/Magnum/Trade/MaterialData.h
  5. 45
      src/Magnum/Trade/Test/MaterialDataTest.cpp
  6. 2
      src/Magnum/Trade/Trade.h

14
src/Magnum/Trade/AbstractMaterialData.cpp

@ -38,20 +38,6 @@ AbstractMaterialData::AbstractMaterialData(const MaterialType type, const Flags
AbstractMaterialData::~AbstractMaterialData() = default; AbstractMaterialData::~AbstractMaterialData() = default;
Debug& operator<<(Debug& debug, const MaterialType value) {
debug << "Trade::MaterialType" << Debug::nospace;
switch(value) {
/* LCOV_EXCL_START */
#define _c(value) case MaterialType::value: return debug << "::" #value;
_c(Phong)
#undef _c
/* LCOV_EXCL_STOP */
}
return debug << "(" << Debug::nospace << reinterpret_cast<void*>(UnsignedByte(value)) << Debug::nospace << ")";
}
Debug& operator<<(Debug& debug, const AbstractMaterialData::Flag value) { Debug& operator<<(Debug& debug, const AbstractMaterialData::Flag value) {
debug << "Trade::AbstractMaterialData::Flag" << Debug::nospace; debug << "Trade::AbstractMaterialData::Flag" << Debug::nospace;

17
src/Magnum/Trade/AbstractMaterialData.h

@ -29,22 +29,10 @@
* @brief Class @ref Magnum::Trade::AbstractMaterialData, enum @ref Magnum::Trade::MaterialType * @brief Class @ref Magnum::Trade::AbstractMaterialData, enum @ref Magnum::Trade::MaterialType
*/ */
#include <Corrade/Containers/EnumSet.h> #include "Magnum/Trade/MaterialData.h"
#include "Magnum/Magnum.h"
#include "Magnum/Trade/visibility.h"
namespace Magnum { namespace Trade { namespace Magnum { namespace Trade {
/**
@brief Material type
@see @ref AbstractMaterialData::type()
*/
enum class MaterialType: UnsignedByte {
Phong /**< Phong shading (see @ref PhongMaterialData) */
};
/** /**
@brief Material alpha mode @brief Material alpha mode
@ -165,9 +153,6 @@ class MAGNUM_TRADE_EXPORT AbstractMaterialData {
const void* _importerState; const void* _importerState;
}; };
/** @debugoperatorenum{MaterialType} */
MAGNUM_TRADE_EXPORT Debug& operator<<(Debug& debug, MaterialType value);
/** @debugoperatorenum{MaterialAlphaMode} */ /** @debugoperatorenum{MaterialAlphaMode} */
MAGNUM_TRADE_EXPORT Debug& operator<<(Debug& debug, MaterialAlphaMode value); MAGNUM_TRADE_EXPORT Debug& operator<<(Debug& debug, MaterialAlphaMode value);

27
src/Magnum/Trade/MaterialData.cpp

@ -27,6 +27,7 @@
#include <cstring> #include <cstring>
#include <algorithm> #include <algorithm>
#include <Corrade/Containers/EnumSet.hpp>
#include "Magnum/Math/Vector4.h" #include "Magnum/Math/Vector4.h"
#include "Magnum/Math/Matrix.h" #include "Magnum/Math/Matrix.h"
@ -126,7 +127,7 @@ const void* MaterialAttributeData::value() const {
return _data.data + Implementation::MaterialAttributeDataSize - materialAttributeTypeSize(_data.type); return _data.data + Implementation::MaterialAttributeDataSize - materialAttributeTypeSize(_data.type);
} }
MaterialData::MaterialData(Containers::Array<MaterialAttributeData>&& data, const void* const importerState) noexcept: _data{std::move(data)}, _importerState{importerState} { MaterialData::MaterialData(const MaterialTypes types, Containers::Array<MaterialAttributeData>&& data, const void* const importerState) noexcept: _data{std::move(data)}, _types{types}, _importerState{importerState} {
#ifndef CORRADE_NO_ASSERT #ifndef CORRADE_NO_ASSERT
/* Not checking what's already done in MaterialAttributeData constructor. /* Not checking what's already done in MaterialAttributeData constructor.
Done before sorting so the index refers to the actual input index. */ Done before sorting so the index refers to the actual input index. */
@ -155,9 +156,9 @@ MaterialData::MaterialData(Containers::Array<MaterialAttributeData>&& data, cons
} }
} }
MaterialData::MaterialData(const std::initializer_list<MaterialAttributeData> data, const void* const importerState): MaterialData{Implementation::initializerListToArrayWithDefaultDeleter(data), importerState} {} MaterialData::MaterialData(const MaterialTypes types, const std::initializer_list<MaterialAttributeData> data, const void* const importerState): MaterialData{types, Implementation::initializerListToArrayWithDefaultDeleter(data), importerState} {}
MaterialData::MaterialData(DataFlags, const Containers::ArrayView<const MaterialAttributeData> data, const void* const importerState) noexcept: _data{Containers::Array<MaterialAttributeData>{const_cast<MaterialAttributeData*>(data.data()), data.size(), [](MaterialAttributeData*, std::size_t){}}}, _importerState{importerState} { MaterialData::MaterialData(const MaterialTypes types, DataFlags, const Containers::ArrayView<const MaterialAttributeData> data, const void* const importerState) noexcept: _data{Containers::Array<MaterialAttributeData>{const_cast<MaterialAttributeData*>(data.data()), data.size(), [](MaterialAttributeData*, std::size_t){}}}, _types{types}, _importerState{importerState} {
#ifndef CORRADE_NO_ASSERT #ifndef CORRADE_NO_ASSERT
/* Not checking what's already done in MaterialAttributeData constructor */ /* Not checking what's already done in MaterialAttributeData constructor */
for(std::size_t i = 0; i != _data.size(); ++i) for(std::size_t i = 0; i != _data.size(); ++i)
@ -323,4 +324,24 @@ Debug& operator<<(Debug& debug, const MaterialAttributeType value) {
return debug << "(" << Debug::nospace << reinterpret_cast<void*>(UnsignedShort(value)) << Debug::nospace << ")"; return debug << "(" << Debug::nospace << reinterpret_cast<void*>(UnsignedShort(value)) << Debug::nospace << ")";
} }
Debug& operator<<(Debug& debug, const MaterialType value) {
debug << "Trade::MaterialType" << Debug::nospace;
switch(value) {
/* LCOV_EXCL_START */
#define _c(value) case MaterialType::value: return debug << "::" #value;
_c(Phong)
#undef _c
/* LCOV_EXCL_STOP */
}
return debug << "(" << Debug::nospace << reinterpret_cast<void*>(UnsignedByte(value)) << Debug::nospace << ")";
}
Debug& operator<<(Debug& debug, const MaterialTypes value) {
return Containers::enumSetDebugOutput(debug, value, "Trade::MaterialTypes{}", {
MaterialType::Phong
});
}
}} }}

45
src/Magnum/Trade/MaterialData.h

@ -507,6 +507,32 @@ class MAGNUM_TRADE_EXPORT MaterialAttributeData {
static_assert(sizeof(Storage) == Implementation::MaterialAttributeDataSize, "something is off, huh"); static_assert(sizeof(Storage) == Implementation::MaterialAttributeDataSize, "something is off, huh");
}; };
/**
@brief Material type
@see @ref MaterialTypes, @ref MaterialData::types(),
@ref AbstractMaterialData::type()
*/
enum class MaterialType: UnsignedInt {
Phong = 1 << 0 /**< Phong shading */
};
/** @debugoperatorenum{MaterialType} */
MAGNUM_TRADE_EXPORT Debug& operator<<(Debug& debug, MaterialType value);
/**
@brief Material types
@m_since_latest
@see @ref MaterialData::types()
*/
typedef Containers::EnumSet<MaterialType> MaterialTypes;
CORRADE_ENUMSET_OPERATORS(MaterialTypes)
/** @debugoperatorenum{MaterialTypes} */
MAGNUM_TRADE_EXPORT Debug& operator<<(Debug& debug, MaterialTypes value);
/** /**
@brief Material data @brief Material data
@m_since_latest @m_since_latest
@ -543,19 +569,23 @@ class MAGNUM_TRADE_EXPORT MaterialData {
public: public:
/** /**
* @brief Construct * @brief Construct
* @param types Which material types are described by this
* data. Can be an empty set.
* @param data Attribute data * @param data Attribute data
* @param importerState Importer-specific state * @param importerState Importer-specific state
* *
* The @p data gets sorted by name internally, expecting no duplicates. * The @p data gets sorted by name internally, expecting no duplicates.
*/ */
explicit MaterialData(Containers::Array<MaterialAttributeData>&& data, const void* importerState = nullptr) noexcept; explicit MaterialData(MaterialTypes types, Containers::Array<MaterialAttributeData>&& data, const void* importerState = nullptr) noexcept;
/** @overload */ /** @overload */
/* Not noexcept because allocation happens inside */ /* Not noexcept because allocation happens inside */
explicit MaterialData(std::initializer_list<MaterialAttributeData> data, const void* importerState = nullptr); explicit MaterialData(MaterialTypes types, std::initializer_list<MaterialAttributeData> data, const void* importerState = nullptr);
/** /**
* @brief Construct a non-owned material data * @brief Construct a non-owned material data
* @param types Which material types are described by this
* data. Can be an empty set.
* @param dataFlags Ignored. Used only for a safer distinction * @param dataFlags Ignored. Used only for a safer distinction
* from the owning constructor. * from the owning constructor.
* @param data Attribute data * @param data Attribute data
@ -564,7 +594,7 @@ class MAGNUM_TRADE_EXPORT MaterialData {
* The @p data is expected to be already sorted by name, without * The @p data is expected to be already sorted by name, without
* duplicates. * duplicates.
*/ */
explicit MaterialData(DataFlags dataFlags, Containers::ArrayView<const MaterialAttributeData> data, const void* importerState = nullptr) noexcept; explicit MaterialData(MaterialTypes types, DataFlags dataFlags, Containers::ArrayView<const MaterialAttributeData> data, const void* importerState = nullptr) noexcept;
~MaterialData(); ~MaterialData();
@ -580,6 +610,14 @@ class MAGNUM_TRADE_EXPORT MaterialData {
/** @brief Move assignment */ /** @brief Move assignment */
MaterialData& operator=(MaterialData&&) noexcept; MaterialData& operator=(MaterialData&&) noexcept;
/**
* @brief Material types
*
* Each type indicates that the material data can be interpreted as
* given type. For custom materials the set can also be empty.
*/
MaterialTypes types() const { return _types; }
/** /**
* @brief Raw attribute data * @brief Raw attribute data
* *
@ -731,6 +769,7 @@ class MAGNUM_TRADE_EXPORT MaterialData {
UnsignedInt attributeFor(Containers::StringView name) const; UnsignedInt attributeFor(Containers::StringView name) const;
Containers::Array<MaterialAttributeData> _data; Containers::Array<MaterialAttributeData> _data;
MaterialTypes _types;
const void* _importerState; const void* _importerState;
}; };

45
src/Magnum/Trade/Test/MaterialDataTest.cpp

@ -97,6 +97,7 @@ class MaterialDataTest: public TestSuite::Tester {
void debugAttributeType(); void debugAttributeType();
void debugType(); void debugType();
void debugTypes();
void debugFlag(); void debugFlag();
void debugFlags(); void debugFlags();
void debugAlphaMode(); void debugAlphaMode();
@ -186,6 +187,7 @@ MaterialDataTest::MaterialDataTest() {
&MaterialDataTest::debugAttributeType, &MaterialDataTest::debugAttributeType,
&MaterialDataTest::debugType, &MaterialDataTest::debugType,
&MaterialDataTest::debugTypes,
&MaterialDataTest::debugFlag, &MaterialDataTest::debugFlag,
&MaterialDataTest::debugFlags, &MaterialDataTest::debugFlags,
&MaterialDataTest::debugAlphaMode, &MaterialDataTest::debugAlphaMode,
@ -422,13 +424,14 @@ void MaterialDataTest::constructAttributeWrongAccessType() {
void MaterialDataTest::construct() { void MaterialDataTest::construct() {
int state; int state;
MaterialData data{{ MaterialData data{MaterialType::Phong, {
{MaterialAttribute::DoubleSided, true}, {MaterialAttribute::DoubleSided, true},
{MaterialAttribute::DiffuseTextureCoordinates, 5u}, {MaterialAttribute::DiffuseTextureCoordinates, 5u},
{"highlightColor", 0x335566ff_rgbaf}, {"highlightColor", 0x335566ff_rgbaf},
{MaterialAttribute::AmbientTextureMatrix, Matrix3::scaling({0.5f, 1.0f})} {MaterialAttribute::AmbientTextureMatrix, Matrix3::scaling({0.5f, 1.0f})}
}, &state}; }, &state};
CORRADE_COMPARE(data.types(), MaterialType::Phong);
CORRADE_COMPARE(data.attributeCount(), 4); CORRADE_COMPARE(data.attributeCount(), 4);
CORRADE_COMPARE(data.data().size(), 4); CORRADE_COMPARE(data.data().size(), 4);
CORRADE_COMPARE(data.importerState(), &state); CORRADE_COMPARE(data.importerState(), &state);
@ -509,7 +512,7 @@ void MaterialDataTest::constructEmptyAttribute() {
std::ostringstream out; std::ostringstream out;
Error redirectError{&out}; Error redirectError{&out};
MaterialData{{ MaterialData{{}, {
{"DiffuseTexture"_s, 12u}, {"DiffuseTexture"_s, 12u},
MaterialAttributeData{} MaterialAttributeData{}
}}; }};
@ -537,7 +540,7 @@ void MaterialDataTest::constructDuplicateAttribute() {
std::ostringstream out; std::ostringstream out;
Error redirectError{&out}; Error redirectError{&out};
MaterialData data{std::move(attributes)}; MaterialData data{{}, std::move(attributes)};
/* Because with graceful asserts it doesn't exit on error, the assertion /* Because with graceful asserts it doesn't exit on error, the assertion
might get printed multiple times */ might get printed multiple times */
CORRADE_COMPARE(Utility::String::partition(out.str(), '\n')[0], "Trade::MaterialData: duplicate attribute DiffuseTextureCoordinates"); CORRADE_COMPARE(Utility::String::partition(out.str(), '\n')[0], "Trade::MaterialData: duplicate attribute DiffuseTextureCoordinates");
@ -549,7 +552,7 @@ void MaterialDataTest::constructFromImmutableSortedArray() {
{"yay this is last"_s, Vector4{0.2f, 0.6f, 0.4f, 1.0f}} {"yay this is last"_s, Vector4{0.2f, 0.6f, 0.4f, 1.0f}}
}; };
MaterialData data{Containers::Array<MaterialAttributeData>{const_cast<MaterialAttributeData*>(attributes), Containers::arraySize(attributes), [](MaterialAttributeData*, std::size_t) {}}}; MaterialData data{{}, Containers::Array<MaterialAttributeData>{const_cast<MaterialAttributeData*>(attributes), Containers::arraySize(attributes), [](MaterialAttributeData*, std::size_t) {}}};
CORRADE_COMPARE(data.attributeCount(), 2); CORRADE_COMPARE(data.attributeCount(), 2);
CORRADE_COMPARE(data.attributeName(0), "hello this is first"); CORRADE_COMPARE(data.attributeName(0), "hello this is first");
@ -567,9 +570,10 @@ void MaterialDataTest::constructNonOwned() {
}; };
int state; int state;
MaterialData data{{}, attributes, &state}; MaterialData data{MaterialType::Phong, {}, attributes, &state};
/* Expecting the same output as in construct() */ /* Expecting the same output as in construct() */
CORRADE_COMPARE(data.types(), MaterialType::Phong);
CORRADE_COMPARE(data.attributeCount(), 4); CORRADE_COMPARE(data.attributeCount(), 4);
CORRADE_COMPARE(data.data().size(), 4); CORRADE_COMPARE(data.data().size(), 4);
CORRADE_COMPARE(data.data().data(), attributes); CORRADE_COMPARE(data.data().data(), attributes);
@ -598,7 +602,7 @@ void MaterialDataTest::constructNonOwnedEmptyAttribute() {
std::ostringstream out; std::ostringstream out;
Error redirectError{&out}; Error redirectError{&out};
/* nullptr to avoid attributes interpreted as importerState */ /* nullptr to avoid attributes interpreted as importerState */
MaterialData{{}, attributes, nullptr}; MaterialData{{}, {}, attributes, nullptr};
CORRADE_COMPARE(out.str(), "Trade::MaterialData: attribute 1 doesn't specify anything\n"); CORRADE_COMPARE(out.str(), "Trade::MaterialData: attribute 1 doesn't specify anything\n");
} }
@ -615,7 +619,7 @@ void MaterialDataTest::constructNonOwnedNotSorted() {
std::ostringstream out; std::ostringstream out;
Error redirectError{&out}; Error redirectError{&out};
/* nullptr to avoid attributes interpreted as importerState */ /* nullptr to avoid attributes interpreted as importerState */
MaterialData{{}, attributes, nullptr}; MaterialData{{}, {}, attributes, nullptr};
CORRADE_COMPARE(out.str(), "Trade::MaterialData: DiffuseTexture has to be sorted before DiffuseTextureCoordinates if passing non-owned data\n"); CORRADE_COMPARE(out.str(), "Trade::MaterialData: DiffuseTexture has to be sorted before DiffuseTextureCoordinates if passing non-owned data\n");
} }
@ -633,7 +637,7 @@ void MaterialDataTest::constructNonOwnedDuplicateAttribute() {
std::ostringstream out; std::ostringstream out;
Error redirectError{&out}; Error redirectError{&out};
/* nullptr to avoid attributes interpreted as importerState */ /* nullptr to avoid attributes interpreted as importerState */
MaterialData{{}, attributes, nullptr}; MaterialData{{}, {}, attributes, nullptr};
CORRADE_COMPARE(out.str(), "Trade::MaterialData: duplicate attribute DiffuseTextureCoordinates\n"); CORRADE_COMPARE(out.str(), "Trade::MaterialData: duplicate attribute DiffuseTextureCoordinates\n");
} }
@ -644,22 +648,24 @@ void MaterialDataTest::constructCopy() {
void MaterialDataTest::constructMove() { void MaterialDataTest::constructMove() {
int state; int state;
MaterialData a{{ MaterialData a{MaterialType::Phong, {
{MaterialAttribute::DoubleSided, true}, {MaterialAttribute::DoubleSided, true},
{"boredomFactor", 5} {"boredomFactor", 5}
}, &state}; }, &state};
MaterialData b{std::move(a)}; MaterialData b{std::move(a)};
CORRADE_COMPARE(a.attributeCount(), 0); CORRADE_COMPARE(a.attributeCount(), 0);
CORRADE_COMPARE(b.types(), MaterialType::Phong);
CORRADE_COMPARE(b.attributeCount(), 2); CORRADE_COMPARE(b.attributeCount(), 2);
CORRADE_COMPARE(b.attributeName(0), "DoubleSided"); CORRADE_COMPARE(b.attributeName(0), "DoubleSided");
CORRADE_COMPARE(b.importerState(), &state); CORRADE_COMPARE(b.importerState(), &state);
MaterialData c{{ MaterialData c{MaterialTypes{}, {
{MaterialAttribute::AlphaMask, 0.5f} {MaterialAttribute::AlphaMask, 0.5f}
}}; }};
c = std::move(b); c = std::move(b);
CORRADE_COMPARE(b.attributeCount(), 1); CORRADE_COMPARE(b.attributeCount(), 1);
CORRADE_COMPARE(c.types(), MaterialType::Phong);
CORRADE_COMPARE(c.attributeCount(), 2); CORRADE_COMPARE(c.attributeCount(), 2);
CORRADE_COMPARE(c.attributeName(0), "DoubleSided"); CORRADE_COMPARE(c.attributeName(0), "DoubleSided");
CORRADE_COMPARE(c.importerState(), &state); CORRADE_COMPARE(c.importerState(), &state);
@ -669,7 +675,7 @@ void MaterialDataTest::constructMove() {
} }
void MaterialDataTest::accessOptional() { void MaterialDataTest::accessOptional() {
MaterialData data{{ MaterialData data{{}, {
{MaterialAttribute::AlphaMask, 0.5f}, {MaterialAttribute::AlphaMask, 0.5f},
{MaterialAttribute::SpecularTexture, 3u} {MaterialAttribute::SpecularTexture, 3u}
}}; }};
@ -698,7 +704,7 @@ void MaterialDataTest::accessOutOfBounds() {
CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions");
#endif #endif
MaterialData data{{ MaterialData data{{}, {
{MaterialAttribute::AlphaMask, 0.5f}, {MaterialAttribute::AlphaMask, 0.5f},
{MaterialAttribute::SpecularTexture, 3u} {MaterialAttribute::SpecularTexture, 3u}
}}; }};
@ -721,7 +727,7 @@ void MaterialDataTest::accessInvalidAttributeName() {
CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions");
#endif #endif
MaterialData data{}; MaterialData data{{}, {}};
std::ostringstream out; std::ostringstream out;
Error redirectError{&out}; Error redirectError{&out};
@ -765,7 +771,7 @@ void MaterialDataTest::accessNotFound() {
CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions");
#endif #endif
MaterialData data{{ MaterialData data{{}, {
{"DiffuseColor", 0xff3366aa_rgbaf} {"DiffuseColor", 0xff3366aa_rgbaf}
}}; }};
@ -789,7 +795,7 @@ void MaterialDataTest::accessWrongType() {
CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions");
#endif #endif
MaterialData data{{ MaterialData data{{}, {
{"DiffuseColor", 0xff3366aa_rgbaf} {"DiffuseColor", 0xff3366aa_rgbaf}
}}; }};
@ -815,7 +821,7 @@ void MaterialDataTest::accessWrongType() {
} }
void MaterialDataTest::release() { void MaterialDataTest::release() {
MaterialData data{{ MaterialData data{{}, {
{"DiffuseColor", 0xff3366aa_rgbaf}, {"DiffuseColor", 0xff3366aa_rgbaf},
{MaterialAttribute::NormalTexture, 0u} {MaterialAttribute::NormalTexture, 0u}
}}; }};
@ -1103,6 +1109,13 @@ void MaterialDataTest::debugType() {
CORRADE_COMPARE(out.str(), "Trade::MaterialType::Phong Trade::MaterialType(0xbe)\n"); CORRADE_COMPARE(out.str(), "Trade::MaterialType::Phong Trade::MaterialType(0xbe)\n");
} }
void MaterialDataTest::debugTypes() {
std::ostringstream out;
Debug{&out} << (MaterialType::Phong|MaterialType(0xe0)) << MaterialTypes{};
CORRADE_COMPARE(out.str(), "Trade::MaterialType::Phong|Trade::MaterialType(0xe0) Trade::MaterialTypes{}\n");
}
void MaterialDataTest::debugFlag() { void MaterialDataTest::debugFlag() {
std::ostringstream out; std::ostringstream out;

2
src/Magnum/Trade/Trade.h

@ -50,7 +50,7 @@ typedef CORRADE_DEPRECATED("use InputFileCallbackPolicy instead") InputFileCallb
enum class MaterialAttribute: UnsignedInt; enum class MaterialAttribute: UnsignedInt;
enum class MaterialAttributeType: UnsignedByte; enum class MaterialAttributeType: UnsignedByte;
enum class MaterialType: UnsignedByte; enum class MaterialType: UnsignedInt;
enum class MaterialAlphaMode: UnsignedByte; enum class MaterialAlphaMode: UnsignedByte;
class AbstractMaterialData; class AbstractMaterialData;
class MaterialAttributeData; class MaterialAttributeData;

Loading…
Cancel
Save