/* Copyright (C) 1996-1997 Id Software, Inc. Copyright (C) 1997 Greg Lewis This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA See file, 'COPYING', for details. */ // tjunc.c #include #include #include struct tjunc_stats_t { // # of degenerate edges reported (with two identical input vertices) std::atomic degenerate; // # of new edges created to close a tjunction // (also technically the # of points detected that lay on other faces' edges) std::atomic tjunctions; // # of faces that were created as a result of splitting faces that are too large // to be contained on a single face std::atomic faceoverflows; // # of faces that were degenerate and were just collapsed altogether. std::atomic facecollapse; // # of faces that were able to be fixed just by rotating the start point. std::atomic rotates; // # of faces that weren't able to be fixed with start point rotation std::atomic norotates; // # of faces that could be successfully retopologized std::atomic retopology; // # of faces generated by retopologization std::atomic faceretopology; // # of faces that were successfully topologized by MWT std::atomic mwt; // # of triangles computed by MWT std::atomic trimwt; // # of faces added by MWT std::atomic facemwt; }; inline std::optional PointOnEdge( const qvec3d &p, const qvec3d &edge_start, const qvec3d &edge_dir, float start = 0, float end = 1) { qvec3d delta = p - edge_start; vec_t dist = qv::dot(delta, edge_dir); // check if off an end if (dist <= start || dist >= end) { return std::nullopt; } qvec3d exact = edge_start + (edge_dir * dist); qvec3d off = p - exact; vec_t error = qv::length(off); // brushbsp-fixme: this was 0.5 in Q2, check? if (fabs(error) > DEFAULT_ON_EPSILON) { // not on the edge return std::nullopt; } return dist; } /* ========== TestEdge Can be recursively reentered ========== */ inline void TestEdge(vec_t start, vec_t end, size_t p1, size_t p2, size_t startvert, const std::vector &edge_verts, const qvec3d &edge_start, const qvec3d &edge_dir, std::vector &superface, tjunc_stats_t &stats) { if (p1 == p2) { // degenerate edge stats.degenerate++; return; } for (size_t k = startvert; k < edge_verts.size(); k++) { size_t j = edge_verts[k]; if (j == p1 || j == p2) { continue; } auto dist = PointOnEdge(map.bsp.dvertexes[j], edge_start, edge_dir, start, end); if (!dist.has_value()) { continue; } // break the edge stats.tjunctions++; TestEdge(start, dist.value(), p1, j, k + 1, edge_verts, edge_start, edge_dir, superface, stats); TestEdge(dist.value(), end, j, p2, k + 1, edge_verts, edge_start, edge_dir, superface, stats); return; } // the edge p1 to p2 is now free of tjunctions superface.push_back(p1); } /* ========== FindEdgeVerts_BruteForce Force a dumb check of everything ========== */ static void FindEdgeVerts_BruteForce( const node_t *, const node_t *, const qvec3d &, const qvec3d &, std::vector &verts) { verts.resize(map.bsp.dvertexes.size()); for (size_t i = 0; i < verts.size(); i++) { verts[i] = i; } } /* ========== FindEdgeVerts_FaceBounds_R Recursive function for matching nodes that intersect the aabb for vertex checking. ========== */ static void FindEdgeVerts_FaceBounds_R(const node_t *node, const aabb3d &aabb, std::vector &verts) { if (node->is_leaf) { return; } else if (node->bounds.disjoint(aabb, 0.0)) { return; } for (auto &face : node->facelist) { for (auto &v : face->original_vertices) { if (aabb.containsPoint(map.bsp.dvertexes[v])) { verts.push_back(v); } } } FindEdgeVerts_FaceBounds_R(node->children[0].get(), aabb, verts); FindEdgeVerts_FaceBounds_R(node->children[1].get(), aabb, verts); } /* ========== FindEdgeVerts_FaceBounds Use a loose AABB around the line and only capture vertices that intersect it. ========== */ static void FindEdgeVerts_FaceBounds( const node_t *headnode, const qvec3d &p1, const qvec3d &p2, std::vector &verts) { // magic number, average of "usual" points per edge verts.reserve(8); FindEdgeVerts_FaceBounds_R(headnode, (aabb3d{} + p1 + p2).grow(qvec3d(1.0, 1.0, 1.0)), verts); } /* ================== SplitFaceIntoFragments The face was created successfully, but may have way too many edges. Cut it down to the minimum amount of faces that are within the max edge count. Modifies `superface`. Adds the results to the end of `output`. ================== */ inline void SplitFaceIntoFragments( std::vector &superface, std::list> &output, tjunc_stats_t &stats) { const int32_t &maxedges = qbsp_options.maxedges.value(); // split into multiple fragments, because of vertex overload while (superface.size() > maxedges) { stats.faceoverflows++; // copy MAXEDGES from our current face std::vector &newf = output.emplace_back(maxedges); std::copy_n(superface.begin(), maxedges, newf.begin()); // remove everything in-between from the superface // except for the last edge we just wrote (0 and MAXEDGES-1) std::copy(superface.begin() + maxedges - 1, superface.end(), superface.begin() + 1); // resize superface; we need enough room to store the two extra verts superface.resize(superface.size() - maxedges + 2); } // move the first face to the end, since that's logically where it belongs now output.splice(output.end(), output, output.begin()); } float AngleOfTriangle(const qvec3d &a, const qvec3d &b, const qvec3d &c) { vec_t num = (b[0] - a[0]) * (c[0] - a[0]) + (b[1] - a[1]) * (c[1] - a[1]) + (b[2] - a[2]) * (c[2] - a[2]); vec_t den = sqrt(pow((b[0] - a[0]), 2) + pow((b[1] - a[1]), 2) + pow((b[2] - a[2]), 2)) * sqrt(pow((c[0] - a[0]), 2) + pow((c[1] - a[1]), 2) + pow((c[2] - a[2]), 2)); return acos(num / den) * (180.0 / 3.141592653589793238463); } // Check whether the given input triangle would be valid // on the given face and not have any other points // intersecting it. inline bool TriangleIsValid(size_t v0, size_t v1, size_t v2, vec_t angle_epsilon) { if (AngleOfTriangle(map.bsp.dvertexes[v0], map.bsp.dvertexes[v1], map.bsp.dvertexes[v2]) < angle_epsilon || AngleOfTriangle(map.bsp.dvertexes[v1], map.bsp.dvertexes[v2], map.bsp.dvertexes[v0]) < angle_epsilon || AngleOfTriangle(map.bsp.dvertexes[v2], map.bsp.dvertexes[v0], map.bsp.dvertexes[v1]) < angle_epsilon) { return false; } return true; } /* ================== CreateSuperFace Generate a superface (the input face `f` but with all of the verts in the world added that lay on the line) and return it ================== */ static std::vector CreateSuperFace(node_t *headnode, face_t *f, tjunc_stats_t &stats) { std::vector superface; superface.reserve(f->original_vertices.size() * 2); // stores all of the verts in the world that are close to // being on a given edge std::vector edge_verts; // find all of the extra vertices that lay on edges, // place them in superface for (size_t i = 0; i < f->original_vertices.size(); i++) { auto v1 = f->original_vertices[i]; auto v2 = f->original_vertices[(i + 1) % f->original_vertices.size()]; qvec3d edge_start = map.bsp.dvertexes[v1]; qvec3d e2 = map.bsp.dvertexes[v2]; edge_verts.clear(); FindEdgeVerts_FaceBounds(headnode, edge_start, e2, edge_verts); vec_t len; qvec3d edge_dir = qv::normalize(e2 - edge_start, len); TestEdge(0, len, v1, v2, 0, edge_verts, edge_start, edge_dir, superface, stats); } return superface; } #include #include using qvectri = qvec; // check if the given triangle exists in the set of triangles // in any permutation std::optional triangle_exists(const std::vector &triangles, size_t a, size_t b, size_t c) { for (size_t i = 0; i < triangles.size(); i++) { auto &tri = triangles[i]; for (size_t s = 0; s < 3; s++) { if (tri[s] == a && tri[(s + 1) % 3] == b && tri[(s + 2) % 3] == c) { return i; } } } return std::nullopt; } // find the triangles best suited to create a // fan out of in the given set of triangles. std::vector find_best_fan(const std::vector &triangles, size_t num_vertices) { // find the triangle with the most fannable vertices. std::vector best_triangles; for (auto &tri : triangles) { // try all three permutations for (size_t perm = 0; perm < 3; perm++) { size_t first = tri[perm]; size_t mid = tri[(perm + 1) % 3]; size_t last = tri[(perm + 2) % 3]; std::vector my_tri; // find any other that can be wound from this edge // TODO: can optimize by only looping around the verts // included in the triangle for (; last != first; last = (last + 1) % num_vertices) { auto ftri = triangle_exists(triangles, first, mid, last); // no triangle found for A B C, so try again // with A B D, etc. if (ftri == std::nullopt) { continue; } // found A B C, so go next (A C D) my_tri.push_back(ftri.value()); mid = last; } if (best_triangles.empty() || my_tri.size() > best_triangles.size()) { best_triangles = std::move(my_tri); } } } return best_triangles; } // find the seed vertex (vertex referenced by the most edges) of // the fan. size_t find_seed_vertex(const std::vector &triangles, const std::vector &fan) { std::unordered_set verts{triangles[fan[0]].begin(), triangles[fan[0]].end()}; for (size_t i = 1; i < fan.size(); i++) { auto &tri = triangles[fan[i]]; // produce intersection for (auto it = verts.begin(); it != verts.end();) { if (std::find(tri.begin(), tri.end(), *it) == tri.end()) { it = verts.erase(it); } else { it++; } } // if there's only one vert left it has to be that one if (verts.size() == 1) { return *verts.begin(); } } // just pick whatever's left return *verts.begin(); } static std::list> compress_triangles_into_fans( std::vector &triangles, const std::vector &vertices) { std::list> tris_compiled; while (triangles.size()) { auto fan = find_best_fan(triangles, vertices.size()); Q_assert(fan.size()); // when we run into only 1 triangle fans left, // just add the rest directly. if (fan.size() == 1) { for (auto &tri : triangles) { tris_compiled.emplace_back(std::vector{vertices[tri[0]], vertices[tri[1]], vertices[tri[2]]}); } triangles.clear(); break; } // a fan can be made! find the seed vertex auto seed = find_seed_vertex(triangles, fan); struct tri_verts_less_pred { size_t seed, vert_count; bool operator()(const size_t &a, const size_t &b) const { size_t ka = a < seed ? vert_count + a : a; size_t kb = b < seed ? vert_count + b : b; return ka < kb; } }; // add all the verts and order them so they match // the proper winding std::set verts(tri_verts_less_pred{seed, vertices.size()}); for (auto tri_index : fan) { auto &tri = triangles[tri_index]; for (auto &v : tri) { verts.insert(v); } } Q_assert(verts.size() >= 3); // add the new winding auto &out_tri = tris_compiled.emplace_back(verts.begin(), verts.end()); for (auto &v : out_tri) { v = vertices[v]; } // remove all of the fans from the triangle list std::sort(fan.begin(), fan.end(), [](auto &l, auto &r) { return l > r; }); for (auto tri_index : fan) { triangles.erase(triangles.begin() + tri_index); } } return tris_compiled; } #include // Function to calculate the weight of optimal triangulation of a convex polygon // represented by a given set of vertices std::vector minimum_weight_triangulation( const std::vector &indices, const std::vector &vertices) { // get the number of vertices in the polygon size_t n = vertices.size(); // create a table for storing the solutions to subproblems // `T[i][j]` stores the weight of the minimum-weight triangulation // of the polygon below edge `ij` std::vector T(n * n); std::vector> K(n * n); // fill the table diagonally using the recurrence relation for (size_t diagonal = 0; diagonal < n; diagonal++) { for (size_t i = 0, j = diagonal; j < n; i++, j++) { // If the polygon has less than 3 vertices, triangulation is not possible if (j < i + 2) { continue; } T[i + (j * n)] = std::numeric_limits::max(); // consider all possible triangles `ikj` within the polygon for (size_t k = i + 1; k <= j - 1; k++) { // The weight of triangulation is the length of its perimeter vec_t weight; if (!TriangleIsValid(indices[i], indices[j], indices[k], 0.01)) { weight = std::nexttoward(std::numeric_limits::max(), 0.0); } else { weight = (qv::distance(vertices[i], vertices[j]) + qv::distance(vertices[j], vertices[k]) + qv::distance(vertices[k], vertices[i])) + T[i + (k * n)] + T[k + (j * n)]; } vec_t &t_weight = T[i + (j * n)]; // choose vertex `k` that leads to the minimum total weight if (weight < t_weight) { t_weight = weight; K[i + (j * n)] = k; } } } } std::vector triangles; std::queue> edge_queue; edge_queue.emplace(0, n - 1); while (!edge_queue.empty()) { auto edge = edge_queue.front(); edge_queue.pop(); if (edge[0] == edge[1]) { continue; } auto &c = K[edge[0] + (edge[1] * n)]; if (!c.has_value()) { continue; } qvectri tri{edge[0], edge[1], c.value()}; std::sort(tri.begin(), tri.end()); triangles.emplace_back(tri); edge_queue.emplace(edge[0], c.value()); edge_queue.emplace(c.value(), edge[1]); } Q_assert(triangles.size() == n - 2); return triangles; } static std::list> mwt_face( const face_t *f, const std::vector &vertices, tjunc_stats_t &stats) { auto p = f->plane; if (f->plane_flipped) { p = -p; } auto [u, v] = qv::MakeTangentAndBitangentUnnormalized(p.normal); qv::normalizeInPlace(u); qv::normalizeInPlace(v); std::vector points_2d(vertices.size()); for (size_t i = 0; i < vertices.size(); i++) { points_2d[i] = {qv::dot(map.bsp.dvertexes[vertices[i]], u), qv::dot(map.bsp.dvertexes[vertices[i]], v)}; } auto tris = minimum_weight_triangulation(vertices, points_2d); stats.trimwt += tris.size(); return compress_triangles_into_fans(tris, vertices); } /* ================== RetopologizeFace A face has T-junctions that can't be resolved from rotation. It's still a convex face with wound vertices, though, so we can split it into several triangle fans. ================== */ static std::list> RetopologizeFace(const face_t *f, const std::vector &vertices) { std::list> result; // the copy we're working on std::vector input(vertices); while (input.size()) { // failure if we somehow degenerated a triangle if (input.size() < 3) { return {}; } size_t seed = 0; int64_t end = 0; // find seed triangle (allowing a wrap around, // because it's possible for only the last two triangles // to be valid). for (; seed < input.size(); seed++) { auto v0 = input[seed]; auto v1 = input[(seed + 1) % input.size()]; end = (seed + 2) % input.size(); auto v2 = input[end]; if (!TriangleIsValid(v0, v1, v2, 0.01)) { continue; } // if the next point lays on the edge of v0-v2, this next // triangle won't be valid. float len; qvec3d dir = qv::normalize(map.bsp.dvertexes[v0] - map.bsp.dvertexes[v2], len); auto dist = PointOnEdge(map.bsp.dvertexes[input[(end + 1) % input.size()]], map.bsp.dvertexes[v2], dir, 0, len); if (dist.has_value()) { continue; } // good one! break; } if (seed == input.size()) { // can't find a non-zero area triangle; failure return {}; } // from the seed vertex, keep winding until we hit a zero-area triangle. // we know that triangle (seed, end - 1, end) is valid, so we wind from // end + 1 until we fully wrap around. We also can't include a triangle // that has a point after it laying on the final edge. size_t wrap = end; end = (end + 1) % input.size(); for (; end != wrap; end = (end + 1) % input.size()) { auto v0 = input[seed]; auto v1 = input[(end - 1) < 0 ? (input.size() - 1) : (end - 1)]; auto v2 = input[end]; // if the next point lays on the edge of v0-v2, this next // triangle won't be valid. float len; qvec3d dir = qv::normalize(map.bsp.dvertexes[v0] - map.bsp.dvertexes[v2], len); auto dist = PointOnEdge(map.bsp.dvertexes[input[(end + 1) % input.size()]], map.bsp.dvertexes[v2], dir, 0, len); if (dist.has_value()) { // the next point lays on this edge, so back up and stop end = (end - 1) < 0 ? input.size() - 1 : (end - 1); break; } } // now we have a fan from seed to end that is valid. // add it to the result, clip off all of the // points between it and restart the algorithm // using that edge. auto &tri = result.emplace_back(); // the whole fan can just be moved; we're finished. if (seed == end) { tri = std::move(input); break; } else if (end == wrap) { // we successfully wrapped around, but the // seed vertex isn't at the start, so rotate it. // copy base -> end tri.resize(input.size()); auto out = std::copy(input.begin() + seed, input.end(), tri.begin()); // copy end -> base std::copy(input.begin(), input.begin() + seed, out); break; } if (end < seed) { // the end point is 'behind' the seed, so we're clipping // off two sides of the input size_t x = seed; bool first = true; while (true) { if (x == end && !first) { break; } tri.emplace_back(input[x]); x = (x + 1) % input.size(); first = false; } } else { // simple case where the end point is ahead of the seed; // copy the range over to the output std::copy(input.begin() + seed, input.begin() + end + 1, std::back_inserter(tri)); } Q_assert(seed != end); if (end < seed) { // slightly more complex case: the end point is behind the seed point. // 0 end 2 3 seed 5 6 // end 2 3 seed // calculate new size size_t new_size = (seed + 1) - end; // move back the end to the start std::copy(input.begin() + end, input.begin() + seed + 1, input.begin()); // clip input.resize(new_size); } else { // simple case: the end point is ahead of the seed point. // collapse the range after it backwards over top of the seed // and clip it off // 0 1 2 seed 4 5 6 end 8 9 // 0 1 2 seed end 8 9 // calculate new size size_t new_size = input.size() - (end - seed - 1); // move range std::copy(input.begin() + end, input.end(), input.begin() + seed + 1); // clip input.resize(new_size); } } // finished return result; } /* ================== FixFaceEdges If the face has any T-junctions, fix them here. ================== */ static void FixFaceEdges(node_t *headnode, face_t *f, tjunc_stats_t &stats) { // we were asked not to bother fixing any of the faces. if (qbsp_options.tjunc.value() == settings::tjunclevel_t::NONE) { f->fragments.emplace_back(face_fragment_t{f->original_vertices}); return; } std::vector superface = CreateSuperFace(headnode, f, stats); if (superface.size() < 3) { // entire face collapsed stats.facecollapse++; return; } else if (superface.size() == 3) { // no need to adjust this either f->fragments.emplace_back(face_fragment_t{f->original_vertices}); return; } // faces with 4 or more vertices can be done better. // temporary storage for result faces; stored as a list // since a resize may steal references out from underneath us // as the functions do their work. std::list> faces; // do MWT first; it will generate optimal results for everything. if (qbsp_options.tjunc.value() >= settings::tjunclevel_t::MWT) { faces = mwt_face(f, superface, stats); if (faces.size()) { stats.mwt++; stats.facemwt += faces.size() - 1; } } // brute force rotating the start point until we find a valid winding // that doesn't have any T-junctions if (!faces.size() && qbsp_options.tjunc.value() >= settings::tjunclevel_t::ROTATE) { size_t i = 0; for (; i < superface.size(); i++) { size_t x = 0; // try vertex i as the base, see if we find any zero-area triangles for (; x < superface.size() - 2; x++) { auto v0 = superface[i]; auto v1 = superface[(i + x + 1) % superface.size()]; auto v2 = superface[(i + x + 2) % superface.size()]; if (!TriangleIsValid(v0, v1, v2, 0.01)) { break; } } if (x == superface.size() - 2) { // found one! break; } } if (i == superface.size()) { // can't simply rotate to eliminate zero-area triangles, so we have // to do a bit of re-topology. if (qbsp_options.tjunc.value() >= settings::tjunclevel_t::RETOPOLOGIZE) { if (auto retopology = RetopologizeFace(f, superface); retopology.size() > 1) { stats.retopology++; stats.faceretopology += retopology.size() - 1; faces = std::move(retopology); } } if (!faces.size()) { // unable to re-topologize, so just stick with the superface. // it's got zero-area triangles that fill in the gaps. stats.norotates++; } } else if (i != 0) { // was able to rotate the superface to eliminate zero-area triangles. stats.rotates++; auto &output = faces.emplace_back(superface.size()); // copy base -> end auto out = std::copy(superface.begin() + i, superface.end(), output.begin()); // copy end -> base std::copy(superface.begin(), superface.begin() + i, out); } } // the other techniques all failed, or we asked to not // try them. just move the superface in directly. if (!faces.size()) { faces.emplace_back(std::move(superface)); } Q_assert(faces.size()); // split giant superfaces into subfaces if we have an edge limit. if (qbsp_options.maxedges.value()) { for (auto &face : faces) { Q_assert(face.size() >= 3); SplitFaceIntoFragments(face, faces, stats); Q_assert(face.size() >= 3); } } // move the results into the face f->fragments.reserve(faces.size()); for (auto &face : faces) { f->fragments.emplace_back(face_fragment_t{std::move(face)}); } for (auto &frag : f->fragments) { Q_assert(frag.output_vertices.size() >= 3); } } #include /* ================== FixEdges_r ================== */ static void FindFaces_r(node_t *node, std::unordered_set &faces) { if (node->is_leaf) { return; } for (auto &f : node->facelist) { // might have been omitted, so `original_vertices` will be empty if (f->original_vertices.size()) { faces.insert(f.get()); } } FindFaces_r(node->children[0].get(), faces); FindFaces_r(node->children[1].get(), faces); } /* =========== TJunc fixing entry point =========== */ void TJunc(node_t *headnode) { logging::print(logging::flag::PROGRESS, "---- {} ----\n", __func__); tjunc_stats_t stats{}; std::unordered_set faces; FindFaces_r(headnode, faces); logging::parallel_for_each(faces, [&](auto &face) { FixFaceEdges(headnode, face, stats); }); if (stats.degenerate) { logging::print(logging::flag::STAT, "{:5} edges degenerated\n", stats.degenerate); } if (stats.facecollapse) { logging::print(logging::flag::STAT, "{:5} faces degenerated\n", stats.facecollapse); } if (stats.tjunctions) { logging::print(logging::flag::STAT, "{:5} edges added by tjunctions\n", stats.tjunctions); } if (stats.mwt) { logging::print(logging::flag::STAT, "{:5} faces ran through MWT\n", stats.mwt); logging::print( logging::flag::STAT, "{:5} new faces added via MWT (from {} triangles)\n", stats.facemwt, stats.trimwt); } if (stats.retopology) { logging::print(logging::flag::STAT, "{:5} faces re-topologized\n", stats.retopology); logging::print(logging::flag::STAT, "{:5} new faces added by re-topology\n", stats.faceretopology); } if (stats.rotates) { logging::print(logging::flag::STAT, "{:5} faces rotated\n", stats.rotates); } if (stats.norotates) { logging::print(logging::flag::STAT, "{:5} faces unable to be rotated or re-topologized\n", stats.norotates); } if (stats.faceoverflows) { logging::print(logging::flag::STAT, "{:5} faces added by splitting large faces\n", stats.faceoverflows); } }