add q3map2-style -blocksize option, using the same algorithm from it. it's optional and disabled by default.

pass through the "use mid split" boolean again
remove node_t::side; appeared to be unused in our current code, and needs to be removed anyways to support the other plane splitters
re-introduce ChooseMidPlaneFromList, but comment it out as it currently fails on a lot of BSPs.
This commit is contained in:
Jonathan 2022-07-30 06:39:14 -04:00
parent 360daea172
commit e60babdb9c
4 changed files with 402 additions and 69 deletions

View File

@ -39,7 +39,6 @@ constexpr vec_t EDGE_LENGTH_EPSILON = 0.2;
bool WindingIsTiny(const winding_t &w, double size = EDGE_LENGTH_EPSILON); bool WindingIsTiny(const winding_t &w, double size = EDGE_LENGTH_EPSILON);
std::unique_ptr<bspbrush_t> BrushFromBounds(const aabb3d &bounds); std::unique_ptr<bspbrush_t> BrushFromBounds(const aabb3d &bounds);
std::unique_ptr<tree_t> BrushBSP(std::vector<std::unique_ptr<bspbrush_t>> brushlist);
// compatibility version // compatibility version
std::unique_ptr<tree_t> BrushBSP(mapentity_t *entity); std::unique_ptr<tree_t> BrushBSP(mapentity_t *entity, bool use_mid_split);

View File

