From a40c721fac34d2693ed50af4474f71623af35b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Wed, 15 Dec 2021 18:14:06 +0100 Subject: [PATCH] MeshTools: a family of transform*() APIs operating on a MeshData. And doing all the automagic of unpacking packed types, converting positions *and* normals/tangents/bitangents, and also an overload for transforming texture coordinates. Such a simple thing and yet so complex and nasty to test. --- doc/changelog.dox | 4 + src/Magnum/MeshTools/CMakeLists.txt | 3 +- src/Magnum/MeshTools/Test/CMakeLists.txt | 2 +- src/Magnum/MeshTools/Test/TransformTest.cpp | 1452 +++++++++++++++++++ src/Magnum/MeshTools/Transform.cpp | 311 ++++ src/Magnum/MeshTools/Transform.h | 160 +- 6 files changed, 1929 insertions(+), 3 deletions(-) create mode 100644 src/Magnum/MeshTools/Transform.cpp diff --git a/doc/changelog.dox b/doc/changelog.dox index 2a2ecf21f..4ecfa4cce 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -155,6 +155,10 @@ See also: - Added @ref MeshTools::generateQuadIndices() for quad triangulation including non-convex and non-planar quads +- New family of @ref MeshTools::transform2D(), @ref MeshTools::transform3D() + and @ref MeshTools::transformTextureCoordinates2D() APIs for converting + positions, normals, tangents, bitangents and texture coordinates directly + in @ref Trade::MeshData instances @subsubsection changelog-latest-new-platform Platform libraries diff --git a/src/Magnum/MeshTools/CMakeLists.txt b/src/Magnum/MeshTools/CMakeLists.txt index d88cdc7cd..3d4a98c99 100644 --- a/src/Magnum/MeshTools/CMakeLists.txt +++ b/src/Magnum/MeshTools/CMakeLists.txt @@ -38,7 +38,8 @@ set(MagnumMeshTools_GracefulAssert_SRCS GenerateNormals.cpp Interleave.cpp Reference.cpp - RemoveDuplicates.cpp) + RemoveDuplicates.cpp + Transform.cpp) set(MagnumMeshTools_HEADERS Combine.h diff --git a/src/Magnum/MeshTools/Test/CMakeLists.txt b/src/Magnum/MeshTools/Test/CMakeLists.txt index 6d7f421ef..5d1d9bb1c 100644 --- a/src/Magnum/MeshTools/Test/CMakeLists.txt +++ b/src/Magnum/MeshTools/Test/CMakeLists.txt @@ -35,7 +35,7 @@ corrade_add_test(MeshToolsReferenceTest ReferenceTest.cpp LIBRARIES MagnumMeshTo corrade_add_test(MeshToolsRemoveDuplicatesTest RemoveDuplicatesTest.cpp LIBRARIES MagnumMeshToolsTestLib) corrade_add_test(MeshToolsSubdivideTest SubdivideTest.cpp LIBRARIES Magnum MagnumPrimitives) corrade_add_test(MeshToolsTipsifyTest TipsifyTest.cpp LIBRARIES MagnumMeshTools) -corrade_add_test(MeshToolsTransformTest TransformTest.cpp LIBRARIES MagnumMeshTools) +corrade_add_test(MeshToolsTransformTest TransformTest.cpp LIBRARIES MagnumMeshToolsTestLib) # Graceful assert for testing set_property(TARGET diff --git a/src/Magnum/MeshTools/Test/TransformTest.cpp b/src/Magnum/MeshTools/Test/TransformTest.cpp index 66ef78de4..d440aeea3 100644 --- a/src/Magnum/MeshTools/Test/TransformTest.cpp +++ b/src/Magnum/MeshTools/Test/TransformTest.cpp @@ -24,11 +24,19 @@ */ #include +#include +#include +#include #include +#include +#include +#include +#include "Magnum/Math/Half.h" #include "Magnum/Math/Matrix3.h" #include "Magnum/Magnum.h" #include "Magnum/MeshTools/Transform.h" +#include "Magnum/Trade/MeshData.h" namespace Magnum { namespace MeshTools { namespace Test { namespace { @@ -40,6 +48,212 @@ struct TransformTest: TestSuite::Tester { void transformPoints2D(); void transformPoints3D(); + + template void meshData2D(); + void meshData2DNoPosition(); + void meshData2DNot2D(); + void meshData2DRvaluePassthrough(); + void meshData2DRvaluePassthroughIndexDataNotOwned(); + void meshData2DRvaluePassthroughVertexDataNotOwned(); + void meshData2DRvaluePassthroughNoPosition(); + void meshData2DRvaluePassthroughWrongFormat(); + /* in-place variant called from the others and as such tested sufficiently, + except for the asserts below */ + void meshData2DInPlaceNotMutable(); + void meshData2DInPlaceNoPosition(); + void meshData2DInPlaceWrongFormat(); + + template void meshData3D(); + void meshData3DNoPosition(); + void meshData3DNot3D(); + void meshData3DRvaluePassthrough(); + void meshData3DRvaluePassthroughIndexDataNotOwned(); + void meshData3DRvaluePassthroughVertexDataNotOwned(); + void meshData3DRvaluePassthroughNoPosition(); + void meshData3DRvaluePassthroughWrongFormat(); + /* in-place variant called from the others and as such tested sufficiently, + except for the asserts below */ + void meshData3DInPlaceNotMutable(); + void meshData3DInPlaceNoPosition(); + void meshData3DInPlaceWrongFormat(); + + template void meshDataTextureCoordinates2D(); + void meshDataTextureCoordinates2DNoCoordinates(); + void meshDataTextureCoordinates2DRvaluePassthrough(); + void meshDataTextureCoordinates2DRvaluePassthroughIndexDataNotOwned(); + void meshDataTextureCoordinates2DRvaluePassthroughVertexDataNotOwned(); + void meshDataTextureCoordinates2DRvaluePassthroughNoCoordinates(); + void meshDataTextureCoordinates2DRvaluePassthroughWrongFormat(); + /* in-place variant called from the others and as such tested sufficiently, + except for the asserts below */ + void meshDataTextureCoordinates2DInPlaceNotMutable(); + void meshDataTextureCoordinates2DInPlaceNoCoordinates(); + void meshDataTextureCoordinates2DInPlaceWrongFormat(); +}; + +using namespace Math::Literals; + +const struct { + const char* name; + bool indexed; + UnsignedInt id; + Matrix3 transformation; +} MeshData2DData[]{ + {"", false, 0, + Matrix3::translation({1.5f, 3.0f})* + Matrix3::rotation(35.0_degf)}, + {"indexed", true, 0, + Matrix3::translation({1.5f, 3.0f})* + Matrix3::rotation(35.0_degf)}, + {"second set", false, 1, + Matrix3::translation({1.5f, 3.0f})* + Matrix3::rotation(35.0_degf)} + /** @todo negative scaling that flips face winding */ +}; + +const struct { + const char* name; + bool indexed; + UnsignedInt id; +} MeshData2DRvaluePassthroughData[]{ + {"", false, 0}, + {"indexed", true, 0}, + {"second set", false, 1} +}; + +const struct { + const char* name; + bool indexed; + bool tangents, tangents4, bitangents, normals; + UnsignedInt id; + Matrix4 transformation; + Matrix3x3 expectedNormalTransformation; +} MeshData3DData[]{ + {"", false, false, false, false, false, 0, + Matrix4::translation({1.5f, 3.0f, -0.5f})* + Matrix4::rotationX(35.0_degf), + Matrix4::rotationX(35.0_degf).rotationScaling()}, + {"indexed", true, false, false, false, false, 0, + Matrix4::rotationX(35.0_degf), + Matrix4::rotationX(35.0_degf).rotationScaling()}, + {"normals", false, false, false, false, true, 0, + Matrix4::translation({1.5f, 3.0f, -0.5f})* + Matrix4::rotationX(35.0_degf), + Matrix4::rotationX(35.0_degf).rotationScaling()}, + {"tangents", false, true, false, false, false, 0, + Matrix4::translation({1.5f, 3.0f, -0.5f})* + Matrix4::rotationX(35.0_degf), + Matrix4::rotationX(35.0_degf).rotationScaling()}, + {"four-component tangents", false, true, true, false, false, 0, + Matrix4::translation({1.5f, 3.0f, -0.5f})* + Matrix4::rotationX(35.0_degf), + Matrix4::rotationX(35.0_degf).rotationScaling()}, + {"bitangents", false, false, false, true, false, 0, + Matrix4::translation({1.5f, 3.0f, -0.5f})* + Matrix4::rotationX(35.0_degf), + Matrix4::rotationX(35.0_degf).rotationScaling()}, + {"all + indexed", true, true, true, true, true, 0, + Matrix4::translation({1.5f, 3.0f, -0.5f})* + Matrix4::rotationX(35.0_degf), + Matrix4::rotationX(35.0_degf).rotationScaling()}, + {"second set ", false, false, false, false, false, 1, + Matrix4::rotationX(35.0_degf), + Matrix4::rotationX(35.0_degf).rotationScaling()}, + {"second set + normals", false, false, false, false, true, 1, + Matrix4::rotationX(35.0_degf), + Matrix4::rotationX(35.0_degf).rotationScaling()}, + {"second set + tangents", false, true, false, false, false, 1, + Matrix4::rotationX(35.0_degf), + Matrix4::rotationX(35.0_degf).rotationScaling()}, + {"second set + four-component tangents", false, true, true, false, false, 1, + Matrix4::rotationX(35.0_degf), + Matrix4::rotationX(35.0_degf).rotationScaling()}, + {"second set + bitangents", false, false, false, true, false, 1, + Matrix4::rotationX(35.0_degf), + Matrix4::rotationX(35.0_degf).rotationScaling()}, + {"second set + all + indexed", true, true, true, true, true, 1, + Matrix4::rotationX(35.0_degf), + Matrix4::rotationX(35.0_degf).rotationScaling()}, + {"non-uniform scaling", false, true, true, true, true, 0, + Matrix4::translation({1.5f, 3.0f, -0.5f})* + Matrix4::rotationX(35.0_degf)* + Matrix4::scaling({2.0f, 1.0f, 0.5f}), + (Matrix4::rotationX(35.0_degf)* + Matrix4::scaling({2.0f, 1.0f, 0.5f})).normalMatrix()}, + /** @todo negative scaling that flips face winding */ +}; + +const struct { + const char* name; + bool indexed; + bool tangents, tangents4, bitangents, normals; + UnsignedInt id; +} MeshData3DRvaluePassthroughData[]{ + {"", false, false, false, false, false, 0}, + {"indexed", true, false, false, false, false, 0}, + {"normals", false, false, false, false, true, 0}, + {"tangents", false, true, false, false, false, 0}, + {"four-component tangents", false, true, true, false, false, 0}, + {"bitangents", false, false, false, true, false, 0}, + {"all + indexed", true, true, true, true, true, 0}, + {"second set ", false, false, false, false, false, 1}, + {"second set + normals", false, false, false, false, true, 1}, + {"second set + tangents", false, true, false, false, false, 1}, + {"second set + four-component tangents", false, true, true, false, false, 1}, + {"second set + bitangents", false, false, false, true, false, 1}, + {"second set + all + indexed", true, true, true, true, true, 1}, +}; + +const struct { + const char* name; + VertexFormat positionFormat; + VertexFormat tangentFormat; + VertexFormat bitangentFormat; + VertexFormat normalFormat; + const char* message; +} MeshData3DWrongFormatData[]{ + {"packed positions", VertexFormat::Vector3b, + VertexFormat{}, VertexFormat{}, VertexFormat{}, + "MeshTools::transform3DInPlace(): expected VertexFormat::Vector3 positions but got VertexFormat::Vector3b\n"}, + {"packed normals", VertexFormat::Vector3, + VertexFormat{}, VertexFormat{}, VertexFormat::Vector3sNormalized, + "MeshTools::transform3DInPlace(): expected VertexFormat::Vector3 normals but got VertexFormat::Vector3sNormalized\n"}, + {"packed tangents", VertexFormat::Vector3, + VertexFormat::Vector3bNormalized, VertexFormat{}, VertexFormat{}, + "MeshTools::transform3DInPlace(): expected VertexFormat::Vector3 or VertexFormat::Vector4 tangents but got VertexFormat::Vector3bNormalized\n"}, + {"packed four-component tangents", VertexFormat::Vector3, + VertexFormat::Vector4bNormalized, VertexFormat{}, VertexFormat{}, + "MeshTools::transform3DInPlace(): expected VertexFormat::Vector3 or VertexFormat::Vector4 tangents but got VertexFormat::Vector4bNormalized\n"}, + {"packed bitangents", VertexFormat::Vector3, + VertexFormat{}, VertexFormat::Vector3h, VertexFormat{}, + "MeshTools::transform3DInPlace(): expected VertexFormat::Vector3 bitangents but got VertexFormat::Vector3h\n"} +}; + +const struct { + const char* name; + bool indexed; + UnsignedInt id; + Matrix3 transformation; +} MeshDataTextureCoordinatesData[]{ + {"", false, 0, + Matrix3::translation({1.5f, 3.0f})* + Matrix3::rotation(35.0_degf)}, + {"indexed", true, 0, + Matrix3::translation({1.5f, 3.0f})* + Matrix3::rotation(35.0_degf)}, + {"second set", false, 1, + Matrix3::translation({1.5f, 3.0f})* + Matrix3::rotation(35.0_degf)} +}; + +const struct { + const char* name; + bool indexed; + UnsignedInt id; +} MeshDataTextureCoordinatesRvaluePassthroughData[]{ + {"", false, 0}, + {"indexed", true, 0}, + {"second set", false, 1} }; TransformTest::TransformTest() { @@ -48,6 +262,70 @@ TransformTest::TransformTest() { &TransformTest::transformPoints2D, &TransformTest::transformPoints3D}); + + addInstancedTests({ + &TransformTest::meshData2D, + &TransformTest::meshData2D + }, Containers::arraySize(MeshData2DData)); + + addTests({&TransformTest::meshData2DNoPosition, + &TransformTest::meshData2DNot2D}); + + addInstancedTests({&TransformTest::meshData2DRvaluePassthrough}, + Containers::arraySize(MeshData2DRvaluePassthroughData)); + + addTests({&TransformTest::meshData2DRvaluePassthroughIndexDataNotOwned, + &TransformTest::meshData2DRvaluePassthroughVertexDataNotOwned, + &TransformTest::meshData2DRvaluePassthroughNoPosition, + &TransformTest::meshData2DRvaluePassthroughWrongFormat}); + + addTests({&TransformTest::meshData2DInPlaceNotMutable, + &TransformTest::meshData2DInPlaceNoPosition, + &TransformTest::meshData2DInPlaceWrongFormat}); + + addInstancedTests({ + &TransformTest::meshData3D, + &TransformTest::meshData3D, + &TransformTest::meshData3D, + }, Containers::arraySize(MeshData3DData)); + + addTests({&TransformTest::meshData3DNoPosition, + &TransformTest::meshData3DNot3D}); + + addInstancedTests({&TransformTest::meshData3DRvaluePassthrough}, + Containers::arraySize(MeshData3DRvaluePassthroughData)); + + addTests({&TransformTest::meshData3DRvaluePassthroughIndexDataNotOwned, + &TransformTest::meshData3DRvaluePassthroughVertexDataNotOwned, + &TransformTest::meshData3DRvaluePassthroughNoPosition}); + + addInstancedTests({&TransformTest::meshData3DRvaluePassthroughWrongFormat}, + Containers::arraySize(MeshData3DWrongFormatData)); + + addTests({&TransformTest::meshData3DInPlaceNotMutable, + &TransformTest::meshData3DInPlaceNoPosition}); + + addInstancedTests({&TransformTest::meshData3DInPlaceWrongFormat}, + Containers::arraySize(MeshData3DWrongFormatData)); + + addInstancedTests({ + &TransformTest::meshDataTextureCoordinates2D, + &TransformTest::meshDataTextureCoordinates2D + }, Containers::arraySize(MeshDataTextureCoordinatesData)); + + addTests({&TransformTest::meshDataTextureCoordinates2DNoCoordinates}); + + addInstancedTests({&TransformTest::meshDataTextureCoordinates2DRvaluePassthrough}, + Containers::arraySize(MeshDataTextureCoordinatesRvaluePassthroughData)); + + addTests({&TransformTest::meshDataTextureCoordinates2DRvaluePassthroughIndexDataNotOwned, + &TransformTest::meshDataTextureCoordinates2DRvaluePassthroughVertexDataNotOwned, + &TransformTest::meshDataTextureCoordinates2DRvaluePassthroughNoCoordinates, + &TransformTest::meshDataTextureCoordinates2DRvaluePassthroughWrongFormat}); + + addTests({&TransformTest::meshDataTextureCoordinates2DInPlaceNotMutable, + &TransformTest::meshDataTextureCoordinates2DInPlaceNoCoordinates, + &TransformTest::meshDataTextureCoordinates2DInPlaceWrongFormat}); } constexpr static std::array points2D{{ @@ -116,6 +394,1180 @@ void TransformTest::transformPoints3D() { CORRADE_COMPARE(quaternion, points3DRotatedTranslated); } +template void TransformTest::meshData2D() { + auto&& data = MeshData2DData[testCaseInstanceId()]; + setTestCaseTemplateName(Math::TypeTraits::name()); + setTestCaseDescription(data.name); + + const UnsignedShort indices[]{ + 1, 2, 0 + }; + const struct Vertex { + Vector2b secondaryPosition; + Math::Vector2 position; + Float somethingElse; + } vertices[]{ + {{15, -34}, {T(0.0), T(0.0)}, 7.0f}, + {{11, -25}, {T(1.0), T(0.0)}, 5.5f}, + {{16, -27}, {T(0.0), T(2.0)}, 3.0f} + }; + Containers::StridedArrayView1D view = vertices; + + Containers::Array attributes; + if(data.id == 1) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Position, view.slice(&Vertex::secondaryPosition)}); + else CORRADE_COMPARE(data.id, 0); + arrayAppend(attributes, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, view.slice(&Vertex::position)}, + Trade::MeshAttributeData{Trade::meshAttributeCustom(0), view.slice(&Vertex::somethingElse)} + }); + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + {}, data.indexed ? Containers::arrayView(indices) : nullptr, + data.indexed ? Trade::MeshIndexData{indices} : Trade::MeshIndexData{nullptr}, + {}, vertices, std::move(attributes)}; + CORRADE_COMPARE(mesh.isIndexed(), data.indexed); + + Trade::MeshData out = transform2D(mesh, data.transformation, data.id); + + /* Indices should be preserved */ + if(data.indexed) { + CORRADE_VERIFY(out.isIndexed()); + CORRADE_COMPARE_AS(out.indices(), + Containers::arrayView(indices), + TestSuite::Compare::Container); + } + + /* The extra attribute should be preserved */ + CORRADE_COMPARE(out.attributeCount(), mesh.attributeCount()); + CORRADE_COMPARE_AS(out.attribute(Trade::meshAttributeCustom(0)), + view.slice(&Vertex::somethingElse), + TestSuite::Compare::Container); + + /* The vertices should be expanded to floats and transformed */ + CORRADE_COMPARE(out.attributeFormat(Trade::MeshAttribute::Position, data.id), VertexFormat::Vector2); + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Position, data.id), Containers::arrayView({ + data.transformation.transformPoint({0.0f, 0.0f}), + data.transformation.transformPoint({1.0f, 0.0f}), + data.transformation.transformPoint({0.0f, 2.0f}) + }), TestSuite::Compare::Container); +} + +void TransformTest::meshData2DNoPosition() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Trade::MeshData mesh{MeshPrimitive::Points, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, VertexFormat::Vector2, nullptr}, + }}; + + std::ostringstream out; + Error redirectError{&out}; + transform2D(mesh, {}, 1); + CORRADE_COMPARE(out.str(), "MeshTools::transform2D(): the mesh has no positions with index 1\n"); +} + +void TransformTest::meshData2DNot2D() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Trade::MeshData mesh{MeshPrimitive::Points, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, VertexFormat::Vector3, nullptr} + }}; + + std::ostringstream out; + Error redirectError{&out}; + transform2D(mesh, {}); + CORRADE_COMPARE(out.str(), "MeshTools::transform2D(): expected 2D positions but got VertexFormat::Vector3\n"); +} + +void TransformTest::meshData2DRvaluePassthrough() { + auto&& data = MeshData2DRvaluePassthroughData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Array indexData{NoInit, 3*sizeof(UnsignedShort)}; + auto indices = Containers::arrayCast(indexData); + /* Not using copy({...}) as we'd have no way to compare against the + original unchanged data */ + const UnsignedShort indicesExpected[]{1, 2, 0}; + Utility::copy(indicesExpected, indices); + + struct Vertex { + Vector2b secondaryPosition; + Vector2 position; + }; + Containers::Array vertexData{NoInit, 3*sizeof(Vertex)}; + auto vertices = stridedArrayView(Containers::arrayCast(vertexData)); + Utility::copy({ + {{15, -34}, {0.0f, 0.0f}}, + {{11, -25}, {1.0f, 0.0f}}, + {{16, -27}, {0.0f, 2.0f}} + }, vertices); + + Containers::Array attributes; + if(data.id == 1) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Position, vertices.slice(&Vertex::secondaryPosition)}); + else CORRADE_COMPARE(data.id, 0); + arrayAppend(attributes, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, vertices.slice(&Vertex::position)} + }); + const void* originalAttributeData = attributes; + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + data.indexed ? std::move(indexData) : nullptr, + data.indexed ? Trade::MeshIndexData{indices} : Trade::MeshIndexData{nullptr}, + std::move(vertexData), std::move(attributes)}; + + const Matrix3 transformation = Matrix3::rotation(35.0_degf); + Trade::MeshData out = transform2D(std::move(mesh), transformation, data.id); + + /* Indices should be passed through unchanged */ + if(data.indexed) { + CORRADE_VERIFY(out.isIndexed()); + CORRADE_COMPARE_AS(out.indices(), + Containers::arrayView(indicesExpected), + TestSuite::Compare::Container); + } + + /* The vertices should be expanded to floats and transformed */ + CORRADE_COMPARE(out.attributeFormat(Trade::MeshAttribute::Position, data.id), VertexFormat::Vector2); + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Position, data.id), Containers::arrayView({ + transformation.transformPoint({0.0f, 0.0f}), + transformation.transformPoint({1.0f, 0.0f}), + transformation.transformPoint({0.0f, 2.0f}) + }), TestSuite::Compare::Container); + + /* The memory should point to the original data */ + if(data.indexed) + CORRADE_COMPARE(out.indexData().data(), static_cast(indices.data())); + CORRADE_COMPARE(out.vertexData().data(), static_cast(vertices.data())); + CORRADE_COMPARE(out.attributeData().data(), originalAttributeData); +} + +void TransformTest::meshData2DRvaluePassthroughIndexDataNotOwned() { + const UnsignedShort indices[]{ + 1, 2, 0 + }; + Containers::Array vertexData{NoInit, 3*sizeof(Vector2)}; + auto vertices = Containers::arrayCast(vertexData); + Utility::copy({ + {0.0f, 0.0f}, + {1.0f, 0.0f}, + {0.0f, 2.0f} + }, vertices); + + Containers::Array attributes{InPlaceInit, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, vertices} + }}; + const void* originalAttributeData = attributes; + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + {}, indices, Trade::MeshIndexData{indices}, + std::move(vertexData), std::move(attributes)}; + + const Matrix3 transformation = Matrix3::rotation(35.0_degf); + Trade::MeshData out = transform2D(std::move(mesh), transformation); + + /* Indices should be passed through unchanged */ + CORRADE_VERIFY(out.isIndexed()); + CORRADE_COMPARE_AS(out.indices(), + Containers::arrayView(indices), + TestSuite::Compare::Container); + + /* The vertices should be transformed */ + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Position), Containers::arrayView({ + transformation.transformPoint({0.0f, 0.0f}), + transformation.transformPoint({1.0f, 0.0f}), + transformation.transformPoint({0.0f, 2.0f}) + }), TestSuite::Compare::Container); + + /* The memory should not point to the original data */ + CORRADE_VERIFY(out.indexData().data() != static_cast(indices)); + CORRADE_VERIFY(out.vertexData().data() != static_cast(vertices.data())); + CORRADE_VERIFY(out.attributeData().data() != static_cast(originalAttributeData)); +} + +void TransformTest::meshData2DRvaluePassthroughVertexDataNotOwned() { + Containers::Array indexData{NoInit, 3*sizeof(UnsignedShort)}; + auto indices = Containers::arrayCast(indexData); + /* Not using copy({...}) as we'd have no way to compare against the + original unchanged data */ + const UnsignedShort indicesExpected[]{1, 2, 0}; + Utility::copy(indicesExpected, indices); + const Vector2 vertices[]{ + {0.0f, 0.0f}, + {1.0f, 0.0f}, + {0.0f, 2.0f} + }; + + Containers::Array attributes{InPlaceInit, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, Containers::arrayView(vertices)} + }}; + const void* originalAttributeData = attributes; + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + std::move(indexData), Trade::MeshIndexData{indices}, + {}, vertices, std::move(attributes)}; + + const Matrix3 transformation = Matrix3::rotation(35.0_degf); + Trade::MeshData out = transform2D(std::move(mesh), transformation); + + /* Indices should be passed through unchanged */ + CORRADE_VERIFY(out.isIndexed()); + CORRADE_COMPARE_AS(out.indices(), + Containers::arrayView(indices), + TestSuite::Compare::Container); + + /* The vertices should be transformed */ + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Position), Containers::arrayView({ + transformation.transformPoint({0.0f, 0.0f}), + transformation.transformPoint({1.0f, 0.0f}), + transformation.transformPoint({0.0f, 2.0f}) + }), TestSuite::Compare::Container); + + /* The memory should not point to the original data */ + CORRADE_VERIFY(out.indexData().data() != static_cast(indices.data())); + CORRADE_VERIFY(out.vertexData().data() != static_cast(vertices)); + CORRADE_VERIFY(out.attributeData().data() != static_cast(originalAttributeData)); +} + +void TransformTest::meshData2DRvaluePassthroughNoPosition() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + /* Mainly to verify there's no other accidental assertion from checking + vertex format, this message comes from the l-value overload */ + std::ostringstream out; + Error redirectError{&out}; + transform2D(Trade::MeshData{MeshPrimitive::Points, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, VertexFormat::Vector2, nullptr}, + }}, {}, 1); + CORRADE_COMPARE(out.str(), "MeshTools::transform2D(): the mesh has no positions with index 1\n"); +} + +void TransformTest::meshData2DRvaluePassthroughWrongFormat() { + Containers::Array vertexData{NoInit, 3*sizeof(Vector2ub)}; + auto vertices = Containers::arrayCast(vertexData); + Utility::copy({ + {0, 0}, + {1, 0}, + {0, 2} + }, vertices); + + Containers::Array attributes{InPlaceInit, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, vertices} + }}; + const void* originalAttributeData = attributes; + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + std::move(vertexData), std::move(attributes)}; + + const Matrix3 transformation = Matrix3::rotation(35.0_degf); + Trade::MeshData out = transform2D(std::move(mesh), transformation); + + /* The vertices should be expanded to floats and transformed */ + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Position), Containers::arrayView({ + transformation.transformPoint({0.0f, 0.0f}), + transformation.transformPoint({1.0f, 0.0f}), + transformation.transformPoint({0.0f, 2.0f}) + }), TestSuite::Compare::Container); + + /* The memory should not point to the original data */ + CORRADE_VERIFY(out.vertexData().data() != static_cast(vertices.data())); + CORRADE_VERIFY(out.attributeData().data() != static_cast(originalAttributeData)); +} + +void TransformTest::meshData2DInPlaceNotMutable() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Trade::MeshData mesh{MeshPrimitive::Points, Trade::DataFlags{}, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, VertexFormat::Vector2, nullptr}, + }}; + + std::ostringstream out; + Error redirectError{&out}; + transform2DInPlace(mesh, {}); + CORRADE_COMPARE(out.str(), "MeshTools::transform2DInPlace(): vertex data not mutable\n"); +} + +void TransformTest::meshData2DInPlaceNoPosition() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Trade::MeshData mesh{MeshPrimitive::Points, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, VertexFormat::Vector2, nullptr}, + }}; + + std::ostringstream out; + Error redirectError{&out}; + transform2DInPlace(mesh, {}, 1); + CORRADE_COMPARE(out.str(), "MeshTools::transform2DInPlace(): the mesh has no positions with index 1\n"); +} + +void TransformTest::meshData2DInPlaceWrongFormat() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Trade::MeshData mesh{MeshPrimitive::Points, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, VertexFormat::Vector2, nullptr}, + Trade::MeshAttributeData{Trade::MeshAttribute::Position, VertexFormat::Vector2us, nullptr} + }}; + + std::ostringstream out; + Error redirectError{&out}; + transform2DInPlace(mesh, {}, 1); + CORRADE_COMPARE(out.str(), "MeshTools::transform2DInPlace(): expected VertexFormat::Vector2 positions but got VertexFormat::Vector2us\n"); +} + +template void TransformTest::meshData3D() { + auto&& data = MeshData3DData[testCaseInstanceId()]; + setTestCaseTemplateName({Math::TypeTraits::name(), + Math::TypeTraits::name(), + Math::TypeTraits::name(), + Math::TypeTraits::name()}); + setTestCaseDescription(data.name); + + const UnsignedShort indices[]{ + 1, 2, 0 + }; + const struct Vertex { + Vector3b secondaryPositionTangentBitangentNormal; + Math::Vector3 position; + Math::Vector4 tangent; + Math::Vector3 bitangent; + Math::Vector3 normal; + Float somethingElse; + } vertices[]{ + /** @todo use some *real* values here to verify the handedness change + also */ + {{15, -34, 6}, {T(0.0), T(0.0), T(-1.0)}, + {U(0.0), U(1.0), U(0.0), U(1.0)}, + {V(-1.0), V(0.0), V(0.0)}, + {W(0.0), W(0.0), W(1.0)}, 7.0f}, + {{11, -25, 3}, {T(1.0), T(0.0), T(-2.0)}, + {U(1.0), U(0.0), U(0.0), U(1.0)}, + {V(0.0), V(-1.0), V(0.0)}, + {W(0.0), W(0.0), W(1.0)}, 5.5f}, + {{16, -27, 0}, {T(0.0), T(2.0), T(-1.0)}, + {U(0.0), U(1.0), U(0.0), U(-1.0)}, + {V(0.0), V(0.0), V(-1.0)}, + {W(1.0), W(0.0), W(0.0)}, 3.0f} + }; + Containers::StridedArrayView1D view = vertices; + + Containers::Array attributes; + if(data.id == 1) { + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Position, view.slice(&Vertex::secondaryPositionTangentBitangentNormal)}); + if(data.tangents) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Tangent, VertexFormat::Vector3bNormalized, view.slice(&Vertex::secondaryPositionTangentBitangentNormal)}); + if(data.bitangents) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Bitangent, VertexFormat::Vector3bNormalized, view.slice(&Vertex::secondaryPositionTangentBitangentNormal)}); + if(data.normals) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Normal, VertexFormat::Vector3bNormalized, view.slice(&Vertex::secondaryPositionTangentBitangentNormal)}); + } else CORRADE_COMPARE(data.id, 0); + arrayAppend(attributes, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, view.slice(&Vertex::position)}, + Trade::MeshAttributeData{Trade::meshAttributeCustom(0), view.slice(&Vertex::somethingElse)} + }); + if(data.tangents) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Tangent, data.tangents4 ? Trade::Implementation::vertexFormatFor>() : Trade::Implementation::vertexFormatFor>(), view.slice(&Vertex::tangent)}); + if(data.bitangents) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Bitangent, view.slice(&Vertex::bitangent)}); + if(data.normals) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Normal, view.slice(&Vertex::normal)}); + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + {}, data.indexed ? Containers::arrayView(indices) : nullptr, + data.indexed ? Trade::MeshIndexData{indices} : Trade::MeshIndexData{nullptr}, + {}, vertices, std::move(attributes)}; + CORRADE_COMPARE(mesh.isIndexed(), data.indexed); + + Trade::MeshData out = transform3D(mesh, data.transformation, data.id); + + /* Indices should be preserved */ + if(data.indexed) { + CORRADE_VERIFY(out.isIndexed()); + CORRADE_COMPARE_AS(out.indices(), + Containers::arrayView(indices), + TestSuite::Compare::Container); + } + + /* The extra attribute should be preserved */ + CORRADE_COMPARE(out.attributeCount(), mesh.attributeCount()); + CORRADE_COMPARE_AS(out.attribute(Trade::meshAttributeCustom(0)), + view.slice(&Vertex::somethingElse), + TestSuite::Compare::Container); + + /* The vertices should be expanded to floats and transformed */ + CORRADE_COMPARE(out.attributeFormat(Trade::MeshAttribute::Position, data.id), VertexFormat::Vector3); + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Position, data.id), Containers::arrayView({ + data.transformation.transformPoint({0.0f, 0.0f, -1.0f}), + data.transformation.transformPoint({1.0f, 0.0f, -2.0f}), + data.transformation.transformPoint({0.0f, 2.0f, -1.0f}) + }), TestSuite::Compare::Container); + + /** @todo verify it stays orthogonal */ + const Matrix3x3 normalMatrix = data.transformation.normalMatrix(); + + if(data.tangents) { + CORRADE_COMPARE(out.attributeFormat(Trade::MeshAttribute::Tangent, data.id), data.tangents4 ? VertexFormat::Vector4 : VertexFormat::Vector3); + if(data.tangents4) + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Tangent, data.id), Containers::arrayView({ + {normalMatrix*Vector3{0.0f, 1.0f, 0.0f}, 1.0f}, + {normalMatrix*Vector3{1.0f, 0.0f, 0.0f}, 1.0f}, + {normalMatrix*Vector3{0.0f, 1.0f, 0.0f}, -1.0f} + }), TestSuite::Compare::Container); + else + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Tangent, data.id), Containers::arrayView({ + normalMatrix*Vector3{0.0f, 1.0f, 0.0f}, + normalMatrix*Vector3{1.0f, 0.0f, 0.0f}, + normalMatrix*Vector3{0.0f, 1.0f, 0.0f} + }), TestSuite::Compare::Container); + } + + if(data.bitangents) { + CORRADE_COMPARE(out.attributeFormat(Trade::MeshAttribute::Bitangent, data.id), VertexFormat::Vector3); + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Bitangent, data.id), Containers::arrayView({ + normalMatrix*Vector3{-1.0f, 0.0f, 0.0f}, + normalMatrix*Vector3{0.0f, -1.0f, 0.0f}, + normalMatrix*Vector3{0.0f, 0.0f, -1.0f} + }), TestSuite::Compare::Container); + } + + if(data.normals) { + CORRADE_COMPARE(out.attributeFormat(Trade::MeshAttribute::Normal, data.id), VertexFormat::Vector3); + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Normal, data.id), Containers::arrayView({ + normalMatrix*Vector3{0.0f, 0.0f, 1.0f}, + normalMatrix*Vector3{0.0f, 0.0f, 1.0f}, + normalMatrix*Vector3{1.0f, 0.0f, 0.0f} + }), TestSuite::Compare::Container); + } +} + +void TransformTest::meshData3DNoPosition() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Trade::MeshData mesh{MeshPrimitive::Points, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, VertexFormat::Vector3, nullptr}, + }}; + + std::ostringstream out; + Error redirectError{&out}; + transform3D(mesh, {}, 1); + CORRADE_COMPARE(out.str(), "MeshTools::transform3D(): the mesh has no positions with index 1\n"); +} + +void TransformTest::meshData3DNot3D() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Trade::MeshData mesh{MeshPrimitive::Points, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, VertexFormat::Vector2, nullptr} + }}; + + std::ostringstream out; + Error redirectError{&out}; + transform3D(mesh, {}); + CORRADE_COMPARE(out.str(), "MeshTools::transform3D(): expected 3D positions but got VertexFormat::Vector2\n"); +} + +void TransformTest::meshData3DRvaluePassthrough() { + auto&& data = MeshData3DRvaluePassthroughData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Array indexData{NoInit, 3*sizeof(UnsignedShort)}; + auto indices = Containers::arrayCast(indexData); + /* Not using copy({...}) as we'd have no way to compare against the + original unchanged data */ + const UnsignedShort indicesExpected[]{1, 2, 0}; + Utility::copy(indicesExpected, indices); + + struct Vertex { + Vector3b secondaryPositionTangentBitangentNormal; + Vector3 position; + Vector4 tangent; + Vector3 bitangent; + Vector3 normal; + }; + Containers::Array vertexData{NoInit, 3*sizeof(Vertex)}; + auto vertices = stridedArrayView(Containers::arrayCast(vertexData)); + Utility::copy({ + {{15, -34, 6}, {0.0f, 0.0f, -1.0f}, + {0.0f, 1.0f, 0.0f, 1.0f}, + {-1.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f}}, + {{11, -25, 3}, {1.0f, 0.0f, -2.0f}, + {1.0f, 0.0f, 0.0f, 1.0f}, + {0.0f, -1.0f, 0.0f}, + {0.0f, 0.0f, 1.0f}}, + {{16, -27, 0}, {0.0f, 2.0f, -1.0f}, + {0.0f, 1.0f, 0.0f, -1.0f}, + {0.0f, 0.0f, -1.0f}, + {1.0f, 0.0f, 0.0f}} + }, vertices); + + Containers::Array attributes; + if(data.id == 1) { + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Position, vertices.slice(&Vertex::secondaryPositionTangentBitangentNormal)}); + if(data.tangents) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Tangent, VertexFormat::Vector3bNormalized, vertices.slice(&Vertex::secondaryPositionTangentBitangentNormal)}); + if(data.bitangents) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Bitangent, VertexFormat::Vector3bNormalized, vertices.slice(&Vertex::secondaryPositionTangentBitangentNormal)}); + if(data.normals) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Normal, VertexFormat::Vector3bNormalized, vertices.slice(&Vertex::secondaryPositionTangentBitangentNormal)}); + } else CORRADE_COMPARE(data.id, 0); + arrayAppend(attributes, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, vertices.slice(&Vertex::position)} + }); + if(data.tangents) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Tangent, data.tangents4 ? VertexFormat::Vector4 : VertexFormat::Vector3, vertices.slice(&Vertex::tangent)}); + if(data.bitangents) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Bitangent, vertices.slice(&Vertex::bitangent)}); + if(data.normals) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Normal, vertices.slice(&Vertex::normal)}); + const void* originalAttributeData = attributes; + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + data.indexed ? std::move(indexData) : nullptr, + data.indexed ? Trade::MeshIndexData{indices} : Trade::MeshIndexData{nullptr}, + std::move(vertexData), std::move(attributes)}; + + const Matrix4 transformation = Matrix4::rotationX(35.0_degf); + Trade::MeshData out = transform3D(std::move(mesh), transformation, data.id); + + /* Indices should be passed through unchanged */ + if(data.indexed) { + CORRADE_VERIFY(out.isIndexed()); + CORRADE_COMPARE_AS(out.indices(), + Containers::arrayView(indicesExpected), + TestSuite::Compare::Container); + } + + /* The vertices should be expanded to floats and transformed */ + CORRADE_COMPARE(out.attributeFormat(Trade::MeshAttribute::Position, data.id), VertexFormat::Vector3); + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Position, data.id), Containers::arrayView({ + transformation.transformPoint({0.0f, 0.0f, -1.0f}), + transformation.transformPoint({1.0f, 0.0f, -2.0f}), + transformation.transformPoint({0.0f, 2.0f, -1.0f}) + }), TestSuite::Compare::Container); + + if(data.tangents) { + CORRADE_COMPARE(out.attributeFormat(Trade::MeshAttribute::Tangent, data.id), data.tangents4 ? VertexFormat::Vector4 : VertexFormat::Vector3); + if(data.tangents4) + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Tangent, data.id), Containers::arrayView({ + {transformation.transformVector({0.0f, 1.0f, 0.0f}), 1.0f}, + {transformation.transformVector({1.0f, 0.0f, 0.0f}), 1.0f}, + {transformation.transformVector({0.0f, 1.0f, 0.0f}), -1.0f} + }), TestSuite::Compare::Container); + else + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Tangent, data.id), Containers::arrayView({ + transformation.transformVector({0.0f, 1.0f, 0.0f}), + transformation.transformVector({1.0f, 0.0f, 0.0f}), + transformation.transformVector({0.0f, 1.0f, 0.0f}) + }), TestSuite::Compare::Container); + } + + if(data.bitangents) { + CORRADE_COMPARE(out.attributeFormat(Trade::MeshAttribute::Bitangent, data.id), VertexFormat::Vector3); + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Bitangent, data.id), Containers::arrayView({ + transformation.transformVector({-1.0f, 0.0f, 0.0f}), + transformation.transformVector({0.0f, -1.0f, 0.0f}), + transformation.transformVector({0.0f, 0.0f, -1.0f}) + }), TestSuite::Compare::Container); + } + + if(data.normals) { + CORRADE_COMPARE(out.attributeFormat(Trade::MeshAttribute::Normal, data.id), VertexFormat::Vector3); + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Normal, data.id), Containers::arrayView({ + transformation.transformVector({0.0f, 0.0f, 1.0f}), + transformation.transformVector({0.0f, 0.0f, 1.0f}), + transformation.transformVector({1.0f, 0.0f, 0.0f}) + }), TestSuite::Compare::Container); + } + + /* The memory should point to the original data */ + if(data.indexed) + CORRADE_COMPARE(out.indexData().data(), static_cast(indices.data())); + CORRADE_COMPARE(out.vertexData().data(), static_cast(vertices.data())); + CORRADE_COMPARE(out.attributeData().data(), originalAttributeData); +} + +void TransformTest::meshData3DRvaluePassthroughIndexDataNotOwned() { + const UnsignedShort indices[]{ + 1, 2, 0 + }; + Containers::Array vertexData{NoInit, 3*sizeof(Vector3)}; + auto vertices = Containers::arrayCast(vertexData); + Utility::copy({ + {0.0f, 0.0f, -1.0f}, + {1.0f, 0.0f, -2.0f}, + {0.0f, 2.0f, -1.0f} + }, vertices); + + Containers::Array attributes{InPlaceInit, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, vertices} + }}; + const void* originalAttributeData = attributes; + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + {}, indices, Trade::MeshIndexData{indices}, + std::move(vertexData), std::move(attributes)}; + + const Matrix4 transformation = Matrix4::rotationX(35.0_degf); + Trade::MeshData out = transform3D(std::move(mesh), transformation); + + /* Indices should be passed through unchanged */ + CORRADE_VERIFY(out.isIndexed()); + CORRADE_COMPARE_AS(out.indices(), + Containers::arrayView(indices), + TestSuite::Compare::Container); + + /* The vertices should be transformed */ + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Position), Containers::arrayView({ + transformation.transformPoint({0.0f, 0.0f, -1.0f}), + transformation.transformPoint({1.0f, 0.0f, -2.0f}), + transformation.transformPoint({0.0f, 2.0f, -1.0f}) + }), TestSuite::Compare::Container); + + /* The memory should not point to the original data */ + CORRADE_VERIFY(out.indexData().data() != static_cast(indices)); + CORRADE_VERIFY(out.vertexData().data() != static_cast(vertices.data())); + CORRADE_VERIFY(out.attributeData().data() != static_cast(originalAttributeData)); +} + +void TransformTest::meshData3DRvaluePassthroughVertexDataNotOwned() { + Containers::Array indexData{NoInit, 3*sizeof(UnsignedShort)}; + auto indices = Containers::arrayCast(indexData); + /* Not using copy({...}) as we'd have no way to compare against the + original unchanged data */ + const UnsignedShort indicesExpected[]{1, 2, 0}; + Utility::copy(indicesExpected, indices); + const Vector3 vertices[]{ + {0.0f, 0.0f, -1.0f}, + {1.0f, 0.0f, -2.0f}, + {0.0f, 2.0f, -1.0f} + }; + + Containers::Array attributes{InPlaceInit, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, Containers::arrayView(vertices)} + }}; + const void* originalAttributeData = attributes; + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + std::move(indexData), Trade::MeshIndexData{indices}, + {}, vertices, std::move(attributes)}; + + const Matrix4 transformation = Matrix4::rotationX(35.0_degf); + Trade::MeshData out = transform3D(std::move(mesh), transformation); + + /* Indices should be passed through unchanged */ + CORRADE_VERIFY(out.isIndexed()); + CORRADE_COMPARE_AS(out.indices(), + Containers::arrayView(indices), + TestSuite::Compare::Container); + + /* The vertices should be transformed */ + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Position), Containers::arrayView({ + transformation.transformPoint({0.0f, 0.0f, -1.0f}), + transformation.transformPoint({1.0f, 0.0f, -2.0f}), + transformation.transformPoint({0.0f, 2.0f, -1.0f}) + }), TestSuite::Compare::Container); + + /* The memory should not point to the original data */ + CORRADE_VERIFY(out.indexData().data() != static_cast(indices.data())); + CORRADE_VERIFY(out.vertexData().data() != static_cast(vertices)); + CORRADE_VERIFY(out.attributeData().data() != static_cast(originalAttributeData)); +} + +void TransformTest::meshData3DRvaluePassthroughNoPosition() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + /* Mainly to verify there's no other accidental assertion from checking + vertex format, this message comes from the l-value overload */ + std::ostringstream out; + Error redirectError{&out}; + transform3D(Trade::MeshData{MeshPrimitive::Points, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, VertexFormat::Vector3, nullptr}, + }}, {}, 1); + CORRADE_COMPARE(out.str(), "MeshTools::transform3D(): the mesh has no positions with index 1\n"); +} + +void TransformTest::meshData3DRvaluePassthroughWrongFormat() { + auto&& data = MeshData3DWrongFormatData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + struct Vertex { + Vector3b position; + Vector4b tangent; + Vector3h bitangent; + Vector3s normal; + }; + Containers::Array vertexData{NoInit, 3*sizeof(Vertex)}; + auto vertices = stridedArrayView(Containers::arrayCast(vertexData)); + Utility::copy({ + {{0, 0, -1}, + {0, 127, 0, 127}, + {-1.0_h, 0.0_h, 0.0_h}, + {0, 0, 32767}}, + {{1, 0, -2}, + {127, 0, 0, 127}, + {0.0_h, -1.0_h, 0.0_h}, + {0, 0, 32767}}, + {{0, 2, -1}, + {0, 127, 0, -127}, + {0.0_h, 0.0_h, -1.0_h}, + {32767, 0, 0}} + }, vertices); + + Containers::Array attributes{InPlaceInit, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, vertices.slice(&Vertex::position)} + }}; + if(data.tangentFormat != VertexFormat{}) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Tangent, vertexFormatComponentCount(data.tangentFormat) == 4 ? VertexFormat::Vector4bNormalized : VertexFormat::Vector3bNormalized, vertices.slice(&Vertex::tangent)}); + if(data.bitangentFormat != VertexFormat{}) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Bitangent, vertices.slice(&Vertex::bitangent)}); + if(data.normalFormat != VertexFormat{}) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Normal, VertexFormat::Vector3sNormalized, vertices.slice(&Vertex::normal)}); + const void* originalAttributeData = attributes; + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + std::move(vertexData), std::move(attributes)}; + + const Matrix4 transformation = Matrix4::rotationX(35.0_degf); + Trade::MeshData out = transform3D(std::move(mesh), transformation); + + /* The vertices should be expanded to floats and transformed */ + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Position), Containers::arrayView({ + transformation.transformPoint({0.0f, 0.0f, -1.0f}), + transformation.transformPoint({1.0f, 0.0f, -2.0f}), + transformation.transformPoint({0.0f, 2.0f, -1.0f}) + }), TestSuite::Compare::Container); + + if(data.tangentFormat != VertexFormat{}) { + if(vertexFormatComponentCount(data.tangentFormat) == 4) + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Tangent), Containers::arrayView({ + {transformation.transformVector({0.0f, 1.0f, 0.0f}), 1.0f}, + {transformation.transformVector({1.0f, 0.0f, 0.0f}), 1.0f}, + {transformation.transformVector({0.0f, 1.0f, 0.0f}), -1.0f} + }), TestSuite::Compare::Container); + else + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Tangent), Containers::arrayView({ + transformation.transformVector({0.0f, 1.0f, 0.0f}), + transformation.transformVector({1.0f, 0.0f, 0.0f}), + transformation.transformVector({0.0f, 1.0f, 0.0f}) + }), TestSuite::Compare::Container); + } + + if(data.bitangentFormat != VertexFormat{}) { + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Bitangent), Containers::arrayView({ + transformation.transformVector({-1.0f, 0.0f, 0.0f}), + transformation.transformVector({0.0f, -1.0f, 0.0f}), + transformation.transformVector({0.0f, 0.0f, -1.0f}) + }), TestSuite::Compare::Container); + } + + if(data.normalFormat != VertexFormat{}) { + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::Normal), Containers::arrayView({ + transformation.transformVector({0.0f, 0.0f, 1.0f}), + transformation.transformVector({0.0f, 0.0f, 1.0f}), + transformation.transformVector({1.0f, 0.0f, 0.0f}) + }), TestSuite::Compare::Container); + } + + /* The memory should not point to the original data */ + CORRADE_VERIFY(out.vertexData().data() != static_cast(vertices.data())); + CORRADE_VERIFY(out.attributeData().data() != static_cast(originalAttributeData)); +} + +void TransformTest::meshData3DInPlaceNotMutable() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Trade::MeshData mesh{MeshPrimitive::Points, Trade::DataFlags{}, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, VertexFormat::Vector3, nullptr}, + }}; + + std::ostringstream out; + Error redirectError{&out}; + transform3DInPlace(mesh, {}); + CORRADE_COMPARE(out.str(), "MeshTools::transform3DInPlace(): vertex data not mutable\n"); +} + +void TransformTest::meshData3DInPlaceNoPosition() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Trade::MeshData mesh{MeshPrimitive::Points, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, VertexFormat::Vector3, nullptr}, + }}; + + std::ostringstream out; + Error redirectError{&out}; + transform3DInPlace(mesh, {}, 1); + CORRADE_COMPARE(out.str(), "MeshTools::transform3DInPlace(): the mesh has no positions with index 1\n"); +} + +void TransformTest::meshData3DInPlaceWrongFormat() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + auto&& data = MeshData3DWrongFormatData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Array attributes{InPlaceInit, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, VertexFormat::Vector3, nullptr}, + Trade::MeshAttributeData{Trade::MeshAttribute::Tangent, VertexFormat::Vector3, nullptr}, + Trade::MeshAttributeData{Trade::MeshAttribute::Bitangent, VertexFormat::Vector3, nullptr}, + Trade::MeshAttributeData{Trade::MeshAttribute::Normal, VertexFormat::Vector3, nullptr}, + Trade::MeshAttributeData{Trade::MeshAttribute::Position, data.positionFormat, nullptr}, + }}; + if(data.tangentFormat != VertexFormat{}) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Tangent, data.tangentFormat, nullptr}); + if(data.bitangentFormat != VertexFormat{}) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Bitangent, data.bitangentFormat, nullptr}); + if(data.normalFormat != VertexFormat{}) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::Normal, data.normalFormat, nullptr}); + + Trade::MeshData mesh{MeshPrimitive::Points, nullptr, std::move(attributes)}; + + std::ostringstream out; + Error redirectError{&out}; + transform3DInPlace(mesh, {}, 1); + CORRADE_COMPARE(out.str(), data.message); +} + +template void TransformTest::meshDataTextureCoordinates2D() { + auto&& data = MeshDataTextureCoordinatesData[testCaseInstanceId()]; + setTestCaseTemplateName(Math::TypeTraits::name()); + setTestCaseDescription(data.name); + + const UnsignedShort indices[]{ + 1, 2, 0 + }; + const struct Vertex { + Vector2us secondaryTextureCoordinates; + Math::Vector2 textureCoordinates; + Float somethingElse; + } vertices[]{ + {{15, 34}, {T(0.0), T(0.0)}, 7.0f}, + {{11, 25}, {T(1.0), T(0.0)}, 5.5f}, + {{16, 27}, {T(0.0), T(2.0)}, 3.0f} + }; + Containers::StridedArrayView1D view = vertices; + + Containers::Array attributes; + if(data.id == 1) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, view.slice(&Vertex::secondaryTextureCoordinates)}); + else CORRADE_COMPARE(data.id, 0); + arrayAppend(attributes, { + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, view.slice(&Vertex::textureCoordinates)}, + Trade::MeshAttributeData{Trade::meshAttributeCustom(0), view.slice(&Vertex::somethingElse)} + }); + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + {}, data.indexed ? Containers::arrayView(indices) : nullptr, + data.indexed ? Trade::MeshIndexData{indices} : Trade::MeshIndexData{nullptr}, + {}, vertices, std::move(attributes)}; + CORRADE_COMPARE(mesh.isIndexed(), data.indexed); + + Trade::MeshData out = transformTextureCoordinates2D(mesh, data.transformation, data.id); + + /* Indices should be preserved */ + if(data.indexed) { + CORRADE_VERIFY(out.isIndexed()); + CORRADE_COMPARE_AS(out.indices(), + Containers::arrayView(indices), + TestSuite::Compare::Container); + } + + /* The extra attribute should be preserved */ + CORRADE_COMPARE(out.attributeCount(), mesh.attributeCount()); + CORRADE_COMPARE_AS(out.attribute(Trade::meshAttributeCustom(0)), + view.slice(&Vertex::somethingElse), + TestSuite::Compare::Container); + + /* The vertices should be expanded to floats and transformed */ + CORRADE_COMPARE(out.attributeFormat(Trade::MeshAttribute::TextureCoordinates, data.id), VertexFormat::Vector2); + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::TextureCoordinates, data.id), Containers::arrayView({ + data.transformation.transformPoint({0.0f, 0.0f}), + data.transformation.transformPoint({1.0f, 0.0f}), + data.transformation.transformPoint({0.0f, 2.0f}) + }), TestSuite::Compare::Container); +} + +void TransformTest::meshDataTextureCoordinates2DNoCoordinates() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Trade::MeshData mesh{MeshPrimitive::Points, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, VertexFormat::Vector2, nullptr}, + }}; + + std::ostringstream out; + Error redirectError{&out}; + transformTextureCoordinates2D(mesh, {}, 1); + CORRADE_COMPARE(out.str(), "MeshTools::transformTextureCoordinates2D(): the mesh has no texture coordinates with index 1\n"); +} + +void TransformTest::meshDataTextureCoordinates2DRvaluePassthrough() { + auto&& data = MeshDataTextureCoordinatesRvaluePassthroughData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Array indexData{NoInit, 3*sizeof(UnsignedShort)}; + auto indices = Containers::arrayCast(indexData); + /* Not using copy({...}) as we'd have no way to compare against the + original unchanged data */ + const UnsignedShort indicesExpected[]{1, 2, 0}; + Utility::copy(indicesExpected, indices); + + struct Vertex { + Vector2ub secondaryTextureCoordinates; + Vector2 textureCoordinates; + }; + Containers::Array vertexData{NoInit, 3*sizeof(Vertex)}; + auto vertices = stridedArrayView(Containers::arrayCast(vertexData)); + Utility::copy({ + {{15, 34}, {0.0f, 0.0f}}, + {{11, 25}, {1.0f, 0.0f}}, + {{16, 27}, {0.0f, 2.0f}} + }, vertices); + + Containers::Array attributes; + if(data.id == 1) + arrayAppend(attributes, Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices.slice(&Vertex::secondaryTextureCoordinates)}); + else CORRADE_COMPARE(data.id, 0); + arrayAppend(attributes, { + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices.slice(&Vertex::textureCoordinates)} + }); + const void* originalAttributeData = attributes; + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + data.indexed ? std::move(indexData) : nullptr, + data.indexed ? Trade::MeshIndexData{indices} : Trade::MeshIndexData{nullptr}, + std::move(vertexData), std::move(attributes)}; + + const Matrix3 transformation = Matrix3::rotation(35.0_degf); + Trade::MeshData out = transformTextureCoordinates2D(std::move(mesh), transformation, data.id); + + /* Indices should be passed through unchanged */ + if(data.indexed) { + CORRADE_VERIFY(out.isIndexed()); + CORRADE_COMPARE_AS(out.indices(), + Containers::arrayView(indicesExpected), + TestSuite::Compare::Container); + } + + /* The vertices should be expanded to floats and transformed */ + CORRADE_COMPARE(out.attributeFormat(Trade::MeshAttribute::TextureCoordinates, data.id), VertexFormat::Vector2); + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::TextureCoordinates, data.id), Containers::arrayView({ + transformation.transformPoint({0.0f, 0.0f}), + transformation.transformPoint({1.0f, 0.0f}), + transformation.transformPoint({0.0f, 2.0f}) + }), TestSuite::Compare::Container); + + /* The memory should point to the original data */ + if(data.indexed) + CORRADE_COMPARE(out.indexData().data(), static_cast(indices.data())); + CORRADE_COMPARE(out.vertexData().data(), static_cast(vertices.data())); + CORRADE_COMPARE(out.attributeData().data(), originalAttributeData); +} + +void TransformTest::meshDataTextureCoordinates2DRvaluePassthroughIndexDataNotOwned() { + const UnsignedShort indices[]{ + 1, 2, 0 + }; + Containers::Array vertexData{NoInit, 3*sizeof(Vector2)}; + auto vertices = Containers::arrayCast(vertexData); + Utility::copy({ + {0.0f, 0.0f}, + {1.0f, 0.0f}, + {0.0f, 2.0f} + }, vertices); + + Containers::Array attributes{InPlaceInit, { + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices} + }}; + const void* originalAttributeData = attributes; + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + {}, indices, Trade::MeshIndexData{indices}, + std::move(vertexData), std::move(attributes)}; + + const Matrix3 transformation = Matrix3::rotation(35.0_degf); + Trade::MeshData out = transformTextureCoordinates2D(std::move(mesh), transformation); + + /* Indices should be passed through unchanged */ + CORRADE_VERIFY(out.isIndexed()); + CORRADE_COMPARE_AS(out.indices(), + Containers::arrayView(indices), + TestSuite::Compare::Container); + + /* The vertices should be transformed */ + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::TextureCoordinates), Containers::arrayView({ + transformation.transformPoint({0.0f, 0.0f}), + transformation.transformPoint({1.0f, 0.0f}), + transformation.transformPoint({0.0f, 2.0f}) + }), TestSuite::Compare::Container); + + /* The memory should not point to the original data */ + CORRADE_VERIFY(out.indexData().data() != static_cast(indices)); + CORRADE_VERIFY(out.vertexData().data() != static_cast(vertices.data())); + CORRADE_VERIFY(out.attributeData().data() != static_cast(originalAttributeData)); +} + +void TransformTest::meshDataTextureCoordinates2DRvaluePassthroughVertexDataNotOwned() { + Containers::Array indexData{NoInit, 3*sizeof(UnsignedShort)}; + auto indices = Containers::arrayCast(indexData); + /* Not using copy({...}) as we'd have no way to compare against the + original unchanged data */ + const UnsignedShort indicesExpected[]{1, 2, 0}; + Utility::copy(indicesExpected, indices); + const Vector2 vertices[]{ + {0.0f, 0.0f}, + {1.0f, 0.0f}, + {0.0f, 2.0f} + }; + + Containers::Array attributes{InPlaceInit, { + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, Containers::arrayView(vertices)} + }}; + const void* originalAttributeData = attributes; + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + std::move(indexData), Trade::MeshIndexData{indices}, + {}, vertices, std::move(attributes)}; + + const Matrix3 transformation = Matrix3::rotation(35.0_degf); + Trade::MeshData out = transformTextureCoordinates2D(std::move(mesh), transformation); + + /* Indices should be passed through unchanged */ + CORRADE_VERIFY(out.isIndexed()); + CORRADE_COMPARE_AS(out.indices(), + Containers::arrayView(indices), + TestSuite::Compare::Container); + + /* The vertices should be transformed */ + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::TextureCoordinates), Containers::arrayView({ + transformation.transformPoint({0.0f, 0.0f}), + transformation.transformPoint({1.0f, 0.0f}), + transformation.transformPoint({0.0f, 2.0f}) + }), TestSuite::Compare::Container); + + /* The memory should not point to the original data */ + CORRADE_VERIFY(out.indexData().data() != static_cast(indices.data())); + CORRADE_VERIFY(out.vertexData().data() != static_cast(vertices)); + CORRADE_VERIFY(out.attributeData().data() != static_cast(originalAttributeData)); +} + +void TransformTest::meshDataTextureCoordinates2DRvaluePassthroughNoCoordinates() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + /* Mainly to verify there's no other accidental assertion from checking + vertex format, this message comes from the l-value overload */ + std::ostringstream out; + Error redirectError{&out}; + transformTextureCoordinates2D(Trade::MeshData{MeshPrimitive::Points, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, VertexFormat::Vector2, nullptr}, + }}, {}, 1); + CORRADE_COMPARE(out.str(), "MeshTools::transformTextureCoordinates2D(): the mesh has no texture coordinates with index 1\n"); +} + +void TransformTest::meshDataTextureCoordinates2DRvaluePassthroughWrongFormat() { + Containers::Array vertexData{NoInit, 3*sizeof(Vector2ub)}; + auto vertices = Containers::arrayCast(vertexData); + Utility::copy({ + {0, 0}, + {1, 0}, + {0, 2} + }, vertices); + + Containers::Array attributes{InPlaceInit, { + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, vertices} + }}; + const void* originalAttributeData = attributes; + + Trade::MeshData mesh{MeshPrimitive::TriangleStrip, + std::move(vertexData), std::move(attributes)}; + + const Matrix3 transformation = Matrix3::rotation(35.0_degf); + Trade::MeshData out = transformTextureCoordinates2D(std::move(mesh), transformation); + + /* The vertices should be expanded to floats and transformed */ + CORRADE_COMPARE_AS(out.attribute(Trade::MeshAttribute::TextureCoordinates), Containers::arrayView({ + transformation.transformPoint({0.0f, 0.0f}), + transformation.transformPoint({1.0f, 0.0f}), + transformation.transformPoint({0.0f, 2.0f}) + }), TestSuite::Compare::Container); + + /* The memory should not point to the original data */ + CORRADE_VERIFY(out.vertexData().data() != static_cast(vertices.data())); + CORRADE_VERIFY(out.attributeData().data() != static_cast(originalAttributeData)); +} + +void TransformTest::meshDataTextureCoordinates2DInPlaceNotMutable() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Trade::MeshData mesh{MeshPrimitive::Points, Trade::DataFlags{}, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, VertexFormat::Vector2, nullptr}, + }}; + + std::ostringstream out; + Error redirectError{&out}; + transformTextureCoordinates2DInPlace(mesh, {}); + CORRADE_COMPARE(out.str(), "MeshTools::transformTextureCoordinates2DInPlace(): vertex data not mutable\n"); +} + +void TransformTest::meshDataTextureCoordinates2DInPlaceNoCoordinates() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Trade::MeshData mesh{MeshPrimitive::Points, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, VertexFormat::Vector2, nullptr}, + }}; + + std::ostringstream out; + Error redirectError{&out}; + transformTextureCoordinates2DInPlace(mesh, {}, 1); + CORRADE_COMPARE(out.str(), "MeshTools::transformTextureCoordinates2DInPlace(): the mesh has no texture coordinates with index 1\n"); +} + +void TransformTest::meshDataTextureCoordinates2DInPlaceWrongFormat() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + Trade::MeshData mesh{MeshPrimitive::Points, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, VertexFormat::Vector2, nullptr}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, VertexFormat::Vector2us, nullptr} + }}; + + std::ostringstream out; + Error redirectError{&out}; + transformTextureCoordinates2DInPlace(mesh, {}, 1); + CORRADE_COMPARE(out.str(), "MeshTools::transformTextureCoordinates2DInPlace(): expected VertexFormat::Vector2 texture coordinates but got VertexFormat::Vector2us\n"); +} + }}}} CORRADE_TEST_MAIN(Magnum::MeshTools::Test::TransformTest) diff --git a/src/Magnum/MeshTools/Transform.cpp b/src/Magnum/MeshTools/Transform.cpp new file mode 100644 index 000000000..c5ec783c7 --- /dev/null +++ b/src/Magnum/MeshTools/Transform.cpp @@ -0,0 +1,311 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 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 "Transform.h" + +#include + +#include "Magnum/MeshTools/Interleave.h" +#include "Magnum/Trade/MeshData.h" + +namespace Magnum { namespace MeshTools { + +Trade::MeshData transform2D(const Trade::MeshData& data, const Matrix3& transformation, const UnsignedInt id) { + const Containers::Optional positionAttributeId = data.findAttributeId(Trade::MeshAttribute::Position, id); + CORRADE_ASSERT(positionAttributeId, + "MeshTools::transform2D(): the mesh has no positions with index" << id, + (Trade::MeshData{MeshPrimitive::Triangles, 0})); + const VertexFormat positionAttributeFormat = data.attributeFormat(*positionAttributeId); + CORRADE_ASSERT(vertexFormatComponentCount(positionAttributeFormat) == 2, + "MeshTools::transform2D(): expected 2D positions but got" << positionAttributeFormat, + (Trade::MeshData{MeshPrimitive::Triangles, 0})); + + /* Copy original attributes to a mutable array so we can update the + position attribute format, if needed. Not using Utility::copy() here as + the view returned by attributeData() might have offset-only attributes + which interleave() doesn't want. */ + Containers::Array attributes{data.attributeCount()}; + for(UnsignedInt i = 0; i != data.attributeCount(); ++i) + attributes[i] = data.attributeData(i); + + /* If the position attribute isn't in a desired format, replace it with an + empty placeholder that we'll unpack the data into */ + if(positionAttributeFormat != VertexFormat::Vector2) + attributes[*positionAttributeId] = Trade::MeshAttributeData{Trade::MeshAttribute::Position, VertexFormat::Vector2, nullptr}; + + /* Create the output mesh, making more room for the full formats if + necessary */ + Trade::MeshData out = interleave(Trade::MeshData{data.primitive(), + /* If data is not indexed, the reference will be also non-indexed */ + {}, data.indexData(), Trade::MeshIndexData{data.indices()}, + {}, data.vertexData(), {}, data.vertexCount()}, attributes); + + /* If the position attribute wasn't in a desired format, unpack it */ + if(positionAttributeFormat != VertexFormat::Vector2) + data.positions2DInto(out.mutableAttribute(*positionAttributeId), id); + + /* Delegate to the in-place implementation and return */ + transform2DInPlace(out, transformation, id); + return out; +} + +Trade::MeshData transform2D(Trade::MeshData&& data, const Matrix3& transformation, const UnsignedInt id) { + /* Perform the operation in-place, if we can transfer the ownership and + have positions in the right format already. Explicitly checking for + presence of the position attribute so we don't need to duplicate the + assert here again. */ + if((data.indexDataFlags() & Trade::DataFlag::Owned) && + (data.vertexDataFlags() & Trade::DataFlag::Owned) && + data.attributeCount(Trade::MeshAttribute::Position) > id && + data.attributeFormat(Trade::MeshAttribute::Position, id) == VertexFormat::Vector2) + { + transform2DInPlace(data, transformation, id); + return std::move(data); + } + + /* Otherwise delegate to the function that does all the copying and format + expansion */ + return transform2D(data, transformation, id); +} + +void transform2DInPlace(Trade::MeshData& data, const Matrix3& transformation, const UnsignedInt id) { + CORRADE_ASSERT(data.vertexDataFlags() & Trade::DataFlag::Mutable, + "MeshTools::transform2DInPlace(): vertex data not mutable", ); + const Containers::Optional positionAttributeId = data.findAttributeId(Trade::MeshAttribute::Position, id); + CORRADE_ASSERT(positionAttributeId, + "MeshTools::transform2DInPlace(): the mesh has no positions with index" << id, ); + CORRADE_ASSERT(data.attributeFormat(*positionAttributeId) == VertexFormat::Vector2, + "MeshTools::transform2DInPlace(): expected" << VertexFormat::Vector2 << "positions but got" << data.attributeFormat(*positionAttributeId), ); + + /** @todo this needs a proper batch implementation */ + for(Vector2& position: data.mutableAttribute(*positionAttributeId)) + position = transformation.transformPoint(position); +} + +Trade::MeshData transform3D(const Trade::MeshData& data, const Matrix4& transformation, const UnsignedInt id) { + const Containers::Optional positionAttributeId = data.findAttributeId(Trade::MeshAttribute::Position, id); + CORRADE_ASSERT(positionAttributeId, + "MeshTools::transform3D(): the mesh has no positions with index" << id, + (Trade::MeshData{MeshPrimitive::Triangles, 0})); + const VertexFormat positionAttributeFormat = data.attributeFormat(*positionAttributeId); + CORRADE_ASSERT(vertexFormatComponentCount(positionAttributeFormat) == 3, + "MeshTools::transform3D(): expected 3D positions but got" << positionAttributeFormat, + (Trade::MeshData{MeshPrimitive::Triangles, 0})); + const Containers::Optional tangentAttributeId = data.findAttributeId(Trade::MeshAttribute::Tangent, id); + const VertexFormat desiredTangentVertexFormat = tangentAttributeId ? + (vertexFormatComponentCount(data.attributeFormat(*tangentAttributeId)) == 4 ? + VertexFormat::Vector4 : VertexFormat::Vector3) : VertexFormat{}; + const Containers::Optional bitangentAttributeId = data.findAttributeId(Trade::MeshAttribute::Bitangent, id); + const Containers::Optional normalAttributeId = data.findAttributeId(Trade::MeshAttribute::Normal, id); + + /* Copy original attributes to a mutable array so we can update the + position attribute format, if needed. Not using Utility::copy() here as + the view returned by attributeData() might have offset-only attributes + which interleave() doesn't want. */ + Containers::Array attributes{data.attributeCount()}; + for(UnsignedInt i = 0; i != data.attributeCount(); ++i) + attributes[i] = data.attributeData(i); + + /* If the position/TBN attributes aren't in a desired format, replace them + with an empty placeholder that we'll unpack the data into */ + if(positionAttributeFormat != VertexFormat::Vector3) + attributes[*positionAttributeId] = Trade::MeshAttributeData{Trade::MeshAttribute::Position, VertexFormat::Vector3, nullptr}; + if(tangentAttributeId && data.attributeFormat(*tangentAttributeId) != desiredTangentVertexFormat) + attributes[*tangentAttributeId] = Trade::MeshAttributeData{Trade::MeshAttribute::Tangent, desiredTangentVertexFormat, nullptr}; + if(bitangentAttributeId && data.attributeFormat(*bitangentAttributeId) != VertexFormat::Vector3) + attributes[*bitangentAttributeId] = Trade::MeshAttributeData{Trade::MeshAttribute::Bitangent, VertexFormat::Vector3, nullptr}; + if(normalAttributeId && data.attributeFormat(*normalAttributeId) != VertexFormat::Vector3) + attributes[*normalAttributeId] = Trade::MeshAttributeData{Trade::MeshAttribute::Normal, VertexFormat::Vector3, nullptr}; + + /* Create the output mesh, making more room for the full formats if + necessary */ + Trade::MeshData out = interleave(Trade::MeshData{data.primitive(), + /* If data is not indexed, the reference will be also non-indexed */ + {}, data.indexData(), Trade::MeshIndexData{data.indices()}, + {}, data.vertexData(), {}, data.vertexCount()}, attributes); + + /* If the position/TBN attributes weren't in a desired format, unpack them */ + if(positionAttributeFormat != VertexFormat::Vector3) + data.positions3DInto(out.mutableAttribute(*positionAttributeId), id); + if(tangentAttributeId && data.attributeFormat(*tangentAttributeId) != desiredTangentVertexFormat) { + if(desiredTangentVertexFormat == VertexFormat::Vector4) { + data.tangentsInto(out.mutableAttribute(*tangentAttributeId).slice(&Vector4::xyz), id); + data.bitangentSignsInto(out.mutableAttribute(*tangentAttributeId).slice(&Vector4::w), id); + } else { + data.tangentsInto(out.mutableAttribute(*tangentAttributeId), id); + } + } + if(bitangentAttributeId && data.attributeFormat(*bitangentAttributeId) != VertexFormat::Vector3) + data.bitangentsInto(out.mutableAttribute(*bitangentAttributeId), id); + if(normalAttributeId && data.attributeFormat(*normalAttributeId) != VertexFormat::Vector3) + data.normalsInto(out.mutableAttribute(*normalAttributeId), id); + + /* Delegate to the in-place implementation and return */ + transform3DInPlace(out, transformation, id); + return out; +} + +Trade::MeshData transform3D(Trade::MeshData&& data, const Matrix4& transformation, const UnsignedInt id) { + /* Perform the operation in-place, if we can transfer the ownership and + have positions in the right format already. Explicitly checking for + presence of the position attribute so we don't need to duplicate the + assert here again. */ + const Containers::Optional positionAttributeId = data.findAttributeId(Trade::MeshAttribute::Position, id); + const Containers::Optional tangentAttributeId = data.findAttributeId(Trade::MeshAttribute::Tangent, id); + const Containers::Optional bitangentAttributeId = data.findAttributeId(Trade::MeshAttribute::Bitangent, id); + const Containers::Optional normalAttributeId = data.findAttributeId(Trade::MeshAttribute::Normal, id); + if((data.indexDataFlags() & Trade::DataFlag::Owned) && + (data.vertexDataFlags() & Trade::DataFlag::Owned) && + positionAttributeId && + data.attributeFormat(*positionAttributeId) == VertexFormat::Vector3 && + (!tangentAttributeId || data.attributeFormat(*tangentAttributeId) == VertexFormat::Vector3 || data.attributeFormat(*tangentAttributeId) == VertexFormat::Vector4) && + (!bitangentAttributeId || data.attributeFormat(*bitangentAttributeId) == VertexFormat::Vector3) && + (!normalAttributeId || data.attributeFormat(*normalAttributeId) == VertexFormat::Vector3)) + { + transform3DInPlace(data, transformation, id); + return std::move(data); + } + + /* Otherwise delegate to the function that does all the copying and format + expansion */ + return transform3D(data, transformation, id); +} + +void transform3DInPlace(Trade::MeshData& data, const Matrix4& transformation, const UnsignedInt id) { + CORRADE_ASSERT(data.vertexDataFlags() & Trade::DataFlag::Mutable, + "MeshTools::transform3DInPlace(): vertex data not mutable", ); + const Containers::Optional positionAttributeId = data.findAttributeId(Trade::MeshAttribute::Position, id); + CORRADE_ASSERT(positionAttributeId, + "MeshTools::transform3DInPlace(): the mesh has no positions with index" << id, ); + CORRADE_ASSERT(data.attributeFormat(*positionAttributeId) == VertexFormat::Vector3, + "MeshTools::transform3DInPlace(): expected" << VertexFormat::Vector3 << "positions but got" << data.attributeFormat(*positionAttributeId), ); + const Containers::Optional tangentAttributeId = data.findAttributeId(Trade::MeshAttribute::Tangent, id); + const VertexFormat tangentAttributeFormat = tangentAttributeId ? data.attributeFormat(*tangentAttributeId) : VertexFormat{}; + CORRADE_ASSERT(!tangentAttributeId || tangentAttributeFormat == VertexFormat::Vector3 || tangentAttributeFormat == VertexFormat::Vector4, + "MeshTools::transform3DInPlace(): expected" << VertexFormat::Vector3 << "or" << VertexFormat::Vector4 << "tangents but got" << data.attributeFormat(*tangentAttributeId), ); + const Containers::Optional bitangentAttributeId = data.findAttributeId(Trade::MeshAttribute::Bitangent, id); + CORRADE_ASSERT(!bitangentAttributeId || data.attributeFormat(*bitangentAttributeId) == VertexFormat::Vector3, + "MeshTools::transform3DInPlace(): expected" << VertexFormat::Vector3 << "bitangents but got" << data.attributeFormat(*bitangentAttributeId), ); + const Containers::Optional normalAttributeId = data.findAttributeId(Trade::MeshAttribute::Normal, id); + CORRADE_ASSERT(!normalAttributeId || data.attributeFormat(*normalAttributeId) == VertexFormat::Vector3, + "MeshTools::transform3DInPlace(): expected" << VertexFormat::Vector3 << "normals but got" << data.attributeFormat(*normalAttributeId), ); + + /** @todo this needs a proper batch implementation */ + for(Vector3& position: data.mutableAttribute(*positionAttributeId)) + position = transformation.transformPoint(position); + + /* If no other attributes are present, nothing to do */ + if(!tangentAttributeId && !bitangentAttributeId && !normalAttributeId) + return; + + const Matrix3x3 normalMatrix = transformation.normalMatrix(); + if(tangentAttributeId) { + /** @todo this needs a proper batch implementation */ + if(tangentAttributeFormat == VertexFormat::Vector3) + for(Vector3& tangent: data.mutableAttribute(*tangentAttributeId)) + tangent = normalMatrix*tangent; + else for(Vector4& tangent: data.mutableAttribute(*tangentAttributeId)) { + tangent.xyz() = normalMatrix*tangent.xyz(); + /** @todo figure out the fourth component, probably has to get + flipped when the scale changes handedness? */ + } + } + /** @todo this needs a proper batch implementation */ + if(bitangentAttributeId) for(Vector3& bitangent: data.mutableAttribute(*bitangentAttributeId)) + bitangent = normalMatrix*bitangent; + /** @todo this needs a proper batch implementation */ + if(normalAttributeId) for(Vector3& normal: data.mutableAttribute(*normalAttributeId)) + normal = normalMatrix*normal; +} + +Trade::MeshData transformTextureCoordinates2D(const Trade::MeshData& data, const Matrix3& transformation, const UnsignedInt id) { + const Containers::Optional textureCoordinateAttributeId = data.findAttributeId(Trade::MeshAttribute::TextureCoordinates, id); + CORRADE_ASSERT(textureCoordinateAttributeId, + "MeshTools::transformTextureCoordinates2D(): the mesh has no texture coordinates with index" << id, + (Trade::MeshData{MeshPrimitive::Triangles, 0})); + + /* Copy original attributes to a mutable array so we can update the + position attribute format, if needed. Not using Utility::copy() here as + the view returned by attributeData() might have offset-only attributes + which interleave() doesn't want. */ + Containers::Array attributes{data.attributeCount()}; + for(UnsignedInt i = 0; i != data.attributeCount(); ++i) + attributes[i] = data.attributeData(i); + + /* If the position attribute isn't in a desired format, replace it with an + empty placeholder that we'll unpack the data into */ + if(data.attributeFormat(*textureCoordinateAttributeId) != VertexFormat::Vector2) + attributes[*textureCoordinateAttributeId] = Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, VertexFormat::Vector2, nullptr}; + + /* Create the output mesh, making more room for the full formats if + necessary */ + Trade::MeshData out = interleave(Trade::MeshData{data.primitive(), + /* If data is not indexed, the reference will be also non-indexed */ + {}, data.indexData(), Trade::MeshIndexData{data.indices()}, + {}, data.vertexData(), {}, data.vertexCount()}, attributes); + + /* If the position attribute wasn't in a desired format, unpack it */ + if(data.attributeFormat(*textureCoordinateAttributeId) != VertexFormat::Vector2) + data.textureCoordinates2DInto(out.mutableAttribute(*textureCoordinateAttributeId), id); + + /* Delegate to the in-place implementation and return */ + transformTextureCoordinates2DInPlace(out, transformation, id); + return out; +} + +Trade::MeshData transformTextureCoordinates2D(Trade::MeshData&& data, const Matrix3& transformation, const UnsignedInt id) { + /* Perform the operation in-place, if we can transfer the ownership and + have positions in the right format already. Explicitly checking for + presence of the position attribute so we don't need to duplicate the + assert here again. */ + if((data.indexDataFlags() & Trade::DataFlag::Owned) && + (data.vertexDataFlags() & Trade::DataFlag::Owned) && + data.attributeCount(Trade::MeshAttribute::TextureCoordinates) > id && + data.attributeFormat(Trade::MeshAttribute::TextureCoordinates, id) == VertexFormat::Vector2) + { + transformTextureCoordinates2DInPlace(data, transformation, id); + return std::move(data); + } + + /* Otherwise delegate to the function that does all the copying and format + expansion */ + return transformTextureCoordinates2D(data, transformation, id); +} + +void transformTextureCoordinates2DInPlace(Trade::MeshData& data, const Matrix3& transformation, const UnsignedInt id) { + CORRADE_ASSERT(data.vertexDataFlags() & Trade::DataFlag::Mutable, + "MeshTools::transformTextureCoordinates2DInPlace(): vertex data not mutable", ); + const Containers::Optional textureCoordinateAttributeId = data.findAttributeId(Trade::MeshAttribute::TextureCoordinates, id); + CORRADE_ASSERT(textureCoordinateAttributeId, + "MeshTools::transformTextureCoordinates2DInPlace(): the mesh has no texture coordinates with index" << id, ); + CORRADE_ASSERT(data.attributeFormat(*textureCoordinateAttributeId) == VertexFormat::Vector2, + "MeshTools::transformTextureCoordinates2DInPlace(): expected" << VertexFormat::Vector2 << "texture coordinates but got" << data.attributeFormat(*textureCoordinateAttributeId), ); + + /** @todo this needs a proper batch implementation */ + for(Vector2& position: data.mutableAttribute(*textureCoordinateAttributeId)) + position = transformation.transformPoint(position); +} + +}} diff --git a/src/Magnum/MeshTools/Transform.h b/src/Magnum/MeshTools/Transform.h index 05bb35ebc..9a830ba33 100644 --- a/src/Magnum/MeshTools/Transform.h +++ b/src/Magnum/MeshTools/Transform.h @@ -26,11 +26,13 @@ */ /** @file - * @brief Function @ref Magnum::MeshTools::transformVectorsInPlace(), @ref Magnum::MeshTools::transformVectors(), @ref Magnum::MeshTools::transformPointsInPlace(), @ref Magnum::MeshTools::transformPoints() + * @brief Function @ref Magnum::MeshTools::transformVectorsInPlace(), @ref Magnum::MeshTools::transformVectors(), @ref Magnum::MeshTools::transformPointsInPlace(), @ref Magnum::MeshTools::transformPoints(), @ref Magnum::MeshTools::transform2D(), @ref Magnum::MeshTools::transform2DInPlace(), @ref Magnum::MeshTools::transform3D(), @ref Magnum::MeshTools::transform3DInPlace(), @ref Magnum::MeshTools::transformTextureCoordinates2D(), @ref Magnum::MeshTools::transformTextureCoordinates2DInPlace() */ #include "Magnum/Math/DualQuaternion.h" #include "Magnum/Math/DualComplex.h" +#include "Magnum/MeshTools/visibility.h" +#include "Magnum/Trade/Trade.h" namespace Magnum { namespace MeshTools { @@ -137,6 +139,162 @@ template U transformPoints(const T& transformation, U vectors) return result; } +/** +@brief Transform 2D positions in a mesh data +@m_since_latest + +Expects that the mesh contains a two-dimensional +@ref Trade::MeshAttribute::Position with index @p id. To avoid data loss with +packed types, the positions are always converted to @ref VertexFormat::Vector2. +Other attributes, position attributes other than @p id, and indices (if any) +are passed through untouched. + +See also @ref transform2D(Trade::MeshData&&, const Matrix3&, UnsignedInt) for a +potentially more efficient operation instead of always performing a full copy, +you can also do an in-place transformation using @ref transform2DInPlace(). +@see @ref transform3D(), @ref transformTextureCoordinates2D(), + @ref Trade::MeshData::attributeCount(MeshAttribute) const, + @ref Trade::MeshData::attributeFormat(MeshAttribute, UnsignedInt) const +*/ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData transform2D(const Trade::MeshData& data, const Matrix3& transformation, UnsignedInt id = 0); + +/** +@brief Transform 2D positions in a mesh data +@m_since_latest + +Compared to @ref transform2D(const Trade::MeshData&, const Matrix3&, UnsignedInt) +this function can can perform the transformation in-place, transferring the +data ownership to the returned instance, if both vertex and index data is +owned, vertex data is mutable and the positions with index @p id are +@ref VertexFormat::Vector2. +*/ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData transform2D(Trade::MeshData&& data, const Matrix3& transformation, UnsignedInt id = 0); + +/** +@brief Transform 2D positions in a mesh data in-place +@m_since_latest + +Expects that the mesh has mutable vertex data and contains a two-dimensional +@ref Trade::MeshAttribute::Position with index @p id. To avoid data loss with +packed types, the in-place operation requires the position type to be +@ref VertexFormat::Vector2 --- if you can't guarantee that, use +@ref transform2D() instead. Other attributes, position attributes other than +@p id, and indices (if any) are left untouched. +@see @ref transform3DInPlace(), @ref transformTextureCoordinates2DInPlace(), + @ref Trade::MeshData::vertexDataFlags(), + @ref Trade::MeshData::attributeCount(MeshAttribute) const, + @ref Trade::MeshData::attributeFormat(MeshAttribute, UnsignedInt) const +*/ +MAGNUM_MESHTOOLS_EXPORT void transform2DInPlace(Trade::MeshData& data, const Matrix3& transformation, UnsignedInt id = 0); + +/** +@brief Transform 3D positions, normals, tangents and bitangents in a mesh data +@m_since_latest + +Expects that the mesh contains a three-dimensional +@ref Trade::MeshAttribute::Position with index @p id. If +@ref Trade::MeshAttribute::Normal, @ref Trade::MeshAttribute::Tangent or +@ref Trade::MeshAttribute::Bitangent with index @p id are present as well, +those get transformed with @ref Matrix4::normalMatrix() extracted out of +@p transformation. To avoid data loss with packed types, the positions, normals +and bitangents are always converted to @ref VertexFormat::Vector3, tangents to +either @ref VertexFormat::Vector3 or @ref VertexFormat::Vector4. Other +attributes, additional position/TBN attributes other than @p id, and indices +(if any) are passed through untouched. + +See also @ref transform3D(Trade::MeshData&&, const Matrix4&, UnsignedInt) for a +potentially more efficient operation instead of always performing a full copy, +you can also do an in-place transformation using @ref transform3DInPlace(). +@see @ref transform2D(), @ref transformTextureCoordinates2D(), + @ref Trade::MeshData::attributeCount(MeshAttribute) const, + @ref Trade::MeshData::attributeFormat(MeshAttribute, UnsignedInt) const +*/ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData transform3D(const Trade::MeshData& data, const Matrix4& transformation, UnsignedInt id = 0); + +/** +@brief Transform 3D positions, normals, tangenta and bitangents in a mesh data +@m_since_latest + +Compared to @ref transform3D(const Trade::MeshData&, const Matrix4&, UnsignedInt) +this function can can perform the transformation in-place, transferring the +data ownership to the returned instance, if both vertex and index data is +owned, vertex data is mutable, positions, normals and bitangents (if present) +are @ref VertexFormat::Vector3 and tangents (if present) either +@ref VertexFormat::Vector3 or @ref VertexFormat::Vector4. +*/ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData transform3D(Trade::MeshData&& data, const Matrix4& transformation, UnsignedInt id = 0); + +/** +@brief Transform 3D positions, normals, tangents and bitangents in a mesh data in-place +@m_since_latest + +Expects that the mesh has mutable vertex data and contains at least a +three-dimensional @ref Trade::MeshAttribute::Position with index @p id; +optionally also @ref Trade::MeshAttribute::Normal, +@ref Trade::MeshAttribute::Tangent or @ref Trade::MeshAttribute::Bitangent with +index @p id, those get transformed with @ref Matrix4::normalMatrix() extracted +out of @p transformation. To avoid data loss with packed types, the in-place +operation requires the position, normal and bitangent types to be +@ref VertexFormat::Vector3 and tangent either @ref VertexFormat::Vector3 or +@ref VertexFormat::Vector4 --- if you can't guarantee that, use +@ref transform3D() instead. Other attributes, position/TBN attributes other +than @p id, and indices (if any) are left untouched. +@see @ref transform2DInPlace(), @ref transformTextureCoordinates2DInPlace(), + @ref Trade::MeshData::vertexDataFlags(), + @ref Trade::MeshData::attributeCount(MeshAttribute) const, + @ref Trade::MeshData::attributeFormat(MeshAttribute, UnsignedInt) const +*/ +MAGNUM_MESHTOOLS_EXPORT void transform3DInPlace(Trade::MeshData& data, const Matrix4& transformation, UnsignedInt id = 0); + +/** +@brief Transform 2D texture coordinates in a mesh data +@m_since_latest + +Expects that the mesh contains a @ref Trade::MeshAttribute::TextureCoordinates +with index id. To avoid data loss with packed types, the coordinattes are +always converted to @ref VertexFormat::Vector2. Other attributes, texture +coordinate attributes other than @p id, and indices (if any) are passed through +untouched. + +See also @ref transformTextureCoordinates2D(Trade::MeshData&&, const Matrix3&, UnsignedInt) +for a potentially more efficient operation instead of always performing a full +copy, you can also do an in-place transformation using +@ref transformTextureCoordinates2DInPlace(). +@see @ref transform2D(), @ref transform3D(), + @ref Trade::MeshData::attributeCount(MeshAttribute) const +*/ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData transformTextureCoordinates2D(const Trade::MeshData& data, const Matrix3& transformation, UnsignedInt id = 0); + +/** +@brief Transform 2D texture coordinates in a mesh data +@m_since_latest + +Compared to @ref transformTextureCoordinates2D(const Trade::MeshData&, const Matrix3&, UnsignedInt) +this function can can perform the transformation in-place, transferring the +data ownership to the returned instance, if both vertex and index data is +owned, vertex data is mutable and the coordinates with index @p id are +@ref VertexFormat::Vector2. +*/ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData transformTextureCoordinates2D(Trade::MeshData&& data, const Matrix3& transformation, UnsignedInt id = 0); + +/** +@brief Transform 2D texture coordinates in a mesh data in-place +@m_since_latest + +Expects that the mesh has mutable vertex data and contains a +@ref Trade::MeshAttribute::TextureCoordinates with index @p id. To avoid data +loss with packed types, the in-place operation requires the coordinate type to +be @ref VertexFormat::Vector2 --- if you can't guarantee that, use +@ref transformTextureCoordinates2D() instead. Other attributes, texture +coordinate attributes other than @p id, and indices (if any) are passed through +untouched. +@see @ref transform2DInPlace(), @ref transform3DInPlace(), + @ref Trade::MeshData::vertexDataFlags(), + @ref Trade::MeshData::attributeCount(MeshAttribute) const, + @ref Trade::MeshData::attributeFormat(MeshAttribute, UnsignedInt) const +*/ +MAGNUM_MESHTOOLS_EXPORT void transformTextureCoordinates2DInPlace(Trade::MeshData& data, const Matrix3& transformation, UnsignedInt id = 0); + }} #endif