From e3ee1e561e6194c93d94fd0a0dcfb1bfb07a0911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Wed, 19 Feb 2020 17:58:22 +0100 Subject: [PATCH] Trade: typeless access to MeshData attributes. Similarly to ImageData::pixels() which return a strided array view of one dimension more. --- src/Magnum/Trade/MeshData.cpp | 48 +++++++++++ src/Magnum/Trade/MeshData.h | 103 +++++++++++++++++++---- src/Magnum/Trade/Test/MeshDataTest.cpp | 109 +++++++++++++++++++++++++ 3 files changed, 244 insertions(+), 16 deletions(-) diff --git a/src/Magnum/Trade/MeshData.cpp b/src/Magnum/Trade/MeshData.cpp index 3841e56a6..f5296c472 100644 --- a/src/Magnum/Trade/MeshData.cpp +++ b/src/Magnum/Trade/MeshData.cpp @@ -49,6 +49,16 @@ MeshAttributeData::MeshAttributeData(const MeshAttribute name, const VertexForma "Trade::MeshAttributeData: view stride" << data.stride() << "is not large enough to contain" << format, ); } +MeshAttributeData::MeshAttributeData(const MeshAttribute name, const VertexFormat format, const Containers::StridedArrayView2D& data) noexcept: MeshAttributeData{name, format, Containers::StridedArrayView1D{{data.data(), ~std::size_t{}}, data.size()[0], data.stride()[0]}, nullptr} { + /* Yes, this calls into a constexpr function defined in the header -- + because I feel that makes more sense than duplicating the full assert + logic */ + CORRADE_ASSERT(data.empty()[0] || vertexFormatSize(format) == data.size()[1], + "Trade::MeshAttributeData: second view dimension size" << data.size()[1] << "doesn't match" << format, ); + CORRADE_ASSERT(data.isContiguous<1>(), + "Trade::MeshAttributeData: second view dimension is not contiguous", ); +} + Containers::Array meshAttributeDataNonOwningArray(const Containers::ArrayView view) { /* Ugly, eh? */ return Containers::Array{const_cast(view.data()), view.size(), reinterpret_cast(Trade::Implementation::nonOwnedArrayDeleter)}; @@ -230,6 +240,44 @@ UnsignedInt MeshData::attributeStride(MeshAttribute name, UnsignedInt id) const return attributeStride(attributeId); } +Containers::StridedArrayView2D MeshData::attribute(UnsignedInt id) const { + CORRADE_ASSERT(id < _attributes.size(), + "Trade::MeshData::attribute(): index" << id << "out of range for" << _attributes.size() << "attributes", nullptr); + /* Build a 2D view using information about attribute type size */ + return Containers::arrayCast<2, const char>(_attributes[id].data, + vertexFormatSize(_attributes[id].format)); +} + +Containers::StridedArrayView2D MeshData::mutableAttribute(UnsignedInt id) { + CORRADE_ASSERT(_vertexDataFlags & DataFlag::Mutable, + "Trade::MeshData::mutableAttribute(): vertex data not mutable", {}); + CORRADE_ASSERT(id < _attributes.size(), + "Trade::MeshData::mutableAttribute(): index" << id << "out of range for" << _attributes.size() << "attributes", nullptr); + /* Build a 2D view using information about attribute type size */ + auto out = Containers::arrayCast<2, const char>(_attributes[id].data, + vertexFormatSize(_attributes[id].format)); + /** @todo some arrayConstCast? UGH */ + return Containers::StridedArrayView2D{ + /* The view size is there only for a size assert, we're pretty sure the + view is valid */ + {static_cast(const_cast(out.data())), ~std::size_t{}}, + out.size(), out.stride()}; +} + +Containers::StridedArrayView2D MeshData::attribute(MeshAttribute name, UnsignedInt id) const { + const UnsignedInt attributeId = attributeFor(name, id); + CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::attribute(): index" << id << "out of range for" << attributeCount(name) << name << "attributes", {}); + return attribute(attributeId); +} + +Containers::StridedArrayView2D MeshData::mutableAttribute(MeshAttribute name, UnsignedInt id) { + CORRADE_ASSERT(_vertexDataFlags & DataFlag::Mutable, + "Trade::MeshData::mutableAttribute(): vertex data not mutable", {}); + const UnsignedInt attributeId = attributeFor(name, id); + CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::mutableAttribute(): index" << id << "out of range for" << attributeCount(name) << name << "attributes", {}); + return mutableAttribute(attributeId); +} + namespace { template void convertIndices(const Containers::ArrayView data, const Containers::ArrayView destination) { diff --git a/src/Magnum/Trade/MeshData.h b/src/Magnum/Trade/MeshData.h index b69ca987e..fbb5be122 100644 --- a/src/Magnum/Trade/MeshData.h +++ b/src/Magnum/Trade/MeshData.h @@ -229,6 +229,20 @@ class MAGNUM_TRADE_EXPORT MeshAttributeData { */ explicit MeshAttributeData(MeshAttribute name, VertexFormat format, const Containers::StridedArrayView1D& data) noexcept; + /** + * @brief Constructor + * @param name Attribute name + * @param format Vertex format + * @param data Attribute data + * + * Expects that the second dimension of @p data is contiguous and its + * size matches @p type; and that @p type corresponds to @p name. + */ + explicit MeshAttributeData(MeshAttribute name, VertexFormat format, const Containers::StridedArrayView2D& data) noexcept; + + /** @overload */ + explicit MeshAttributeData(MeshAttribute name, VertexFormat format, std::nullptr_t) noexcept: MeshAttributeData{name, format, nullptr, nullptr} {} + /** * @brief Constructor * @param name Attribute name @@ -762,6 +776,26 @@ class MAGNUM_TRADE_EXPORT MeshData { /** * @brief Data for given attribute array * + * The @p id is expected to be smaller than @ref attributeCount() const. + * The second dimension represents the actual data type (its size is + * equal to type size) and is guaranteed to be contiguous. Use the + * templated overload below to get the attribute in a concrete type. + * @see @ref Corrade::Containers::StridedArrayView::isContiguous() + */ + Containers::StridedArrayView2D attribute(UnsignedInt id) const; + + /** + * @brief Mutable data for given attribute array + * + * Like @ref attribute(UnsignedInt) const, but returns a mutable view. + * Expects that the mesh is mutable. + * @see @ref vertexDataFlags() + */ + Containers::StridedArrayView2D mutableAttribute(UnsignedInt id); + + /** + * @brief Data for given attribute array in a concrete type + * * The @p id is expected to be smaller than @ref attributeCount() const * and @p T is expected to correspond to * @ref attributeFormat(UnsignedInt) const. You can also use the @@ -776,7 +810,7 @@ class MAGNUM_TRADE_EXPORT MeshData { template Containers::StridedArrayView1D attribute(UnsignedInt id) const; /** - * @brief Mutable data for given attribute array + * @brief Mutable data for given attribute array in a concrete type * * Like @ref attribute(UnsignedInt) const, but returns a mutable view. * Expects that the mesh is mutable. @@ -788,6 +822,29 @@ class MAGNUM_TRADE_EXPORT MeshData { * @brief Data for given named attribute array * * The @p id is expected to be smaller than + * @ref attributeCount(MeshAttribute) const. The second dimension + * represents the actual data type (its size is equal to type size) and + * is guaranteed to be contiguous. Use the templated overload below to + * get the attribute in a concrete type. + * @see @ref attribute(UnsignedInt) const, + * @ref mutableAttribute(MeshAttribute, UnsignedInt), + * @ref Corrade::Containers::StridedArrayView::isContiguous() + */ + Containers::StridedArrayView2D attribute(MeshAttribute name, UnsignedInt id = 0) const; + + /** + * @brief Mutable data for given named attribute array + * + * Like @ref attribute(MeshAttribute, UnsignedInt) const, but returns a + * mutable view. Expects that the mesh is mutable. + * @see @ref vertexDataFlags() + */ + Containers::StridedArrayView2D mutableAttribute(MeshAttribute name, UnsignedInt id = 0); + + /** + * @brief Data for given named attribute array in a concrete type + * + * The @p id is expected to be smaller than * @ref attributeCount(MeshAttribute) const and @p T is expected to * correspond to @ref attributeFormat(MeshAttribute, UnsignedInt) const. * You can also use the non-templated @ref positions2DAsArray(), @@ -802,7 +859,7 @@ class MAGNUM_TRADE_EXPORT MeshData { template Containers::StridedArrayView1D attribute(MeshAttribute name, UnsignedInt id = 0) const; /** - * @brief Mutable data for given named attribute array + * @brief Mutable data for given named attribute array in a concrete type * * Like @ref attribute(MeshAttribute, UnsignedInt) const, but returns a * mutable view. Expects that the mesh is mutable. @@ -1066,35 +1123,49 @@ template Containers::ArrayView MeshData::mutableIndices() { } template Containers::StridedArrayView1D MeshData::attribute(UnsignedInt id) const { - CORRADE_ASSERT(id < _attributes.size(), - "Trade::MeshData::attribute(): index" << id << "out of range for" << _attributes.size() << "attributes", nullptr); + Containers::StridedArrayView2D data = attribute(id); + #ifdef CORRADE_GRACEFUL_ASSERT /* Sigh. Brittle. Better idea? */ + if(!data.stride()[1]) return {}; + #endif CORRADE_ASSERT(Implementation::vertexFormatFor() == _attributes[id].format, "Trade::MeshData::attribute(): improper type requested for" << _attributes[id].name << "of format" << _attributes[id].format, nullptr); - return Containers::arrayCast(_attributes[id].data); + return Containers::arrayCast<1, const T>(data); } template Containers::StridedArrayView1D MeshData::mutableAttribute(UnsignedInt id) { - CORRADE_ASSERT(_vertexDataFlags & DataFlag::Mutable, - "Trade::MeshData::mutableAttribute(): vertex data not mutable", {}); - CORRADE_ASSERT(id < _attributes.size(), - "Trade::MeshData::mutableAttribute(): index" << id << "out of range for" << _attributes.size() << "attributes", nullptr); + Containers::StridedArrayView2D data = mutableAttribute(id); + #ifdef CORRADE_GRACEFUL_ASSERT /* Sigh. Brittle. Better idea? */ + if(!data.stride()[1]) return {}; + #endif CORRADE_ASSERT(Implementation::vertexFormatFor() == _attributes[id].format, "Trade::MeshData::mutableAttribute(): improper type requested for" << _attributes[id].name << "of format" << _attributes[id].format, nullptr); - return Containers::arrayCast(reinterpret_cast&>(_attributes[id].data)); + return Containers::arrayCast<1, T>(data); } template Containers::StridedArrayView1D MeshData::attribute(MeshAttribute name, UnsignedInt id) const { + Containers::StridedArrayView2D data = attribute(name, id); + #ifdef CORRADE_GRACEFUL_ASSERT /* Sigh. Brittle. Better idea? */ + if(!data.stride()[1]) return {}; + #endif + #ifndef CORRADE_NO_ASSERT const UnsignedInt attributeId = attributeFor(name, id); - CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::attribute(): index" << id << "out of range for" << attributeCount(name) << name << "attributes", {}); - return attribute(attributeId); + #endif + CORRADE_ASSERT(Implementation::vertexFormatFor() == _attributes[attributeId].format, + "Trade::MeshData::attribute(): improper type requested for" << _attributes[attributeId].name << "of format" << _attributes[attributeId].format, nullptr); + return Containers::arrayCast<1, const T>(data); } template Containers::StridedArrayView1D MeshData::mutableAttribute(MeshAttribute name, UnsignedInt id) { - CORRADE_ASSERT(_vertexDataFlags & DataFlag::Mutable, - "Trade::MeshData::mutableAttribute(): vertex data not mutable", {}); + Containers::StridedArrayView2D data = mutableAttribute(name, id); + #ifdef CORRADE_GRACEFUL_ASSERT /* Sigh. Brittle. Better idea? */ + if(!data.stride()[1]) return {}; + #endif + #ifndef CORRADE_NO_ASSERT const UnsignedInt attributeId = attributeFor(name, id); - CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::mutableAttribute(): index" << id << "out of range for" << attributeCount(name) << name << "attributes", {}); - return mutableAttribute(attributeId); + #endif + CORRADE_ASSERT(Implementation::vertexFormatFor() == _attributes[attributeId].format, + "Trade::MeshData::mutableAttribute(): improper type requested for" << _attributes[attributeId].name << "of type" << _attributes[attributeId].format, nullptr); + return Containers::arrayCast<1, T>(data); } }} diff --git a/src/Magnum/Trade/Test/MeshDataTest.cpp b/src/Magnum/Trade/Test/MeshDataTest.cpp index 3741e020b..37f2900d7 100644 --- a/src/Magnum/Trade/Test/MeshDataTest.cpp +++ b/src/Magnum/Trade/Test/MeshDataTest.cpp @@ -49,8 +49,12 @@ struct MeshDataTest: TestSuite::Tester { void constructAttribute(); void constructAttributeCustom(); void constructAttributeWrongFormat(); + void constructAttribute2D(); + void constructAttribute2DWrongSize(); + void constructAttribute2DNonContiguous(); void constructAttributeTypeErased(); void constructAttributeTypeErasedWrongStride(); + void constructAttributeNullptr(); void constructAttributeNonOwningArray(); void construct(); @@ -140,8 +144,12 @@ MeshDataTest::MeshDataTest() { &MeshDataTest::constructAttribute, &MeshDataTest::constructAttributeCustom, &MeshDataTest::constructAttributeWrongFormat, + &MeshDataTest::constructAttribute2D, + &MeshDataTest::constructAttribute2DWrongSize, + &MeshDataTest::constructAttribute2DNonContiguous, &MeshDataTest::constructAttributeTypeErased, &MeshDataTest::constructAttributeTypeErasedWrongStride, + &MeshDataTest::constructAttributeNullptr, &MeshDataTest::constructAttributeNonOwningArray, &MeshDataTest::construct, @@ -376,6 +384,41 @@ void MeshDataTest::constructAttributeWrongFormat() { CORRADE_COMPARE(out.str(), "Trade::MeshAttributeData: VertexFormat::Vector2 is not a valid format for Trade::MeshAttribute::Color\n"); } +void MeshDataTest::constructAttribute2D() { + Containers::Array positionData{4*sizeof(Vector2)}; + auto positionView = Containers::StridedArrayView2D{positionData, + {4, sizeof(Vector2)}}.every(2); + + MeshAttributeData positions{MeshAttribute::Position, VertexFormat::Vector2, positionView}; + MeshData data{MeshPrimitive::Points, std::move(positionData), {positions}}; + CORRADE_COMPARE(data.attributeName(0), MeshAttribute::Position); + CORRADE_COMPARE(data.attributeFormat(0), VertexFormat::Vector2); + CORRADE_COMPARE(static_cast(data.attribute(0).data()), + positionView.data()); +} + +void MeshDataTest::constructAttribute2DWrongSize() { + Containers::Array positionData{4*sizeof(Vector2)}; + + std::ostringstream out; + Error redirectError{&out}; + MeshAttributeData{MeshAttribute::Position, VertexFormat::Vector3, + Containers::StridedArrayView2D{positionData, + {4, sizeof(Vector2)}}.every(2)}; + CORRADE_COMPARE(out.str(), "Trade::MeshAttributeData: second view dimension size 8 doesn't match VertexFormat::Vector3\n"); +} + +void MeshDataTest::constructAttribute2DNonContiguous() { + Containers::Array positionData{4*sizeof(Vector2)}; + + std::ostringstream out; + Error redirectError{&out}; + MeshAttributeData{MeshAttribute::Position, VertexFormat::Vector2, + Containers::StridedArrayView2D{positionData, + {2, sizeof(Vector2)*2}}.every({1, 2})}; + CORRADE_COMPARE(out.str(), "Trade::MeshAttributeData: second view dimension is not contiguous\n"); +} + void MeshDataTest::constructAttributeTypeErased() { Containers::Array positionData{3*sizeof(Vector3)}; auto positionView = Containers::arrayCast(positionData); @@ -397,6 +440,14 @@ void MeshDataTest::constructAttributeTypeErasedWrongStride() { CORRADE_COMPARE(out.str(), "Trade::MeshAttributeData: view stride 1 is not large enough to contain VertexFormat::Vector3\n"); } +void MeshDataTest::constructAttributeNullptr() { + MeshAttributeData positions{MeshAttribute::Position, VertexFormat::Vector2, nullptr}; + MeshData data{MeshPrimitive::LineLoop, nullptr, {positions}}; + CORRADE_COMPARE(data.attributeName(0), MeshAttribute::Position); + CORRADE_COMPARE(data.attributeFormat(0), VertexFormat::Vector2); + CORRADE_VERIFY(!data.attribute(0).data()); +} + void MeshDataTest::constructAttributeNonOwningArray() { const MeshAttributeData data[3]; Containers::Array array = meshAttributeDataNonOwningArray(data); @@ -491,6 +542,30 @@ void MeshDataTest::construct() { CORRADE_COMPARE(data.attributeStride(1), sizeof(Vertex)); CORRADE_COMPARE(data.attributeStride(2), sizeof(Vertex)); CORRADE_COMPARE(data.attributeStride(3), sizeof(Vertex)); + + /* Typeless access by ID with a cast later */ + CORRADE_COMPARE((Containers::arrayCast<1, const Vector3>( + data.attribute(0))[1]), (Vector3{0.4f, 0.5f, 0.6f})); + CORRADE_COMPARE((Containers::arrayCast<1, const Vector2>( + data.attribute(1))[0]), (Vector2{0.000f, 0.125f})); + CORRADE_COMPARE((Containers::arrayCast<1, const Vector3>( + data.attribute(2))[2]), Vector3::zAxis()); + CORRADE_COMPARE((Containers::arrayCast<1, const Vector2>( + data.attribute(3))[1]), (Vector2{0.250f, 0.375f})); + CORRADE_COMPARE((Containers::arrayCast<1, const Short>( + data.attribute(4))[0]), 15); + CORRADE_COMPARE((Containers::arrayCast<1, Vector3>( + data.mutableAttribute(0))[1]), (Vector3{0.4f, 0.5f, 0.6f})); + CORRADE_COMPARE((Containers::arrayCast<1, Vector2>( + data.mutableAttribute(1))[0]), (Vector2{0.000f, 0.125f})); + CORRADE_COMPARE((Containers::arrayCast<1, Vector3>( + data.mutableAttribute(2))[2]), Vector3::zAxis()); + CORRADE_COMPARE((Containers::arrayCast<1, Vector2>( + data.mutableAttribute(3))[1]), (Vector2{0.250f, 0.375f})); + CORRADE_COMPARE((Containers::arrayCast<1, Short>( + data.mutableAttribute(4))[0]), 15); + + /* Typed access by ID */ CORRADE_COMPARE(data.attribute(0)[1], (Vector3{0.4f, 0.5f, 0.6f})); CORRADE_COMPARE(data.attribute(1)[0], (Vector2{0.000f, 0.125f})); CORRADE_COMPARE(data.attribute(2)[2], Vector3::zAxis()); @@ -534,6 +609,30 @@ void MeshDataTest::construct() { CORRADE_COMPARE(data.attributeStride(MeshAttribute::TextureCoordinates, 0), sizeof(Vertex)); CORRADE_COMPARE(data.attributeStride(MeshAttribute::TextureCoordinates, 1), sizeof(Vertex)); CORRADE_COMPARE(data.attributeStride(meshAttributeCustom(13)), sizeof(Vertex)); + + /* Typeless access by name with a cast later */ + CORRADE_COMPARE((Containers::arrayCast<1, const Vector3>( + data.attribute(MeshAttribute::Position))[1]), (Vector3{0.4f, 0.5f, 0.6f})); + CORRADE_COMPARE((Containers::arrayCast<1, const Vector3>( + data.attribute(MeshAttribute::Normal))[2]), Vector3::zAxis()); + CORRADE_COMPARE((Containers::arrayCast<1, const Vector2>( + data.attribute(MeshAttribute::TextureCoordinates, 0))[0]), (Vector2{0.000f, 0.125f})); + CORRADE_COMPARE((Containers::arrayCast<1, const Vector2>( + data.attribute(MeshAttribute::TextureCoordinates, 1))[1]), (Vector2{0.250f, 0.375f})); + CORRADE_COMPARE((Containers::arrayCast<1, const Short>( + data.attribute(meshAttributeCustom(13)))[1]), -374); + CORRADE_COMPARE((Containers::arrayCast<1, const Vector3>( + data.mutableAttribute(MeshAttribute::Position))[1]), (Vector3{0.4f, 0.5f, 0.6f})); + CORRADE_COMPARE((Containers::arrayCast<1, Vector3>( + data.mutableAttribute(MeshAttribute::Normal))[2]), Vector3::zAxis()); + CORRADE_COMPARE((Containers::arrayCast<1, Vector2>( + data.mutableAttribute(MeshAttribute::TextureCoordinates, 0))[0]), (Vector2{0.000f, 0.125f})); + CORRADE_COMPARE((Containers::arrayCast<1, Vector2>( + data.mutableAttribute(MeshAttribute::TextureCoordinates, 1))[1]), (Vector2{0.250f, 0.375f})); + CORRADE_COMPARE((Containers::arrayCast<1, Short>( + data.mutableAttribute(meshAttributeCustom(13)))[1]), -374); + + /* Typed access by name */ CORRADE_COMPARE(data.attribute(MeshAttribute::Position)[1], (Vector3{0.4f, 0.5f, 0.6f})); CORRADE_COMPARE(data.attribute(MeshAttribute::Normal)[2], Vector3::zAxis()); CORRADE_COMPARE(data.attribute(MeshAttribute::TextureCoordinates, 0)[0], (Vector2{0.000f, 0.125f})); @@ -1275,13 +1374,17 @@ void MeshDataTest::mutableAccessNotAllowed() { data.mutableIndexData(); data.mutableVertexData(); data.mutableIndices(); + data.mutableAttribute(0); data.mutableAttribute(0); + data.mutableAttribute(MeshAttribute::Position); data.mutableAttribute(MeshAttribute::Position); CORRADE_COMPARE(out.str(), "Trade::MeshData::mutableIndexData(): index data not mutable\n" "Trade::MeshData::mutableVertexData(): vertex data not mutable\n" "Trade::MeshData::mutableIndices(): index data not mutable\n" "Trade::MeshData::mutableAttribute(): vertex data not mutable\n" + "Trade::MeshData::mutableAttribute(): vertex data not mutable\n" + "Trade::MeshData::mutableAttribute(): vertex data not mutable\n" "Trade::MeshData::mutableAttribute(): vertex data not mutable\n"); } @@ -1327,6 +1430,7 @@ void MeshDataTest::attributeNotFound() { data.attributeFormat(2); data.attributeOffset(2); data.attributeStride(2); + data.attribute(2); data.attribute(2); data.attributeFormat(MeshAttribute::Position); data.attributeFormat(MeshAttribute::Color, 2); @@ -1334,6 +1438,8 @@ void MeshDataTest::attributeNotFound() { data.attributeOffset(MeshAttribute::Color, 2); data.attributeStride(MeshAttribute::Position); data.attributeStride(MeshAttribute::Color, 2); + data.attribute(MeshAttribute::Position); + data.attribute(MeshAttribute::Color, 2); data.attribute(MeshAttribute::Position); data.attribute(MeshAttribute::Color, 2); data.positions2DAsArray(); @@ -1347,6 +1453,7 @@ void MeshDataTest::attributeNotFound() { "Trade::MeshData::attributeOffset(): index 2 out of range for 2 attributes\n" "Trade::MeshData::attributeStride(): index 2 out of range for 2 attributes\n" "Trade::MeshData::attribute(): index 2 out of range for 2 attributes\n" + "Trade::MeshData::attribute(): index 2 out of range for 2 attributes\n" "Trade::MeshData::attributeFormat(): index 0 out of range for 0 Trade::MeshAttribute::Position attributes\n" "Trade::MeshData::attributeFormat(): index 2 out of range for 2 Trade::MeshAttribute::Color attributes\n" "Trade::MeshData::attributeOffset(): index 0 out of range for 0 Trade::MeshAttribute::Position attributes\n" @@ -1355,6 +1462,8 @@ void MeshDataTest::attributeNotFound() { "Trade::MeshData::attributeStride(): index 2 out of range for 2 Trade::MeshAttribute::Color attributes\n" "Trade::MeshData::attribute(): index 0 out of range for 0 Trade::MeshAttribute::Position attributes\n" "Trade::MeshData::attribute(): index 2 out of range for 2 Trade::MeshAttribute::Color attributes\n" + "Trade::MeshData::attribute(): index 0 out of range for 0 Trade::MeshAttribute::Position attributes\n" + "Trade::MeshData::attribute(): index 2 out of range for 2 Trade::MeshAttribute::Color attributes\n" "Trade::MeshData::positions2DInto(): index 0 out of range for 0 position attributes\n" "Trade::MeshData::positions3DInto(): index 0 out of range for 0 position attributes\n" "Trade::MeshData::normalsInto(): index 0 out of range for 0 normal attributes\n"