@ -192,6 +192,58 @@ public:
} }
}; };
// like qvec3f, but integer and allows up to three values (xyz, x y, or x y z)
// defaults to 1024 if assigned, otherwise zero.
class setting_blocksize : public setting_value<qvec3i>
{
public:
inline setting_blocksize(setting_container *dictionary, const nameset &names, qvec3i val,
const setting_group *group = nullptr, const char *description = "")
: setting_value(dictionary, names, val, group, description)
{
}
bool parse(const std::string &settingName, parser_base_t &parser, source source) override
{
qvec3d vec = { 1024, 1024, 1024 };
for (int i = 0; i < 3; i++) {
if (!parser.parse_token(PARSE_PEEK)) {
return false;
}
// don't allow negatives
if (parser.token[0] != '-') {
try {
vec[i] = std::stol(parser.token);
parser.parse_token();
continue;
} catch (std::exception &) {
// intentional fall-through
}
}
// if we didn't parse a valid number, fail
if (i == 0) {
return false;
} else if (i == 1) {
// we parsed one valid number; use it all the way through
vec[1] = vec[2] = vec[0];
}
// for [x, y] z will be left default
}
setValue(vec, source);
return true;
}
std::string stringValue() const override { return qv::to_string(_value); }
std::string format() const override { return "[x [y [z]]]"; }
};
class qbsp_settings : public common_settings class qbsp_settings : public common_settings
{ {
public: public:
@ -296,6 +348,10 @@ public:
[](setting_int32 &setting) { return setting.value() == 0 || setting.value() >= 3; }, this, "maxedges", 64, [](setting_int32 &setting) { return setting.value() == 0 || setting.value() >= 3; }, this, "maxedges", 64,
&map_development_group, &map_development_group,
"the max number of edges/vertices on a single face before it is split into another face"}; "the max number of edges/vertices on a single face before it is split into another face"};
// FIXME: this block size default is from Q3, and is basically derived from having 128x128x128 chunks of the world
// since the max world size in Q3 is {-65536, -65536, -65536, 65536, 65536, 65536}. should we dynamically change this?
// should we automatically turn this on if the world gets too big but leave it off for smaller worlds?
setting_blocksize blocksize{this, "blocksize", { 0, 0, 0 }, &common_format_group, "from q3map2; split the world by x/y/z sized chunks, speeding up split decisions"};
void setParameters(int argc, const char **argv) override void setParameters(int argc, const char **argv) override
{ {
@ -568,7 +624,6 @@ struct node_t
twosided<std::unique_ptr<node_t>> twosided<std::unique_ptr<node_t>>
children; // children[0] = front side, children[1] = back side of plane. only valid for decision nodes children; // children[0] = front side, children[1] = back side of plane. only valid for decision nodes
std::list<std::unique_ptr<face_t>> facelist; // decision nodes only, list for both sides std::list<std::unique_ptr<face_t>> facelist; // decision nodes only, list for both sides
side_t *side; // decision node only, the side that created the node
// information for leafs // information for leafs
contentflags_t contents; // leaf nodes (0 for decision nodes) contentflags_t contents; // leaf nodes (0 for decision nodes)

View File

@ -54,6 +54,8 @@ struct bspstats_t
std::atomic<int> c_nodes; std::atomic<int> c_nodes;
// number of nodes created by splitting on a side_t which had !visible // number of nodes created by splitting on a side_t which had !visible
std::atomic<int> c_nonvis; std::atomic<int> c_nonvis;
// total number of nodes created by block splitting
std::atomic<int> c_blocksplit;
// total number of leafs // total number of leafs
std::atomic<int> c_leafs; std::atomic<int> c_leafs;
}; };
@ -270,8 +272,12 @@ TestBrushToPlanenum
static int TestBrushToPlanenum( static int TestBrushToPlanenum(
const bspbrush_t &brush, const qbsp_plane_t &plane, int *numsplits, bool *hintsplit, int *epsilonbrush) const bspbrush_t &brush, const qbsp_plane_t &plane, int *numsplits, bool *hintsplit, int *epsilonbrush)
{ {
if (numsplits) {
*numsplits = 0; *numsplits = 0;
}
if (hintsplit) {
*hintsplit = false; *hintsplit = false;
}
// if the brush actually uses the planenum, // if the brush actually uses the planenum,
// we can tell the side for sure // we can tell the side for sure
@ -291,6 +297,7 @@ static int TestBrushToPlanenum(
if (s != PSIDE_BOTH) if (s != PSIDE_BOTH)
return s; return s;
if (numsplits && hintsplit && epsilonbrush) {
// if both sides, count the visible faces split // if both sides, count the visible faces split
vec_t d_front = 0; vec_t d_front = 0;
vec_t d_back = 0; vec_t d_back = 0;
@ -327,8 +334,10 @@ static int TestBrushToPlanenum(
} }
} }
if ((d_front > 0.0 && d_front < 1.0) || (d_back < 0.0 && d_back > -1.0)) if ((d_front > 0.0 && d_front < 1.0) || (d_back < 0.0 && d_back > -1.0)) {
(*epsilonbrush)++; (*epsilonbrush)++;
}
}
return s; return s;
} }
@ -622,17 +631,270 @@ static bool CheckPlaneAgainstVolume(const qbsp_plane_t &plane, node_t *node)
return good; return good;
} }
/*
* Calculate the split plane metric for axial planes
*/
inline vec_t SplitPlaneMetric_Axial(const qbsp_plane_t &p, const aabb3d &bounds)
{
vec_t value = 0;
for (int i = 0; i < 3; i++) {
if (static_cast<plane_type_t>(i) == p.get_type()) {
const vec_t dist = p.get_dist() * p.get_normal()[i];
value += (bounds.maxs()[i] - dist) * (bounds.maxs()[i] - dist);
value += (dist - bounds.mins()[i]) * (dist - bounds.mins()[i]);
} else {
value += 2 * (bounds.maxs()[i] - bounds.mins()[i]) * (bounds.maxs()[i] - bounds.mins()[i]);
}
}
return value;
}
/*
* Split a bounding box by a plane; The front and back bounds returned
* are such that they completely contain the portion of the input box
* on that side of the plane. Therefore, if the split plane is
* non-axial, then the returned bounds will overlap.
*/
inline void DivideBounds(const aabb3d &in_bounds, const qbsp_plane_t &split, aabb3d &front_bounds, aabb3d &back_bounds)
{
int a, b, c, i, j;
vec_t dist1, dist2, mid, split_mins, split_maxs;
qvec3d corner;
front_bounds = back_bounds = in_bounds;
if (split.get_type() < plane_type_t::PLANE_ANYX) {
front_bounds[0][static_cast<size_t>(split.get_type())] = back_bounds[1][static_cast<size_t>(split.get_type())] = split.get_dist();
return;
}
/* Make proper sloping cuts... */
for (a = 0; a < 3; ++a) {
/* Check for parallel case... no intersection */
if (fabs(split.get_normal()[a]) < NORMAL_EPSILON)
continue;
b = (a + 1) % 3;
c = (a + 2) % 3;
split_mins = in_bounds.maxs()[a];
split_maxs = in_bounds.mins()[a];
for (i = 0; i < 2; ++i) {
corner[b] = in_bounds[i][b];
for (j = 0; j < 2; ++j) {
corner[c] = in_bounds[j][c];
corner[a] = in_bounds[0][a];
dist1 = split.distance_to(corner);
corner[a] = in_bounds[1][a];
dist2 = split.distance_to(corner);
mid = in_bounds[1][a] - in_bounds[0][a];
mid *= (dist1 / (dist1 - dist2));
mid += in_bounds[0][a];
split_mins = max(min(mid, split_mins), in_bounds.mins()[a]);
split_maxs = min(max(mid, split_maxs), in_bounds.maxs()[a]);
}
}
if (split.get_normal()[a] > 0) {
front_bounds[0][a] = split_mins;
back_bounds[1][a] = split_maxs;
} else {
back_bounds[0][a] = split_mins;
front_bounds[1][a] = split_maxs;
}
}
}
/*
* Calculate the split plane metric for non-axial planes
*/
inline vec_t SplitPlaneMetric_NonAxial(const qbsp_plane_t &p, const aabb3d &bounds)
{
aabb3d f, b;
vec_t value = 0.0;
DivideBounds(bounds, p, f, b);
for (int i = 0; i < 3; i++) {
value += (f.maxs()[i] - f.mins()[i]) * (f.maxs()[i] - f.mins()[i]);
value += (b.maxs()[i] - b.mins()[i]) * (b.maxs()[i] - b.mins()[i]);
}
return value;
}
inline vec_t SplitPlaneMetric(const qbsp_plane_t &p, const aabb3d &bounds)
{
if (p.get_type() < plane_type_t::PLANE_ANYX) {
return SplitPlaneMetric_Axial(p, bounds);
} else {
return SplitPlaneMetric_NonAxial(p, bounds);
}
}
/*
==================
ChooseMidPlaneFromList
The clipping hull BSP doesn't worry about avoiding splits
==================
*/
static std::optional<qbsp_plane_t> ChooseMidPlaneFromList(const std::vector<std::unique_ptr<bspbrush_t>> &brushes, const aabb3d &bounds, bool forced)
{
/* pick the plane that splits the least */
vec_t bestaxialmetric = VECT_MAX;
std::optional<qbsp_plane_t> bestaxialplane;
vec_t bestanymetric = VECT_MAX;
std::optional<qbsp_plane_t> bestanyplane;
for (int pass = 0; pass < 2; pass++) {
for (auto &brush : brushes) {
if ((pass & 1) && !brush->original->contents.is_any_detail(qbsp_options.target_game)) {
continue;
}
if (!(pass & 1) && brush->original->contents.is_any_detail(qbsp_options.target_game)) {
continue;
}
for (auto &side : brush->sides) {
if (side.bevel) {
continue; // never use a bevel as a spliter
}
if (!side.w) {
continue; // nothing visible, so it can't split
}
if (side.onnode) {
continue; // allready a node splitter
}
if (side.get_texinfo().flags.is_hintskip) {
continue; // skip surfaces are never chosen
}
const qbsp_plane_t &plane = side.plane;
/* calculate the split metric, smaller values are better */
const vec_t metric = SplitPlaneMetric(plane, bounds);
if (metric < bestanymetric) {
bestanymetric = metric;
bestanyplane = plane;
}
/* check for axis aligned surfaces */
if (plane.get_type() < plane_type_t::PLANE_ANYX) {
if (metric < bestaxialmetric) {
bestaxialmetric = metric;
bestaxialplane = plane;
}
}
}
}
if (bestanyplane || bestaxialplane) {
break;
}
}
// prefer the axial split
auto bestsurface = !bestaxialplane ? bestanyplane : bestaxialplane;
if (!bestsurface) {
FError("No valid planes in surface list");
}
// ericw -- (!forced) is true on the final SolidBSP phase for the world.
// !bestsurface->has_struct means all surfaces in this node are detail, so
// mark the surface as a detail separator.
// fixme-brushbsp: what to do here?
#if 0
if (!forced && !bestsurface->has_struct) {
bestsurface->detail_separator = true;
}
#endif
return bestsurface;
}
/* /*
================ ================
SelectSplitSide SelectSplitPlane
Using a hueristic, choses one of the sides out of the brushlist Using heuristics, chooses a plane to partition the brushes with.
to partition the brushes with. Returns nullopt if there are no valid planes to split with.
Returns NULL if there are no valid planes to split with..
================ ================
*/ */
side_t *SelectSplitSide(const std::vector<std::unique_ptr<bspbrush_t>> &brushes, node_t *node) static std::optional<qbsp_plane_t> SelectSplitPlane(const std::vector<std::unique_ptr<bspbrush_t>> &brushes, node_t *node, bool use_mid_split, bspstats_t &stats)
{ {
// no brushes left to split, so we can't use any plane.
if (!brushes.size()) {
return std::nullopt;
}
// if it is crossing a block boundary, force a split;
// this is optional q3map2 mode
for (size_t i = 0; i < 3; i++) {
if (qbsp_options.blocksize.value()[i] <= 0) {
continue;
}
vec_t dist = qbsp_options.blocksize.value()[i] * (floor(node->bounds.mins()[i] / qbsp_options.blocksize.value()[i]) + 1);
if (node->bounds.maxs()[i] > dist) {
qplane3d plane{};
plane.normal[i] = 1.0;
plane.dist = dist;
qbsp_plane_t bsp_plane = plane;
stats.c_blocksplit++;
for (auto &b : brushes) {
b->side = TestBrushToPlanenum(*b, bsp_plane, nullptr, nullptr, nullptr);
}
return bsp_plane;
}
}
// fixme-brushbsp: re-introduce
#if 0
// how much of the map are we partitioning?
double fractionOfMap = brushes.size() / (double) map.brushes.size();
bool largenode = false;
if (!use_mid_split) {
// decide if we should switch to the midsplit method
if (qbsp_options.midsplitsurffraction.value() != 0.0) {
// new way (opt-in)
largenode = (fractionOfMap > qbsp_options.midsplitsurffraction.value());
} else {
// old way (ericw-tools 0.15.2+)
if (qbsp_options.maxnodesize.value() >= 64) {
const vec_t maxnodesize = qbsp_options.maxnodesize.value() - qbsp_options.epsilon.value();
largenode = (node->bounds.maxs()[0] - node->bounds.mins()[0]) > maxnodesize ||
(node->bounds.maxs()[1] - node->bounds.mins()[1]) > maxnodesize ||
(node->bounds.maxs()[2] - node->bounds.mins()[2]) > maxnodesize;
}
}
}
// do fast way for clipping hull
if (use_mid_split || largenode) {
if (auto mid_plane = ChooseMidPlaneFromList(brushes, node->bounds, use_mid_split)) {
for (auto &b : brushes) {
b->side = TestBrushToPlanenum(*b, mid_plane.value(), nullptr, nullptr, nullptr);
}
return mid_plane;
}
}
#endif
side_t *bestside = nullptr; side_t *bestside = nullptr;
int bestvalue = -99999; int bestvalue = -99999;
int bestsplits = 0; int bestsplits = 0;
@ -749,7 +1011,15 @@ side_t *SelectSplitSide(const std::vector<std::unique_ptr<bspbrush_t>> &brushes,
} }
} }
return bestside; if (!bestside) {
return std::nullopt;
}
if (!bestside->visible) {
stats.c_nonvis++;
}
return bestside->plane;
} }
/* /*
@ -758,7 +1028,7 @@ SplitBrushList
================ ================
*/ */
static std::array<std::vector<std::unique_ptr<bspbrush_t>>, 2> SplitBrushList( static std::array<std::vector<std::unique_ptr<bspbrush_t>>, 2> SplitBrushList(
std::vector<std::unique_ptr<bspbrush_t>> brushes, const node_t *node) std::vector<std::unique_ptr<bspbrush_t>> brushes, const qbsp_plane_t &plane)
{ {
std::array<std::vector<std::unique_ptr<bspbrush_t>>, 2> result; std::array<std::vector<std::unique_ptr<bspbrush_t>>, 2> result;
@ -767,7 +1037,7 @@ static std::array<std::vector<std::unique_ptr<bspbrush_t>>, 2> SplitBrushList(
if (sides == PSIDE_BOTH) { if (sides == PSIDE_BOTH) {
// split into two brushes // split into two brushes
auto [front, back] = SplitBrush(brush->copy_unique(), node->plane); auto [front, back] = SplitBrush(brush->copy_unique(), plane);
if (front) { if (front) {
result[0].push_back(std::move(front)); result[0].push_back(std::move(front));
@ -784,7 +1054,7 @@ static std::array<std::vector<std::unique_ptr<bspbrush_t>>, 2> SplitBrushList(
// as a splitter again // as a splitter again
if (sides & PSIDE_FACING) { if (sides & PSIDE_FACING) {
for (auto &side : brush->sides) { for (auto &side : brush->sides) {
if (qv::epsilonEqual(side.plane, node->plane)) { if (qv::epsilonEqual(side.plane, plane)) {
side.onnode = true; side.onnode = true;
} }
} }
@ -810,13 +1080,13 @@ BuildTree_r
Called in parallel. Called in parallel.
================== ==================
*/ */
static void BuildTree_r(node_t *node, std::vector<std::unique_ptr<bspbrush_t>> brushes, bspstats_t &stats) static void BuildTree_r(node_t *node, std::vector<std::unique_ptr<bspbrush_t>> brushes, bool use_mid_split, bspstats_t &stats)
{ {
// find the best plane to use as a splitter // find the best plane to use as a splitter
auto *bestside = const_cast<side_t *>(SelectSplitSide(brushes, node)); auto bestplane = SelectSplitPlane(brushes, node, use_mid_split, stats);
if (!bestside) {
if (!bestplane) {
// this is a leaf node // this is a leaf node
node->side = nullptr;
node->is_leaf = true; node->is_leaf = true;
stats.c_leafs++; stats.c_leafs++;
@ -827,19 +1097,24 @@ static void BuildTree_r(node_t *node, std::vector<std::unique_ptr<bspbrush_t>> b
// this is a splitplane node // this is a splitplane node
stats.c_nodes++; stats.c_nodes++;
if (!bestside->visible) {
stats.c_nonvis++;
}
node->side = bestside; node->plane.set_plane(bestplane.value(), true); // always use front facing
node->plane.set_plane(bestside->plane, true); // always use front facing
auto children = SplitBrushList(std::move(brushes), node); auto children = SplitBrushList(std::move(brushes), node->plane);
// allocate children before recursing // allocate children before recursing
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
auto &newnode = node->children[i] = std::make_unique<node_t>(); auto &newnode = node->children[i] = std::make_unique<node_t>();
newnode->parent = node; newnode->parent = node;
newnode->bounds = node->bounds;
}
for (int i = 0; i < 3; i++) {
if (bestplane->get_normal()[i] == 1.0) {
node->children[0]->bounds[0][i] = bestplane->get_dist();
node->children[1]->bounds[1][i] = bestplane->get_dist();
break;
}
} }
auto children_volumes = SplitBrush(node->volume->copy_unique(), node->plane); auto children_volumes = SplitBrush(node->volume->copy_unique(), node->plane);
@ -848,8 +1123,8 @@ static void BuildTree_r(node_t *node, std::vector<std::unique_ptr<bspbrush_t>> b
// recursively process children // recursively process children
tbb::task_group g; tbb::task_group g;
g.run([&]() { BuildTree_r(node->children[0].get(), std::move(children[0]), stats); }); g.run([&]() { BuildTree_r(node->children[0].get(), std::move(children[0]), use_mid_split, stats); });
g.run([&]() { BuildTree_r(node->children[1].get(), std::move(children[1]), stats); }); g.run([&]() { BuildTree_r(node->children[1].get(), std::move(children[1]), use_mid_split, stats); });
g.wait(); g.wait();
} }
@ -858,7 +1133,7 @@ static void BuildTree_r(node_t *node, std::vector<std::unique_ptr<bspbrush_t>> b
BrushBSP BrushBSP
================== ==================
*/ */
static std::unique_ptr<tree_t> BrushBSP(mapentity_t *entity, std::vector<std::unique_ptr<bspbrush_t>> brushlist) static std::unique_ptr<tree_t> BrushBSP(mapentity_t *entity, std::vector<std::unique_ptr<bspbrush_t>> brushlist, bool use_mid_split)
{ {
auto tree = std::make_unique<tree_t>(); auto tree = std::make_unique<tree_t>();
@ -926,24 +1201,24 @@ static std::unique_ptr<tree_t> BrushBSP(mapentity_t *entity, std::vector<std::un
auto node = std::make_unique<node_t>(); auto node = std::make_unique<node_t>();
node->volume = BrushFromBounds(tree->bounds.grow(SIDESPACE)); node->volume = BrushFromBounds(tree->bounds.grow(SIDESPACE));
node->bounds = tree->bounds.grow(SIDESPACE);
tree->headnode = std::move(node); tree->headnode = std::move(node);
bspstats_t stats{}; bspstats_t stats{};
stats.leafstats = qbsp_options.target_game->create_content_stats(); stats.leafstats = qbsp_options.target_game->create_content_stats();
BuildTree_r(tree->headnode.get(), std::move(brushlist), stats); BuildTree_r(tree->headnode.get(), std::move(brushlist), use_mid_split, stats);
logging::print(logging::flag::STAT, " {:8} visible nodes\n", stats.c_nodes - stats.c_nonvis); logging::print(logging::flag::STAT, " {:8} visible nodes\n", stats.c_nodes - stats.c_nonvis);
logging::print(logging::flag::STAT, " {:8} nonvis nodes\n", stats.c_nonvis); logging::print(logging::flag::STAT, " {:8} nonvis nodes\n", stats.c_nonvis);
logging::print(logging::flag::STAT, " {:8} block split nodes\n", stats.c_blocksplit);
logging::print(logging::flag::STAT, " {:8} leafs\n", stats.c_leafs); logging::print(logging::flag::STAT, " {:8} leafs\n", stats.c_leafs);
qbsp_options.target_game->print_content_stats(*stats.leafstats, "leafs"); qbsp_options.target_game->print_content_stats(*stats.leafstats, "leafs");
return tree; return tree;
} }
std::unique_ptr<tree_t> BrushBSP(mapentity_t *entity) std::unique_ptr<tree_t> BrushBSP(mapentity_t *entity, bool use_mid_split)
{ {
auto tree = BrushBSP(entity, MakeBspBrushList(entity)); return BrushBSP(entity, MakeBspBrushList(entity), use_mid_split);
return tree;
} }

View File

@ -589,13 +589,13 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum)
std::unique_ptr<tree_t> tree = nullptr; std::unique_ptr<tree_t> tree = nullptr;
if (hullnum > 0) { if (hullnum > 0) {
tree = BrushBSP(entity); tree = BrushBSP(entity, true);
if (entity == map.world_entity() && !qbsp_options.nofill.value()) { if (entity == map.world_entity() && !qbsp_options.nofill.value()) {
// assume non-world bmodels are simple // assume non-world bmodels are simple
MakeTreePortals(tree.get()); MakeTreePortals(tree.get());
if (FillOutside(entity, tree.get(), hullnum)) { if (FillOutside(entity, tree.get(), hullnum)) {
// make a really good tree // make a really good tree
tree = BrushBSP(entity); tree = BrushBSP(entity, false);
// fill again so PruneNodes works // fill again so PruneNodes works
MakeTreePortals(tree.get()); MakeTreePortals(tree.get());
@ -607,7 +607,11 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum)
// fixme-brushbsp: return here? // fixme-brushbsp: return here?
} else { } else {
tree = BrushBSP(entity); if (qbsp_options.forcegoodtree.value()) {
tree = BrushBSP(entity, false);
} else {
tree = BrushBSP(entity, entity == map.world_entity());
}
// build all the portals in the bsp tree // build all the portals in the bsp tree
// some portals are solid polygons, and some are paths to other leafs // some portals are solid polygons, and some are paths to other leafs
@ -620,7 +624,7 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum)
// (effectively expanding those brush sides outwards). // (effectively expanding those brush sides outwards).
if (!qbsp_options.nofill.value() && FillOutside(entity, tree.get(), hullnum)) { if (!qbsp_options.nofill.value() && FillOutside(entity, tree.get(), hullnum)) {
// make a really good tree // make a really good tree
tree = BrushBSP(entity); tree = BrushBSP(entity, false);
// make the real portals for vis tracing // make the real portals for vis tracing
MakeTreePortals(tree.get()); MakeTreePortals(tree.get());
@ -638,7 +642,7 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum)
FillBrushEntity(entity, tree.get(), hullnum); FillBrushEntity(entity, tree.get(), hullnum);
// rebuild BSP now that we've marked invisible brush sides // rebuild BSP now that we've marked invisible brush sides
tree = BrushBSP(entity); tree = BrushBSP(entity, false);
} }
MakeTreePortals(tree.get()); MakeTreePortals(tree.get());