/* This file is part of Magnum. Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026 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/Mesh.h" #include "Magnum/Math/Functions.h" #include "Magnum/Math/FunctionsBatch.h" /* minmax() */ #include "Magnum/Math/Vector4.h" #include "Magnum/Primitives/Cube.h" #include "Magnum/Trade/MeshData.h" namespace Magnum { namespace Primitives { namespace Test { namespace { struct CubeTest: TestSuite::Tester { explicit CubeTest(); void solid(); template void solidTextureCoordinates(); void solidInvalid(); void solidStrip(); void solidStripGlsl(); void wireframe(); }; const struct { const char* name; Containers::Optional flags; } SolidData[]{ {"", {}}, {"explicit empty flags", CubeFlags{}} }; enum class CubeEdge { /* 0 is reserved */ /* Horizontal edges */ BottomBack = 1, /* {0, -1, +1} */ BottomFront, /* {0, -1, -1} */ TopBack, /* {0, +1, +1} */ TopFront, /* {0, +1, -1} */ /* Vertical edges */ BackLeft, /* {-1, 0, +1} */ BackRight, /* {+1, 0, +1} */ FrontLeft, /* {-1, 0, -1} */ FrontRight, /* {+1, 0, -1} */ /* "Depth" edges */ BottomLeft, /* {-1, -1, 0} */ BottomRight, /* {+1, -1, 0} */ TopLeft, /* {-1, +1, 0} */ TopRight, /* {+1, +1, 0} */ }; Debug& operator<<(Debug& out, CubeEdge edge) { switch(edge) { #define _c(value) case CubeEdge::value: return out << #value; _c(BottomBack) _c(BottomFront) _c(TopBack) _c(TopFront) _c(BackLeft) _c(BackRight) _c(FrontLeft) _c(FrontRight) _c(BottomLeft) _c(BottomRight) _c(TopLeft) _c(TopRight) #undef _c } CORRADE_INTERNAL_ASSERT_UNREACHABLE(); } const struct { const char* name; CubeFlags flags; /* +X, -X, +Y, -Y, +Z, -Z (same order as GL::CubeMapCoordinate) */ Vector2 expectedCenters[6]; /* Cases where less than 12 edges are shared have the rest zero-filled */ CubeEdge expectedSharedEdges[12]; } SolidTextureCoordinatesData[]{ {"all same", CubeFlag::TextureCoordinatesAllSame, { {0.5f, 0.5f}, {0.5f, 0.5f}, {0.5f, 0.5f}, {0.5f, 0.5f}, {0.5f, 0.5f}, {0.5f, 0.5f} }, { /* (No shared edges in this case) */ }}, /* +----+----+----+ 1.0 | +X | +Y | +Z | 0.75 +----+----+----+ 0.5 | -X | -Y | -Z | 0.25 +----+----+----+ 0.0 0.0 0.333 0.667 1.0 0.167 0.5 0.833 */ {"+ up, - down", CubeFlag::TextureCoordinatesPositiveUpNegativeDown, { {0.16667f, 0.75f}, /* +X */ {0.16667f, 0.25f}, /* -X */ {0.5f, 0.75f}, /* +Y */ {0.5f, 0.25f}, /* -Y */ {0.83333f, 0.75f}, /* +Z */ {0.83333f, 0.25f} /* -Z */ }, { /* (*Deliberately* no shared edges in this case either. They could be but it'd mean some faces would be rotated, which is just weird. */ }}, /* +-----+ 1.0 | +Y | 0.833 +-tl--+-----+-----+-----+ 0.667 | -X bl +Z br +X fr -Z | 0.5 +-bl--+-----+-----+-----+ 0.333 | -Y | 0.167 +-----+ 0.0 0.0 0.25 0.5 0.75 1.0 0.125 0.375 0.625 0.875 */ {"-X up, -X down", CubeFlag::TextureCoordinatesNegativeXUpNegativeXDown, { {0.625f, 0.5f}, /* +X */ {0.125f, 0.5f}, /* -X */ {0.125f, 0.83333f}, /* +Y */ {0.125f, 0.16667f}, /* -Y */ {0.375f, 0.5f}, /* +Z */ {0.875f, 0.5f} /* -Z */ }, { CubeEdge::TopLeft, CubeEdge::BottomLeft, CubeEdge::BackLeft, CubeEdge::BackRight, CubeEdge::FrontRight, }}, /* +-----+ | +Y | +-tl--+-----+-----+-----+ | -X bl +Z br +X fr -Z | +-----+-bb--+-----+-----+ | -Y | +-----+ 0.25 0.5 0.375 */ {"-X up, +Z down", CubeFlag::TextureCoordinatesNegativeXUpPositiveZDown, { {0.625f, 0.5f}, /* +X */ {0.125f, 0.5f}, /* -X */ {0.125f, 0.83333f}, /* +Y */ {0.375f, 0.16667f}, /* -Y */ {0.375f, 0.5f}, /* +Z */ {0.875f, 0.5f} /* -Z */ }, { CubeEdge::TopLeft, CubeEdge::BackLeft, CubeEdge::BottomBack, CubeEdge::BackRight, CubeEdge::FrontRight, }}, /* +-----+ | +Y | +-tl--+-----+-----+-----+ | -X bl +Z br +X fr -Z | +-----+-----+-br--+-----+ | -Y | +-----+ 0.5 0.75 0.625 */ {"-X up, +X down", CubeFlag::TextureCoordinatesNegativeXUpPositiveXDown, { {0.625f, 0.5f}, /* +X */ {0.125f, 0.5f}, /* -X */ {0.125f, 0.83333f}, /* +Y */ {0.625f, 0.16667f}, /* -Y */ {0.375f, 0.5f}, /* +Z */ {0.875f, 0.5f} /* -Z */ }, { CubeEdge::TopLeft, CubeEdge::BackLeft, CubeEdge::BackRight, CubeEdge::BottomRight, CubeEdge::FrontRight, }}, /* +-----+ | +Y | +-tl--+-----+-----+-----+ | -X bl +Z br +X fr -Z | +-----+-----+-----+-bf--+ | -Y | +-----+ 0.75 1.0 0.875 */ {"-X up, -Z down", CubeFlag::TextureCoordinatesNegativeXUpNegativeZDown, { {0.625f, 0.5f}, /* +X */ {0.125f, 0.5f}, /* -X */ {0.125f, 0.83333f}, /* +Y */ {0.875f, 0.16667f}, /* -Y */ {0.375f, 0.5f}, /* +Z */ {0.875f, 0.5f} /* -Z */ }, { CubeEdge::TopLeft, CubeEdge::BackLeft, CubeEdge::BackRight, CubeEdge::FrontRight, CubeEdge::BottomFront, }}, /* +-----+ | +Y | +-----+-tb--+-----+-----+ | -X bl +Z br +X fr -Z | +-----+-bb--+-----+-----+ | -Y | +-----+ 0.25 0.5 0.375 */ {"+Z up, +Z down", CubeFlag::TextureCoordinatesPositiveZUpPositiveZDown, { {0.625f, 0.5f}, /* +X */ {0.125f, 0.5f}, /* -X */ {0.375f, 0.83333f}, /* +Y */ {0.375f, 0.16667f}, /* -Y */ {0.375f, 0.5f}, /* +Z */ {0.875f, 0.5f} /* -Z */ }, { CubeEdge::TopBack, CubeEdge::BackLeft, CubeEdge::BackRight, CubeEdge::BottomBack, CubeEdge::FrontRight, }}, /* +-----+ | +Y | +-----+-tb--+-----+-----+ | -X bl +Z br +X fr -Z | +-----+-bb--+-br--+-----+ | -Y | +-----+ 0.5 0.75 0.625 */ {"+Z up, +X down", CubeFlag::TextureCoordinatesPositiveZUpPositiveXDown, { {0.625f, 0.5f}, /* +X */ {0.125f, 0.5f}, /* -X */ {0.375f, 0.83333f}, /* +Y */ {0.625f, 0.16667f}, /* -Y */ {0.375f, 0.5f}, /* +Z */ {0.875f, 0.5f} /* -Z */ }, { CubeEdge::TopBack, CubeEdge::BackLeft, CubeEdge::BackRight, CubeEdge::FrontRight, CubeEdge::BottomRight, }}, }; CubeTest::CubeTest() { addInstancedTests({&CubeTest::solid}, Containers::arraySize(SolidData)); addInstancedTests({ &CubeTest::solidTextureCoordinates, &CubeTest::solidTextureCoordinates }, Containers::arraySize(SolidTextureCoordinatesData)); addTests({&CubeTest::solidInvalid, &CubeTest::solidStrip, &CubeTest::solidStripGlsl, &CubeTest::wireframe}); } void CubeTest::solid() { auto&& data = SolidData[testCaseInstanceId()]; setTestCaseDescription(data.name); Trade::MeshData cube = data.flags ? Primitives::cubeSolid(*data.flags) : Primitives::cubeSolid(); CORRADE_COMPARE(cube.primitive(), MeshPrimitive::Triangles); CORRADE_VERIFY(cube.isIndexed()); CORRADE_COMPARE(cube.indexCount(), 36); CORRADE_COMPARE(cube.vertexCount(), 24); CORRADE_COMPARE(cube.attributeCount(), 2); CORRADE_COMPARE(cube.indices()[17], 11); CORRADE_COMPARE(cube.attribute(Trade::MeshAttribute::Position)[4], (Vector3{1.0f, -1.0f, 1.0f})); CORRADE_COMPARE(cube.attribute(Trade::MeshAttribute::Normal)[6], (Vector3{1.0f, 0.0f, 0.0f})); } template T sampleQuad(const T& a, const T& b, const T& c, const T& d, Vector2 t) { /* Assuming the vertex order is the following, which means the second lerp has to be in a flipped direction in order to give expected result. 3--2 | | 0--1 */ return Math::lerp(Math::lerp(a, b, t[0]), Math::lerp(d, c, t[0]), t[1]); } template void CubeTest::solidTextureCoordinates() { auto&& data = SolidTextureCoordinatesData[testCaseInstanceId()]; setTestCaseDescription(data.name); setTestCaseTemplateName(flag == CubeFlag::Tangents ? "CubeFlag::Tangents" : ""); Trade::MeshData cube = Primitives::cubeSolid(data.flags|flag); Containers::StridedArrayView1D positions = cube.attribute(Trade::MeshAttribute::Position); Containers::StridedArrayView1D textureCoordinates = cube.attribute(Trade::MeshAttribute::TextureCoordinates); /* Same as in solid(), to verify basic sanity */ CORRADE_COMPARE(cube.primitive(), MeshPrimitive::Triangles); CORRADE_VERIFY(cube.isIndexed()); CORRADE_COMPARE(cube.indexCount(), 36); CORRADE_COMPARE(cube.vertexCount(), 24); CORRADE_COMPARE(cube.attributeCount(), 3 + (flag == CubeFlag::Tangents ? 1 : 0)); CORRADE_COMPARE(cube.indices()[17], 11); CORRADE_COMPARE(positions[4], (Vector3{1.0f, -1.0f, 1.0f})); CORRADE_COMPARE(cube.attribute(Trade::MeshAttribute::Normal)[6], (Vector3{1.0f, 0.0f, 0.0f})); /* Discover which groups of vertices correspond to which faces, in order matching SolidTextureCoordinatesData::expectedCenters, so +X, -X, +Y, -Y, +Z, -Z. This could be done just once but who cares, it's just a test. It could also be hardcoded but that'll make the test tied too much to the particular data, making it more likely that the test passes with the data actually being completely wrong. */ Vector3 faceCenters[6]{ {+1.0f, 0.0f, 0.0f}, {-1.0f, 0.0f, 0.0f}, {0.0f, +1.0f, 0.0f}, {0.0f, -1.0f, 0.0f}, {0.0f, 0.0f, +1.0f}, {0.0f, 0.0f, -1.0f}, }; UnsignedInt faceVertexOffsets[6]; for(UnsignedInt face = 0; face != 6; ++face) { CORRADE_ITERATION(face); Vector3 center = sampleQuad(positions[face*4 + 0], positions[face*4 + 1], positions[face*4 + 2], positions[face*4 + 3], {0.5f, 0.5f}); UnsignedInt candidate = 0; for(; candidate != Containers::arraySize(faceCenters); ++candidate) { if(center == faceCenters[candidate]) { faceVertexOffsets[candidate] = face*4; break; } } CORRADE_VERIFY(candidate != Containers::arraySize(faceCenters)); } /* Discover which groups of vertices correspond to which edges, in order matching the CubeEdge enum above. Same as above, this could be done just once, or hardcoded, etc., but it's not. */ Vector3 edgeCenters[12] { {0.0f, -1.0f, +1.0f}, /* BottomBack */ {0.0f, -1.0f, -1.0f}, /* BottomFront */ {0.0f, +1.0f, +1.0f}, /* TopBack */ {0.0f, +1.0f, -1.0f}, /* TopFront */ {-1.0f, 0.0f, +1.0f}, /* BackLeft */ {+1.0f, 0.0f, +1.0f}, /* BackRight */ {-1.0f, 0.0f, -1.0f}, /* FrontLeft */ {+1.0f, 0.0f, -1.0f}, /* FrontRight */ {-1.0f, -1.0f, 0.0f}, /* BottomLeft */ {+1.0f, -1.0f, 0.0f}, /* BottomRight */ {-1.0f, +1.0f, 0.0f}, /* TopLeft */ {+1.0f, +1.0f, 0.0f}, /* TopRight */ }; /* Each of 12 edges is shared by exactly 2 faces */ Vector2ui edgeVertices[12][2]; for(UnsignedInt face = 0; face != 6; ++face) { CORRADE_ITERATION(face); /* Four edges of the quad. Assuming ordering like below, if that wouldn't be the case, the CORRADE_VERIFY after would fail. 3--2 | | 0--1 */ for(Vector2ui edge: { Vector2ui{face*4 + 0, face*4 + 1}, Vector2ui{face*4 + 1, face*4 + 2}, Vector2ui{face*4 + 2, face*4 + 3}, Vector2ui{face*4 + 3, face*4 + 0} }) { Vector3 center = Math::lerp(positions[edge[0]], positions[edge[1]], 0.5f); UnsignedInt candidate = 0; for(; candidate != Containers::arraySize(edgeCenters); ++candidate) { if(center == edgeCenters[candidate]) { if(edgeVertices[candidate][0].isZero()) edgeVertices[candidate][0] = edge; else if(edgeVertices[candidate][1].isZero()) edgeVertices[candidate][1] = edge; else CORRADE_FAIL("Too many shared edges."); break; } } CORRADE_ITERATION(edge); CORRADE_VERIFY(candidate != Containers::arraySize(edgeCenters)); } } /* At this point, if neither the above CORRADE_VERIFY() nor the CORRADE_FAIL() fire, for each of the 6 faces the 4 edges were assigned, filling all 24 array entries */ /* For each face verify that the sampled texture coordinates at the center match the expectation */ for(UnsignedInt face = 0; face != 6; ++face) { UnsignedInt vertexOffset = faceVertexOffsets[face]; CORRADE_ITERATION("face" << face << "at offset" << vertexOffset); Vector2 center = sampleQuad( textureCoordinates[vertexOffset + 0], textureCoordinates[vertexOffset + 1], textureCoordinates[vertexOffset + 2], textureCoordinates[vertexOffset + 3], {0.5f, 0.5f}); CORRADE_COMPARE(center, data.expectedCenters[face]); } /* Verify that the expected shared edges indeed have the same texture coordinates for both faces */ for(CubeEdge edge: data.expectedSharedEdges) { /* When we reach an edge that's zero it's the end of the list */ if(edge == CubeEdge{}) break; /* Sanity check -- the two edges should be filled and have contents that aren't the same */ const Vector2ui(&vertices)[2] = edgeVertices[UnsignedInt(edge) - 1]; CORRADE_ITERATION(edge << "edge with vertices" << Debug::packed << vertices[0] << "and" << Debug::packed << vertices[1]); CORRADE_VERIFY(!vertices[0].isZero() && !vertices[1].isZero()); CORRADE_VERIFY(vertices[0] != vertices[1] && vertices[0] != vertices[1].flipped()); /* The edge should match in one or the other direction */ Vector2 a0 = textureCoordinates[vertices[0][0]]; Vector2 a1 = textureCoordinates[vertices[0][1]]; Vector2 b0 = textureCoordinates[vertices[1][0]]; Vector2 b1 = textureCoordinates[vertices[1][1]]; CORRADE_VERIFY((a0 == b0 && a1 == b1) || (a0 == b1 && a1 == b0)); } /* The texture coordinates should always span the whole [0, 0] to [1, 1] range. That may mean the faces won't be square if using a square texture, but in practice the texture would have a size matching the texture coordinate layout, so e.g. with a 4:3 ratio for the NegativeXUpNegativeXDown variant. */ CORRADE_COMPARE(Math::minmax(textureCoordinates), Containers::pair(Vector2{0.0f}, Vector2{1.0f})); /* If tangents are enabled, check their properties also */ if(flag == CubeFlag::Tangents) { Containers::StridedArrayView1D normals = cube.attribute(Trade::MeshAttribute::Normal); Containers::StridedArrayView1D tangents = cube.attribute(Trade::MeshAttribute::Tangent); /* Normals and tangents should be the same for all vertices in a face, and perpendicular in all cases */ for(UnsignedInt face = 0; face != 6; ++face) { CORRADE_ITERATION(face); CORRADE_COMPARE(Math::dot(normals[face*4], tangents[face*4].xyz()), 0.0f); CORRADE_COMPARE(normals[face*4].dot(), 1.0f); CORRADE_COMPARE(tangents[face*4].xyz().dot(), 1.0f); CORRADE_COMPARE(Math::abs(tangents[face*4].w()), 1.0f); for(UnsignedInt vertex = 1; vertex != 4; ++vertex) { CORRADE_ITERATION(vertex); CORRADE_COMPARE(normals[face*4 + vertex], normals[face*4]); CORRADE_COMPARE(tangents[face*4 + vertex], tangents[face*4]); } } /* For each face, sample in a position off center on X and Y */ for(UnsignedInt face = 0; face != 6; ++face) { CORRADE_ITERATION(face); Vector3 center = sampleQuad( positions[face*4 + 0], positions[face*4 + 1], positions[face*4 + 2], positions[face*4 + 3], {0.5f, 0.5f}); Vector2 centerTexture = sampleQuad( textureCoordinates[face*4 + 0], textureCoordinates[face*4 + 1], textureCoordinates[face*4 + 2], textureCoordinates[face*4 + 3], {0.5f, 0.5f}); Vector3 tangent = tangents[face*4].xyz(); Vector3 bitangent = Math::cross(normals[face*4], tangents[face*4].xyz())*tangents[face*4].w(); Vector3 offset[]{ sampleQuad(positions[face*4 + 0], positions[face*4 + 1], positions[face*4 + 2], positions[face*4 + 3], {0.75f, 0.5f}), sampleQuad(positions[face*4 + 0], positions[face*4 + 1], positions[face*4 + 2], positions[face*4 + 3], {0.5f, 0.75f}) }; Vector2 offsetTexture[]{ sampleQuad(textureCoordinates[face*4 + 0], textureCoordinates[face*4 + 1], textureCoordinates[face*4 + 2], textureCoordinates[face*4 + 3], {0.75f, 0.5f}), sampleQuad(textureCoordinates[face*4 + 0], textureCoordinates[face*4 + 1], textureCoordinates[face*4 + 2], textureCoordinates[face*4 + 3], {0.5f, 0.75f}) }; for(Int i: {0, 1}) { CORRADE_ITERATION(i); /* If the shift is in direction of tangent, texture coordinates should be the same in Y and different with a matching sign in X */ if(Math::notEqual(Math::dot(offset[i] - center, tangent), 0.0f)) { Vector2 delta = offsetTexture[i] - centerTexture; CORRADE_COMPARE(delta.y(), 0.0f); CORRADE_COMPARE(Math::sign(delta.x()), Math::sign(Math::dot(offset[i] - center, tangent))); /* If the shift is in direction of bitangent, texture coordinates should be the same in X and different with a matching sign in Y */ } else if(Math::notEqual(Math::dot(offset[i] - center, bitangent), 0.0f)) { Vector2 delta = offsetTexture[i] - centerTexture; CORRADE_COMPARE(delta.x(), 0.0f); CORRADE_COMPARE(Math::sign(delta.y()), Math::sign(Math::dot(offset[i] - center, bitangent))); } else CORRADE_FAIL(Debug::packed << offset[i] - center << "is parallel to neither" << Debug::packed << tangent << "nor" << Debug::packed << bitangent); } } } } void CubeTest::solidInvalid() { CORRADE_SKIP_IF_NO_ASSERT(); Containers::String out; Error redirectError{&out}; Primitives::cubeSolid(CubeFlag::Tangents); Primitives::cubeSolid(CubeFlag(UnsignedInt(CubeFlag::TextureCoordinatesPositiveZUpPositiveXDown) + 2)); CORRADE_COMPARE_AS(out, "Primitives::cubeSolid(): a texture coordinate option has to be picked if tangents are enabled\n" "Primitives::cubeSolid(): unrecognized texture coordinate option 0x12\n", TestSuite::Compare::String); } void CubeTest::solidStrip() { Trade::MeshData cube = Primitives::cubeSolidStrip(); CORRADE_COMPARE(cube.primitive(), MeshPrimitive::TriangleStrip); CORRADE_VERIFY(!cube.isIndexed()); CORRADE_COMPARE(cube.vertexCount(), 14); CORRADE_COMPARE(cube.attributeCount(), 1); CORRADE_COMPARE(cube.attribute(Trade::MeshAttribute::Position)[4], (Vector3{-1.0f, -1.0f, -1.0f})); } void CubeTest::solidStripGlsl() { Trade::MeshData cube = Primitives::cubeSolidStrip(); /* Yes, really. */ auto solidStripVertex = [](UnsignedInt gl_VertexID) { typedef Vector3 vec3; #define mix Math::lerp #include "data.glsl" #undef mix return corner; }; auto vertices = cube.attribute(Trade::MeshAttribute::Position); for(UnsignedInt i = 0; i != cube.vertexCount(); ++i) { CORRADE_ITERATION(i); CORRADE_COMPARE(solidStripVertex(i), vertices[i]); } } void CubeTest::wireframe() { Trade::MeshData cube = Primitives::cubeWireframe(); CORRADE_COMPARE(cube.primitive(), MeshPrimitive::Lines); CORRADE_VERIFY(cube.isIndexed()); CORRADE_COMPARE(cube.indexCount(), 24); CORRADE_COMPARE(cube.vertexCount(), 8); CORRADE_COMPARE(cube.attributeCount(), 1); CORRADE_COMPARE(cube.indices()[5], 3); CORRADE_COMPARE(cube.attribute(Trade::MeshAttribute::Position)[5], (Vector3{1.0f, -1.0f, -1.0f})); } }}}} CORRADE_TEST_MAIN(Magnum::Primitives::Test::CubeTest)