From 2d8827e031a4b7b545cd8d0b634d5e35ef6ff3bb Mon Sep 17 00:00:00 2001 From: Jonathan Date: Sun, 21 Aug 2022 15:04:26 -0400 Subject: [PATCH] Revert "Q3 didn't need chop, we don't either!" This reverts commit f57ecaf59924e2ab65a51315562ee7a4e2759343. # Conflicts: # include/qbsp/brushbsp.hh --- include/qbsp/brushbsp.hh | 1 + include/qbsp/qbsp.hh | 1 + qbsp/brushbsp.cc | 218 +++++++++++++++++++++++++++++++++++++++ qbsp/qbsp.cc | 5 + tests/test_qbsp.cc | 10 ++ 5 files changed, 235 insertions(+) diff --git a/include/qbsp/brushbsp.hh b/include/qbsp/brushbsp.hh index a482c510..dc4bbf76 100644 --- a/include/qbsp/brushbsp.hh +++ b/include/qbsp/brushbsp.hh @@ -100,3 +100,4 @@ enum tree_split_t bspbrush_t::ptr BrushFromBounds(const aabb3d &bounds); void BrushBSP(tree_t &tree, mapentity_t &entity, const bspbrush_t::container &brushes, tree_split_t split_type); +void ChopBrushes(bspbrush_t::container &brushes); \ No newline at end of file diff --git a/include/qbsp/qbsp.hh b/include/qbsp/qbsp.hh index e38501b1..08254082 100644 --- a/include/qbsp/qbsp.hh +++ b/include/qbsp/qbsp.hh @@ -361,6 +361,7 @@ public: setting_bool leaktest{this, "leaktest", false, &map_development_group, "make compilation fail if the map leaks"}; setting_bool outsidedebug{this, "outsidedebug", false, &debugging_group, "write a .map after outside filling showing non-visible brush sides"}; + setting_bool debugchop{this, "debugchop", false, &debugging_group, "write a .map after ChopBrushes"}; setting_debugexpand debugexpand{this, "debugexpand", &debugging_group, "write expanded hull .map for debugging/inspecting hulls/brush bevelling"}; setting_bool keepprt{this, "keepprt", false, &debugging_group, "avoid deleting the .prt file on leaking maps"}; setting_bool includeskip{this, "includeskip", false, &common_format_group, diff --git a/qbsp/brushbsp.cc b/qbsp/brushbsp.cc index 2fb644c5..1d02ce69 100644 --- a/qbsp/brushbsp.cc +++ b/qbsp/brushbsp.cc @@ -1278,3 +1278,221 @@ void BrushBSP(tree_t &tree, mapentity_t &entity, const bspbrush_t::container &br logging::header("CountLeafs"); qbsp_options.target_game->print_content_stats(*stats.leafstats, "leafs"); } + +/* +================== +BrushGE + +Returns true if b1 is allowed to bite b2 +================== +*/ +inline bool BrushGE(const bspbrush_t &b1, const bspbrush_t &b2) +{ + // detail brushes never bite structural brushes + if ((b1.contents.is_any_detail(qbsp_options.target_game)) + && !(b2.contents.is_any_detail(qbsp_options.target_game))) { + return false; + } + if (b1.contents.is_any_solid(qbsp_options.target_game)) { + return true; + } + return false; +} + +/* +=============== +BrushesDisjoint + +Returns true if the two brushes definately do not intersect. +There will be false negatives for some non-axial combinations. +=============== +*/ +inline bool BrushesDisjoint (const bspbrush_t &a, const bspbrush_t &b) +{ + if (a.bounds.disjoint_or_touching(b.bounds)) { + // bounding boxes don't overlap + return true; + } + + // check for opposing planes + for (auto &as : a.sides) { + for (auto &bs : b.sides) { + if (as.planenum == (bs.planenum ^ 1)) { + // opposite planes, so not touching + return true; + } + } + } + + return false; // might intersect +} + +/* +=============== +SubtractBrush + +Returns a list of brushes that remain after B is subtracted from A. +May by empty if A is contained inside B. + +The originals are undisturbed. +=============== +*/ +inline bspbrush_t::list SubtractBrush(const bspbrush_t::ptr &a, const bspbrush_t::ptr &b) +{ + bspbrush_t::list out; + bspbrush_t::ptr in = a; + + for (auto &side : b->sides) { + auto [ front, back ] = SplitBrush(in, side.planenum, std::nullopt); + + if (front) { + // add to list + out.push_front(front); + } + + in = back; + + if (!in) { + // didn't really intersect + return { a }; + } + } + + return out; +} + +struct chopstats_t +{ + size_t c_swallowed = 0; // number of brushes completely swallowed + size_t c_from_split = 0; // number of new brushes created from being consumed + + ~chopstats_t() + { + if (c_swallowed) { + logging::print(logging::flag::STAT, " {:8} brushes swallowed\n", c_swallowed); + } + if (c_from_split) { + logging::print(logging::flag::STAT, " {:8} brushes created from the chompening\n", c_from_split); + } + } +}; + +/* +================= +ChopBrushes + +Carves any intersecting solid brushes into the minimum number +of non-intersecting brushes. + +Modifies the input list and may free destroyed brushes. +================= +*/ +void ChopBrushes(bspbrush_t::container &brushes) +{ + size_t original_count = brushes.size(); + logging::funcheader(); + + // convert brush container to list, so we don't lose + // track of the original ptrs and so we can re-organize things + bspbrush_t::list list { std::make_move_iterator(brushes.begin()), std::make_move_iterator(brushes.end()) }; + + // clear original list + brushes.clear(); + + logging::percent_clock clock(list.size()); + chopstats_t stats; + + decltype(list)::iterator b1_it = list.begin(); + +newlist: + + if (!list.size()) { + // clear output since this is kind of an error... + brushes.clear(); + return; + } + + decltype(list)::iterator next; + + for (; b1_it != list.end(); b1_it = next) + { + clock.max = list.size(); + next = std::next(b1_it); + + auto &b1 = *b1_it; + + for (auto b2_it = next; b2_it != list.end(); b2_it++) + { + auto &b2 = *b2_it; + + if (BrushesDisjoint(*b1, *b2)) { + continue; + } + + bspbrush_t::list sub, sub2; + size_t c1 = std::numeric_limits::max(), c2 = c1; + + if (BrushGE(*b2, *b1)) { + sub = SubtractBrush(b1, b2); + if (sub.size() == 1 && sub.front() == b1) { + continue; // didn't really intersect + } + + if (sub.empty()) { // b1 is swallowed by b2 + b1_it = list.erase(b1_it); // continue after b1_it + stats.c_swallowed++; + goto newlist; + } + c1 = sub.size(); + } + + if (BrushGE (*b1, *b2)) { + sub2 = SubtractBrush (b2, b1); + if (sub2.size() == 1 && sub2.front() == b2) { + continue; // didn't really intersect + } + if (sub2.empty()) { // b2 is swallowed by b1 + list.erase(b2_it); + // continue where b1_it was + stats.c_swallowed++; + goto newlist; + } + c2 = sub2.size(); + } + + if (sub.empty() && sub2.empty()) { + continue; // neither one can bite + } + + // only accept if it didn't fragment + // (commenting this out allows full fragmentation) + if (c1 > 1 && c2 > 1) { + continue; + } + + if (c1 < c2) { + stats.c_from_split += sub.size(); + list.splice(list.end(), sub); + b1_it = list.erase(b1_it); // start from after b1_it + goto newlist; + } else { + stats.c_from_split += sub2.size(); + list.splice(list.end(), sub2); + list.erase(b2_it); + // start from where b1_it left off + goto newlist; + } + } + + clock(); + } + + // since chopbrushes can remove stuff, exact counts are hard... + clock.max = list.size(); + clock.print(); + + brushes.insert(brushes.begin(), std::make_move_iterator(list.begin()), std::make_move_iterator(list.end())); + logging::print(logging::flag::STAT, "chopped {} brushes into {}\n", original_count, brushes.size()); + + //WriteBspBrushMap("chopped.map", brushes); +} \ No newline at end of file diff --git a/qbsp/qbsp.cc b/qbsp/qbsp.cc index 4bf54a1d..cfa5766c 100644 --- a/qbsp/qbsp.cc +++ b/qbsp/qbsp.cc @@ -467,6 +467,11 @@ static void ProcessEntity(mapentity_t &entity, hull_index_t hullnum) logging::print(logging::flag::STAT, "INFO: calculating BSP for {} brushes with {} sides\n", brushes.size(), num_sides); + // always chop the other hulls to reduce brush tests + if (qbsp_options.chop.value() || hullnum.value_or(0)) { + ChopBrushes(brushes); + } + // we're discarding the brush if (discarded_trigger) { entity.epairs.set("mins", fmt::to_string(entity.bounds.mins())); diff --git a/tests/test_qbsp.cc b/tests/test_qbsp.cc index 3b09c4dc..53b58648 100644 --- a/tests/test_qbsp.cc +++ b/tests/test_qbsp.cc @@ -552,6 +552,16 @@ TEST_CASE("options_reset2", "[testmaps_q1]") CHECK_FALSE(qbsp_options.transsky.value()); } +/** + * The brushes are touching but not intersecting, so ChopBrushes shouldn't change anything. + */ +TEST_CASE("chop_no_change", "[testmaps_q1]") +{ + LoadTestmapQ1("qbsp_chop_no_change.map"); + + // TODO: ideally we should check we get back the same brush pointers from ChopBrushes +} + TEST_CASE("simple_sealed", "[testmaps_q1]") { auto mapname = GENERATE("qbsp_simple_sealed.map", "qbsp_simple_sealed_rotated.map");