diff --git a/bsputil/decompile.cpp b/bsputil/decompile.cpp index b44b0553..8506bdba 100644 --- a/bsputil/decompile.cpp +++ b/bsputil/decompile.cpp @@ -113,47 +113,10 @@ struct planepoints : std::array } }; -template -std::tuple MakeTangentAndBitangentUnnormalized(const qvec &normal) -{ - // 0, 1, or 2 - const int axis = qv::indexOfLargestMagnitudeComponent(normal); - const int otherAxisA = (axis + 1) % 3; - const int otherAxisB = (axis + 2) % 3; - - // setup two other vectors that are perpendicular to each other - qvec3d otherVecA{}; - otherVecA[otherAxisA] = 1.0; - - qvec3d otherVecB{}; - otherVecB[otherAxisB] = 1.0; - - qvec3d tangent = qv::cross(normal, otherVecA); - qvec3d bitangent = qv::cross(normal, otherVecB); - - // We want `test` to point in the same direction as normal. - // Swap the tangent bitangent if we got the direction wrong. - qvec3d test = qv::cross(tangent, bitangent); - - if (qv::dot(test, normal) < 0) { - std::swap(tangent, bitangent); - } - - // debug test - if (0) { - auto n = qv::normalize(qv::cross(tangent, bitangent)); - double d = qv::distance(n, normal); - - assert(d < 0.0001); - } - - return {tangent, bitangent}; -} - template static planepoints NormalDistanceToThreePoints(const qplane3 &plane) { - std::tuple tanBitan = MakeTangentAndBitangentUnnormalized(plane.normal); + std::tuple tanBitan = qv::MakeTangentAndBitangentUnnormalized(plane.normal); qvec3d point0 = plane.normal * plane.dist; diff --git a/include/common/qvec.hh b/include/common/qvec.hh index 0747a051..1462002b 100644 --- a/include/common/qvec.hh +++ b/include/common/qvec.hh @@ -27,6 +27,7 @@ #include #include #include +#include #include "common/mathlib.hh" #include "common/cmdlib.hh" @@ -499,6 +500,43 @@ template return largestIndex; } +template +std::tuple, qvec> MakeTangentAndBitangentUnnormalized(const qvec &normal) +{ + // 0, 1, or 2 + const int axis = qv::indexOfLargestMagnitudeComponent(normal); + const int otherAxisA = (axis + 1) % 3; + const int otherAxisB = (axis + 2) % 3; + + // setup two other vectors that are perpendicular to each other + qvec otherVecA{}; + otherVecA[otherAxisA] = 1.0; + + qvec otherVecB{}; + otherVecB[otherAxisB] = 1.0; + + auto tangent = qv::cross(normal, otherVecA); + auto bitangent = qv::cross(normal, otherVecB); + + // We want `test` to point in the same direction as normal. + // Swap the tangent bitangent if we got the direction wrong. + qvec test = qv::cross(tangent, bitangent); + + if (qv::dot(test, normal) < 0) { + std::swap(tangent, bitangent); + } + + // debug test + if (0) { + auto n = qv::normalize(qv::cross(tangent, bitangent)); + double d = qv::distance(n, normal); + + assert(d < 0.0001); + } + + return {tangent, bitangent}; +} + template [[nodiscard]] inline T TriangleArea(const qvec &v0, const qvec &v1, const qvec &v2) { diff --git a/include/common/settings.hh b/include/common/settings.hh index b0c26a4c..ff020fde 100644 --- a/include/common/settings.hh +++ b/include/common/settings.hh @@ -657,6 +657,35 @@ public: using setting_vec3::setting_vec3; }; +// a simple wrapper type that allows you to provide +// extra validation to an existing type without needing +// to create a whole new type for it. +template +class setting_validator : public T +{ +protected: + std::function _validator; + +public: + template + inline setting_validator(const decltype(_validator) &validator, Args&&... args) : + T(std::forward(args)...), + _validator(validator) + { + } + + bool parse(const std::string &settingName, parser_base_t &parser, source source) override + { + bool result = this->T::parse(settingName, parser, source); + + if (result) { + return _validator(*this); + } + + return result; + } +}; + // settings dictionary class setting_container diff --git a/include/qbsp/qbsp.hh b/include/qbsp/qbsp.hh index 60341a0a..5ff480f4 100644 --- a/include/qbsp/qbsp.hh +++ b/include/qbsp/qbsp.hh @@ -165,6 +165,31 @@ enum class filltype_t INSIDE }; +enum class tjunclevel_t +{ + NONE, // don't attempt to adjust faces at all - pass them through unchanged + ROTATE, // allow faces' vertices to be rotated to prevent zero-area triangles + RETOPOLOGIZE, // if a face still has zero-area triangles, allow it to be re-topologized + // by splitting it into multiple fans + DELAUNAY // attempt a delaunay triangulation first, only falling back to the prior two steps if it fails. +}; + +struct setting_tjunc : public setting_enum +{ +public: + using setting_enum::setting_enum; + + bool parse(const std::string &settingName, parser_base_t &parser, source source) override + { + if (settingName == "notjunc") { + this->setValue(tjunclevel_t::NONE, source); + return true; + } + + return this->setting_enum::parse(settingName, parser, source); + } +}; + class qbsp_settings : public common_settings { public: @@ -227,7 +252,9 @@ public: setting_int32 leakdist{this, "leakdist", 2, &debugging_group, "space between leakfile points"}; setting_bool forceprt1{ this, "forceprt1", false, &debugging_group, "force a PRT1 output file even if PRT2 is required for vis"}; - setting_bool notjunc{this, "notjunc", false, &debugging_group, "don't fix T-junctions"}; + setting_tjunc tjunc{this, { "tjunc", "notjunc" }, tjunclevel_t::DELAUNAY, + { { "none", tjunclevel_t::NONE }, { "rotate", tjunclevel_t::ROTATE }, { "retopologize", tjunclevel_t::RETOPOLOGIZE }, { "delaunay", tjunclevel_t::DELAUNAY } }, + &debugging_group, "T-junction fix level"}; setting_bool objexport{ this, "objexport", false, &debugging_group, "export the map file as .OBJ models during various CSG phases"}; setting_bool wrbrushes{this, {"wrbrushes", "bspx"}, false, &common_format_group, @@ -253,7 +280,10 @@ public: setting_enum filltype{this, "filltype", filltype_t::AUTO, { { "auto", filltype_t::AUTO }, { "inside", filltype_t::INSIDE }, { "outside", filltype_t::OUTSIDE } }, &common_format_group, "whether to fill the map from the outside in (lenient), from the inside out (aggressive), or to automatically decide based on the hull being used."}; setting_invertible_bool allow_upgrade{this, "allowupgrade", true, &common_format_group, "allow formats to \"upgrade\" to compatible extended formats when a limit is exceeded (ie Quake BSP to BSP2)"}; - setting_int32 maxedges{this, "maxedges", 64, &map_development_group, "the max number of edges/vertices on a single face before it is split into another face"}; + setting_validator maxedges{ + [](setting_int32 &setting) { + return setting.value() == 0 || setting.value() >= 3; + }, this, "maxedges", 64, &map_development_group, "the max number of edges/vertices on a single face before it is split into another face"}; void setParameters(int argc, const char **argv) override { @@ -325,7 +355,7 @@ class mapentity_t; struct face_fragment_t { - std::vector output_vertices, original_vertices; // filled in by EmitVertices & TJunc + std::vector output_vertices; // filled in by TJunc std::vector edges; // only filled in MakeFaceEdges std::optional outputnumber; // only valid for original faces after // write surfaces @@ -333,7 +363,7 @@ struct face_fragment_t struct portal_t; -struct face_t : face_fragment_t +struct face_t { int planenum; planeside_t planeside; // which side is the front of the face @@ -341,13 +371,12 @@ struct face_t : face_fragment_t contentflags_t contents; // contents on the front of the face int16_t lmshift; winding_t w; + std::vector original_vertices; // the vertices of this face before fragmentation; filled in by EmitVertices + std::vector fragments; // the vertices of this face post-fragmentation; filled in by TJunc qvec3d origin; vec_t radius; - // only valid after tjunction code - std::vector fragments; - portal_t *portal; }; diff --git a/qbsp/faces.cc b/qbsp/faces.cc index e4380e96..d97ba9fb 100644 --- a/qbsp/faces.cc +++ b/qbsp/faces.cc @@ -77,13 +77,11 @@ static void EmitFaceVertices(face_t *f) return; } - f->output_vertices.resize(f->w.size()); + f->original_vertices.resize(f->w.size()); for (size_t i = 0; i < f->w.size(); i++) { - EmitVertex(f->w[i], f->output_vertices[i]); + EmitVertex(f->w[i], f->original_vertices[i]); } - - f->original_vertices = f->output_vertices; } static void EmitVertices_R(node_t *node) @@ -160,11 +158,6 @@ FindFaceEdges */ static void FindFaceEdges(face_t *face) { - if (ShouldOmitFace(face)) - return; - - FindFaceFragmentEdges(face, face); - for (auto &fragment : face->fragments) { FindFaceFragmentEdges(face, &fragment); } @@ -230,23 +223,6 @@ static void EmitFaceFragment(face_t *face, face_fragment_t *fragment) out.numedges = static_cast(map.bsp.dsurfedges.size()) - out.firstedge; } -/* -============== -EmitFace -============== -*/ -static void EmitFace(face_t *face) -{ - if (ShouldOmitFace(face)) - return; - - EmitFaceFragment(face, face); - - for (auto &fragment : face->fragments) { - EmitFaceFragment(face, &fragment); - } -} - /* ============== GrowNodeRegion @@ -263,7 +239,9 @@ static void GrowNodeRegion(node_t *node) //Q_assert(face->planenum == node->planenum); // emit a region - EmitFace(face.get()); + for (auto &fragment : face->fragments) { + EmitFaceFragment(face.get(), &fragment); + } } node->numfaces = static_cast(map.bsp.dfaces.size()) - node->firstface; diff --git a/qbsp/qbsp.cc b/qbsp/qbsp.cc index 7fcb79ca..b3a28de2 100644 --- a/qbsp/qbsp.cc +++ b/qbsp/qbsp.cc @@ -652,9 +652,7 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum) // output vertices first, since TJunc needs it EmitVertices(tree->headnode.get()); - if (!qbsp_options.notjunc.value()) { - TJunc(tree->headnode.get()); - } + TJunc(tree->headnode.get()); if (qbsp_options.objexport.value() && entity == map.world_entity()) { ExportObj_Nodes("pre_makefaceedges_plane_faces", tree->headnode.get()); diff --git a/qbsp/tjunc.cc b/qbsp/tjunc.cc index 4ed8eb79..479174c9 100644 --- a/qbsp/tjunc.cc +++ b/qbsp/tjunc.cc @@ -24,11 +24,29 @@ #include #include -std::atomic c_degenerate; -std::atomic c_tjunctions; -std::atomic c_faceoverflows; -std::atomic c_facecollapse; -std::atomic c_norotates, c_rotates, c_retopology, c_faceretopology; +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 delaunay triangulation + std::atomic delaunay; +}; inline std::optional PointOnEdge(const qvec3d &p, const qvec3d &edge_start, const qvec3d &edge_dir, float start = 0, float end = 1) { @@ -60,11 +78,12 @@ 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) +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 - c_degenerate++; + stats.degenerate++; return; } @@ -82,9 +101,10 @@ inline void TestEdge(vec_t start, vec_t end, size_t p1, size_t p2, size_t startv } // break the edge - c_tjunctions++; - TestEdge (start, dist.value(), p1, j, k + 1, edge_verts, edge_start, edge_dir, superface); - TestEdge (dist.value(), end, j, p2, k + 1, edge_verts, edge_start, edge_dir, superface); + 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; } @@ -162,13 +182,13 @@ max edge count. Modifies `superface`. Adds the results to the end of `output`. ================== */ -inline void SplitFaceIntoFragments(std::vector &superface, std::list> &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) { - c_faceoverflows++; + stats.faceoverflows++; // copy MAXEDGES from our current face std::vector &newf = output.emplace_back(maxedges); @@ -217,11 +237,11 @@ 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) +static std::vector CreateSuperFace(node_t *headnode, face_t *f, tjunc_stats_t &stats) { std::vector superface; - superface.reserve(f->output_vertices.size() * 2); + superface.reserve(f->original_vertices.size() * 2); // stores all of the verts in the world that are close to // being on a given edge @@ -229,9 +249,9 @@ static std::vector CreateSuperFace(node_t *headnode, face_t *f) // find all of the extra vertices that lay on edges, // place them in superface - for (size_t i = 0; i < f->output_vertices.size(); i++) { - auto v1 = f->output_vertices[i]; - auto v2 = f->output_vertices[(i + 1) % f->output_vertices.size()]; + 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]; @@ -242,12 +262,70 @@ static std::vector CreateSuperFace(node_t *headnode, face_t *f) 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); + TestEdge(0, len, v1, v2, 0, edge_verts, edge_start, edge_dir, superface, stats); } return superface; } +#include + +static std::list> DelaunayFace(const face_t *f, const std::vector &vertices) +{ + // commented until DelaBella updates to fix cocircular points +#if 0 + auto p = map.planes[f->planenum]; + + if (f->planeside) { + 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) }; + } + + IDelaBella* idb = IDelaBella::Create(); + + idb->SetErrLog([](auto stream, auto fmt, ...) { + logging::print("{}", fmt); + return 0; + }, nullptr); + + std::list> tris_compiled; + + int verts = idb->Triangulate(points_2d.size(), &points_2d[0][0], &points_2d[0][1], sizeof(qvec2d)); + + // if positive, all ok + if (verts > 0) + { + int tris = verts / 3; + const DelaBella_Triangle* dela = idb->GetFirstDelaunayTriangle(); + for (int i = 0; i { vertices[dela->v[0]->i], vertices[dela->v[1]->i], vertices[dela->v[2]->i] }); + dela = dela->next; + } + + c_delaunay++; + } + + idb->Destroy(); + + // ... + + return tris_compiled; +#endif + return {}; +} + /* ================== RetopologizeFace @@ -257,7 +335,7 @@ It's still a convex face with wound vertices, though, so we can split it into several triangle fans. ================== */ -static std::list> RetopologizeFace(const std::vector &vertices) +static std::list> RetopologizeFace(const face_t *f, const std::vector &vertices) { std::list> result; // the copy we're working on @@ -413,90 +491,116 @@ FixFaceEdges If the face has any T-junctions, fix them here. ================== */ -static void FixFaceEdges(node_t *headnode, face_t *f) +static void FixFaceEdges(node_t *headnode, face_t *f, tjunc_stats_t &stats) { - std::vector superface = CreateSuperFace(headnode, f); - - if (superface.size() < 3) { - // entire face collapsed - f->output_vertices.clear(); - c_facecollapse++; - return; - } else if (superface.size() == f->output_vertices.size()) { - // face didn't need any new vertices + // 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 + 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 delaunay first; it will generate optimal results for everything. + if (qbsp_options.tjunc.value() >= settings::tjunclevel_t::DELAUNAY) { + try + { + faces = DelaunayFace(f, superface); + } + catch (std::exception) + { + } + } + // brute force rotating the start point until we find a valid winding // that doesn't have any T-junctions - size_t i = 0; + if (!faces.size() && qbsp_options.tjunc.value() >= settings::tjunclevel_t::ROTATE) { + size_t i = 0; - for (; i < superface.size(); i++) { - size_t x = 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()]; + // 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, superface, 0.01)) { + if (!TriangleIsValid(v0, v1, v2, superface, 0.01)) { + break; + } + } + + if (x == superface.size() - 2) { + // found one! 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); } } - // temporary storage for result faces - std::list> faces; - - if (i == superface.size()) { - // can't simply rotate to eliminate zero-area triangles, so we have - // to do a bit of re-topology. - if (auto retopology = RetopologizeFace(superface); retopology.size() > 1) { - c_retopology++; - c_faceretopology += retopology.size() - 1; - faces = std::move(retopology); - } else { - // unable to re-topologize, so just stick with the superface. - // it's got zero-area triangles that fill in the gaps. - c_norotates++; - faces.emplace_back(std::move(superface)); - } - } else if (i != 0) { - // was able to rotate the superface to eliminate zero-area triangles. - c_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); - } else { - // no need to change topology + // 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 + // split giant superfaces into subfaces if we have an edge limit. if (qbsp_options.maxedges.value()) { for (auto &face : faces) { - SplitFaceIntoFragments(face, faces); + SplitFaceIntoFragments(face, faces, stats); } } // move the results into the face - f->output_vertices = std::move(faces.front()); - f->fragments.resize(faces.size() - 1); + f->fragments.reserve(faces.size()); - i = 0; - for (auto it = ++faces.begin(); it != faces.end(); it++, i++) { - f->fragments[i].output_vertices = std::move(*it); + for (auto &face : faces) { + f->fragments.emplace_back(face_fragment_t { std::move(face) }); } } @@ -514,8 +618,8 @@ static void FindFaces_r(node_t *node, std::unordered_set &faces) } for (auto &f : node->facelist) { - // might have been omitted earlier, so `output_vertices` will be empty - if (f->output_vertices.size()) { + // might have been omitted, so `original_vertices` will be empty + if (f->original_vertices.size()) { faces.insert(f.get()); } } @@ -526,37 +630,47 @@ static void FindFaces_r(node_t *node, std::unordered_set &faces) /* =========== -tjunc +TJunc fixing entry point =========== */ void TJunc(node_t *headnode) { logging::print(logging::flag::PROGRESS, "---- {} ----\n", __func__); - // break edges on tjunctions - c_degenerate = 0; - c_facecollapse = 0; - c_tjunctions = 0; - c_faceoverflows = 0; - c_norotates = 0; - c_rotates = 0; - c_retopology = 0; - c_faceretopology = 0; - + tjunc_stats_t stats; std::unordered_set faces; FindFaces_r(headnode, faces); logging::parallel_for_each(faces, [&](auto &face) { - FixFaceEdges(headnode, face); + FixFaceEdges(headnode, face, stats); }); - logging::print (logging::flag::STAT, "{:5} edges degenerated\n", c_degenerate); - logging::print (logging::flag::STAT, "{:5} faces degenerated\n", c_facecollapse); - logging::print (logging::flag::STAT, "{:5} edges added by tjunctions\n", c_tjunctions); - logging::print (logging::flag::STAT, "{:5} faces rotated\n", c_rotates); - logging::print (logging::flag::STAT, "{:5} faces re-topologized\n", c_retopology); - logging::print (logging::flag::STAT, "{:5} faces added by re-topology\n", c_faceretopology); - logging::print (logging::flag::STAT, "{:5} faces added by splitting large faces\n", c_faceoverflows); - logging::print (logging::flag::STAT, "{:5} faces unable to be rotated or re-topologized\n", c_norotates); + 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.delaunay) { + logging::print (logging::flag::STAT, "{:5} faces delaunay triangulated\n", stats.delaunay); + } + 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.retopology) { + logging::print (logging::flag::STAT, "{:5} faces re-topologized\n", stats.retopology); + } + if (stats.faceretopology) { + logging::print (logging::flag::STAT, "{:5} faces added by re-topology\n", stats.faceretopology); + } + if (stats.faceoverflows) { + logging::print (logging::flag::STAT, "{:5} faces added by splitting large faces\n", stats.faceoverflows); + } } diff --git a/qbsp/writebsp.cc b/qbsp/writebsp.cc index 17a933ad..e5aba80c 100644 --- a/qbsp/writebsp.cc +++ b/qbsp/writebsp.cc @@ -173,16 +173,11 @@ static void ExportLeaf(node_t *node) dleaf.firstmarksurface = static_cast(map.bsp.dleaffaces.size()); for (auto &face : node->markfaces) { - if (!qbsp_options.includeskip.value() && map.mtexinfos.at(face->texinfo).flags.is_skip) + if (!qbsp_options.includeskip.value() && map.mtexinfos.at(face->texinfo).flags.is_skip) { continue; - // FIXME: this can happen when compiling some Q2 maps - // as Q1. - if (face->outputnumber.has_value()) { - /* emit a marksurface */ - map.bsp.dleaffaces.push_back(face->outputnumber.value()); } - /* grab tjunction split faces */ + /* grab final output faces */ for (auto &fragment : face->fragments) { if (fragment.outputnumber.has_value()) { map.bsp.dleaffaces.push_back(fragment.outputnumber.value());