diff --git a/doc/snippets/MagnumTrade.cpp b/doc/snippets/MagnumTrade.cpp index 5146bc33f..43eaad7d6 100644 --- a/doc/snippets/MagnumTrade.cpp +++ b/doc/snippets/MagnumTrade.cpp @@ -887,6 +887,22 @@ static_cast(triangleIds); static_cast(triangleCounts); } +{ +/* [MeshData-jointIdsAsArray] */ +Trade::MeshData data = DOXYGEN_ELLIPSIS(Trade::MeshData{{}, 0}); + +Containers::Array array = data.jointIdsAsArray(); +Containers::StridedArrayView2D array2D{array, + {data.vertexCount(), data.attributeArraySize(Trade::MeshAttribute::JointIds)}}; + +for(Containers::StridedArrayView1D i: array2D) { + for(UnsignedInt j: i) { + DOXYGEN_IGNORE(static_cast(j);)// do something with joint ID j in vertex i + } +} +/* [MeshData-jointIdsAsArray] */ +} + #ifdef MAGNUM_BUILD_DEPRECATED { CORRADE_IGNORE_DEPRECATED_PUSH diff --git a/src/Magnum/Trade/MeshData.cpp b/src/Magnum/Trade/MeshData.cpp index 49dbd7ab9..15d762a57 100644 --- a/src/Magnum/Trade/MeshData.cpp +++ b/src/Magnum/Trade/MeshData.cpp @@ -193,6 +193,18 @@ MeshData::MeshData(const MeshPrimitive primitive, Containers::Array&& inde } } } + + /* Verify that count and array sizes of skin joint IDs and weights match */ + const UnsignedInt jointIdAttributeCount = attributeCount(MeshAttribute::JointIds); + const UnsignedInt weightAttributeCount = attributeCount(MeshAttribute::Weights); + CORRADE_ASSERT(weightAttributeCount == jointIdAttributeCount, + "Trade::MeshData: expected" << jointIdAttributeCount << "weight attributes to match joint IDs but got" << weightAttributeCount, ); + for(UnsignedInt i = 0; i != jointIdAttributeCount; ++i) { + const UnsignedInt jointIdsArraySize = attributeArraySize(MeshAttribute::JointIds, i); + const UnsignedInt weightsArraySize = attributeArraySize(MeshAttribute::Weights, i); + CORRADE_ASSERT(weightsArraySize == jointIdsArraySize, + "Trade::MeshData: expected" << jointIdsArraySize << "array items for weight attribute" << i << "to match joint IDs but got" << weightsArraySize, ); + } #endif } @@ -835,6 +847,77 @@ Containers::Array MeshData::colorsAsArray(const UnsignedInt id) const { return out; } +void MeshData::jointIdsInto(const Containers::StridedArrayView2D& destination, const UnsignedInt id) const { + const UnsignedInt attributeId = findAttributeIdInternal(MeshAttribute::JointIds, id); + CORRADE_ASSERT(attributeId != ~UnsignedInt{}, + "Trade::MeshData::jointIdsInto(): index" << id << "out of range for" << attributeCount(MeshAttribute::JointIds) << "joint ID attributes", ); + const MeshAttributeData& attribute = _attributes[attributeId]; + #ifndef CORRADE_NO_ASSERT + const Containers::Size2D expectedSize{_vertexCount, attribute._arraySize}; + #endif + CORRADE_ASSERT(destination.size() == expectedSize, + "Trade::MeshData::jointIdsInto(): expected a view with" << expectedSize << "elements but got" << destination.size(), ); + CORRADE_ASSERT(destination.isContiguous<1>(), + "Trade::MeshData::jointIdsInto(): second view dimension is not contiguous", ); + CORRADE_ASSERT(!isVertexFormatImplementationSpecific(attribute._format), + "Trade::MeshData::jointIdsInto(): can't extract data out of an implementation-specific vertex format" << reinterpret_cast(vertexFormatUnwrap(attribute._format)), ); + const Containers::StridedArrayView1D attributeData = attributeDataViewInternal(attribute); + + if(attribute._format == VertexFormat::UnsignedInt) + Utility::copy(Containers::arrayCast<2, const UnsignedInt>(attributeData, attribute._arraySize), destination); + else if(attribute._format == VertexFormat::UnsignedByte) + Math::castInto(Containers::arrayCast<2, const UnsignedByte>(attributeData, attribute._arraySize), destination); + else if(attribute._format == VertexFormat::UnsignedShort) + Math::castInto(Containers::arrayCast<2, const UnsignedShort>(attributeData, attribute._arraySize), destination); + else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +Containers::Array MeshData::jointIdsAsArray(const UnsignedInt id) const { + const UnsignedInt attributeId = findAttributeIdInternal(MeshAttribute::JointIds, id); + CORRADE_ASSERT(attributeId != ~UnsignedInt{}, + "Trade::MeshData::jointIdsAsArray(): index" << id << "out of range for" << attributeCount(MeshAttribute::JointIds) << "joint ID attributes", {}); + const MeshAttributeData& attribute = _attributes[attributeId]; + Containers::Array out{_vertexCount*attribute._arraySize}; + jointIdsInto(Containers::StridedArrayView2D{out, {_vertexCount, attribute._arraySize}}, id); + return out; +} + +void MeshData::weightsInto(const Containers::StridedArrayView2D& destination, const UnsignedInt id) const { + const UnsignedInt attributeId = findAttributeIdInternal(MeshAttribute::Weights, id); + CORRADE_ASSERT(attributeId != ~UnsignedInt{}, + "Trade::MeshData::weightsInto(): index" << id << "out of range for" << attributeCount(MeshAttribute::Weights) << "weight attributes", ); + const MeshAttributeData& attribute = _attributes[attributeId]; + #ifndef CORRADE_NO_ASSERT + const Containers::Size2D expectedSize{_vertexCount, attribute._arraySize}; + #endif + CORRADE_ASSERT(destination.size() == expectedSize, + "Trade::MeshData::weightsInto(): expected a view with" << expectedSize << "elements but got" << destination.size(), ); + CORRADE_ASSERT(destination.isContiguous<1>(), + "Trade::MeshData::weightsInto(): second view dimension is not contiguous", ); + CORRADE_ASSERT(!isVertexFormatImplementationSpecific(attribute._format), + "Trade::MeshData::weightsInto(): can't extract data out of an implementation-specific vertex format" << reinterpret_cast(vertexFormatUnwrap(attribute._format)), ); + const Containers::StridedArrayView1D attributeData = attributeDataViewInternal(attribute); + + if(attribute._format == VertexFormat::Float) + Utility::copy(Containers::arrayCast<2, const Float>(attributeData, attribute._arraySize), destination); + else if(attribute._format == VertexFormat::Half) + Math::unpackHalfInto(Containers::arrayCast<2, const UnsignedShort>(attributeData, attribute._arraySize), destination); + else if(attribute._format == VertexFormat::UnsignedByteNormalized) + Math::unpackInto(Containers::arrayCast<2, const UnsignedByte>(attributeData, attribute._arraySize), destination); + else if(attribute._format == VertexFormat::UnsignedShortNormalized) + Math::unpackInto(Containers::arrayCast<2, const UnsignedShort>(attributeData, attribute._arraySize), destination); + else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +Containers::Array MeshData::weightsAsArray(const UnsignedInt id) const { + const UnsignedInt attributeId = findAttributeIdInternal(MeshAttribute::Weights, id); + CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::weightsAsArray(): index" << id << "out of range for" << attributeCount(MeshAttribute::JointIds) << "weight attributes", {}); + const MeshAttributeData& attribute = _attributes[attributeId]; + Containers::Array out{_vertexCount*attribute._arraySize}; + weightsInto(Containers::StridedArrayView2D{out, {_vertexCount, attribute._arraySize}}, id); + return out; +} + void MeshData::objectIdsInto(const Containers::StridedArrayView1D& destination, const UnsignedInt id) const { const UnsignedInt attributeId = findAttributeIdInternal(MeshAttribute::ObjectId, id); CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::objectIdsInto(): index" << id << "out of range for" << attributeCount(MeshAttribute::ObjectId) << "object ID attributes", ); @@ -896,6 +979,8 @@ Debug& operator<<(Debug& debug, const MeshAttribute value) { _c(Normal) _c(TextureCoordinates) _c(Color) + _c(JointIds) + _c(Weights) _c(ObjectId) #undef _c /* LCOV_EXCL_STOP */ diff --git a/src/Magnum/Trade/MeshData.h b/src/Magnum/Trade/MeshData.h index f6bb54bbc..9ce4a7dbe 100644 --- a/src/Magnum/Trade/MeshData.h +++ b/src/Magnum/Trade/MeshData.h @@ -151,6 +151,47 @@ enum class MeshAttribute: UnsignedShort { */ Color, + /** + * Skin joint IDs. Array attribute, type is usually + * @ref VertexFormat::UnsignedInt, but can be also + * @ref VertexFormat::UnsignedShort or + * @ref VertexFormat::UnsignedByte. Array size is not limited or enforced + * in any way, but shaders usually expect at most 8 items, or alternatively + * two instances of this attribute with at most 4 items in each. + * + * Count of instances of this attribute and array size of each instance is + * expected to match instance count and array sizes of + * @ref MeshAttribute::Weights. + * + * Corresponds to @ref Shaders::GenericGL::JointIds and + * @ref Shaders::GenericGL::SecondaryJointIds, divided between them based + * on array size or attribute count. + * @m_since_latest + * @see @ref MeshData::jointIdsAsArray() + */ + JointIds, + + /** + * Skin weights. Array attribute, type is usually @ref VertexFormat::Float, + * but can be also @ref VertexFormat::Half, + * @ref VertexFormat::UnsignedByteNormalized or + * @ref VertexFormat::UnsignedShortNormalized. Array size is not limited or + * enforced in any way, but shaders usually expect at most 8 items, or + * alternatively two instances of this attribute with at most 4 items in + * each. + * + * Count of instances of this attribute and array size of each instance is + * expected to match instance count and array sizes of + * @ref MeshAttribute::JointIds. + * + * Corresponds to @ref Shaders::GenericGL::Weights and + * @ref Shaders::GenericGL::SecondaryWeights, divided between them based + * on array size or attribute count. + * @m_since_latest + * @see @ref MeshData::weightsAsArray() + */ + Weights, + /** * (Instanced) object ID for editor selection or scene annotation. Type is * usually @ref VertexFormat::UnsignedInt, but can be also @@ -406,10 +447,11 @@ class MAGNUM_TRADE_EXPORT MeshAttributeData { * @param arraySize Array size. Use @cpp 0 @ce for non-array * attributes. * - * Expects that @p data stride fits into a signed 16-bit value, - * @p format corresponds to @p name and @p arraySize is zero for - * builtin attributes. The stride can be zero or negative, but note - * that such data layouts are not commonly supported by GPU APIs. + * Expects that @p data stride fits into a signed 16-bit value, and for + * builtin attributes that @p format corresponds to @p name, + * @p arraySize is either zero for non-array attributes or non-zero for + * array attributes. The stride can be zero or negative, but note that + * such data layouts are not commonly supported by GPU APIs. */ explicit MeshAttributeData(MeshAttribute name, VertexFormat format, const Containers::StridedArrayView1D& data, UnsignedShort arraySize = 0) noexcept; @@ -422,8 +464,9 @@ class MAGNUM_TRADE_EXPORT MeshAttributeData { * attributes. * * Expects that the second dimension of @p data is contiguous and its - * size matches @p format and @p arraySize, that @p format corresponds - * to @p name and @p arraySize is zero for builtin attributes. The + * size matches @p format and @p arraySize, and for builtin attributes + * that @p format corresponds to @p name and @p arraySize is either + * zero for non-array attributes or non-zero for array attributes. The * stride is expected to fit into a signed 16-bit value. It can be zero * or negative, but note that such data layouts are not commonly * supported by GPU APIs. @@ -483,9 +526,8 @@ class MAGNUM_TRADE_EXPORT MeshAttributeData { * Detects @ref VertexFormat based on @p T and calls * @ref MeshAttributeData(MeshAttribute, VertexFormat, const Containers::StridedArrayView1D&, UnsignedShort) * with the second dimension size passed to @p arraySize. Expects that - * the second dimension is contiguous. At the moment only custom - * attributes can be arrays, which means this function can't be used - * with a builtin @p name. See @ref MeshAttributeData(MeshAttribute, const Containers::StridedArrayView1D&) + * the second dimension is contiguous, and if @p name is a builtin + * attribute, it's an array attribute. See @ref MeshAttributeData(MeshAttribute, const Containers::StridedArrayView1D&) * for details about @ref VertexFormat detection. */ template constexpr explicit MeshAttributeData(MeshAttribute name, const Containers::StridedArrayView2D& data) noexcept; @@ -506,10 +548,11 @@ class MAGNUM_TRADE_EXPORT MeshAttributeData { * attribute construction time. Note that instances created this way * can't be used in most @ref MeshTools algorithms. * - * Expects that @p arraySize is zero for builtin attributes and - * @p stride fits into a signed 16-bit value. The stride can be zero or - * negative, but note that such data layouts are not commonly supported - * by GPU APIs. + * For builtin attributes expects that @p arraySize is zero for + * non-array attributes and non-zero for array attributes. The + * @p stride is expected to fit into a signed 16-bit value. It can be + * zero or negative, but note that such data layouts are not commonly + * supported by GPU APIs. * * Additionally, for even more flexibility, the @p vertexCount can be * overridden at @ref MeshData construction time, however all attributes @@ -686,13 +729,13 @@ The second simplest usage is accessing attributes through the convenience functions @ref positions2DAsArray(), @ref positions3DAsArray(), @ref tangentsAsArray(), @ref bitangentsAsArray(), @ref normalsAsArray(), @ref tangentsAsArray(), @ref textureCoordinates2DAsArray(), -@ref colorsAsArray() and @ref objectIdsAsArray(). You're expected to check for -attribute presence first with either @ref hasAttribute() (or -@ref attributeCount(MeshAttribute) const, as there can be multiple sets of -texture coordinates, for example). If you are creating a @ref GL::Mesh, the -usual path forward is then to @ref MeshTools::interleave() attributes of -interest, upload them to a @ref GL::Buffer and configure attribute binding for -the mesh. +@ref colorsAsArray(), @ref jointIdsAsArray(), @ref weightsAsArray() and +@ref objectIdsAsArray(). You're expected to check for attribute presence first +with either @ref hasAttribute() (or @ref attributeCount(MeshAttribute) const, +as there can be multiple sets of texture coordinates, for example). If you are +creating a @ref GL::Mesh, the usual path forward is then to +@ref MeshTools::interleave() attributes of interest, upload them to a +@ref GL::Buffer and configure attribute binding for the mesh. The mesh can be also indexed, in which case the index buffer is exposed through @ref indicesAsArray(). @@ -848,7 +891,9 @@ class MAGNUM_TRADE_EXPORT MeshData { * * The @p indices are expected to point to a sub-range of @p indexData. * The @p attributes are expected to reference (sparse) sub-ranges of - * @p vertexData. If the mesh has no attributes, the @p indices are + * @p vertexData. Particular attributes can have additional + * restrictions, see documentation of @ref MeshAttribute values for + * more information. If the mesh has no attributes, the @p indices are * expected to be valid (but can be empty). If you want to create an * attribute-less non-indexed mesh, use * @ref MeshData(MeshPrimitive, UnsignedInt, const void*) to specify @@ -1378,11 +1423,9 @@ class MAGNUM_TRADE_EXPORT MeshData { * * In case given attribute is an array (the equivalent of e.g. * @cpp int[30] @ce), returns array size, otherwise returns @cpp 0 @ce. - * At the moment only custom attributes can be arrays, no builtin - * @ref MeshAttribute is an array attribute. The @p id is expected to - * be smaller than @ref attributeCount() const. You can also use - * @ref attributeArraySize(MeshAttribute, UnsignedInt) const to - * directly get array size of given named attribute. + * The @p id is expected to be smaller than @ref attributeCount() const. + * You can also use @ref attributeArraySize(MeshAttribute, UnsignedInt) const + * to directly get array size of given named attribute. * * Note that this is different from vertex count, which is exposed * through @ref vertexCount(), and is an orthogonal concept to having @@ -1520,7 +1563,8 @@ class MAGNUM_TRADE_EXPORT MeshData { * @ref positions2DAsArray(), @ref positions3DAsArray(), * @ref tangentsAsArray(), @ref bitangentSignsAsArray(), * @ref bitangentsAsArray(), @ref normalsAsArray(), - * @ref textureCoordinates2DAsArray(), @ref colorsAsArray() and + * @ref textureCoordinates2DAsArray(), @ref colorsAsArray(), + * @ref jointIdsAsArray(), @ref weightsAsArray() and * @ref objectIdsAsArray() accessors to get common attributes converted * to usual types in contiguous arrays, but note that these operations * involve extra allocation and data conversion. @@ -1602,7 +1646,8 @@ class MAGNUM_TRADE_EXPORT MeshData { * @ref positions2DAsArray(), @ref positions3DAsArray(), * @ref tangentsAsArray(), @ref bitangentSignsAsArray(), * @ref bitangentsAsArray(), @ref normalsAsArray(), - * @ref textureCoordinates2DAsArray(), @ref colorsAsArray() and + * @ref textureCoordinates2DAsArray(), @ref colorsAsArray(), + * @ref jointIdsAsArray(), @ref weightsAsArray() and * @ref objectIdsAsArray() accessors to get common attributes converted * to usual types in contiguous arrays, but note that these operations * involve extra data conversion and an allocation. @@ -1873,6 +1918,84 @@ class MAGNUM_TRADE_EXPORT MeshData { */ void colorsInto(const Containers::StridedArrayView1D& destination, UnsignedInt id = 0) const; + /** + * @brief Skin joint IDs as unsigned int arrays + * @m_since_latest + * + * Convenience alternative to @ref attribute(MeshAttribute, UnsignedInt) const + * with @ref MeshAttribute::JointIds as the first argument. Converts + * the joint IDs array from an arbitrary underlying type and returns it + * in a newly-allocated array. Expects that the vertex format is *not* + * implementation-specific, in that case you can only access the + * attribute via the typeless @ref attribute(MeshAttribute, UnsignedInt) const. + * + * As it's an array attribute, the returned array has @ref vertexCount() + * times @ref attributeArraySize(MeshAttribute, UnsignedInt) const + * elements. You can make a 2D view onto the result to conveniently + * index the data: + * + * @snippet MagnumTrade.cpp MeshData-jointIdsAsArray + * + * @see @ref weightsAsArray(), @ref jointIdsInto(), + * @ref attributeFormat(), + * @ref isVertexFormatImplementationSpecific() + */ + /* Originally I tried with jointIdsWeightsAsArray() because these two + attributes are usually needed together, and they are also checked to + have matching counts and array sizes on construction. However, it'd + mean the two arrays would have to be interleaved for each vertex + (Array>) which makes them no longer + directly usable for mesh attributes, and there are other hurdles + such as unpackInto() / castInto() expecting the second dimension to + be contiguous. */ + Containers::Array jointIdsAsArray(UnsignedInt id = 0) const; + + /** + * @brief Skin joint IDs as unsigned int arrays into a pre-allocated view + * @m_since_latest + * + * Like @ref jointIdsAsArray(), but puts the result into @p destination + * instead of allocating a new array. Expects that @p destination is + * sized to contain exactly all data --- first dimension being + * @ref vertexCount() and second + * @ref attributeArraySize(MeshAttribute. UnsignedInt) const. The + * second dimension is additionally expected to be contiguous. + */ + void jointIdsInto(const Containers::StridedArrayView2D& destination, UnsignedInt id = 0) const; + + /** + * @brief Skin weights as float arrays + * @m_since_latest + * + * Convenience alternative to @ref attribute(MeshAttribute, UnsignedInt) const + * with @ref MeshAttribute::Weights as the first argument. Converts the + * weights array from an arbitrary underlying types and returns them in + * a newly-allocated array. Expects that the vertex format is *not* + * implementation-specific, in that case you can only access the + * attribute via the typeless @ref attribute(MeshAttribute, UnsignedInt) const. + * + * As it's an array attribute, the returned array has @ref vertexCount() + * times @ref attributeArraySize(MeshAttribute, UnsignedInt) const + * elements. You can make a 2D view onto the result to conveniently + * index the data, see @ref jointIdsAsArray() for an example snippet. + * @see @ref weightsInto(), @ref attributeFormat(), + * @ref isVertexFormatImplementationSpecific() + */ + Containers::Array weightsAsArray(UnsignedInt id = 0) const; + + /** + * @brief Skin weights as float arrays into a pre-allocated view + * @m_since_latest + * + * Like @ref weightsAsArray(), but puts the result into @p destination + * instead of allocating a new array. Expects that @p destination is + * sized to contain exactly all data --- first dimension being + * @ref vertexCount() and second + * @ref attributeArraySize(MeshAttribute. UnsignedInt) const. The + * second dimension is additionally expected to be contiguous. + */ + void weightsInto(const Containers::StridedArrayView2D& weightsDestination, UnsignedInt id = 0) const; + /** * @brief Object IDs as 32-bit integers * @@ -2265,6 +2388,15 @@ namespace Implementation { format == VertexFormat::Vector2usNormalized || format == VertexFormat::Vector2s || format == VertexFormat::Vector2sNormalized)) || + (name == MeshAttribute::JointIds && + (format == VertexFormat::UnsignedInt || + format == VertexFormat::UnsignedByte || + format == VertexFormat::UnsignedShort)) || + (name == MeshAttribute::Weights && + (format == VertexFormat::Float || + format == VertexFormat::Half || + format == VertexFormat::UnsignedByteNormalized || + format == VertexFormat::UnsignedShortNormalized)) || (name == MeshAttribute::ObjectId && (format == VertexFormat::UnsignedInt || format == VertexFormat::UnsignedShort || @@ -2274,7 +2406,16 @@ namespace Implementation { } constexpr bool isAttributeArrayAllowed(MeshAttribute name) { - return isMeshAttributeCustom(name); + return + name == MeshAttribute::JointIds || + name == MeshAttribute::Weights || + /* Custom attributes can be anything */ + isMeshAttributeCustom(name); + } + constexpr bool isAttributeArrayExpected(MeshAttribute name) { + /* Custom attributes don't *have to* be arrays */ + return name == MeshAttribute::JointIds || + name == MeshAttribute::Weights; } #endif } @@ -2288,7 +2429,9 @@ constexpr MeshAttributeData::MeshAttributeData(std::nullptr_t, const MeshAttribu "Trade::MeshAttributeData: expected stride to fit into 16 bits but got" << data.stride()), Short(data.stride()))}, _arraySize{(CORRADE_CONSTEXPR_ASSERT(!arraySize || Implementation::isAttributeArrayAllowed(name), - "Trade::MeshAttributeData:" << name << "can't be an array attribute"), arraySize)}, + "Trade::MeshAttributeData:" << name << "can't be an array attribute"), + CORRADE_CONSTEXPR_ASSERT(arraySize || !Implementation::isAttributeArrayExpected(name), "Trade::MeshAttributeData:" << name << "has to be an array attribute"), + arraySize)}, _data{data.data()} {} constexpr MeshAttributeData::MeshAttributeData(const MeshAttribute name, const VertexFormat format, const std::size_t offset, const UnsignedInt vertexCount, const std::ptrdiff_t stride, UnsignedShort arraySize) noexcept: diff --git a/src/Magnum/Trade/Test/MeshDataTest.cpp b/src/Magnum/Trade/Test/MeshDataTest.cpp index 60745274a..2e94167c5 100644 --- a/src/Magnum/Trade/Test/MeshDataTest.cpp +++ b/src/Magnum/Trade/Test/MeshDataTest.cpp @@ -76,6 +76,7 @@ struct MeshDataTest: TestSuite::Tester { void constructAttributeWrongFormat(); void constructAttributeWrongStride(); void constructAttributeWrongDataAccess(); + void constructAttributeOnlyArrayAllowed(); void constructArrayAttribute(); void constructArrayAttributeNonContiguous(); @@ -117,6 +118,8 @@ struct MeshDataTest: TestSuite::Tester { void constructIndicesNotContained(); void constructAttributeNotContained(); void constructInconsitentVertexCount(); + void constructDifferentJointIdWeightCount(); + void constructInconsistentJointIdWeightArraySizes(); void constructNotOwnedIndexFlagOwned(); void constructNotOwnedVertexFlagOwned(); void constructIndicesNotOwnedFlagOwned(); @@ -164,6 +167,11 @@ struct MeshDataTest: TestSuite::Tester { template void colorsAsArray(); template void colorsAsArrayPackedUnsignedNormalized(); void colorsIntoArrayInvalidSize(); + template void jointIdsAsArray(); + void jointIdsIntoArrayInvalidSizeStride(); + template void weightsAsArray(); + template void weightsAsArrayPackedUnsignedNormalized(); + void weightsIntoArrayInvalidSizeStride(); template void objectIdsAsArray(); void objectIdsIntoArrayInvalidSize(); @@ -250,6 +258,7 @@ MeshDataTest::MeshDataTest() { &MeshDataTest::constructAttributeWrongFormat, &MeshDataTest::constructAttributeWrongStride, &MeshDataTest::constructAttributeWrongDataAccess, + &MeshDataTest::constructAttributeOnlyArrayAllowed, &MeshDataTest::constructArrayAttribute, &MeshDataTest::constructArrayAttributeNonContiguous, @@ -294,6 +303,8 @@ MeshDataTest::MeshDataTest() { &MeshDataTest::constructIndicesNotContained, &MeshDataTest::constructAttributeNotContained, &MeshDataTest::constructInconsitentVertexCount, + &MeshDataTest::constructDifferentJointIdWeightCount, + &MeshDataTest::constructInconsistentJointIdWeightArraySizes, &MeshDataTest::constructNotOwnedIndexFlagOwned, &MeshDataTest::constructNotOwnedVertexFlagOwned, &MeshDataTest::constructIndicesNotOwnedFlagOwned, @@ -396,6 +407,15 @@ MeshDataTest::MeshDataTest() { &MeshDataTest::colorsAsArrayPackedUnsignedNormalized, &MeshDataTest::colorsAsArrayPackedUnsignedNormalized, &MeshDataTest::colorsIntoArrayInvalidSize, + &MeshDataTest::jointIdsAsArray, + &MeshDataTest::jointIdsAsArray, + &MeshDataTest::jointIdsAsArray, + &MeshDataTest::jointIdsIntoArrayInvalidSizeStride, + &MeshDataTest::weightsAsArray, + &MeshDataTest::weightsAsArray, + &MeshDataTest::weightsAsArrayPackedUnsignedNormalized, + &MeshDataTest::weightsAsArrayPackedUnsignedNormalized, + &MeshDataTest::weightsIntoArrayInvalidSizeStride, &MeshDataTest::objectIdsAsArray, &MeshDataTest::objectIdsAsArray, &MeshDataTest::objectIdsAsArray, @@ -991,6 +1011,23 @@ void MeshDataTest::constructAttributeWrongStride() { "Trade::MeshAttributeData: expected padding to fit into 16 bits but got -32769\n"); } +void MeshDataTest::constructAttributeOnlyArrayAllowed() { + CORRADE_SKIP_IF_NO_ASSERT(); + + Vector2 data[3]; + + /* These should be fine */ + MeshAttributeData{MeshAttribute::Weights, VertexFormat::Float, data, 2}; + MeshAttributeData{meshAttributeCustom(25), VertexFormat::Vector2, data}; + MeshAttributeData{meshAttributeCustom(25), VertexFormat::Float, data, 2}; + + std::ostringstream out; + Error redirectError{&out}; + MeshAttributeData{MeshAttribute::Weights, VertexFormat::Float, data}; + CORRADE_COMPARE(out.str(), + "Trade::MeshAttributeData: Trade::MeshAttribute::Weights has to be an array attribute\n"); +} + void MeshDataTest::constructAttributeWrongDataAccess() { CORRADE_SKIP_IF_NO_ASSERT(); @@ -2395,6 +2432,59 @@ void MeshDataTest::constructInconsitentVertexCount() { "Trade::MeshData: attribute 1 has 2 vertices but 3 expected\n"); } +void MeshDataTest::constructDifferentJointIdWeightCount() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct Vertex { + /* Weights required to be here by the constructor */ + Float weights[2]; + UnsignedByte jointIds[2]; + UnsignedShort secondaryJointIds[4]; + } vertices[3]{}; + auto view = Containers::stridedArrayView(vertices); + + std::ostringstream out; + Error redirectError{&out}; + MeshData{MeshPrimitive::Points, {}, vertices, { + /* Weights required to be here by the constructor */ + MeshAttributeData{MeshAttribute::Weights, VertexFormat::Float, + view.slice(&Vertex::weights), 2}, + MeshAttributeData{MeshAttribute::JointIds, VertexFormat::UnsignedByte, + view.slice(&Vertex::jointIds), 2}, + MeshAttributeData{MeshAttribute::JointIds, VertexFormat::UnsignedShort, + view.slice(&Vertex::secondaryJointIds), 4} + }}; + CORRADE_COMPARE(out.str(), "Trade::MeshData: expected 2 weight attributes to match joint IDs but got 1\n"); +} + +void MeshDataTest::constructInconsistentJointIdWeightArraySizes() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct Vertex { + /* Weights required to be here by the constructor */ + Float weights[2]; + UnsignedByte jointIds[2]; + Half secondaryWeights[3]; + UnsignedShort secondaryJointIds[4]; + } vertices[3]{}; + auto view = Containers::stridedArrayView(vertices); + + std::ostringstream out; + Error redirectError{&out}; + MeshData{MeshPrimitive::Points, {}, vertices, { + /* Weights required to be here by the constructor */ + MeshAttributeData{MeshAttribute::Weights, VertexFormat::Float, + view.slice(&Vertex::weights), 2}, + MeshAttributeData{MeshAttribute::JointIds, VertexFormat::UnsignedByte, + view.slice(&Vertex::jointIds), 2}, + MeshAttributeData{MeshAttribute::Weights, VertexFormat::Half, + view.slice(&Vertex::secondaryWeights), 3}, + MeshAttributeData{MeshAttribute::JointIds, VertexFormat::UnsignedShort, + view.slice(&Vertex::secondaryJointIds), 4} + }}; + CORRADE_COMPARE(out.str(), "Trade::MeshData: expected 4 array items for weight attribute 1 to match joint IDs but got 3\n"); +} + void MeshDataTest::constructNotOwnedIndexFlagOwned() { CORRADE_SKIP_IF_NO_ASSERT(); @@ -2586,6 +2676,7 @@ template struct NameTraits; #define _c(format) template<> struct NameTraits { \ static const char* name() { return #format; } \ }; +/* Scalars are in Math::TypeTraits already */ _c(Vector2) _c(Vector2h) _c(Vector2ub) @@ -3322,6 +3413,192 @@ void MeshDataTest::colorsIntoArrayInvalidSize() { "Trade::MeshData::colorsInto(): expected a view with 3 elements but got 2\n"); } +template void MeshDataTest::jointIdsAsArray() { + setTestCaseTemplateName(Math::TypeTraits::name()); + + /* Testing also that it picks the correct attribute. Needs to be + sufficiently representable to have the test work also for half + floats. */ + struct Vertex { + /* Weights required to be here by the constructor */ + Float otherWeights[3]; + Float weights[5]; + UnsignedInt otherJointIds[3]; + UnsignedShort objectId; + T jointIds[5]; + } vertices[]{ + {{}, {}, {}, + 0, + {T(0), T(3), T(20), T(1), T(7)}}, + {{}, {}, {}, + 0, + {T(9), T(1), T(15), T(2), T(3)}}, + {{}, {}, {}, + 0, + {T(25), T(7), T(0), T(2), T(1)}}, + }; + auto view = Containers::stridedArrayView(vertices); + + MeshData data{MeshPrimitive::Points, {}, vertices, { + /* Weights required to be here by the constructor */ + MeshAttributeData{MeshAttribute::Weights, VertexFormat::Float, + view.slice(&Vertex::otherWeights), 3}, + MeshAttributeData{MeshAttribute::Weights, VertexFormat::Float, + view.slice(&Vertex::weights), 5}, + MeshAttributeData{MeshAttribute::JointIds, + Implementation::vertexFormatFor(), + view.slice(&Vertex::otherJointIds), 3}, + MeshAttributeData{MeshAttribute::ObjectId, view.slice(&Vertex::objectId)}, + MeshAttributeData{MeshAttribute::JointIds, + Implementation::vertexFormatFor(), + view.slice(&Vertex::jointIds), 5}, + }}; + CORRADE_COMPARE_AS(data.jointIdsAsArray(1), (Containers::arrayView({ + 0, 3, 20, 1, 7, + 9, 1, 15, 2, 3, + 25, 7, 0, 2, 1 + })), TestSuite::Compare::Container); +} + +void MeshDataTest::jointIdsIntoArrayInvalidSizeStride() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct Vertex { + /* Weights required to be here by the constructor */ + Float weights[2]; + UnsignedByte jointIds[2]; + } vertices[3]{}; + auto view = Containers::stridedArrayView(vertices); + + MeshData data{MeshPrimitive::Points, {}, vertices, { + /* Weights required to be here by the constructor */ + MeshAttributeData{MeshAttribute::Weights, VertexFormat::Float, + view.slice(&Vertex::weights), 2}, + MeshAttributeData{MeshAttribute::JointIds, VertexFormat::UnsignedByte, + view.slice(&Vertex::jointIds), 2} + }}; + + std::ostringstream out; + Error redirectError{&out}; + UnsignedInt jointIds1[3*3]; + UnsignedInt jointIds2[2*2]; + UnsignedInt jointIds3[3*4]; + data.jointIdsInto(Containers::StridedArrayView2D{jointIds1, {3, 3}}); + data.jointIdsInto(Containers::StridedArrayView2D{jointIds2, {2, 2}}); + data.jointIdsInto(Containers::StridedArrayView2D{jointIds3, {3, 4}}.every({1, 2})); + CORRADE_COMPARE(out.str(), + "Trade::MeshData::jointIdsInto(): expected a view with {3, 2} elements but got {3, 3}\n" + "Trade::MeshData::jointIdsInto(): expected a view with {3, 2} elements but got {2, 2}\n" + "Trade::MeshData::jointIdsInto(): second view dimension is not contiguous\n"); +} + +template void MeshDataTest::weightsAsArray() { + setTestCaseTemplateName(Math::TypeTraits::name()); + + /* Testing also that it picks the correct attribute. Needs to be + sufficiently representable to have the test work also for half + floats. */ + struct Vertex { + /* Joint IDs required to be here by the constructor */ + UnsignedInt otherJointIds[3]; + UnsignedInt jointIds[5]; + Float otherWeights[3]; + UnsignedShort objectId; + T weights[5]; + } vertices[]{ + {{}, {}, {}, + 0, + {T(2.0f), T(1.0f), T(0.75f), T(3.0f), T(1.75f)}}, + {{}, {}, {}, + 0, + {T(0.0f), T(-1.0f), T(1.25f), T(1.0f), T(2.25f)}}, + {{}, {}, {}, + 0, + {T(-2.0f), T(3.0f), T(2.5f), T(2.5f), T(0.25f)}}, + }; + auto view = Containers::stridedArrayView(vertices); + + MeshData data{MeshPrimitive::Points, {}, vertices, { + /* Joint IDs required to be here by the constructor */ + MeshAttributeData{MeshAttribute::JointIds, VertexFormat::UnsignedInt, + view.slice(&Vertex::otherJointIds), 3}, + MeshAttributeData{MeshAttribute::JointIds, VertexFormat::UnsignedInt, + view.slice(&Vertex::jointIds), 5}, + MeshAttributeData{MeshAttribute::Weights, + Implementation::vertexFormatFor(), + view.slice(&Vertex::otherWeights), 3}, + MeshAttributeData{MeshAttribute::ObjectId, view.slice(&Vertex::objectId)}, + MeshAttributeData{MeshAttribute::Weights, + Implementation::vertexFormatFor(), + view.slice(&Vertex::weights), 5} + }}; + CORRADE_COMPARE_AS(data.weightsAsArray(1), (Containers::arrayView({ + 2.0f, 1.0f, 0.75f, 3.0f, 1.75f, + 0.0f, -1.0f, 1.25f, 1.0f, 2.25f, + -2.0f, 3.0f, 2.5f, 2.5f, 0.25f, + })), TestSuite::Compare::Container); +} + +template void MeshDataTest::weightsAsArrayPackedUnsignedNormalized() { + setTestCaseTemplateName(Math::TypeTraits::name()); + + struct Vertex { + /* Joint IDs required to be here by the constructor */ + UnsignedByte jointIds[2]; + T weights[2]; + } vertices[2]{ + {{}, {Math::pack(1.0f), Math::pack(0.8f)}}, + {{}, {0, Math::pack(0.4f)}} + }; + auto view = Containers::stridedArrayView(vertices); + + MeshData data{MeshPrimitive::Points, {}, vertices, { + /* Joint IDs required to be here by the constructor */ + MeshAttributeData{MeshAttribute::JointIds, VertexFormat::UnsignedByte, + view.slice(&Vertex::jointIds), 2}, + MeshAttributeData{MeshAttribute::Weights, + vertexFormat(Implementation::vertexFormatFor(), 1, true), + view.slice(&Vertex::weights), 2} + }}; + + CORRADE_COMPARE_AS(data.weightsAsArray(), (Containers::arrayView({ + 1.0f, 0.8f, + 0.0f, 0.4f + })), TestSuite::Compare::Container); +} + +void MeshDataTest::weightsIntoArrayInvalidSizeStride() { + CORRADE_SKIP_IF_NO_ASSERT(); + + struct Vertex { + /* Joint IDs required to be here by the constructor */ + UnsignedInt jointIds[2]; + Half weights[2]; + } vertices[3]{}; + auto view = Containers::stridedArrayView(vertices); + + MeshData data{MeshPrimitive::Points, {}, vertices, { + /* Joint IDs required to be here by the constructor */ + MeshAttributeData{MeshAttribute::JointIds, VertexFormat::UnsignedInt, + view.slice(&Vertex::jointIds), 2}, + MeshAttributeData{MeshAttribute::Weights, VertexFormat::Half, + view.slice(&Vertex::weights), 2} + }}; + + std::ostringstream out; + Error redirectError{&out}; + Float weights1[3*3]; + Float weights2[2*2]; + Float weights3[3*4]; + data.weightsInto(Containers::StridedArrayView2D{weights1, {3, 3}}); + data.weightsInto(Containers::StridedArrayView2D{weights2, {2, 2}}); + data.weightsInto(Containers::StridedArrayView2D{weights3, {3, 4}}.every({1, 2})); + CORRADE_COMPARE(out.str(), + "Trade::MeshData::weightsInto(): expected a view with {3, 2} elements but got {3, 3}\n" + "Trade::MeshData::weightsInto(): expected a view with {3, 2} elements but got {2, 2}\n" + "Trade::MeshData::weightsInto(): second view dimension is not contiguous\n"); +} + template void MeshDataTest::objectIdsAsArray() { setTestCaseTemplateName(Math::TypeTraits::name()); @@ -3404,8 +3681,12 @@ void MeshDataTest::implementationSpecificVertexFormatWrongAccess() { vertexFormatWrap(0xdead5), attribute}, MeshAttributeData{MeshAttribute::Color, vertexFormatWrap(0xdead6), attribute}, + MeshAttributeData{MeshAttribute::JointIds, + vertexFormatWrap(0xdead7), attribute, 2}, + MeshAttributeData{MeshAttribute::Weights, + vertexFormatWrap(0xdead8), attribute, 2}, MeshAttributeData{MeshAttribute::ObjectId, - vertexFormatWrap(0xdead7), attribute}}}; + vertexFormatWrap(0xdead9), attribute}}}; std::ostringstream out; Error redirectError{&out}; @@ -3425,6 +3706,8 @@ void MeshDataTest::implementationSpecificVertexFormatWrongAccess() { data.normalsAsArray(); data.textureCoordinates2DAsArray(); data.colorsAsArray(); + data.jointIdsAsArray(); + data.weightsAsArray(); data.objectIdsAsArray(); CORRADE_COMPARE(out.str(), "Trade::MeshData::attribute(): can't cast data from an implementation-specific vertex format 0xdead1\n" @@ -3443,7 +3726,9 @@ void MeshDataTest::implementationSpecificVertexFormatWrongAccess() { "Trade::MeshData::normalsInto(): can't extract data out of an implementation-specific vertex format 0xdead4\n" "Trade::MeshData::textureCoordinatesInto(): can't extract data out of an implementation-specific vertex format 0xdead5\n" "Trade::MeshData::colorsInto(): can't extract data out of an implementation-specific vertex format 0xdead6\n" - "Trade::MeshData::objectIdsInto(): can't extract data out of an implementation-specific vertex format 0xdead7\n"); + "Trade::MeshData::jointIdsInto(): can't extract data out of an implementation-specific vertex format 0xdead7\n" + "Trade::MeshData::weightsInto(): can't extract data out of an implementation-specific vertex format 0xdead8\n" + "Trade::MeshData::objectIdsInto(): can't extract data out of an implementation-specific vertex format 0xdead9\n"); } void MeshDataTest::mutableAccessNotAllowed() { @@ -3530,9 +3815,14 @@ void MeshDataTest::indicesWrongType() { void MeshDataTest::attributeNotFound() { CORRADE_SKIP_IF_NO_ASSERT(); - MeshAttributeData colors1{MeshAttribute::Color, VertexFormat::Vector3, nullptr}; - MeshAttributeData colors2{MeshAttribute::Color, VertexFormat::Vector4, nullptr}; - MeshData data{MeshPrimitive::Points, nullptr, {colors1, colors2}}; + MeshData data{MeshPrimitive::Points, nullptr, { + MeshAttributeData{MeshAttribute::Color, VertexFormat::Vector3, nullptr}, + MeshAttributeData{MeshAttribute::Weights, VertexFormat::UnsignedByteNormalized, nullptr, 3}, + MeshAttributeData{MeshAttribute::JointIds, VertexFormat::UnsignedByte, nullptr, 3}, + MeshAttributeData{MeshAttribute::Color, VertexFormat::Vector4, nullptr}, + MeshAttributeData{MeshAttribute::Weights, VertexFormat::Float, nullptr, 6}, + MeshAttributeData{MeshAttribute::JointIds, VertexFormat::UnsignedShort, nullptr, 6} + }}; /* This is fine */ CORRADE_COMPARE(data.attributeCount(MeshAttribute::Position), 0); @@ -3541,19 +3831,19 @@ void MeshDataTest::attributeNotFound() { std::ostringstream out; Error redirectError{&out}; - data.attributeData(2); - data.attributeName(2); - data.attributeId(2); - data.attributeFormat(2); - data.attributeOffset(2); - data.attributeStride(2); - data.attributeArraySize(2); - data.attribute(2); - data.attribute(2); - data.attribute(2); - data.mutableAttribute(2); - data.mutableAttribute(2); - data.mutableAttribute(2); + data.attributeData(6); + data.attributeName(6); + data.attributeId(6); + data.attributeFormat(6); + data.attributeOffset(6); + data.attributeStride(6); + data.attributeArraySize(6); + data.attribute(6); + data.attribute(6); + data.attribute(6); + data.mutableAttribute(6); + data.mutableAttribute(6); + data.mutableAttribute(6); data.attributeId(MeshAttribute::Position); data.attributeId(MeshAttribute::Color, 2); @@ -3586,21 +3876,27 @@ void MeshDataTest::attributeNotFound() { data.normalsAsArray(); data.textureCoordinates2DAsArray(); data.colorsAsArray(2); + /* jointIdsAsArray() and weightsAsArray() have their own assert in order to + fetch array size, have to test also Into() for these */ + data.jointIdsAsArray(2); + data.jointIdsInto(nullptr, 2); + data.weightsAsArray(2); + data.weightsInto(nullptr, 2); data.objectIdsAsArray(); CORRADE_COMPARE(out.str(), - "Trade::MeshData::attributeData(): index 2 out of range for 2 attributes\n" - "Trade::MeshData::attributeName(): index 2 out of range for 2 attributes\n" - "Trade::MeshData::attributeId(): index 2 out of range for 2 attributes\n" - "Trade::MeshData::attributeFormat(): index 2 out of range for 2 attributes\n" - "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::attributeArraySize(): 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::attribute(): index 2 out of range for 2 attributes\n" - "Trade::MeshData::mutableAttribute(): index 2 out of range for 2 attributes\n" - "Trade::MeshData::mutableAttribute(): index 2 out of range for 2 attributes\n" - "Trade::MeshData::mutableAttribute(): index 2 out of range for 2 attributes\n" + "Trade::MeshData::attributeData(): index 6 out of range for 6 attributes\n" + "Trade::MeshData::attributeName(): index 6 out of range for 6 attributes\n" + "Trade::MeshData::attributeId(): index 6 out of range for 6 attributes\n" + "Trade::MeshData::attributeFormat(): index 6 out of range for 6 attributes\n" + "Trade::MeshData::attributeOffset(): index 6 out of range for 6 attributes\n" + "Trade::MeshData::attributeStride(): index 6 out of range for 6 attributes\n" + "Trade::MeshData::attributeArraySize(): index 6 out of range for 6 attributes\n" + "Trade::MeshData::attribute(): index 6 out of range for 6 attributes\n" + "Trade::MeshData::attribute(): index 6 out of range for 6 attributes\n" + "Trade::MeshData::attribute(): index 6 out of range for 6 attributes\n" + "Trade::MeshData::mutableAttribute(): index 6 out of range for 6 attributes\n" + "Trade::MeshData::mutableAttribute(): index 6 out of range for 6 attributes\n" + "Trade::MeshData::mutableAttribute(): index 6 out of range for 6 attributes\n" "Trade::MeshData::attributeId(): index 0 out of range for 0 Trade::MeshAttribute::Position attributes\n" "Trade::MeshData::attributeId(): index 2 out of range for 2 Trade::MeshAttribute::Color attributes\n" @@ -3633,6 +3929,10 @@ void MeshDataTest::attributeNotFound() { "Trade::MeshData::normalsInto(): index 0 out of range for 0 normal attributes\n" "Trade::MeshData::textureCoordinates2DInto(): index 0 out of range for 0 texture coordinate attributes\n" "Trade::MeshData::colorsInto(): index 2 out of range for 2 color attributes\n" + "Trade::MeshData::jointIdsAsArray(): index 2 out of range for 2 joint ID attributes\n" + "Trade::MeshData::jointIdsInto(): index 2 out of range for 2 joint ID attributes\n" + "Trade::MeshData::weightsAsArray(): index 2 out of range for 2 weight attributes\n" + "Trade::MeshData::weightsInto(): index 2 out of range for 2 weight attributes\n" "Trade::MeshData::objectIdsInto(): index 0 out of range for 0 object ID attributes\n"); }