blob: 26dbc06521e8a434c0486a130c363ba44a6824b0 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "lib/escher/geometry/indexed_triangle_mesh_clip.h"
#include "gtest/gtest.h"
namespace {
using namespace escher;
// Very simple test that is easy to understand.
TEST(IndexedTriangleMeshClip, OneTriangle2d) {
IndexedTriangleMesh2d<nullptr_t, vec2> mesh{
.indices = {0, 1, 2},
.positions = {vec2(0, 0), vec2(1, 0), vec2(0, 3)},
.attributes2 = {vec2(0, 0), vec2(1, 0), vec2(0, 1)}};
// Clip two vertices, keeping one tip of the original triangle.
std::vector<plane2> planes = {plane2(vec2(1, 0), 0.5f)};
auto result = IndexedTriangleMeshClip(mesh, planes);
auto& output_mesh = result.first;
EXPECT_EQ(output_mesh.indices.size(), 3U);
EXPECT_EQ(output_mesh.positions.size(), 3U);
EXPECT_EQ(output_mesh.attributes2.size(), 3U);
// The unused attributes should be unpopulated.
EXPECT_EQ(output_mesh.attributes1.size(), 0U);
EXPECT_EQ(output_mesh.attributes3.size(), 0U);
for (size_t i = 0; i < output_mesh.vertex_count(); ++i) {
auto& pos = output_mesh.positions[i];
auto& attr = output_mesh.attributes2[i];
if (pos == vec2(1, 0)) {
EXPECT_EQ(attr, vec2(1, 0));
} else if (pos == vec2(0.5f, 0)) {
EXPECT_EQ(attr, vec2(0.5f, 0));
} else if (pos == vec2(0.5f, 1.5f)) {
EXPECT_EQ(attr, vec2(0.5f, 0.5f));
} else {
// This should not be reached.
EXPECT_TRUE(false);
}
}
// Use the same plane (but with the opposite orientation) to clip one tip of
// the triangle, leaving behind a quad that is split into two triangles.
planes = {plane2(vec2(-1, 0), -0.5f)};
result = IndexedTriangleMeshClip(mesh, planes);
EXPECT_EQ(output_mesh.indices.size(), 6U);
EXPECT_EQ(output_mesh.positions.size(), 4U);
EXPECT_EQ(output_mesh.attributes2.size(), 4U);
for (size_t i = 0; i < output_mesh.vertex_count(); ++i) {
auto& pos = output_mesh.positions[i];
auto& attr = output_mesh.attributes2[i];
if (pos == vec2(0, 0)) {
EXPECT_EQ(attr, vec2(0, 0));
} else if (pos == vec2(0, 3)) {
EXPECT_EQ(attr, vec2(0, 1));
} else if (pos == vec2(0.5f, 0)) {
EXPECT_EQ(attr, vec2(0.5f, 0));
} else if (pos == vec2(0.5f, 1.5f)) {
EXPECT_EQ(attr, vec2(0.5f, 0.5f));
} else {
// This should not be reached.
EXPECT_TRUE(false);
}
}
}
// Helper function that returns a standard mesh used for testing. It looks like
// like this in the standard Vulkan coordinate system (positive y down).
// (-1,-1) _______ (1,-1)
// /\ /\ 3 4
// / \ / \ indices:
// /______\/______\ 0 1 2
// (-2,1) (0,1) (2,1)
IndexedTriangleMesh2d<vec2> GetStandardTestMesh2d() {
return IndexedTriangleMesh2d<vec2>{
.positions = {vec2(-2, 1), vec2(0, 1), vec2(2, 1), vec2(-1, -1),
vec2(1, -1)},
.attributes1 = {vec2(0, 1), vec2(0.5f, 1), vec2(1, 1), vec2(0, 0),
vec2(1, 0)},
.indices = {0, 1, 3, 3, 1, 4, 4, 1, 2}};
}
IndexedTriangleMesh3d<vec2> GetStandardTestMesh3d() {
auto mesh2d = GetStandardTestMesh2d();
IndexedTriangleMesh3d<vec2> mesh3d;
mesh3d.indices = mesh2d.indices;
mesh3d.attributes1 = mesh2d.attributes1;
for (const vec2& pos : mesh2d.positions) {
mesh3d.positions.push_back(vec3(pos, 11));
}
return mesh3d;
}
// Helper function that returns a list of planes that tightly bounds the
// standard test mesh.
std::vector<plane2> GetStandardTestMeshBoundingPlanes2d() {
return std::vector<plane2>{{vec2(-2, 0), vec2(1, 0)},
{vec2(2, 0), vec2(-1, 0)},
{vec2(0, 1), vec2(0, -1)},
{vec2(0, -1), vec2(0, 1)},
{vec2(-2, 1), glm::normalize(vec2(2, 1))},
{vec2(2, 1), glm::normalize(vec2(-2, 1))}};
}
std::vector<plane3> GetStandardTestMeshBoundingPlanes3d() {
auto planes2d = GetStandardTestMeshBoundingPlanes2d();
std::vector<plane3> planes3d;
for (const plane2& p : planes2d) {
planes3d.push_back(plane3(vec3(p.dir(), 0), p.dist()));
}
return planes3d;
}
// Test that planes that are tangent to the perimeter edges of the mesh result
// in a completely unclipped mesh.
template <typename MeshT, typename PlaneT>
void TestUnclippedMesh(const MeshT& mesh, const std::vector<PlaneT>& planes) {
// First test against individual planes.
for (auto& p : planes) {
auto result = IndexedTriangleMeshClip(mesh, std::vector<PlaneT>{p});
// Since the plane does not clip any vertices, all indices and vertices are
// left completely unchanged. Also, the resulting list of clipping planes
// is empty, indicating that the plane clipped no vertices.
EXPECT_EQ(mesh.indices, result.first.indices);
EXPECT_EQ(mesh.positions, result.first.positions);
EXPECT_EQ(mesh.attributes1, result.first.attributes1);
EXPECT_TRUE(result.second.empty());
}
// Test clipping against all planes at once.
auto result = IndexedTriangleMeshClip(mesh, planes);
EXPECT_EQ(mesh.indices, result.first.indices);
EXPECT_EQ(mesh.positions, result.first.positions);
EXPECT_EQ(mesh.attributes1, result.first.attributes1);
EXPECT_TRUE(result.second.empty());
}
TEST(IndexedTriangleMeshClip, Unclipped2d) {
TestUnclippedMesh(GetStandardTestMesh2d(),
GetStandardTestMeshBoundingPlanes2d());
}
TEST(IndexedTriangleMeshClip, Unclipped3d) {
TestUnclippedMesh(GetStandardTestMesh3d(),
GetStandardTestMeshBoundingPlanes3d());
}
// Verify expected behavior when insetting the top and bottom bounding planes
// so that they slightly clip the mesh.
template <typename MeshT, typename PlaneT>
void TestMultipleClips(const MeshT& mesh, const std::vector<PlaneT>& planes) {
// Take the planes bounding the (screen space) top and bottom of the standard
// mesh, and inset them slightly so that they clip the mesh.
PlaneT bottom_plane(planes[2].dir(), planes[2].dist() + 0.1f);
PlaneT top_plane(planes[3].dir(), planes[3].dist() + 0.1f);
// Clipping with an inset bottom plane results in two "case 2" clips, and one
// "case 1" clip. As a result, we expect one extra triangle and one extra
// vertex.
auto bottom_result =
IndexedTriangleMeshClip(mesh, std::vector<PlaneT>{bottom_plane});
EXPECT_EQ(4U, bottom_result.first.triangle_count());
EXPECT_EQ(6U, bottom_result.first.vertex_count());
// Clipping with an inset top plane results in two "case 1" clips, and one
// "case 2" clip. As a result, we expect two extra triangles and two extra
// vertices.
auto top_result =
IndexedTriangleMeshClip(mesh, std::vector<PlaneT>{top_plane});
EXPECT_EQ(5U, top_result.first.triangle_count());
EXPECT_EQ(7U, top_result.first.vertex_count());
// Interestingly, clipping with the same two planes in opposite orders gives
// different results. This is because clipping by the top_plane first results
// in two "case 1" diagonal edges being added, which are then clipped by the
// bottom_plane. When clipping by the bottom plane first, only one "case 1"
// diagonal edge is added to later be clipped by the top plane.
auto bottom_top_result = IndexedTriangleMeshClip(
mesh, std::vector<PlaneT>{bottom_plane, top_plane});
auto top_bottom_result = IndexedTriangleMeshClip(
mesh, std::vector<PlaneT>{top_plane, bottom_plane});
EXPECT_EQ(7U, bottom_top_result.first.triangle_count());
EXPECT_EQ(9U, bottom_top_result.first.vertex_count());
EXPECT_EQ(8U, top_bottom_result.first.triangle_count());
EXPECT_EQ(10U, top_bottom_result.first.vertex_count());
// Verify that adding a non-clipping plane in the first/middle/last position
// doesn't affect the result.
PlaneT non_clipping_plane = planes[4];
auto non_clipping_result_1 = IndexedTriangleMeshClip(
mesh, std::vector<PlaneT>{non_clipping_plane, bottom_plane, top_plane});
auto non_clipping_result_2 = IndexedTriangleMeshClip(
mesh, std::vector<PlaneT>{bottom_plane, non_clipping_plane, top_plane});
auto non_clipping_result_3 = IndexedTriangleMeshClip(
mesh, std::vector<PlaneT>{bottom_plane, top_plane, non_clipping_plane});
EXPECT_EQ(bottom_top_result, non_clipping_result_1);
EXPECT_EQ(bottom_top_result, non_clipping_result_2);
EXPECT_EQ(bottom_top_result, non_clipping_result_3);
}
TEST(IndexedTriangleMeshClip, MultipleClips2d) {
TestMultipleClips(GetStandardTestMesh2d(),
GetStandardTestMeshBoundingPlanes2d());
}
TEST(IndexedTriangleMeshClip, MultipleClips3d) {
TestMultipleClips(GetStandardTestMesh3d(),
GetStandardTestMeshBoundingPlanes3d());
}
} // namespace