diff --git a/doc/changelog.dox b/doc/changelog.dox index f4afb5015..e48b9e990 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -160,6 +160,9 @@ See also: @subsubsection changelog-latest-new-trade Trade library +- A new, redesigned @ref Trade::MeshData class that allows much more flexible + access to vertex/index data without unnecessary allocations and data + conversions or copies - Ability to import image mip levels via an additional parameter in @ref Trade::AbstractImporter::image2D(), @ref Trade::AbstractImporter::image2DLevelCount() and similar APIs for 1D diff --git a/doc/snippets/MagnumTrade.cpp b/doc/snippets/MagnumTrade.cpp index 23c4caa94..d9600728e 100644 --- a/doc/snippets/MagnumTrade.cpp +++ b/doc/snippets/MagnumTrade.cpp @@ -30,12 +30,15 @@ #include "Magnum/FileCallback.h" #include "Magnum/ImageView.h" +#include "Magnum/Mesh.h" #include "Magnum/PixelFormat.h" +#include "Magnum/MeshTools/Interleave.h" #include "Magnum/Animation/Player.h" #include "Magnum/MeshTools/Transform.h" #include "Magnum/Trade/AbstractImporter.h" #include "Magnum/Trade/AnimationData.h" #include "Magnum/Trade/ImageData.h" +#include "Magnum/Trade/MeshData.h" #include "Magnum/Trade/MeshData2D.h" #include "Magnum/Trade/MeshData3D.h" #include "Magnum/Trade/ObjectData2D.h" @@ -43,6 +46,8 @@ #include "Magnum/Trade/PhongMaterialData.h" #ifdef MAGNUM_TARGET_GL #include "Magnum/GL/Texture.h" +#include "Magnum/GL/Mesh.h" +#include "Magnum/Shaders/Phong.h" #endif using namespace Magnum; @@ -201,6 +206,66 @@ else } #endif +#ifdef MAGNUM_TARGET_GL +{ +Trade::MeshData data{MeshPrimitive::Points, 0}; +/* [MeshData-usage] */ +/* Check that we have at least positions and normals */ +GL::Mesh mesh{data.primitive()}; +if(!data.hasAttribute(Trade::MeshAttribute::Position) || + !data.hasAttribute(Trade::MeshAttribute::Normal)) + Fatal{} << "Oh well"; + +/* Interleave vertex data */ +GL::Buffer vertices; +vertices.setData(MeshTools::interleave(data.positions3DAsArray(), + data.normalsAsArray())); +mesh.addVertexBuffer(std::move(vertices), 0, + Shaders::Phong::Position{}, Shaders::Phong::Normal{}); + +/* Set up an index buffer, if the mesh is indexed*/ +if(data.isIndexed()) { + GL::Buffer indices; + indices.setData(data.indicesAsArray()); + mesh.setIndexBuffer(std::move(indices), 0, MeshIndexType::UnsignedInt) + .setCount(data.indexCount()); +} else mesh.setCount(data.vertexCount()); +/* [MeshData-usage] */ +} + +{ +Trade::MeshData data{MeshPrimitive::Points, 0}; +GL::Mesh mesh{data.primitive()}; +/* [MeshData-usage-advanced] */ +/* Upload the original packed vertex data */ +GL::Buffer vertices; +vertices.setData(data.vertexData()); + +/* Set up the position attribute */ +Shaders::Phong::Position position; +auto positionFormat = data.attributeFormat(Trade::MeshAttribute::Position); +if(positionFormat == VertexFormat::Vector2) + position = {Shaders::Phong::Position::Components::Two}; +else if(positionFormat == VertexFormat::Vector3) + position = {Shaders::Phong::Position::Components::Three}; +else Fatal{} << "Huh?"; +mesh.addVertexBuffer(vertices, + data.attributeOffset(Trade::MeshAttribute::Position), + data.attributeStride(Trade::MeshAttribute::Position), position); + +// Set up other attributes ... + +/* Upload the original packed index data */ +if(data.isIndexed()) { + GL::Buffer indices; + indices.setData(data.indexData()); + mesh.setIndexBuffer(std::move(indices), 0, data.indexType()) + .setCount(data.indexCount()); +} else mesh.setCount(data.vertexCount()); +/* [MeshData-usage-advanced] */ +} +#endif + { Trade::MeshData2D& foo(); Trade::MeshData2D& data = foo(); diff --git a/src/Magnum/Shaders/Generic.h b/src/Magnum/Shaders/Generic.h index dd3bf9033..18f223882 100644 --- a/src/Magnum/Shaders/Generic.h +++ b/src/Magnum/Shaders/Generic.h @@ -78,21 +78,23 @@ template struct Generic { * @brief Vertex position * * @ref Magnum::Vector2 "Vector2" in 2D and @ref Magnum::Vector3 "Vector3" - * in 3D. + * in 3D. Corresponds to @ref Trade::MeshAttribute::Position. */ typedef GL::Attribute<0, T> Position; /** * @brief 2D texture coordinates * - * @ref Magnum::Vector2 "Vector2". + * @ref Magnum::Vector2 "Vector2". Corresponds to + * @ref Trade::MeshAttribute::TextureCoordinates. */ typedef GL::Attribute<1, Vector2> TextureCoordinates; /** * @brief Vertex normal * - * @ref Magnum::Vector3 "Vector3", defined only in 3D. + * @ref Magnum::Vector3 "Vector3", defined only in 3D. Corresponds to + * @ref Trade::MeshAttribute::Normal. */ typedef GL::Attribute<2, Vector3> Normal; @@ -108,6 +110,7 @@ template struct Generic { * @brief Three-component vertex color. * * @ref Magnum::Color3. Use either this or the @ref Color4 attribute. + * Corresponds to @ref Trade::MeshAttribute::Color. */ typedef GL::Attribute<3, Magnum::Color3> Color3; @@ -115,6 +118,7 @@ template struct Generic { * @brief Four-component vertex color. * * @ref Magnum::Color4. Use either this or the @ref Color3 attribute. + * Corresponds to @ref Trade::MeshAttribute::Color. */ typedef GL::Attribute<3, Magnum::Color4> Color4; diff --git a/src/Magnum/Trade/CMakeLists.txt b/src/Magnum/Trade/CMakeLists.txt index 89d516313..fc21d1d78 100644 --- a/src/Magnum/Trade/CMakeLists.txt +++ b/src/Magnum/Trade/CMakeLists.txt @@ -41,6 +41,7 @@ set(MagnumTrade_GracefulAssert_SRCS AnimationData.cpp CameraData.cpp ImageData.cpp + MeshData.cpp ObjectData2D.cpp ObjectData3D.cpp PhongMaterialData.cpp) @@ -53,6 +54,7 @@ set(MagnumTrade_HEADERS CameraData.h ImageData.h LightData.h + MeshData.h MeshData2D.h MeshData3D.h MeshObjectData2D.h @@ -66,6 +68,9 @@ set(MagnumTrade_HEADERS visibility.h) +set(MagnumTrade_PRIVATE_HEADERS + Implementation/arrayUtilities.h) + if(NOT CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/configure.h) @@ -74,7 +79,8 @@ endif() # Objects shared between main and test library add_library(MagnumTradeObjects OBJECT ${MagnumTrade_SRCS} - ${MagnumTrade_HEADERS}) + ${MagnumTrade_HEADERS} + ${MagnumTrade_PRIVATE_HEADERS}) target_include_directories(MagnumTradeObjects PUBLIC $ $) diff --git a/src/Magnum/Trade/Implementation/arrayUtilities.h b/src/Magnum/Trade/Implementation/arrayUtilities.h new file mode 100644 index 000000000..fc1420fde --- /dev/null +++ b/src/Magnum/Trade/Implementation/arrayUtilities.h @@ -0,0 +1,50 @@ +#ifndef Magnum_Trade_Implementation_arrayUtilities_h +#define Magnum_Trade_Implementation_arrayUtilities_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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 +#include + +#include "Magnum/Magnum.h" + +namespace Magnum { namespace Trade { namespace Implementation { + +/* Can't use InPlaceInit as that uses a custom deleters. Compared to + InPlaceInit it does an an unnecessary default-initialization of all + elements */ +/** @todo isn't there some C++56 feature that would allow me to allocate + without calling constructors? */ +template Containers::Array initializerListToArrayWithDefaultDeleter(const std::initializer_list list) { + Containers::Array out{list.size()}; + /* FFS why initializer list doesn't have an operator[] */ + std::size_t i = 0; + for(auto&& item: list) out[i++] = item; + return out; +} + +}}} + +#endif diff --git a/src/Magnum/Trade/MeshData.cpp b/src/Magnum/Trade/MeshData.cpp new file mode 100644 index 000000000..67c67cd01 --- /dev/null +++ b/src/Magnum/Trade/MeshData.cpp @@ -0,0 +1,357 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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 "MeshData.h" + +#include + +#include "Magnum/Math/Color.h" +#include "Magnum/Trade/Implementation/arrayUtilities.h" + +namespace Magnum { namespace Trade { + +MeshIndexData::MeshIndexData(const MeshIndexType type, const Containers::ArrayView data) noexcept: type{type}, data{reinterpret_cast&>(data)} { + CORRADE_ASSERT(!data.empty(), + "Trade::MeshIndexData: index array can't be empty, create a non-indexed mesh instead", ); + CORRADE_ASSERT(data.size()%meshIndexTypeSize(type) == 0, + "Trade::MeshIndexData: view size" << data.size() << "does not correspond to" << type, ); +} + +MeshAttributeData::MeshAttributeData(const MeshAttribute name, const VertexFormat format, const Containers::StridedArrayView1D& data) noexcept: name{name}, format{format}, data{data} { + /** @todo support zero / negative stride? would be hard to transfer to GL */ + CORRADE_ASSERT(data.empty() || std::ptrdiff_t(vertexFormatSize(format)) <= data.stride(), + "Trade::MeshAttributeData: view stride" << data.stride() << "is not large enough to contain" << format, ); + CORRADE_ASSERT( + (name == MeshAttribute::Position && + (format == VertexFormat::Vector2 || + format == VertexFormat::Vector3)) || + (name == MeshAttribute::Normal && + (format == VertexFormat::Vector3)) || + (name == MeshAttribute::Color && + (format == VertexFormat::Vector3 || + format == VertexFormat::Vector4)) || + (name == MeshAttribute::TextureCoordinates && + (format == VertexFormat::Vector2)) || + isMeshAttributeCustom(name) /* can be any format */, + "Trade::MeshAttributeData:" << format << "is not a valid format for" << name, ); +} + +MeshData::MeshData(const MeshPrimitive primitive, Containers::Array&& indexData, const MeshIndexData& indices, Containers::Array&& vertexData, Containers::Array&& attributes, const void* const importerState) noexcept: _indexType{indices.type}, _primitive{primitive}, _importerState{importerState}, _indexData{std::move(indexData)}, _vertexData{std::move(vertexData)}, _attributes{std::move(attributes)}, _indices{indices.data} { + /* Save vertex count. It's a strided array view, so the size is not + depending on type. */ + if(_attributes.empty()) { + CORRADE_ASSERT(indices.type != MeshIndexType{}, + "Trade::MeshData: indices are expected to be valid if there are no attributes and vertex count isn't passed explicitly", ); + /** @todo some better value? attributeless indexed with defined vertex count? */ + _vertexCount = 0; + } else _vertexCount = _attributes[0].data.size(); + + CORRADE_ASSERT(!_indices.empty() || !_indexData, + "Trade::MeshData: indexData passed for a non-indexed mesh", ); + CORRADE_ASSERT(_indices.empty() || (_indices.begin() >= _indexData.begin() && _indices.end() <= _indexData.end()), + "Trade::MeshData: indices [" << Debug::nospace << static_cast(_indices.begin()) << Debug::nospace << ":" << Debug::nospace << static_cast(_indices.end()) << Debug::nospace << "] are not contained in passed indexData array [" << Debug::nospace << static_cast(_indexData.begin()) << Debug::nospace << ":" << Debug::nospace << static_cast(_indexData.end()) << Debug::nospace << "]", ); + CORRADE_ASSERT(!_attributes.empty() || !_vertexData, + "Trade::MeshData: vertexData passed for an attribute-less mesh", ); + CORRADE_ASSERT(_vertexCount || !_vertexData, + "Trade::MeshData: vertexData passed for a mesh with zero vertices", ); + + #ifndef CORRADE_NO_ASSERT + /* Not checking what's already checked in MeshIndexData / MeshAttributeData + constructors */ + for(std::size_t i = 0; i != _attributes.size(); ++i) { + const MeshAttributeData& attribute = _attributes[i]; + CORRADE_ASSERT(attribute.data.size() == _vertexCount, + "Trade::MeshData: attribute" << i << "has" << attribute.data.size() << "vertices but" << _vertexCount << "expected", ); + CORRADE_ASSERT(attribute.data.empty() || (&attribute.data.front() >= _vertexData.begin() && &attribute.data.back() + vertexFormatSize(attribute.format) <= _vertexData.end()), + "Trade::MeshData: attribute" << i << "[" << Debug::nospace << static_cast(&attribute.data.front()) << Debug::nospace << ":" << Debug::nospace << static_cast(&attribute.data.back() + vertexFormatSize(attribute.format)) << Debug::nospace << "] is not contained in passed vertexData array [" << Debug::nospace << static_cast(_vertexData.begin()) << Debug::nospace << ":" << Debug::nospace << static_cast(_vertexData.end()) << Debug::nospace << "]", ); + } + #endif +} + +MeshData::MeshData(const MeshPrimitive primitive, Containers::Array&& indexData, const MeshIndexData& indices, Containers::Array&& vertexData, std::initializer_list attributes, const void* const importerState): MeshData{primitive, std::move(indexData), indices, std::move(vertexData), Implementation::initializerListToArrayWithDefaultDeleter(attributes), importerState} {} + +MeshData::MeshData(const MeshPrimitive primitive, Containers::Array&& vertexData, Containers::Array&& attributes, const void* const importerState) noexcept: MeshData{primitive, {}, MeshIndexData{}, std::move(vertexData), std::move(attributes), importerState} {} + +MeshData::MeshData(const MeshPrimitive primitive, Containers::Array&& vertexData, const std::initializer_list attributes, const void* const importerState): MeshData{primitive, std::move(vertexData), Implementation::initializerListToArrayWithDefaultDeleter(attributes), importerState} {} + +MeshData::MeshData(const MeshPrimitive primitive, Containers::Array&& indexData, const MeshIndexData& indices, const void* const importerState) noexcept: MeshData{primitive, std::move(indexData), indices, {}, {}, importerState} {} + +MeshData::MeshData(const MeshPrimitive primitive, const UnsignedInt vertexCount, const void* const importerState) noexcept: _vertexCount{vertexCount}, _indexType{}, _primitive{primitive}, _importerState{importerState} {} + +MeshData::~MeshData() = default; + +MeshData::MeshData(MeshData&&) noexcept = default; + +MeshData& MeshData::operator=(MeshData&&) noexcept = default; + +UnsignedInt MeshData::indexCount() const { + CORRADE_ASSERT(isIndexed(), + "Trade::MeshData::indexCount(): the mesh is not indexed", {}); + return _indices.size()/meshIndexTypeSize(_indexType); +} + +MeshIndexType MeshData::indexType() const { + CORRADE_ASSERT(isIndexed(), + "Trade::MeshData::indexType(): the mesh is not indexed", {}); + return _indexType; +} + +MeshAttribute MeshData::attributeName(UnsignedInt id) const { + CORRADE_ASSERT(id < _attributes.size(), + "Trade::MeshData::attributeName(): index" << id << "out of range for" << _attributes.size() << "attributes", {}); + return _attributes[id].name; +} + +VertexFormat MeshData::attributeFormat(UnsignedInt id) const { + CORRADE_ASSERT(id < _attributes.size(), + "Trade::MeshData::attributeFormat(): index" << id << "out of range for" << _attributes.size() << "attributes", {}); + return _attributes[id].format; +} + +std::size_t MeshData::attributeOffset(UnsignedInt id) const { + CORRADE_ASSERT(id < _attributes.size(), + "Trade::MeshData::attributeOffset(): index" << id << "out of range for" << _attributes.size() << "attributes", {}); + return static_cast(_attributes[id].data.data()) - _vertexData.data(); +} + +UnsignedInt MeshData::attributeStride(UnsignedInt id) const { + CORRADE_ASSERT(id < _attributes.size(), + "Trade::MeshData::attributeStride(): index" << id << "out of range for" << _attributes.size() << "attributes", {}); + return _attributes[id].data.stride(); +} + +UnsignedInt MeshData::attributeCount(const MeshAttribute name) const { + UnsignedInt count = 0; + for(const MeshAttributeData& attribute: _attributes) + if(attribute.name == name) ++count; + return count; +} + +UnsignedInt MeshData::attributeFor(const MeshAttribute name, UnsignedInt id) const { + for(std::size_t i = 0; i != _attributes.size(); ++i) { + if(_attributes[i].name != name) continue; + if(id-- == 0) return i; + } + + #ifdef CORRADE_NO_ASSERT + CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + #else + return ~UnsignedInt{}; + #endif +} + +VertexFormat MeshData::attributeFormat(MeshAttribute name, UnsignedInt id) const { + const UnsignedInt attributeId = attributeFor(name, id); + CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::attributeFormat(): index" << id << "out of range for" << attributeCount(name) << name << "attributes", {}); + return attributeFormat(attributeId); +} + +std::size_t MeshData::attributeOffset(MeshAttribute name, UnsignedInt id) const { + const UnsignedInt attributeId = attributeFor(name, id); + CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::attributeOffset(): index" << id << "out of range for" << attributeCount(name) << name << "attributes", {}); + return attributeOffset(attributeId); +} + +UnsignedInt MeshData::attributeStride(MeshAttribute name, UnsignedInt id) const { + const UnsignedInt attributeId = attributeFor(name, id); + CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::attributeStride(): index" << id << "out of range for" << attributeCount(name) << name << "attributes", {}); + return attributeStride(attributeId); +} + +namespace { + +template void convertIndices(const Containers::ArrayView data, const Containers::ArrayView destination) { + const auto input = Containers::arrayCast(data); + for(std::size_t i = 0; i != input.size(); ++i) destination[i] = input[i]; +} + +} + +void MeshData::indicesInto(const Containers::ArrayView destination) const { + CORRADE_ASSERT(isIndexed(), + "Trade::MeshData::indicesInto(): the mesh is not indexed", ); + CORRADE_ASSERT(destination.size() == indexCount(), "Trade::MeshData::indicesInto(): expected a view with" << indexCount() << "elements but got" << destination.size(), ); + + switch(_indexType) { + case MeshIndexType::UnsignedByte: return convertIndices(_indices, destination); + case MeshIndexType::UnsignedShort: return convertIndices(_indices, destination); + case MeshIndexType::UnsignedInt: return convertIndices(_indices, destination); + } + + CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +Containers::Array MeshData::indicesAsArray() const { + /* Repeating the assert here because otherwise it would fire in + indexCount() which may be confusing */ + CORRADE_ASSERT(isIndexed(), "Trade::MeshData::indicesAsArray(): the mesh is not indexed", {}); + Containers::Array output{indexCount()}; + indicesInto(output); + return output; +} + +void MeshData::positions2DInto(const Containers::StridedArrayView1D destination, const UnsignedInt id) const { + const UnsignedInt attributeId = attributeFor(MeshAttribute::Position, id); + CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::positions2DInto(): index" << id << "out of range for" << attributeCount(MeshAttribute::Position) << "position attributes", ); + CORRADE_ASSERT(destination.size() == _vertexCount, "Trade::MeshData::positions2DInto(): expected a view with" << _vertexCount << "elements but got" << destination.size(), ); + const MeshAttributeData& attribute = _attributes[attributeId]; + + /* Copy 2D positions as-is, for 3D positions ignore Z */ + if(attribute.format == VertexFormat::Vector2 || + attribute.format == VertexFormat::Vector3) + Utility::copy(Containers::arrayCast(attribute.data), destination); + else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +Containers::Array MeshData::positions2DAsArray(const UnsignedInt id) const { + Containers::Array out{_vertexCount}; + positions2DInto(out, id); + return out; +} + +void MeshData::positions3DInto(const Containers::StridedArrayView1D destination, const UnsignedInt id) const { + const UnsignedInt attributeId = attributeFor(MeshAttribute::Position, id); + CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::positions3DInto(): index" << id << "out of range for" << attributeCount(MeshAttribute::Position) << "position attributes", ); + CORRADE_ASSERT(destination.size() == _vertexCount, "Trade::MeshData::positions3DInto(): expected a view with" << _vertexCount << "elements but got" << destination.size(), ); + const MeshAttributeData& attribute = _attributes[attributeId]; + + /* For 2D positions copy the XY part to the first two components and then + fill the Z with a single value */ + if(attribute.format == VertexFormat::Vector2) { + Utility::copy(Containers::arrayCast(attribute.data), + Containers::arrayCast(destination)); + constexpr Float z[1]{0.0f}; + Utility::copy( + Containers::stridedArrayView(z).broadcasted<0>(_vertexCount), + Containers::arrayCast<2, Float>(destination).transposed<0, 1>()[2]); + /* Copy 3D positions as-is */ + } else if(attribute.format == VertexFormat::Vector3) { + Utility::copy(Containers::arrayCast(attribute.data), destination); + } else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +Containers::Array MeshData::positions3DAsArray(const UnsignedInt id) const { + Containers::Array out{_vertexCount}; + positions3DInto(out, id); + return out; +} + +void MeshData::normalsInto(const Containers::StridedArrayView1D destination, const UnsignedInt id) const { + const UnsignedInt attributeId = attributeFor(MeshAttribute::Normal, id); + CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::normalsInto(): index" << id << "out of range for" << attributeCount(MeshAttribute::Normal) << "normal attributes", ); + CORRADE_ASSERT(destination.size() == _vertexCount, "Trade::MeshData::normalsInto(): expected a view with" << _vertexCount << "elements but got" << destination.size(), ); + const MeshAttributeData& attribute = _attributes[attributeId]; + + if(attribute.format == VertexFormat::Vector3) + Utility::copy(Containers::arrayCast(attribute.data), destination); + else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +Containers::Array MeshData::normalsAsArray(const UnsignedInt id) const { + Containers::Array out{_vertexCount}; + normalsInto(out, id); + return out; +} + +void MeshData::textureCoordinates2DInto(const Containers::StridedArrayView1D destination, const UnsignedInt id) const { + const UnsignedInt attributeId = attributeFor(MeshAttribute::TextureCoordinates, id); + CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::textureCoordinates2DInto(): index" << id << "out of range for" << attributeCount(MeshAttribute::TextureCoordinates) << "texture coordinate attributes", ); + CORRADE_ASSERT(destination.size() == _vertexCount, "Trade::MeshData::textureCoordinates2DInto(): expected a view with" << _vertexCount << "elements but got" << destination.size(), ); + const MeshAttributeData& attribute = _attributes[attributeId]; + + if(attribute.format == VertexFormat::Vector2) + Utility::copy(Containers::arrayCast(attribute.data), destination); + else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +Containers::Array MeshData::textureCoordinates2DAsArray(const UnsignedInt id) const { + Containers::Array out{_vertexCount}; + textureCoordinates2DInto(out, id); + return out; +} + +void MeshData::colorsInto(const Containers::StridedArrayView1D destination, const UnsignedInt id) const { + const UnsignedInt attributeId = attributeFor(MeshAttribute::Color, id); + CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::colorsInto(): index" << id << "out of range for" << attributeCount(MeshAttribute::Color) << "color attributes", ); + CORRADE_ASSERT(destination.size() == _vertexCount, "Trade::MeshData::colorsInto(): expected a view with" << _vertexCount << "elements but got" << destination.size(), ); + const MeshAttributeData& attribute = _attributes[attributeId]; + + /* For three-component colors copy the RGB part to the first three + components and then fill the alpha with a single value */ + if(attribute.format == VertexFormat::Vector3) { + Utility::copy(Containers::arrayCast(attribute.data), + Containers::arrayCast(destination)); + constexpr Float alpha[1]{1.0f}; + Utility::copy( + Containers::stridedArrayView(alpha).broadcasted<0>(_vertexCount), + Containers::arrayCast<2, Float>(destination).transposed<0, 1>()[3]); + /* Copy four-component colors as-is */ + } else if(attribute.format == VertexFormat::Vector4) { + Utility::copy(Containers::arrayCast(attribute.data), + Containers::arrayCast(destination)); + } else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +Containers::Array MeshData::colorsAsArray(const UnsignedInt id) const { + Containers::Array out{_vertexCount}; + colorsInto(out, id); + return out; +} + +Containers::Array MeshData::releaseIndexData() { + _indexType = MeshIndexType{}; /* so isIndexed() returns false */ + _indices = nullptr; + return std::move(_indexData); +} + +Containers::Array MeshData::releaseVertexData() { + _attributes = nullptr; + return std::move(_vertexData); +} + +Debug& operator<<(Debug& debug, const MeshAttribute value) { + debug << "Trade::MeshAttribute" << Debug::nospace; + + if(UnsignedShort(value) >= UnsignedShort(MeshAttribute::Custom)) + return debug << "::Custom(" << Debug::nospace << (UnsignedShort(value) - UnsignedShort(MeshAttribute::Custom)) << Debug::nospace << ")"; + + switch(value) { + /* LCOV_EXCL_START */ + #define _c(value) case MeshAttribute::value: return debug << "::" << Debug::nospace << #value; + _c(Position) + _c(Normal) + _c(TextureCoordinates) + _c(Color) + #undef _c + /* LCOV_EXCL_STOP */ + + /* To silence compiler warning about unhandled values */ + case MeshAttribute::Custom: CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + } + + return debug << "(" << Debug::nospace << reinterpret_cast(UnsignedShort(value)) << Debug::nospace << ")"; +} + +}} diff --git a/src/Magnum/Trade/MeshData.h b/src/Magnum/Trade/MeshData.h new file mode 100644 index 000000000..6df4f05fa --- /dev/null +++ b/src/Magnum/Trade/MeshData.h @@ -0,0 +1,793 @@ +#ifndef Magnum_Trade_MeshData_h +#define Magnum_Trade_MeshData_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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 Class @ref Magnum::Trade::MeshData, @ref Magnum::Trade::MeshIndexData, @ref Magnum::Trade::MeshAttributeData, enum @ref Magnum::Trade::MeshAttribute, function @ref Magnum::Trade::isMeshAttributeCustom(), @ref Magnum::Trade::meshAttributeCustom() + * @m_since_latest + */ + +#include +#include + +#include "Magnum/Mesh.h" +#include "Magnum/VertexFormat.h" +#include "Magnum/Trade/Trade.h" +#include "Magnum/Trade/visibility.h" + +namespace Magnum { namespace Trade { + +/** +@brief Mesh attribute name +@m_since_latest + +@see @ref MeshData, @ref MeshAttributeData, @ref VertexFormat +*/ +/* 16 bits because 8 bits is not enough to cover all potential per-edge, + per-face, per-instance and per-meshlet attributes */ +enum class MeshAttribute: UnsignedShort { + /** + * Position. Type is usually @ref Magnum::Vector2 "Vector2" for 2D and + * @ref Magnum::Vector3 "Vector3" for 3D. Corresponds to + * @ref Shaders::Generic::Position. + * @see @ref VertexFormat::Vector2, @ref VertexFormat::Vector3, + * @ref MeshData::positions2DAsArray(), + * @ref MeshData::positions3DAsArray() + */ + Position, + + /** + * Normal. Type is usually @ref Magnum::Vector3 "Vector3". Corresponds to + * @ref Shaders::Generic::Normal. + * @see @ref VertexFormat::Vector3, @ref MeshData::normalsAsArray() + */ + Normal, + + /** + * Texture coordinates. Type is usually @ref Magnum::Vector2 "Vector2" for + * 2D coordinates. Corresponds to @ref Shaders::Generic::TextureCoordinates. + * @see @ref VertexFormat::Vector2, + * @ref MeshData::textureCoordinates2DAsArray() + */ + TextureCoordinates, + + /** + * Vertex color. Type is usually @ref Magnum::Vector3 "Vector3" or + * @ref Magnum::Vector4 "Vector4" (or @ref Color3 / @ref Color4). + * Corresponds to @ref Shaders::Generic::Color3 or + * @ref Shaders::Generic::Color4. + * @see @ref VertexFormat::Vector3, @ref VertexFormat::Vector4, + * @ref MeshData::colorsAsArray() + */ + Color, + + /** + * This and all higher values are for importer-specific attributes. Can be + * of any type. See documentation of a particular importer for details. + * @see @ref isMeshAttributeCustom(MeshAttribute) + * @ref meshAttributeCustom(MeshAttribute), + * @ref meshAttributeCustom(UnsignedShort) + */ + Custom = 32768 +}; + +/** +@debugoperatorenum{MeshAttribute} +@m_since_latest +*/ +MAGNUM_TRADE_EXPORT Debug& operator<<(Debug& debug, MeshAttribute value); + +/** +@brief Whether a mesh attribute is custom +@m_since_latest + +Returns @cpp true @ce if @p name has a value larger or equal to +@ref MeshAttribute::Custom, @cpp false @ce otherwise. +@see @ref meshAttributeCustom(UnsignedShort), + @ref meshAttributeCustom(MeshAttribute) +*/ +constexpr bool isMeshAttributeCustom(MeshAttribute name) { + return UnsignedShort(name) >= UnsignedShort(MeshAttribute::Custom); +} + +/** +@brief Create a custom mesh attribute +@m_since_latest + +Returns a custom mesh attribute with index @p id. The index is expected to be +less than the value of @ref MeshAttribute::Custom. Use +@ref meshAttributeCustom(MeshAttribute) to get the index back. +*/ +/* Constexpr so it's usable for creating compile-time MeshAttributeData + instances */ +constexpr MeshAttribute meshAttributeCustom(UnsignedShort id) { + return CORRADE_CONSTEXPR_ASSERT(id < UnsignedShort(MeshAttribute::Custom), + "Trade::meshAttributeCustom(): index" << id << "too large"), + MeshAttribute(UnsignedShort(MeshAttribute::Custom) + id); +} + +/** +@brief Get index of a custom mesh attribute +@m_since_latest + +Inverse to @ref meshAttributeCustom(UnsignedShort). Expects that the attribute +is custom. +@see @ref isMeshAttributeCustom() +*/ +constexpr UnsignedShort meshAttributeCustom(MeshAttribute name) { + return CORRADE_CONSTEXPR_ASSERT(isMeshAttributeCustom(name), + "Trade::meshAttributeCustom():" << name << "is not custom"), + UnsignedShort(name) - UnsignedShort(MeshAttribute::Custom); +} + +/** +@brief Mesh index data +@m_since_latest + +Convenience type for populating @ref MeshData. Has no accessors, as the data +are then accessible through @ref MeshData APIs. +@see @ref MeshAttributeData +*/ +class MAGNUM_TRADE_EXPORT MeshIndexData { + public: + /** @brief Construct for a non-indexed mesh */ + explicit MeshIndexData() noexcept: type{} {} + + /** + * @brief Construct with a runtime-specified index type + * @param type Mesh index type + * @param data Index data + * + * The @p data size is expected to correspond to given @p type (e.g., + * for @ref MeshIndexType::UnsignedInt the @p data array size should + * be divisible by 4). If you know the @p type at compile time, you can + * use one of the @ref MeshIndexData(Containers::ArrayView), + * @ref MeshIndexData(Containers::ArrayView) or + * @ref MeshIndexData(Containers::ArrayView) + * constructors, which infer the index type automatically. + */ + explicit MeshIndexData(MeshIndexType type, Containers::ArrayView data) noexcept; + + /** @brief Construct with unsigned byte indices */ + explicit MeshIndexData(Containers::ArrayView data) noexcept: MeshIndexData{MeshIndexType::UnsignedByte, data} {} + + /** @brief Construct with unsigned short indices */ + explicit MeshIndexData(Containers::ArrayView data) noexcept: MeshIndexData{MeshIndexType::UnsignedShort, data} {} + + /** @brief Construct with unsigned int indices */ + explicit MeshIndexData(Containers::ArrayView data) noexcept: MeshIndexData{MeshIndexType::UnsignedInt, data} {} + + private: + /* Not prefixed with _ because we use them like public in MeshData */ + friend MeshData; + MeshIndexType type; + Containers::ArrayView data; +}; + +/** +@brief Mesh attribute data +@m_since_latest + +Convenience type for populating @ref MeshData. Has no accessors, as the data +are then accessible through @ref MeshData APIs. +*/ +class MAGNUM_TRADE_EXPORT MeshAttributeData { + public: + /** + * @brief Default constructor + * + * Leaves contents at unspecified values. Provided as a convenience for + * initialization of the attribute array for @ref MeshData, expected to + * be replaced with concrete values later. + */ + explicit MeshAttributeData() noexcept: name{}, format{}, data{} {} + + /** + * @brief Type-erased constructor + * @param name Attribute name + * @param format Vertex format + * @param data Attribute data + * + * Expects that @p data stride is large enough to fit @p type and that + * @p type corresponds to @p name. + */ + explicit MeshAttributeData(MeshAttribute name, VertexFormat format, const Containers::StridedArrayView1D& data) noexcept; + + /** + * @brief Constructor + * @param name Attribute name + * @param data Attribute data + * + * Detects @ref VertexFormat based on @p T and calls + * @ref MeshAttributeData(MeshAttribute, VertexFormat, const Containers::StridedArrayView1D&). + */ + template explicit MeshAttributeData(MeshAttribute name, const Containers::StridedArrayView1D& data) noexcept; + + /** @overload */ + template explicit MeshAttributeData(MeshAttribute name, const Containers::ArrayView& data) noexcept: MeshAttributeData{name, Containers::stridedArrayView(data)} {} + + private: + /* Not prefixed with _ because we use them like public in MeshData */ + friend MeshData; + MeshAttribute name; + /* Here's some room for flags */ + VertexFormat format; + Containers::StridedArrayView1D data; +}; + +/** +@brief Mesh data +@m_since_latest + +Provides access to mesh vertex and index data, together with additional +information such as primitive type. + +@section Trade-MeshData-usage Basic usage + +The simplest usage is through the convenience functions @ref positions2DAsArray(), +@ref positions3DAsArray(), @ref normalsAsArray(), @ref textureCoordinates2DAsArray() +and @ref colorsAsArray(). Each of these takes an index (as there can be +multiple sets of texture coordinates, for example) and you're expected to check +for attribute presence first with either @ref hasAttribute() or +@ref attributeCount(MeshAttribute) const: + +@snippet MagnumTrade.cpp MeshData-usage + +@section Trade-MeshData-usage-advanced Advanced usage + +The @ref positions2DAsArray(), ... functions shown above always return a +newly-allocated @ref Corrade::Containers::Array instance with a clearly defined +type that's large enough to represent most data. While that's fine for many use +cases, sometimes you may want to minimize the import time of a large model or +the imported data may be already in a well-optimized layout and format that you +want to preserve. The @ref MeshData class internally stores a contiguous blob +of data, which you can directly upload, and then use provided metadata to let +the GPU know of the format and layout: + +@snippet MagnumTrade.cpp MeshData-usage-advanced +*/ +class MAGNUM_TRADE_EXPORT MeshData { + public: + /** + * @brief Construct an indexed mesh data + * @param primitive Primitive + * @param indexData Index data + * @param indices Index data description + * @param vertexData Vertex data + * @param attributes Description of all vertex attribute data + * @param importerState Importer-specific state + * + * 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 + * expected to be valid and non-empty. If you want to create an + * index-less attribute-less mesh, use + * @ref MeshData(MeshPrimitive, UnsignedInt, const void*) to specify + * desired vertex count. + */ + explicit MeshData(MeshPrimitive primitive, Containers::Array&& indexData, const MeshIndexData& indices, Containers::Array&& vertexData, Containers::Array&& attributes, const void* importerState = nullptr) noexcept; + + /** @overload */ + /* Not noexcept because allocation happens inside */ + explicit MeshData(MeshPrimitive primitive, Containers::Array&& indexData, const MeshIndexData& indices, Containers::Array&& vertexData, std::initializer_list attributes, const void* importerState = nullptr); + + /** + * @brief Construct a non-indexed mesh data + * @param primitive Primitive + * @param vertexData Vertex data + * @param attributes Description of all vertex attribute data + * @param importerState Importer-specific state + * + * Same as calling @ref MeshData(MeshPrimitive, Containers::Array&&, const MeshIndexData&, Containers::Array&&, Containers::Array&&, const void*) + * with default-constructed @p indexData and @p indices arguments. + */ + explicit MeshData(MeshPrimitive primitive, Containers::Array&& vertexData, Containers::Array&& attributes, const void* importerState = nullptr) noexcept; + + /** @overload */ + /* Not noexcept because allocation happens inside */ + explicit MeshData(MeshPrimitive primitive, Containers::Array&& vertexData, std::initializer_list attributes, const void* importerState = nullptr); + + /** + * @brief Construct an attribute-less indexed mesh data + * @param primitive Primitive + * @param indexData Index data + * @param indices Index data description + * @param importerState Importer-specific state + * + * Same as calling @ref MeshData(MeshPrimitive, Containers::Array&&, const MeshIndexData&, Containers::Array&&, Containers::Array&&, const void*) + * with default-constructed @p vertexData and @p attributes arguments. + * The @p indices are expected to be valid and non-empty. If you want + * to create an index-less attribute-less mesh, use + * @ref MeshData(MeshPrimitive, UnsignedInt, const void*) to specify + * desired vertex count. + */ + explicit MeshData(MeshPrimitive primitive, Containers::Array&& indexData, const MeshIndexData& indices, const void* importerState = nullptr) noexcept; + + /** + * @brief Construct an index-less attribute-less mesh data + * @param primitive Primitive + * @param vertexCount Desired count of vertices to draw + * @param importerState Importer-specific state + * + * Useful in case the drawing is fully driven by a shader. + */ + explicit MeshData(MeshPrimitive primitive, UnsignedInt vertexCount, const void* importerState = nullptr) noexcept; + + ~MeshData(); + + /** @brief Copying is not allowed */ + MeshData(const MeshData&) = delete; + + /** @brief Move constructor */ + MeshData(MeshData&&) noexcept; + + /** @brief Copying is not allowed */ + MeshData& operator=(const MeshData&) = delete; + + /** @brief Move assignment */ + MeshData& operator=(MeshData&&) noexcept; + + /** @brief Primitive */ + MeshPrimitive primitive() const { return _primitive; } + + /** + * @brief Raw index data + * + * Returns @cpp nullptr @ce if the mesh is not indexed. + * @see @ref isIndexed(), @ref indexCount(), @ref indexType(), + * @ref indices(), @ref releaseIndexData() + */ + Containers::ArrayView indexData() const & { return _indexData; } + + /** @brief Taking a view to a r-value instance is not allowed */ + Containers::ArrayView indexData() const && = delete; + + /** + * @brief Raw vertex data + * + * Contains data for all vertex attributes. Returns @cpp nullptr @ce if + * the mesh has no attributes. + * @see @ref attributeCount(), @ref attributeName(), + * @ref attributeFormat(), @ref attribute(), + * @ref releaseVertexData() + */ + Containers::ArrayView vertexData() const & { return _vertexData; } + + /** @brief Taking a view to a r-value instance is not allowed */ + Containers::ArrayView vertexData() const && = delete; + + /** @brief Whether the mesh is indexed */ + bool isIndexed() const { return _indexType != MeshIndexType{}; } + + /** + * @brief Index count + * + * Count of elements in the @ref indices() array. Expects that the + * mesh is indexed; returned value is always non-zero. See also + * @ref vertexCount() which returns count of elements in every + * @ref attribute() array, and @ref attributeCount() which returns + * count of different per-vertex attribute arrays. + * @see @ref isIndexed(), @ref indexType() + */ + UnsignedInt indexCount() const; + + /** + * @brief Index type + * + * Expects that the mesh is indexed. + * @see @ref isIndexed(), @ref attributeFormat() + */ + MeshIndexType indexType() const; + + /** + * @brief Mesh indices + * + * Expects that the mesh is indexed and that @p T corresponds to + * @ref indexType(). You can also use the non-templated + * @ref indicesAsArray() accessor to get indices converted to 32-bit, + * but note that such operation involves extra allocation and data + * conversion. + * @see @ref isIndexed(), @ref attribute() + */ + template Containers::ArrayView indices() const; + + /** + * @brief Mesh vertex count + * + * Count of elements in every attribute array returned by + * @ref attribute() (or, in case of an attribute-less mesh, the + * desired vertex count). See also @ref indexCount() which returns + * count of elements in the @ref indices() array, and + * @ref attributeCount() which returns count of different per-vertex + * attribute arrays. + */ + UnsignedInt vertexCount() const { return _vertexCount; } + + /** + * @brief Attribute array count + * + * Count of different per-vertex attribute arrays, or @cpp 0 @ce for an + * attribute-less mesh. See also @ref indexCount() which returns count + * of elements in the @ref indices() array and @ref vertexCount() which + * returns count of elements in every @ref attribute() array. + * @see @ref attributeCount(MeshAttribute) const + */ + UnsignedInt attributeCount() const { return _attributes.size(); } + + /** + * @brief Attribute name + * + * The @p id is expected to be smaller than @ref attributeCount() const. + * @see @ref attributeFormat(), @ref isMeshAttributeCustom() + */ + MeshAttribute attributeName(UnsignedInt id) const; + + /** + * @brief Attribute format + * + * The @p id is expected to be smaller than @ref attributeCount() const. + * You can also use @ref attributeFormat(MeshAttribute, UnsignedInt) const + * to directly get a type of given named attribute. + * @see @ref attributeName(), @ref indexType() + */ + VertexFormat attributeFormat(UnsignedInt id) const; + + /** + * @brief Attribute offset + * + * Byte offset of the first element of given attribute from the + * beginning of the @ref vertexData() array, or a byte difference + * between pointers returned from @ref vertexData() and a particular + * @ref attribute(). The @p id is expected to be smaller than + * @ref attributeCount() const. You can also use + * @ref attributeOffset(MeshAttribute, UnsignedInt) const to + * directly get an offset of given named attribute. + */ + std::size_t attributeOffset(UnsignedInt id) const; + + /** + * @brief Attribute stride + * + * Stride between consecutive elements of given attribute in the + * @ref vertexData() array. The @p id is expected to be smaller + * than @ref attributeCount() const. You can also use + * @ref attributeStride(MeshAttribute, UnsignedInt) const to + * directly get a stride of given named attribute. + */ + UnsignedInt attributeStride(UnsignedInt id) const; + + /** + * @brief Whether the mesh has given attribute + * + * @see @ref attributeCount(MeshAttribute) const + */ + bool hasAttribute(MeshAttribute name) const { + return attributeCount(name); + } + + /** + * @brief Count of given named attribute + * + * Unlike @ref attributeCount() const this returns count for given + * attribute name --- for example a mesh can have more than one set of + * texture coordinates. + * @see @ref hasAttribute() + */ + UnsignedInt attributeCount(MeshAttribute name) const; + + /** + * @brief Format of a named attribute + * + * The @p id is expected to be smaller than + * @ref attributeCount(MeshAttribute) const. + * @see @ref attributeFormat(UnsignedInt) const + */ + VertexFormat attributeFormat(MeshAttribute name, UnsignedInt id = 0) const; + + /** + * @brief Offset of a named attribute + * + * Byte offset of the first element of given named attribute from the + * beginning of the @ref vertexData() array. The @p id is expected to + * be smaller than @ref attributeCount(MeshAttribute) const. + * @see @ref attributeOffset(UnsignedInt) const + */ + std::size_t attributeOffset(MeshAttribute name, UnsignedInt id = 0) const; + + /** + * @brief Stride of a named attribute + * + * Stride between consecutive elements of given named attribute in the + * @ref vertexData() array. The @p id is expected to be smaller than + * @ref attributeCount(MeshAttribute) const. + * @see @ref attributeStride(UnsignedInt) const + */ + UnsignedInt attributeStride(MeshAttribute name, UnsignedInt id = 0) const; + + /** + * @brief Data for given attribute array + * + * 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 + * non-templated @ref positions2DAsArray(), @ref positions3DAsArray(), + * @ref normalsAsArray(), @ref textureCoordinates2DAsArray() and + * @ref colorsAsArray() accessors to get common attributes converted to + * usual types, but note that these operations involve extra allocation + * and data conversion. + * @see @ref attribute(MeshAttribute, UnsignedInt) const + */ + template Containers::StridedArrayView1D attribute(UnsignedInt id) const; + + /** + * @brief Data for given named attribute array + * + * 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(), + * @ref positions3DAsArray(), @ref normalsAsArray(), + * @ref textureCoordinates2DAsArray() and @ref colorsAsArray() + * accessors to get common attributes converted to usual types, but + * note that these operations involve extra data conversion and an + * allocation. + * @see @ref attribute(UnsignedInt) const + */ + template Containers::StridedArrayView1D attribute(MeshAttribute name, UnsignedInt id = 0) const; + + /** + * @brief Indices as 32-bit integers + * + * Convenience alternative to the templated @ref indices(). Converts + * the index array from an arbitrary underlying type and returns it in + * a newly-allocated array. + * @see @ref indicesInto() + */ + Containers::Array indicesAsArray() const; + + /** + * @brief Positions as 32-bit integers into a pre-allocated view + * + * Like @ref indicesAsArray(), but puts the result into @p destination + * instead of allocating a new array. Expects that @p destination is + * sized to contain exactly all data. + * @see @ref indexCount() + */ + void indicesInto(Containers::ArrayView destination) const; + + /** + * @brief Positions as 2D float vectors + * + * Convenience alternative to @ref attribute(MeshAttribute, UnsignedInt) const + * with @ref MeshAttribute::Position as the first argument. Converts + * the position array from an arbitrary underlying type and returns it + * in a newly-allocated array. If the underlying type is + * three-component, the last component is dropped. + * @see @ref positions2DInto() + */ + Containers::Array positions2DAsArray(UnsignedInt id = 0) const; + + /** + * @brief Positions as 2D float vectors into a pre-allocated view + * + * Like @ref positions2DAsArray(), but puts the result into + * @p destination instead of allocating a new array. Expects that + * @p destination is sized to contain exactly all data. + * @see @ref vertexCount() + */ + void positions2DInto(Containers::StridedArrayView1D destination, UnsignedInt id = 0) const; + + /** + * @brief Positions as 3D float vectors + * + * Convenience alternative to @ref attribute(MeshAttribute, UnsignedInt) const + * with @ref MeshAttribute::Position as the first argument. Converts + * the position array from an arbitrary underlying type and returns it + * in a newly-allocated array. If the underlying type is two-component, + * the Z component is set to @cpp 0.0f @ce. + * @see @ref positions3DInto() + */ + Containers::Array positions3DAsArray(UnsignedInt id = 0) const; + + /** + * @brief Positions as 3D float vectors into a pre-allocated view + * + * Like @ref positions3DAsArray(), but puts the result into + * @p destination instead of allocating a new array. Expects that + * @p destination is sized to contain exactly all data. + * @see @ref vertexCount() + */ + void positions3DInto(Containers::StridedArrayView1D destination, UnsignedInt id = 0) const; + + /** + * @brief Normals as 3D float vectors + * + * Convenience alternative to @ref attribute(MeshAttribute, UnsignedInt) const + * with @ref MeshAttribute::Normal as the first argument. Converts the + * normal array from an arbitrary underlying type and returns it in a + * newly-allocated array. + * @see @ref normalsInto() + */ + Containers::Array normalsAsArray(UnsignedInt id = 0) const; + + /** + * @brief Normals as 3D float vectors into a pre-allocated view + * + * Like @ref normalsAsArray(), but puts the result into @p destination + * instead of allocating a new array. Expects that @p destination is + * sized to contain exactly all data. + * @see @ref vertexCount() + */ + void normalsInto(Containers::StridedArrayView1D destination, UnsignedInt id = 0) const; + + /** + * @brief Texture coordinates as 2D float vectors + * + * Convenience alternative to @ref attribute(MeshAttribute, UnsignedInt) const + * with @ref MeshAttribute::TextureCoordinates as the first argument. + * Converts the texture coordinate array from an arbitrary underlying + * type and returns it in a newly-allocated array. + * @see @ref textureCoordinates2DInto() + */ + Containers::Array textureCoordinates2DAsArray(UnsignedInt id = 0) const; + + /** + * @brief Texture coordinates as 2D float vectors into a pre-allocated view + * + * Like @ref textureCoordinates2DAsArray(), but puts the result into + * @p destination instead of allocating a new array. Expects that + * @p destination is sized to contain exactly all data. + * @see @ref vertexCount() + */ + void textureCoordinates2DInto(Containers::StridedArrayView1D destination, UnsignedInt id = 0) const; + + /** + * @brief Colors as RGBA floats + * + * Convenience alternative to @ref attribute(MeshAttribute, UnsignedInt) const + * with @ref MeshAttribute::Color as the first argument. Converts the + * color array from an arbitrary underlying type and returns it in a + * newly-allocated array. If the underlying type is three-component, + * the alpha component is set to @cpp 1.0f @ce. + * @see @ref colorsInto() + */ + Containers::Array colorsAsArray(UnsignedInt id = 0) const; + + /** + * @brief Colors as RGBA floats into a pre-allocated view + * + * Like @ref colorsAsArray(), but puts the result into @p destination + * instead of allocating a new array. Expects that @p destination is + * sized to contain exactly all data. + * @see @ref vertexCount() + */ + void colorsInto(Containers::StridedArrayView1D destination, UnsignedInt id = 0) const; + + /** + * @brief Release index data storage + * + * Releases the ownership of the index data array and resets internal + * index-related state to default. The mesh then behaves like + * non-indexed. + * @see @ref indexData() + */ + Containers::Array releaseIndexData(); + + /** + * @brief Release vertex data storage + * + * Releases the ownership of the index data array and resets internal + * attribute-related state to default. The mesh then behaves like if + * it has no attributes. + * @see @ref vertexData() + */ + Containers::Array releaseVertexData(); + + /** + * @brief Importer-specific state + * + * See @ref AbstractImporter::importerState() for more information. + */ + const void* importerState() const { return _importerState; } + + private: + UnsignedInt attributeFor(MeshAttribute name, UnsignedInt id) const; + + UnsignedInt _vertexCount; + MeshIndexType _indexType; + MeshPrimitive _primitive; + const void* _importerState; + Containers::Array _indexData, _vertexData; + Containers::Array _attributes; + /* MeshIndexData are "unpacked" in order to avoid excessive padding */ + Containers::ArrayView _indices; +}; + +#if !defined(CORRADE_NO_ASSERT) || defined(CORRADE_GRACEFUL_ASSERT) +namespace Implementation { + /* LCOV_EXCL_START */ + template constexpr MeshIndexType meshIndexTypeFor() { + /* C++ why there isn't an obvious way to do such a thing?! */ + static_assert(sizeof(T) == 0, "unsupported index type"); + return {}; + } + template<> constexpr MeshIndexType meshIndexTypeFor() { return MeshIndexType::UnsignedByte; } + template<> constexpr MeshIndexType meshIndexTypeFor() { return MeshIndexType::UnsignedShort; } + template<> constexpr MeshIndexType meshIndexTypeFor() { return MeshIndexType::UnsignedInt; } + + template constexpr VertexFormat vertexFormatFor() { + /* C++ why there isn't an obvious way to do such a thing?! */ + static_assert(sizeof(T) == 0, "unsupported attribute type"); + return {}; + } + #ifndef DOXYGEN_GENERATING_OUTPUT + #define _c(format) \ + template<> constexpr VertexFormat vertexFormatFor() { return VertexFormat::format; } + _c(Float) + _c(UnsignedByte) + _c(Byte) + _c(UnsignedShort) + _c(Short) + _c(UnsignedInt) + _c(Int) + _c(Vector2) + _c(Vector3) + _c(Vector4) + #undef _c + #endif + template<> constexpr VertexFormat vertexFormatFor() { return VertexFormat::Vector3; } + template<> constexpr VertexFormat vertexFormatFor() { return VertexFormat::Vector4; } + /* LCOV_EXCL_STOP */ +} +#endif + +template MeshAttributeData::MeshAttributeData(MeshAttribute name, const Containers::StridedArrayView1D& data) noexcept: MeshAttributeData{name, Implementation::vertexFormatFor::type>(), Containers::arrayCast(data)} {} + +template Containers::ArrayView MeshData::indices() const { + CORRADE_ASSERT(isIndexed(), + "Trade::MeshData::indices(): the mesh is not indexed", {}); + CORRADE_ASSERT(Implementation::meshIndexTypeFor() == _indexType, + "Trade::MeshData::indices(): improper type requested for" << _indexType, nullptr); + return Containers::arrayCast(_indices); +} + +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); + 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); +} + +template Containers::StridedArrayView1D 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); +} + +}} + +#endif diff --git a/src/Magnum/Trade/Test/CMakeLists.txt b/src/Magnum/Trade/Test/CMakeLists.txt index 1ed9a9454..fc781c5c1 100644 --- a/src/Magnum/Trade/Test/CMakeLists.txt +++ b/src/Magnum/Trade/Test/CMakeLists.txt @@ -47,6 +47,7 @@ corrade_add_test(TradeCameraDataTest CameraDataTest.cpp LIBRARIES MagnumTradeTes corrade_add_test(TradeImageDataTest ImageDataTest.cpp LIBRARIES MagnumTradeTestLib) corrade_add_test(TradeLightDataTest LightDataTest.cpp LIBRARIES MagnumTrade) corrade_add_test(TradeMaterialDataTest MaterialDataTest.cpp LIBRARIES MagnumTradeTestLib) +corrade_add_test(TradeMeshDataTest MeshDataTest.cpp LIBRARIES MagnumTradeTestLib) corrade_add_test(TradeMeshData2DTest MeshData2DTest.cpp LIBRARIES MagnumTrade) corrade_add_test(TradeMeshData3DTest MeshData3DTest.cpp LIBRARIES MagnumTrade) corrade_add_test(TradeObjectData2DTest ObjectData2DTest.cpp LIBRARIES MagnumTradeTestLib) @@ -56,6 +57,7 @@ corrade_add_test(TradeTextureDataTest TextureDataTest.cpp LIBRARIES MagnumTrade) set_property(TARGET TradeAnimationDataTest + TradeMeshDataTest APPEND PROPERTY COMPILE_DEFINITIONS "CORRADE_GRACEFUL_ASSERT") set_target_properties( diff --git a/src/Magnum/Trade/Test/MeshDataTest.cpp b/src/Magnum/Trade/Test/MeshDataTest.cpp new file mode 100644 index 000000000..c3cd6a6c5 --- /dev/null +++ b/src/Magnum/Trade/Test/MeshDataTest.cpp @@ -0,0 +1,969 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + 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 +#include +#include +#include + +#include "Magnum/Math/Color.h" +#include "Magnum/Trade/MeshData.h" + +namespace Magnum { namespace Trade { namespace Test { namespace { + +struct MeshDataTest: TestSuite::Tester { + explicit MeshDataTest(); + + void customAttributeName(); + void customAttributeNameTooLarge(); + void customAttributeNameNotCustom(); + void debugAttributeName(); + + void constructIndex(); + void constructIndexZeroCount(); + void constructIndexTypeErased(); + void constructIndexTypeErasedWrongSize(); + + void constructAttribute(); + void constructAttributeCustom(); + void constructAttributeWrongFormat(); + void constructAttributeTypeErased(); + void constructAttributeTypeErasedWrongStride(); + + void construct(); + void constructIndexless(); + void constructIndexlessZeroVertices(); + void constructAttributeless(); + void constructIndexlessAttributeless(); + void constructIndexlessAttributelessZeroVertices(); + + void constructIndexDataButNotIndexed(); + void constructVertexDataButNoAttributes(); + void constructVertexDataButNoVertices(); + void constructAttributelessInvalidIndices(); + void constructIndicesNotContained(); + void constructAttributeNotContained(); + void constructInconsitentVertexCount(); + + void constructCopy(); + void constructMove(); + + template void indicesAsArray(); + void indicesIntoArrayInvalidSize(); + template void positions2DAsArray(); + void positions2DIntoArrayInvalidSize(); + template void positions3DAsArray(); + void positions3DIntoArrayInvalidSize(); + template void normalsAsArray(); + void normalsIntoArrayInvalidSize(); + template void textureCoordinates2DAsArray(); + void textureCoordinates2DIntoArrayInvalidSize(); + template void colorsAsArray(); + void colorsIntoArrayInvalidSize(); + + void indicesNotIndexed(); + void indicesWrongType(); + + void attributeNotFound(); + void attributeWrongType(); + + void releaseIndexData(); + void releaseVertexData(); +}; + +MeshDataTest::MeshDataTest() { + addTests({&MeshDataTest::customAttributeName, + &MeshDataTest::customAttributeNameTooLarge, + &MeshDataTest::customAttributeNameNotCustom, + &MeshDataTest::debugAttributeName, + + &MeshDataTest::constructIndex, + &MeshDataTest::constructIndexZeroCount, + &MeshDataTest::constructIndexTypeErased, + &MeshDataTest::constructIndexTypeErasedWrongSize, + + &MeshDataTest::constructAttribute, + &MeshDataTest::constructAttributeCustom, + &MeshDataTest::constructAttributeWrongFormat, + &MeshDataTest::constructAttributeTypeErased, + &MeshDataTest::constructAttributeTypeErasedWrongStride, + + &MeshDataTest::construct, + &MeshDataTest::constructIndexless, + &MeshDataTest::constructIndexlessZeroVertices, + &MeshDataTest::constructAttributeless, + &MeshDataTest::constructIndexlessAttributeless, + &MeshDataTest::constructIndexlessAttributelessZeroVertices, + + &MeshDataTest::constructIndexDataButNotIndexed, + &MeshDataTest::constructVertexDataButNoAttributes, + &MeshDataTest::constructVertexDataButNoVertices, + &MeshDataTest::constructAttributelessInvalidIndices, + &MeshDataTest::constructIndicesNotContained, + &MeshDataTest::constructAttributeNotContained, + &MeshDataTest::constructInconsitentVertexCount, + + &MeshDataTest::constructCopy, + &MeshDataTest::constructMove, + + &MeshDataTest::indicesAsArray, + &MeshDataTest::indicesAsArray, + &MeshDataTest::indicesAsArray, + &MeshDataTest::indicesIntoArrayInvalidSize, + &MeshDataTest::positions2DAsArray, + &MeshDataTest::positions2DAsArray, + &MeshDataTest::positions2DIntoArrayInvalidSize, + &MeshDataTest::positions3DAsArray, + &MeshDataTest::positions3DAsArray, + &MeshDataTest::positions3DIntoArrayInvalidSize, + &MeshDataTest::normalsAsArray, + &MeshDataTest::normalsIntoArrayInvalidSize, + &MeshDataTest::textureCoordinates2DAsArray, + &MeshDataTest::textureCoordinates2DIntoArrayInvalidSize, + &MeshDataTest::colorsAsArray, + &MeshDataTest::colorsAsArray, + &MeshDataTest::colorsIntoArrayInvalidSize, + + &MeshDataTest::indicesNotIndexed, + &MeshDataTest::indicesWrongType, + + &MeshDataTest::attributeNotFound, + &MeshDataTest::attributeWrongType, + + &MeshDataTest::releaseIndexData, + &MeshDataTest::releaseVertexData}); +} + +void MeshDataTest::customAttributeName() { + CORRADE_VERIFY(!isMeshAttributeCustom(MeshAttribute::Position)); + CORRADE_VERIFY(!isMeshAttributeCustom(MeshAttribute(32767))); + CORRADE_VERIFY(isMeshAttributeCustom(MeshAttribute::Custom)); + CORRADE_VERIFY(isMeshAttributeCustom(MeshAttribute(65535))); + + CORRADE_COMPARE(UnsignedShort(meshAttributeCustom(0)), 32768); + CORRADE_COMPARE(UnsignedShort(meshAttributeCustom(8290)), 41058); + CORRADE_COMPARE(UnsignedShort(meshAttributeCustom(32767)), 65535); + + CORRADE_COMPARE(meshAttributeCustom(MeshAttribute::Custom), 0); + CORRADE_COMPARE(meshAttributeCustom(MeshAttribute(41058)), 8290); + CORRADE_COMPARE(meshAttributeCustom(MeshAttribute(65535)), 32767); + + constexpr bool is = isMeshAttributeCustom(MeshAttribute(41058)); + CORRADE_VERIFY(is); + constexpr MeshAttribute a = meshAttributeCustom(8290); + CORRADE_COMPARE(UnsignedShort(a), 41058); + constexpr UnsignedShort b = meshAttributeCustom(a); + CORRADE_COMPARE(b, 8290); +} + +void MeshDataTest::customAttributeNameTooLarge() { + std::ostringstream out; + Error redirectError{&out}; + meshAttributeCustom(32768); + CORRADE_COMPARE(out.str(), "Trade::meshAttributeCustom(): index 32768 too large\n"); +} + +void MeshDataTest::customAttributeNameNotCustom() { + std::ostringstream out; + Error redirectError{&out}; + meshAttributeCustom(MeshAttribute::TextureCoordinates); + CORRADE_COMPARE(out.str(), "Trade::meshAttributeCustom(): Trade::MeshAttribute::TextureCoordinates is not custom\n"); +} + +void MeshDataTest::debugAttributeName() { + std::ostringstream out; + Debug{&out} << MeshAttribute::Position << meshAttributeCustom(73) << MeshAttribute(0x73); + CORRADE_COMPARE(out.str(), "Trade::MeshAttribute::Position Trade::MeshAttribute::Custom(73) Trade::MeshAttribute(0x73)\n"); +} + +using namespace Math::Literals; + +void MeshDataTest::constructIndex() { + { + Containers::Array indexData{3*1}; + auto indexView = Containers::arrayCast(indexData); + + MeshIndexData indices{indexView}; + MeshData data{MeshPrimitive::Points, std::move(indexData), indices}; + CORRADE_COMPARE(data.indexType(), MeshIndexType::UnsignedByte); + CORRADE_COMPARE(static_cast(data.indices().data()), indexView.data()); + CORRADE_COMPARE(data.indexCount(), 3); + } { + Containers::Array indexData{3*2}; + auto indexView = Containers::arrayCast(indexData); + + MeshIndexData indices{indexView}; + MeshData data{MeshPrimitive::Points, std::move(indexData), indices}; + CORRADE_COMPARE(data.indexType(), MeshIndexType::UnsignedShort); + CORRADE_COMPARE(static_cast(data.indices().data()), indexView.data()); + CORRADE_COMPARE(data.indexCount(), 3); + } { + Containers::Array indexData{3*4}; + auto indexView = Containers::arrayCast(indexData); + + MeshIndexData indices{indexView}; + MeshData data{MeshPrimitive::Points, std::move(indexData), indices}; + CORRADE_COMPARE(data.indexType(), MeshIndexType::UnsignedInt); + CORRADE_COMPARE(static_cast(data.indices().data()), indexView.data()); + CORRADE_COMPARE(data.indexCount(), 3); + } +} + +void MeshDataTest::constructIndexZeroCount() { + std::ostringstream out; + Error redirectError{&out}; + MeshIndexData{MeshIndexType::UnsignedInt, nullptr}; + CORRADE_COMPARE(out.str(), "Trade::MeshIndexData: index array can't be empty, create a non-indexed mesh instead\n"); +} + +void MeshDataTest::constructIndexTypeErased() { + Containers::Array indexData{3*2}; + auto indexView = Containers::arrayCast(indexData); + + MeshIndexData indices{MeshIndexType::UnsignedShort, indexData}; + MeshData data{MeshPrimitive::Points, std::move(indexData), indices}; + CORRADE_COMPARE(data.indexType(), MeshIndexType::UnsignedShort); + CORRADE_COMPARE(static_cast(data.indices().data()), indexView.data()); + CORRADE_COMPARE(data.indexCount(), 3); +} + +void MeshDataTest::constructIndexTypeErasedWrongSize() { + Containers::Array indexData{3*2}; + + std::ostringstream out; + Error redirectError{&out}; + MeshIndexData{MeshIndexType::UnsignedInt, indexData}; + CORRADE_COMPARE(out.str(), "Trade::MeshIndexData: view size 6 does not correspond to MeshIndexType::UnsignedInt\n"); +} + +void MeshDataTest::constructAttribute() { + Containers::Array positionData{3*sizeof(Vector2)}; + auto positionView = Containers::arrayCast(positionData); + + MeshAttributeData positions{MeshAttribute::Position, 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::constructAttributeCustom() { + Containers::Array idData{3*sizeof(Short)}; + auto idView = Containers::arrayCast(idData); + + MeshAttributeData ids{meshAttributeCustom(13), idView}; + MeshData data{MeshPrimitive::Points, std::move(idData), {ids}}; + CORRADE_COMPARE(data.attributeName(0), meshAttributeCustom(13)); + CORRADE_COMPARE(data.attributeFormat(0), VertexFormat::Short); + CORRADE_COMPARE(static_cast(data.attribute(0).data()), + idView.data()); +} + +void MeshDataTest::constructAttributeWrongFormat() { + Containers::Array positionData{3*sizeof(Vector2)}; + + std::ostringstream out; + Error redirectError{&out}; + MeshAttributeData{MeshAttribute::Color, Containers::arrayCast(positionData)}; + CORRADE_COMPARE(out.str(), "Trade::MeshAttributeData: VertexFormat::Vector2 is not a valid format for Trade::MeshAttribute::Color\n"); +} + +void MeshDataTest::constructAttributeTypeErased() { + Containers::Array positionData{3*sizeof(Vector3)}; + auto positionView = Containers::arrayCast(positionData); + + MeshAttributeData positions{MeshAttribute::Position, VertexFormat::Vector3, Containers::arrayCast(Containers::stridedArrayView(positionView))}; + MeshData data{MeshPrimitive::Points, std::move(positionData), {positions}}; + CORRADE_COMPARE(data.attributeName(0), MeshAttribute::Position); + CORRADE_COMPARE(data.attributeFormat(0), VertexFormat::Vector3); + CORRADE_COMPARE(static_cast(data.attribute(0).data()), + positionView.data()); +} + +void MeshDataTest::constructAttributeTypeErasedWrongStride() { + Containers::Array positionData{3*sizeof(Vector3)}; + + std::ostringstream out; + Error redirectError{&out}; + MeshAttributeData{MeshAttribute::Position, VertexFormat::Vector3, Containers::arrayCast(positionData)}; + CORRADE_COMPARE(out.str(), "Trade::MeshAttributeData: view stride 1 is not large enough to contain VertexFormat::Vector3\n"); +} + +void MeshDataTest::construct() { + struct Vertex { + Vector3 position; + Vector3 normal; + Vector2 textureCoordinate; + Short id; + }; + + Containers::Array indexData{6*sizeof(UnsignedShort)}; + auto indexView = Containers::arrayCast(indexData); + indexView[0] = 0; + indexView[1] = 1; + indexView[2] = 2; + indexView[3] = 0; + indexView[4] = 2; + indexView[5] = 1; + + Containers::Array vertexData{3*sizeof(Vertex)}; + auto vertexView = Containers::arrayCast(vertexData); + vertexView[0].position = {0.1f, 0.2f, 0.3f}; + vertexView[1].position = {0.4f, 0.5f, 0.6f}; + vertexView[2].position = {0.7f, 0.8f, 0.9f}; + vertexView[0].normal = Vector3::xAxis(); + vertexView[1].normal = Vector3::yAxis(); + vertexView[2].normal = Vector3::zAxis(); + vertexView[0].textureCoordinate = {0.000f, 0.125f}; + vertexView[1].textureCoordinate = {0.250f, 0.375f}; + vertexView[2].textureCoordinate = {0.500f, 0.625f}; + vertexView[0].id = 15; + vertexView[1].id = -374; + vertexView[2].id = 22; + + int importerState; + MeshIndexData indices{indexView}; + MeshAttributeData positions{MeshAttribute::Position, + Containers::StridedArrayView1D{vertexData, &vertexView[0].position, vertexView.size(), sizeof(Vertex)}}; + MeshAttributeData normals{MeshAttribute::Normal, + Containers::StridedArrayView1D{vertexData, &vertexView[0].normal, vertexView.size(), sizeof(Vertex)}}; + MeshAttributeData textureCoordinates{MeshAttribute::TextureCoordinates, + Containers::StridedArrayView1D{vertexData, &vertexView[0].textureCoordinate, vertexView.size(), sizeof(Vertex)}}; + MeshAttributeData ids{meshAttributeCustom(13), + Containers::StridedArrayView1D{vertexData, &vertexView[0].id, vertexView.size(), sizeof(Vertex)}}; + MeshData data{MeshPrimitive::Triangles, + std::move(indexData), indices, + /* Texture coordinates deliberately twice (though aliased) */ + std::move(vertexData), {positions, textureCoordinates, normals, textureCoordinates, ids}, &importerState}; + + /* Basics */ + CORRADE_COMPARE(data.primitive(), MeshPrimitive::Triangles); + CORRADE_COMPARE(static_cast(data.indexData()), indexView.data()); + CORRADE_COMPARE(static_cast(data.vertexData()), vertexView.data()); + CORRADE_COMPARE(data.importerState(), &importerState); + + /* Index access */ + CORRADE_VERIFY(data.isIndexed()); + CORRADE_COMPARE(data.indexCount(), 6); + CORRADE_COMPARE(data.indexType(), MeshIndexType::UnsignedShort); + CORRADE_COMPARE(data.indices()[0], 0); + CORRADE_COMPARE(data.indices()[2], 2); + CORRADE_COMPARE(data.indices()[5], 1); + + /* Attribute access by ID */ + CORRADE_COMPARE(data.vertexCount(), 3); + CORRADE_COMPARE(data.attributeCount(), 5); + CORRADE_COMPARE(data.attributeName(0), MeshAttribute::Position); + CORRADE_COMPARE(data.attributeName(1), MeshAttribute::TextureCoordinates); + CORRADE_COMPARE(data.attributeName(2), MeshAttribute::Normal); + CORRADE_COMPARE(data.attributeName(3), MeshAttribute::TextureCoordinates); + CORRADE_COMPARE(data.attributeName(4), meshAttributeCustom(13)); + CORRADE_COMPARE(data.attributeFormat(0), VertexFormat::Vector3); + CORRADE_COMPARE(data.attributeFormat(1), VertexFormat::Vector2); + CORRADE_COMPARE(data.attributeFormat(2), VertexFormat::Vector3); + CORRADE_COMPARE(data.attributeFormat(3), VertexFormat::Vector2); + CORRADE_COMPARE(data.attributeFormat(4), VertexFormat::Short); + CORRADE_COMPARE(data.attributeOffset(0), 0); + CORRADE_COMPARE(data.attributeOffset(1), 2*sizeof(Vector3)); + CORRADE_COMPARE(data.attributeOffset(2), sizeof(Vector3)); + CORRADE_COMPARE(data.attributeOffset(3), 2*sizeof(Vector3)); + CORRADE_COMPARE(data.attributeOffset(4), 2*sizeof(Vector3) + sizeof(Vector2)); + CORRADE_COMPARE(data.attributeStride(0), sizeof(Vertex)); + CORRADE_COMPARE(data.attributeStride(1), sizeof(Vertex)); + CORRADE_COMPARE(data.attributeStride(2), sizeof(Vertex)); + CORRADE_COMPARE(data.attributeStride(3), sizeof(Vertex)); + 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()); + CORRADE_COMPARE(data.attribute(3)[1], (Vector2{0.250f, 0.375f})); + CORRADE_COMPARE(data.attribute(4)[1], -374); + + /* Attribute access by name */ + CORRADE_VERIFY(data.hasAttribute(MeshAttribute::Position)); + CORRADE_VERIFY(data.hasAttribute(MeshAttribute::Normal)); + CORRADE_VERIFY(data.hasAttribute(MeshAttribute::TextureCoordinates)); + CORRADE_VERIFY(data.hasAttribute(meshAttributeCustom(13))); + CORRADE_VERIFY(!data.hasAttribute(MeshAttribute::Color)); + CORRADE_VERIFY(!data.hasAttribute(meshAttributeCustom(23))); + CORRADE_COMPARE(data.attributeCount(MeshAttribute::Position), 1); + CORRADE_COMPARE(data.attributeCount(MeshAttribute::Normal), 1); + CORRADE_COMPARE(data.attributeCount(MeshAttribute::TextureCoordinates), 2); + CORRADE_COMPARE(data.attributeCount(meshAttributeCustom(13)), 1); + CORRADE_COMPARE(data.attributeCount(MeshAttribute::Color), 0); + CORRADE_COMPARE(data.attributeCount(meshAttributeCustom(23)), 0); + CORRADE_COMPARE(data.attributeFormat(MeshAttribute::Position), + VertexFormat::Vector3); + CORRADE_COMPARE(data.attributeFormat(MeshAttribute::Normal), + VertexFormat::Vector3); + CORRADE_COMPARE(data.attributeFormat(MeshAttribute::TextureCoordinates, 0), + VertexFormat::Vector2); + CORRADE_COMPARE(data.attributeFormat(MeshAttribute::TextureCoordinates, 1), + VertexFormat::Vector2); + CORRADE_COMPARE(data.attributeFormat(meshAttributeCustom(13)), + VertexFormat::Short); + CORRADE_COMPARE(data.attributeOffset(MeshAttribute::Position), 0); + CORRADE_COMPARE(data.attributeOffset(MeshAttribute::Normal), sizeof(Vector3)); + CORRADE_COMPARE(data.attributeOffset(MeshAttribute::TextureCoordinates, 0), 2*sizeof(Vector3)); + CORRADE_COMPARE(data.attributeOffset(MeshAttribute::TextureCoordinates, 1), 2*sizeof(Vector3)); CORRADE_COMPARE(data.attributeOffset(meshAttributeCustom(13)), 2*sizeof(Vector3) + sizeof(Vector2)); + CORRADE_COMPARE(data.attributeStride(MeshAttribute::Position), sizeof(Vertex)); + CORRADE_COMPARE(data.attributeStride(MeshAttribute::Normal), sizeof(Vertex)); + 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)); + 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})); + CORRADE_COMPARE(data.attribute(MeshAttribute::TextureCoordinates, 1)[1], (Vector2{0.250f, 0.375f})); + CORRADE_COMPARE(data.attribute(meshAttributeCustom(13))[2], 22); +} + +void MeshDataTest::constructIndexless() { + Containers::Array vertexData{3*sizeof(Vector2)}; + auto vertexView = Containers::arrayCast(vertexData); + vertexView[0] = {0.1f, 0.2f}; + vertexView[1] = {0.4f, 0.5f}; + vertexView[2] = {0.7f, 0.8f}; + + int importerState; + MeshAttributeData positions{MeshAttribute::Position, vertexView}; + MeshData data{MeshPrimitive::LineLoop, std::move(vertexData), {positions}, &importerState}; + CORRADE_COMPARE(data.primitive(), MeshPrimitive::LineLoop); + CORRADE_COMPARE(data.indexData(), nullptr); + CORRADE_COMPARE(data.importerState(), &importerState); + + CORRADE_VERIFY(!data.isIndexed()); + CORRADE_COMPARE(data.vertexCount(), 3); + CORRADE_COMPARE(data.attributeCount(), 1); + CORRADE_COMPARE(data.attributeFormat(MeshAttribute::Position), VertexFormat::Vector2); + CORRADE_COMPARE(data.attribute(MeshAttribute::Position)[1], (Vector2{0.4f, 0.5f})); +} + +void MeshDataTest::constructIndexlessZeroVertices() { + MeshAttributeData positions{MeshAttribute::Position, VertexFormat::Vector2, nullptr}; + MeshData data{MeshPrimitive::LineLoop, nullptr, {positions}}; + CORRADE_COMPARE(data.primitive(), MeshPrimitive::LineLoop); + CORRADE_COMPARE(data.indexData(), nullptr); + CORRADE_COMPARE(data.vertexData(), nullptr); + + CORRADE_VERIFY(!data.isIndexed()); + CORRADE_COMPARE(data.vertexCount(), 0); + CORRADE_COMPARE(data.attributeCount(), 1); + CORRADE_COMPARE(data.attributeFormat(MeshAttribute::Position), VertexFormat::Vector2); +} + +void MeshDataTest::constructAttributeless() { + Containers::Array indexData{6*sizeof(UnsignedInt)}; + auto indexView = Containers::arrayCast(indexData); + indexView[0] = 0; + indexView[1] = 1; + indexView[2] = 2; + indexView[3] = 0; + indexView[4] = 2; + indexView[5] = 1; + + int importerState; + MeshIndexData indices{indexView}; + MeshData data{MeshPrimitive::TriangleStrip, std::move(indexData), indices, &importerState}; + CORRADE_COMPARE(data.primitive(), MeshPrimitive::TriangleStrip); + CORRADE_COMPARE(data.vertexData(), nullptr); + CORRADE_COMPARE(data.importerState(), &importerState); + + CORRADE_VERIFY(data.isIndexed()); + CORRADE_COMPARE(data.indexCount(), 6); + CORRADE_COMPARE(data.indexType(), MeshIndexType::UnsignedInt); + CORRADE_COMPARE(data.indices()[0], 0); + CORRADE_COMPARE(data.indices()[2], 2); + CORRADE_COMPARE(data.indices()[5], 1); + + CORRADE_COMPARE(data.vertexCount(), 0); /** @todo what to return here? */ + CORRADE_COMPARE(data.attributeCount(), 0); +} + +void MeshDataTest::constructIndexlessAttributeless() { + int importerState; + MeshData data{MeshPrimitive::TriangleStrip, 37, &importerState}; + CORRADE_COMPARE(data.primitive(), MeshPrimitive::TriangleStrip); + CORRADE_COMPARE(data.indexData(), nullptr); + CORRADE_COMPARE(data.vertexData(), nullptr); + CORRADE_COMPARE(data.importerState(), &importerState); + + CORRADE_VERIFY(!data.isIndexed()); + CORRADE_COMPARE(data.vertexCount(), 37); + CORRADE_COMPARE(data.attributeCount(), 0); +} + +void MeshDataTest::constructIndexlessAttributelessZeroVertices() { + int importerState; + MeshData data{MeshPrimitive::TriangleStrip, 0, &importerState}; + CORRADE_COMPARE(data.primitive(), MeshPrimitive::TriangleStrip); + CORRADE_COMPARE(data.indexData(), nullptr); + CORRADE_COMPARE(data.vertexData(), nullptr); + CORRADE_COMPARE(data.importerState(), &importerState); + + CORRADE_VERIFY(!data.isIndexed()); + CORRADE_COMPARE(data.vertexCount(), 0); + CORRADE_COMPARE(data.attributeCount(), 0); +} + +void MeshDataTest::constructIndexDataButNotIndexed() { + Containers::Array indexData{6}; + + std::ostringstream out; + Error redirectError{&out}; + MeshAttributeData positions{MeshAttribute::Position, VertexFormat::Vector2, nullptr}; + MeshData{MeshPrimitive::Points, std::move(indexData), MeshIndexData{}, nullptr, {positions}}; + CORRADE_COMPARE(out.str(), "Trade::MeshData: indexData passed for a non-indexed mesh\n"); +} + +void MeshDataTest::constructVertexDataButNoAttributes() { + Containers::Array indexData{6}; + Containers::Array vertexData{6}; + + std::ostringstream out; + Error redirectError{&out}; + MeshData{MeshPrimitive::Points, std::move(indexData), MeshIndexData{Containers::arrayCast(indexData)}, std::move(vertexData), {}}; + CORRADE_COMPARE(out.str(), "Trade::MeshData: vertexData passed for an attribute-less mesh\n"); +} + +void MeshDataTest::constructVertexDataButNoVertices() { + Containers::Array vertexData{6}; + + std::ostringstream out; + Error redirectError{&out}; + MeshAttributeData positions{MeshAttribute::Position, VertexFormat::Vector2, nullptr}; + MeshData{MeshPrimitive::LineLoop, std::move(vertexData), {positions}}; + CORRADE_COMPARE(out.str(), "Trade::MeshData: vertexData passed for a mesh with zero vertices\n"); +} + +void MeshDataTest::constructAttributelessInvalidIndices() { + std::ostringstream out; + Error redirectError{&out}; + MeshData{MeshPrimitive::Points, nullptr, MeshIndexData{}}; + CORRADE_COMPARE(out.str(), "Trade::MeshData: indices are expected to be valid if there are no attributes and vertex count isn't passed explicitly\n"); +} + +void MeshDataTest::constructIndicesNotContained() { + Containers::Array indexData{reinterpret_cast(0xbadda9), 6, [](char*, std::size_t){}}; + Containers::ArrayView indexData2{reinterpret_cast(0xdead), 3}; + MeshIndexData indices{indexData2}; + + std::ostringstream out; + Error redirectError{&out}; + MeshData{MeshPrimitive::Triangles, std::move(indexData), indices}; + MeshData{MeshPrimitive::Triangles, nullptr, indices}; + CORRADE_COMPARE(out.str(), + "Trade::MeshData: indices [0xdead:0xdeb3] are not contained in passed indexData array [0xbadda9:0xbaddaf]\n" + "Trade::MeshData: indices [0xdead:0xdeb3] are not contained in passed indexData array [0x0:0x0]\n"); +} + +void MeshDataTest::constructAttributeNotContained() { + Containers::Array vertexData{reinterpret_cast(0xbadda9), 24, [](char*, std::size_t){}}; + Containers::ArrayView vertexData2{reinterpret_cast(0xdead), 3}; + MeshAttributeData positions{MeshAttribute::Position, Containers::arrayCast(vertexData)}; + MeshAttributeData positions2{MeshAttribute::Position, Containers::arrayView(vertexData2)}; + + std::ostringstream out; + Error redirectError{&out}; + MeshData{MeshPrimitive::Triangles, std::move(vertexData), {positions, positions2}}; + MeshData{MeshPrimitive::Triangles, nullptr, {positions}}; + CORRADE_COMPARE(out.str(), + "Trade::MeshData: attribute 1 [0xdead:0xdec5] is not contained in passed vertexData array [0xbadda9:0xbaddc1]\n" + "Trade::MeshData: attribute 0 [0xbadda9:0xbaddc1] is not contained in passed vertexData array [0x0:0x0]\n"); +} + +void MeshDataTest::constructInconsitentVertexCount() { + Containers::Array vertexData{24}; + MeshAttributeData positions{MeshAttribute::Position, Containers::arrayCast(vertexData)}; + MeshAttributeData positions2{MeshAttribute::Position, Containers::arrayCast(vertexData).prefix(2)}; + + std::ostringstream out; + Error redirectError{&out}; + MeshData{MeshPrimitive::Triangles, std::move(vertexData), {positions, positions2}}; + CORRADE_COMPARE(out.str(), + "Trade::MeshData: attribute 1 has 2 vertices but 3 expected\n"); +} + +void MeshDataTest::constructCopy() { + CORRADE_VERIFY(!(std::is_constructible{})); + CORRADE_VERIFY(!(std::is_assignable{})); +} + +void MeshDataTest::constructMove() { + Containers::Array indexData{3*sizeof(UnsignedShort)}; + auto indexView = Containers::arrayCast(indexData); + indexView[0] = 0; + indexView[1] = 1; + indexView[2] = 0; + + Containers::Array vertexData{2*sizeof(Vector2)}; + auto vertexView = Containers::arrayCast(vertexData); + vertexView[0] = {0.1f, 0.2f}; + vertexView[1] = {0.4f, 0.5f}; + + int importerState; + MeshIndexData indices{indexView}; + MeshAttributeData positions{MeshAttribute::Position, vertexView}; + MeshData a{MeshPrimitive::Triangles, std::move(indexData), indices, std::move(vertexData), {positions}, &importerState}; + + MeshData b{std::move(a)}; + + CORRADE_COMPARE(b.primitive(), MeshPrimitive::Triangles); + CORRADE_COMPARE(static_cast(b.indexData()), indexView.data()); + CORRADE_COMPARE(static_cast(b.vertexData()), vertexView.data()); + CORRADE_COMPARE(b.importerState(), &importerState); + + CORRADE_VERIFY(b.isIndexed()); + CORRADE_COMPARE(b.indexCount(), 3); + CORRADE_COMPARE(b.indexType(), MeshIndexType::UnsignedShort); + CORRADE_COMPARE(b.indices()[1], 1); + CORRADE_COMPARE(b.indices()[2], 0); + + CORRADE_COMPARE(b.vertexCount(), 2); + CORRADE_COMPARE(b.attributeCount(), 1); + CORRADE_COMPARE(b.attributeName(0), MeshAttribute::Position); + CORRADE_COMPARE(b.attributeFormat(0), VertexFormat::Vector2); + CORRADE_COMPARE(b.attributeOffset(0), 0); + CORRADE_COMPARE(b.attributeStride(0), sizeof(Vector2)); + CORRADE_COMPARE(b.attribute(0)[0], (Vector2{0.1f, 0.2f})); + CORRADE_COMPARE(b.attribute(0)[1], (Vector2{0.4f, 0.5f})); + + MeshData c{MeshPrimitive::LineLoop, 37}; + c = std::move(b); + + CORRADE_COMPARE(c.primitive(), MeshPrimitive::Triangles); + CORRADE_COMPARE(static_cast(c.indexData()), indexView.data()); + CORRADE_COMPARE(static_cast(c.vertexData()), vertexView.data()); + CORRADE_COMPARE(c.importerState(), &importerState); + + CORRADE_VERIFY(c.isIndexed()); + CORRADE_COMPARE(c.indexCount(), 3); + CORRADE_COMPARE(c.indexType(), MeshIndexType::UnsignedShort); + CORRADE_COMPARE(c.indices()[1], 1); + CORRADE_COMPARE(c.indices()[2], 0); + + CORRADE_COMPARE(c.vertexCount(), 2); + CORRADE_COMPARE(c.attributeCount(), 1); + CORRADE_COMPARE(c.attributeName(0), MeshAttribute::Position); + CORRADE_COMPARE(c.attributeFormat(0), VertexFormat::Vector2); + CORRADE_COMPARE(c.attributeOffset(0), 0); + CORRADE_COMPARE(c.attributeStride(0), sizeof(Vector2)); + CORRADE_COMPARE(c.attribute(0)[0], (Vector2{0.1f, 0.2f})); + CORRADE_COMPARE(c.attribute(0)[1], (Vector2{0.4f, 0.5f})); + + CORRADE_VERIFY(std::is_nothrow_move_constructible::value); + CORRADE_VERIFY(std::is_nothrow_move_assignable::value); +} + +template struct NameTraits; +#define _c(format) template<> struct NameTraits { \ + static const char* name() { return #format; } \ + }; +_c(Vector2) +_c(Vector3) +_c(Color3) +_c(Color4) +#undef _c + +template void MeshDataTest::indicesAsArray() { + setTestCaseTemplateName(Math::TypeTraits::name()); + + Containers::Array indexData{3*sizeof(T)}; + auto indexView = Containers::arrayCast(indexData); + indexView[0] = 75; + indexView[1] = 131; + indexView[2] = 240; + + MeshData data{MeshPrimitive::Points, std::move(indexData), MeshIndexData{indexView}}; + CORRADE_COMPARE_AS(data.indicesAsArray(), + Containers::arrayView({75, 131, 240}), + TestSuite::Compare::Container); +} + +void MeshDataTest::indicesIntoArrayInvalidSize() { + Containers::Array indexData{3*sizeof(UnsignedInt)}; + MeshData data{MeshPrimitive::Points, std::move(indexData), MeshIndexData{Containers::arrayCast(indexData)}}; + + std::ostringstream out; + Error redirectError{&out}; + UnsignedInt destination[2]; + data.indicesInto(destination); + CORRADE_COMPARE(out.str(), + "Trade::MeshData::indicesInto(): expected a view with 3 elements but got 2\n"); +} + +template void MeshDataTest::positions2DAsArray() { + setTestCaseTemplateName(NameTraits::name()); + + Containers::Array vertexData{3*sizeof(T)}; + auto positionsView = Containers::arrayCast(vertexData); + positionsView[0] = T::pad(Vector2{2.0f, 1.0f}); + positionsView[1] = T::pad(Vector2{0.0f, -1.0f}); + positionsView[2] = T::pad(Vector2{-2.0f, 3.0f}); + + MeshData data{MeshPrimitive::Points, std::move(vertexData), {MeshAttributeData{MeshAttribute::Position, positionsView}}}; + CORRADE_COMPARE_AS(data.positions2DAsArray(), + Containers::arrayView({{2.0f, 1.0f}, {0.0f, -1.0f}, {-2.0f, 3.0f}}), + TestSuite::Compare::Container); +} + +void MeshDataTest::positions2DIntoArrayInvalidSize() { + Containers::Array vertexData{3*sizeof(Vector2)}; + MeshData data{MeshPrimitive::Points, std::move(vertexData), {MeshAttributeData{MeshAttribute::Position, Containers::arrayCast(vertexData)}}}; + + std::ostringstream out; + Error redirectError{&out}; + Vector2 destination[2]; + data.positions2DInto(destination); + CORRADE_COMPARE(out.str(), + "Trade::MeshData::positions2DInto(): expected a view with 3 elements but got 2\n"); +} + +template void MeshDataTest::positions3DAsArray() { + setTestCaseTemplateName(NameTraits::name()); + + Containers::Array vertexData{3*sizeof(T)}; + auto positionsView = Containers::arrayCast(vertexData); + positionsView[0] = T::pad(Vector3{2.0f, 1.0f, 0.3f}); + positionsView[1] = T::pad(Vector3{0.0f, -1.0f, 1.1f}); + positionsView[2] = T::pad(Vector3{-2.0f, 3.0f, 2.2f}); + + MeshData data{MeshPrimitive::Points, std::move(vertexData), {MeshAttributeData{MeshAttribute::Position, positionsView}}}; + CORRADE_COMPARE_AS(data.positions3DAsArray(), Containers::arrayView({ + Vector3::pad(T::pad(Vector3{2.0f, 1.0f, 0.3f})), + Vector3::pad(T::pad(Vector3{0.0f, -1.0f, 1.1f})), + Vector3::pad(T::pad(Vector3{-2.0f, 3.0f, 2.2f})) + }), TestSuite::Compare::Container); +} + +void MeshDataTest::positions3DIntoArrayInvalidSize() { + Containers::Array vertexData{3*sizeof(Vector3)}; + MeshData data{MeshPrimitive::Points, std::move(vertexData), {MeshAttributeData{MeshAttribute::Position, Containers::arrayCast(vertexData)}}}; + + std::ostringstream out; + Error redirectError{&out}; + Vector3 destination[2]; + data.positions3DInto(destination); + CORRADE_COMPARE(out.str(), + "Trade::MeshData::positions3DInto(): expected a view with 3 elements but got 2\n"); +} + +template void MeshDataTest::normalsAsArray() { + setTestCaseTemplateName(NameTraits::name()); + + Containers::Array vertexData{3*sizeof(T)}; + auto normalsView = Containers::arrayCast(vertexData); + normalsView[0] = {2.0f, 1.0f, 0.3f}; + normalsView[1] = {0.0f, -1.0f, 1.1f}; + normalsView[2] = {-2.0f, 3.0f, 2.2f}; + + MeshData data{MeshPrimitive::Points, std::move(vertexData), {MeshAttributeData{MeshAttribute::Normal, normalsView}}}; + CORRADE_COMPARE_AS(data.normalsAsArray(), Containers::arrayView({ + {2.0f, 1.0f, 0.3f}, {0.0f, -1.0f, 1.1f}, {-2.0f, 3.0f, 2.2f}, + }), TestSuite::Compare::Container); +} + +void MeshDataTest::normalsIntoArrayInvalidSize() { + Containers::Array vertexData{3*sizeof(Vector3)}; + MeshData data{MeshPrimitive::Points, std::move(vertexData), {MeshAttributeData{MeshAttribute::Normal, Containers::arrayCast(vertexData)}}}; + + std::ostringstream out; + Error redirectError{&out}; + Vector3 destination[2]; + data.normalsInto(destination); + CORRADE_COMPARE(out.str(), + "Trade::MeshData::normalsInto(): expected a view with 3 elements but got 2\n"); +} + +template void MeshDataTest::textureCoordinates2DAsArray() { + setTestCaseTemplateName(NameTraits::name()); + + Containers::Array vertexData{3*sizeof(T)}; + auto textureCoordinatesView = Containers::arrayCast(vertexData); + textureCoordinatesView[0] = {2.0f, 1.0f}; + textureCoordinatesView[1] = {0.0f, -1.0f}; + textureCoordinatesView[2] = {-2.0f, 3.0f}; + + MeshData data{MeshPrimitive::Points, std::move(vertexData), {MeshAttributeData{MeshAttribute::TextureCoordinates, textureCoordinatesView}}}; + CORRADE_COMPARE_AS(data.textureCoordinates2DAsArray(), Containers::arrayView({ + {2.0f, 1.0f}, {0.0f, -1.0f}, {-2.0f, 3.0f}, + }), TestSuite::Compare::Container); +} + +void MeshDataTest::textureCoordinates2DIntoArrayInvalidSize() { + Containers::Array vertexData{3*sizeof(Vector2)}; + MeshData data{MeshPrimitive::Points, std::move(vertexData), {MeshAttributeData{MeshAttribute::TextureCoordinates, Containers::arrayCast(vertexData)}}}; + + std::ostringstream out; + Error redirectError{&out}; + Vector2 destination[2]; + data.textureCoordinates2DInto(destination); + CORRADE_COMPARE(out.str(), + "Trade::MeshData::textureCoordinates2DInto(): expected a view with 3 elements but got 2\n"); +} + +template void MeshDataTest::colorsAsArray() { + setTestCaseTemplateName(NameTraits::name()); + + Containers::Array vertexData{3*sizeof(T)}; + auto colorsView = Containers::arrayCast(vertexData); + colorsView[0] = 0xff3366_rgbf; + colorsView[1] = 0x99aacc_rgbf; + colorsView[2] = 0x3377ff_rgbf; + + MeshData data{MeshPrimitive::Points, std::move(vertexData), {MeshAttributeData{MeshAttribute::Color, colorsView}}}; + CORRADE_COMPARE_AS(data.colorsAsArray(), Containers::arrayView({ + 0xff3366_rgbf, 0x99aacc_rgbf, 0x3377ff_rgbf + }), TestSuite::Compare::Container); +} + +void MeshDataTest::colorsIntoArrayInvalidSize() { + Containers::Array vertexData{3*sizeof(Color4)}; + MeshData data{MeshPrimitive::Points, std::move(vertexData), {MeshAttributeData{MeshAttribute::Color, Containers::arrayCast(vertexData)}}}; + + std::ostringstream out; + Error redirectError{&out}; + Color4 destination[2]; + data.colorsInto(destination); + CORRADE_COMPARE(out.str(), + "Trade::MeshData::colorsInto(): expected a view with 3 elements but got 2\n"); +} + +void MeshDataTest::indicesNotIndexed() { + MeshData data{MeshPrimitive::Triangles, 37}; + + std::ostringstream out; + Error redirectError{&out}; + data.indexCount(); + data.indexType(); + data.indices(); + data.indicesAsArray(); + UnsignedInt a[1]; + data.indicesInto(a); + CORRADE_COMPARE(out.str(), + "Trade::MeshData::indexCount(): the mesh is not indexed\n" + "Trade::MeshData::indexType(): the mesh is not indexed\n" + "Trade::MeshData::indices(): the mesh is not indexed\n" + "Trade::MeshData::indicesAsArray(): the mesh is not indexed\n" + "Trade::MeshData::indicesInto(): the mesh is not indexed\n"); +} + +void MeshDataTest::indicesWrongType() { + Containers::Array indexData{sizeof(UnsignedShort)}; + auto indexView = Containers::arrayCast(indexData); + indexView[0] = 57616; + MeshData data{MeshPrimitive::Points, std::move(indexData), MeshIndexData{indexView}}; + + std::ostringstream out; + Error redirectError{&out}; + data.indices(); + CORRADE_COMPARE(out.str(), "Trade::MeshData::indices(): improper type requested for MeshIndexType::UnsignedShort\n"); +} + +void MeshDataTest::attributeNotFound() { + MeshAttributeData colors1{MeshAttribute::Color, VertexFormat::Vector3, nullptr}; + MeshAttributeData colors2{MeshAttribute::Color, VertexFormat::Vector4, nullptr}; + MeshData data{MeshPrimitive::Points, nullptr, {colors1, colors2}}; + + std::ostringstream out; + Error redirectError{&out}; + data.attributeName(2); + data.attributeFormat(2); + data.attributeOffset(2); + data.attributeStride(2); + data.attribute(2); + data.attributeFormat(MeshAttribute::Position); + data.attributeFormat(MeshAttribute::Color, 2); + data.attributeOffset(MeshAttribute::Position); + data.attributeOffset(MeshAttribute::Color, 2); + data.attributeStride(MeshAttribute::Position); + data.attributeStride(MeshAttribute::Color, 2); + data.attribute(MeshAttribute::Position); + data.attribute(MeshAttribute::Color, 2); + data.positions2DAsArray(); + data.positions3DAsArray(); + data.normalsAsArray(); + data.textureCoordinates2DAsArray(); + data.colorsAsArray(2); + CORRADE_COMPARE(out.str(), + "Trade::MeshData::attributeName(): 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::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" + "Trade::MeshData::attributeOffset(): index 2 out of range for 2 Trade::MeshAttribute::Color attributes\n" + "Trade::MeshData::attributeStride(): index 0 out of range for 0 Trade::MeshAttribute::Position attributes\n" + "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::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" + "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"); +} + +void MeshDataTest::attributeWrongType() { + MeshAttributeData positions{MeshAttribute::Position, VertexFormat::Vector3, nullptr}; + MeshData data{MeshPrimitive::Points, nullptr, {positions}}; + + std::ostringstream out; + Error redirectError{&out}; + data.attribute(MeshAttribute::Position); + CORRADE_COMPARE(out.str(), "Trade::MeshData::attribute(): improper type requested for Trade::MeshAttribute::Position of format VertexFormat::Vector3\n"); +} + +void MeshDataTest::releaseIndexData() { + Containers::Array indexData{6}; + auto indexView = Containers::arrayCast(indexData); + + MeshData data{MeshPrimitive::TriangleStrip, std::move(indexData), MeshIndexData{indexView}}; + CORRADE_VERIFY(data.isIndexed()); + + Containers::Array released = data.releaseIndexData(); + CORRADE_COMPARE(static_cast(released.data()), indexView.data()); + CORRADE_COMPARE(data.indexData(), nullptr); + CORRADE_VERIFY(!data.isIndexed()); +} + +void MeshDataTest::releaseVertexData() { + Containers::Array vertexData{16}; + auto vertexView = Containers::arrayCast(vertexData); + + MeshAttributeData positions{MeshAttribute::Position, vertexView}; + MeshData data{MeshPrimitive::LineLoop, std::move(vertexData), {positions, positions}}; + CORRADE_COMPARE(data.attributeCount(), 2); + + Containers::Array released = data.releaseVertexData(); + CORRADE_COMPARE(data.vertexData(), nullptr); + CORRADE_COMPARE(data.attributeCount(), 0); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::Trade::Test::MeshDataTest) diff --git a/src/Magnum/Trade/Trade.h b/src/Magnum/Trade/Trade.h index 06c28ca49..0803127ac 100644 --- a/src/Magnum/Trade/Trade.h +++ b/src/Magnum/Trade/Trade.h @@ -65,6 +65,12 @@ typedef ImageData<2> ImageData2D; typedef ImageData<3> ImageData3D; class LightData; + +enum class MeshAttribute: UnsignedShort; +class MeshIndexData; +class MeshAttributeData; +class MeshData; + class MeshData2D; class MeshData3D; class MeshObjectData2D; diff --git a/src/Magnum/VertexFormat.h b/src/Magnum/VertexFormat.h index 1ecdcb771..ce7d3120a 100644 --- a/src/Magnum/VertexFormat.h +++ b/src/Magnum/VertexFormat.h @@ -42,6 +42,8 @@ namespace Magnum { Like @ref PixelFormat, but for mesh attributes --- including double-precision types and matrices. +@see @ref Trade::MeshData, @ref Trade::MeshAttributeData, + @ref Trade::MeshAttribute */ enum class VertexFormat: UnsignedInt { /* Zero reserved for an invalid type (but not being a named value) */