ericw-tools/qbsp/solidbsp.cc

999 lines
30 KiB
C++

/*
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.
*/
#include <qbsp/solidbsp.hh>
#include <climits>
#include <common/vectorutils.hh>
#include <qbsp/brush.hh>
#include <qbsp/csg4.hh>
#include <qbsp/map.hh>
#include <qbsp/portals.hh>
#include <qbsp/qbsp.hh>
#include <list>
#include <atomic>
#include "tbb/task_group.h"
std::atomic<int> splitnodes;
static std::any leafstats;
static bool usemidsplit;
/**
* Total number of brushes in the map
*/
static int mapbrushes;
//============================================================================
void ConvertNodeToLeaf(node_t *node, const contentflags_t &contents)
{
// merge the children's brush lists
node->original_brushes = concat(
node->children[0]->original_brushes,
node->children[1]->original_brushes);
sort_and_remove_duplicates(node->original_brushes);
node->planenum = PLANENUM_LEAF;
for (int i = 0; i < 2; ++i) {
delete node->children[i];
node->children[i] = nullptr;
}
for (auto *face : node->facelist) {
delete face;
}
node->facelist = {};
node->contents = contents;
Q_assert(node->markfaces.empty());
}
void DetailToSolid(node_t *node)
{
if (node->planenum == PLANENUM_LEAF) {
if (options.target_game->id == GAME_QUAKE_II) {
return;
}
// We need to remap CONTENTS_DETAIL to a standard quake content type
if (node->contents.is_detail_solid(options.target_game)) {
node->contents = options.target_game->create_solid_contents();
} else if (node->contents.is_detail_illusionary(options.target_game)) {
node->contents = options.target_game->create_empty_contents();
}
/* N.B.: CONTENTS_DETAIL_FENCE is not remapped to CONTENTS_SOLID until the very last moment,
* because we want to generate a leaf (if we set it to CONTENTS_SOLID now it would use leaf 0).
*/
return;
} else {
DetailToSolid(node->children[0]);
DetailToSolid(node->children[1]);
// If both children are solid, we can merge the two leafs into one.
// DarkPlaces has an assertion that fails if both children are
// solid.
if (node->children[0]->contents.is_solid(options.target_game) &&
node->children[1]->contents.is_solid(options.target_game)) {
// This discards any faces on-node. Should be safe (?)
ConvertNodeToLeaf(node, options.target_game->create_solid_contents());
}
// fixme-brushbsp: merge with PruneNodes
}
}
static void PruneNodes_R(node_t *node, int &count_pruned)
{
if (node->planenum == PLANENUM_LEAF) {
return;
}
PruneNodes_R(node->children[0], count_pruned);
PruneNodes_R(node->children[1], count_pruned);
if (node->children[0]->planenum == PLANENUM_LEAF && node->children[0]->contents.is_solid(options.target_game) &&
node->children[1]->planenum == PLANENUM_LEAF && node->children[1]->contents.is_solid(options.target_game)) {
// This discards any faces on-node. Should be safe (?)
ConvertNodeToLeaf(node, options.target_game->create_solid_contents());
++count_pruned;
}
// fixme-brushbsp: corner case where two solid leafs shouldn't merge is two noclipfaces fence brushes touching
// fixme-brushbsp: also merge other content types
// fixme-brushbsp: maybe merge if same content type, and all faces on node are invisible?
}
void PruneNodes(node_t *node)
{
logging::print(logging::flag::PROGRESS, "---- {} ----\n", __func__);
int count_pruned = 0;
PruneNodes_R(node, count_pruned);
logging::print(logging::flag::STAT, " {:8} pruned nodes\n", count_pruned);
}
/*
==================
FaceSide
For BSP hueristic
==================
*/
static int FaceSide__(const face_t *in, const qbsp_plane_t &split)
{
bool have_front, have_back;
int i;
have_front = have_back = false;
if (split.type < plane_type_t::PLANE_ANYX) {
/* shortcut for axial planes */
const vec_t *p = &in->w[0][static_cast<size_t>(split.type)];
for (i = 0; i < in->w.size(); i++, p += 3) {
if (*p > split.dist + options.epsilon.value()) {
if (have_back)
return SIDE_ON;
have_front = true;
} else if (*p < split.dist - options.epsilon.value()) {
if (have_front)
return SIDE_ON;
have_back = true;
}
}
} else {
/* sloping planes take longer */
for (i = 0; i < in->w.size(); i++) {
const vec_t dot = split.distance_to(in->w[i]);
if (dot > options.epsilon.value()) {
if (have_back)
return SIDE_ON;
have_front = true;
} else if (dot < -options.epsilon.value()) {
if (have_front)
return SIDE_ON;
have_back = true;
}
}
}
if (!have_front)
return SIDE_BACK;
if (!have_back)
return SIDE_FRONT;
return SIDE_ON;
}
inline int FaceSide(const face_t *in, const qbsp_plane_t &split)
{
vec_t dist = split.distance_to(in->origin);
if (dist > in->radius)
return SIDE_FRONT;
else if (dist < -in->radius)
return SIDE_BACK;
else
return FaceSide__(in, split);
}
/*
* 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.
*/
static 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.type < plane_type_t::PLANE_ANYX) {
front_bounds[0][static_cast<size_t>(split.type)] = back_bounds[1][static_cast<size_t>(split.type)] = split.dist;
return;
}
/* Make proper sloping cuts... */
for (a = 0; a < 3; ++a) {
/* Check for parallel case... no intersection */
if (fabs(split.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.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 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.type) {
const vec_t dist = p.dist * p.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;
}
/*
* 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.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 const face_t *ChooseMidPlaneFromList(const std::vector<std::unique_ptr<brush_t>> &brushes, const aabb3d &bounds)
{
/* pick the plane that splits the least */
vec_t bestaxialmetric = VECT_MAX;
face_t *bestaxialsurface = nullptr;
vec_t bestanymetric = VECT_MAX;
face_t *bestanysurface = nullptr;
for (int pass = 0; pass < 2; pass++) {
for (auto &brush : brushes) {
if (brush->contents.is_any_detail(options.target_game) != (pass == 1)) {
continue;
}
for (auto &face : brush->faces) {
if (face.onnode)
continue;
if (!face.visible) {
// never use as a bsp splitter, efffectively filling the brush outwards
continue;
}
const qbsp_plane_t &plane = map.planes.at(face.planenum);
bool axial = false;
/* check for axis aligned surfaces */
if (plane.type < plane_type_t::PLANE_ANYX) {
axial = true;
}
/* calculate the split metric, smaller values are better */
const vec_t metric = SplitPlaneMetric(plane, bounds);
if (metric < bestanymetric) {
bestanymetric = metric;
bestanysurface = &face;
}
if (axial) {
if (metric < bestaxialmetric) {
bestaxialmetric = metric;
bestaxialsurface = &face;
}
}
}
}
if (bestanysurface != nullptr || bestaxialsurface != nullptr) {
break;
}
}
// prefer the axial split
auto bestsurface = (bestaxialsurface == nullptr) ? bestanysurface : bestaxialsurface;
return bestsurface;
}
/*
==================
ChoosePlaneFromList
The real BSP heuristic
fixme-brushbsp: prefer splits that include a lot of brush sides?
==================
*/
static const face_t *ChoosePlaneFromList(const std::vector<std::unique_ptr<brush_t>> &brushes, const aabb3d &bounds)
{
/* pick the plane that splits the least */
int minsplits = INT_MAX - 1;
vec_t bestdistribution = VECT_MAX;
face_t *bestsurface = nullptr;
/* passes:
* 0: structural visible
* 1: structural non-visible
* 2: detail visible
* 3: detail non-visible
* */
for (int pass = 0; pass < 4; pass++) {
const bool pass_wants_detail = (pass >= 2);
const bool pass_wants_visible = (pass == 0 || pass == 2);
for (auto &brush : brushes) {
if (brush->contents.is_any_detail(options.target_game) != pass_wants_detail) {
continue;
}
for (auto &face : brush->faces) {
if (face.onnode) {
continue;
}
if (face.visible != pass_wants_visible) {
continue;
}
const bool hintsplit = map.mtexinfos.at(face.texinfo).flags.is_hint;
const qbsp_plane_t &plane = map.planes.at(face.planenum);
int splits = 0;
// now check all of the other faces in `brushes` and count how many
// would get split if we used `face` as the splitting plane
for (auto &brush2 : brushes) {
for (auto &face2 : brush2->faces) {
if (face2.planenum == face.planenum || face2.onnode)
continue;
if (!face2.visible)
continue; // don't penalize for splitting non-visible
const surfflags_t &flags = map.mtexinfos.at(face2.texinfo).flags;
/* Don't penalize for splitting skip faces */
if (flags.is_skip)
continue;
if (FaceSide(&face2, plane) == SIDE_ON) {
/* Never split a hint face except with a hint */
if (!hintsplit && flags.is_hint) {
splits = INT_MAX;
break;
}
splits++;
if (splits >= minsplits)
break;
}
}
if (splits > minsplits)
break;
}
if (splits > minsplits)
continue;
/*
* if equal numbers axial planes win, otherwise decide on spatial
* subdivision
*/
if (splits < minsplits || (splits == minsplits && plane.type < plane_type_t::PLANE_ANYX)) {
if (plane.type < plane_type_t::PLANE_ANYX) {
const vec_t distribution = SplitPlaneMetric(plane, bounds);
if (distribution > bestdistribution && splits == minsplits)
continue;
bestdistribution = distribution;
}
/* currently the best! */
minsplits = splits;
bestsurface = &face;
}
}
}
/* If we found a candidate on first pass, don't do a second pass */
if (bestsurface != nullptr) {
return bestsurface;
}
}
return bestsurface;
}
/*
==================
SelectPartition
Selects a surface from a linked list of surfaces to split the group on
returns NULL if the surface list can not be divided any more (a leaf)
Called in parallel.
==================
*/
static const face_t *SelectPartition(const std::vector<std::unique_ptr<brush_t>> &brushes)
{
// calculate a bounding box of the entire surfaceset
aabb3d bounds;
for (auto &brush : brushes) {
bounds += brush->bounds;
}
// how much of the map are we partitioning?
double fractionOfMap = brushes.size() / (double)mapbrushes;
bool largenode = false;
// decide if we should switch to the midsplit method
if (options.midsplitsurffraction.value() != 0.0) {
// new way (opt-in)
largenode = (fractionOfMap > options.midsplitsurffraction.value());
} else {
// old way (ericw-tools 0.15.2+)
if (options.maxnodesize.value() >= 64) {
const vec_t maxnodesize = options.maxnodesize.value() - options.epsilon.value();
largenode = (bounds.maxs()[0] - bounds.mins()[0]) > maxnodesize ||
(bounds.maxs()[1] - bounds.mins()[1]) > maxnodesize ||
(bounds.maxs()[2] - bounds.mins()[2]) > maxnodesize;
}
}
if (usemidsplit || largenode) // do fast way for clipping hull
return ChooseMidPlaneFromList(brushes, bounds);
// do slow way to save poly splits for drawing hull
return ChoosePlaneFromList(brushes, bounds);
}
//============================================================================
/*
================
WindingIsTiny
Returns true if the winding would be crunched out of
existance by the vertex snapping.
================
*/
#define EDGE_LENGTH 0.2
bool WindingIsTiny(const winding_t &w, double size)
{
return w.area() < size;
#if 0
if (WindingArea (w) < 1)
return true;
return false;
#elif 0
int edges = 0;
for (size_t i = 0; i < w.size(); i++) {
size_t j = (i + 1) % w.size();
const qvec3d delta = w[j] - w[i];
const double len = qv::length(delta);
if (len > size) {
if (++edges == 3)
return false;
}
}
return true;
#endif
}
/*
================
WindingIsHuge
Returns true if the winding still has one of the points
from basewinding for plane
================
*/
bool WindingIsHuge(const winding_t &w)
{
for (size_t i = 0; i < w.size(); i++) {
for (size_t j = 0; j < 3; j++)
if (fabs(w[i][j]) > options.worldextent.value())
return true;
}
return false;
}
/*
==================
BrushMostlyOnSide
==================
*/
side_t BrushMostlyOnSide(const brush_t &brush, const qplane3d &plane)
{
vec_t max = 0;
side_t side = SIDE_FRONT;
for (auto &face : brush.faces) {
for (size_t j = 0; j < face.w.size(); j++) {
vec_t d = qv::dot(face.w[j], plane.normal) - plane.dist;
if (d > max) {
max = d;
side = SIDE_FRONT;
}
if (-d > max) {
max = -d;
side = SIDE_BACK;
}
}
}
return side;
}
/*
==================
BrushVolume
==================
*/
vec_t BrushVolume(const brush_t &brush)
{
// grab the first valid point as the corner
bool found = false;
qvec3d corner;
for (auto &face : brush.faces) {
if (face.w.size() > 0) {
corner = face.w[0];
found = true;
}
}
if (!found) {
return 0;
}
// make tetrahedrons to all other faces
vec_t volume = 0;
for (auto &face : brush.faces) {
auto plane = Face_Plane(&face);
vec_t d = -(qv::dot(corner, plane.normal) - plane.dist);
vec_t area = face.w.area();
volume += d * area;
}
volume /= 3;
return volume;
}
/*
================
SplitBrush
Note, it's useful to take/return std::unique_ptr so it can quickly return the
input.
https://github.com/id-Software/Quake-2-Tools/blob/master/bsp/qbsp3/brushbsp.c#L935
================
*/
twosided<std::unique_ptr<brush_t>> SplitBrush(std::unique_ptr<brush_t> brush, const qplane3d &split)
{
twosided<std::unique_ptr<brush_t>> result;
// check all points
vec_t d_front = 0;
vec_t d_back = 0;
for (auto &face : brush->faces) {
for (int j = 0; j < face.w.size(); j++) {
vec_t d = qv::dot(face.w[j], split.normal) - split.dist;
if (d > 0 && d > d_front)
d_front = d;
if (d < 0 && d < d_back)
d_back = d;
}
}
if (d_front < 0.1) // PLANESIDE_EPSILON)
{ // only on back
result.back = std::move(brush);
return result;
}
if (d_back > -0.1) // PLANESIDE_EPSILON)
{ // only on front
result.front = std::move(brush);
return result;
}
// create a new winding from the split plane
auto w = std::optional<winding_t>{BaseWindingForPlane(split)};
for (auto &face : brush->faces) {
if (!w) {
break;
}
auto [frontOpt, backOpt] = w->clip(Face_Plane(&face));
w = backOpt;
}
if (!w || WindingIsTiny(*w)) { // the brush isn't really split
side_t side = BrushMostlyOnSide(*brush, split);
if (side == SIDE_FRONT)
result.front = std::move(brush);
else
result.back = std::move(brush);
return result;
}
if (WindingIsHuge(*w)) {
logging::print("WARNING: huge winding\n");
}
winding_t midwinding = *w;
// split it for real
// start with 2 empty brushes
for (int i = 0; i < 2; i++) {
result[i] = std::make_unique<brush_t>();
result[i]->original = brush->original;
// fixme-brushbsp: add a brush_t copy constructor to make sure we get all fields
result[i]->contents = brush->contents;
result[i]->lmshift = brush->lmshift;
result[i]->func_areaportal = brush->func_areaportal;
}
// split all the current windings
for (const auto &face : brush->faces) {
auto cw = face.w.clip(split, 0 /*PLANESIDE_EPSILON*/);
for (size_t j = 0; j < 2; j++) {
if (!cw[j])
continue;
#if 0
if (WindingIsTiny (cw[j]))
{
FreeWinding (cw[j]);
continue;
}
#endif
// add the clipped face to result[j]
face_t faceCopy = face;
faceCopy.w = *cw[j];
// fixme-brushbsp: configure any settings on the faceCopy?
// Q2 does `cs->tested = false;`, why?
result[j]->faces.push_back(std::move(faceCopy));
}
}
// see if we have valid polygons on both sides
for (int i = 0; i < 2; i++) {
result[i]->update_bounds();
bool bogus = false;
for (int j = 0; j < 3; j++) {
if (result[i]->bounds.mins()[j] < -4096 || result[i]->bounds.maxs()[j] > 4096) {
logging::print("bogus brush after clip\n");
bogus = true;
break;
}
}
if (result[i]->faces.size() < 3 || bogus) {
result[i] = nullptr;
}
}
if (!(result[0] && result[1])) {
if (!result[0] && !result[1])
logging::print("split removed brush\n");
else
logging::print("split not on both sides\n");
if (result[0]) {
result.front = std::move(brush);
}
// fixme: use of move here, might move twice. should it be `else`?
if (result[1]) {
result.back = std::move(brush);
}
return result;
}
// add the midwinding to both sides
for (int i = 0; i < 2; i++) {
face_t cs{};
const bool brushOnFront = (i == 0);
// for the brush on the front side of the plane, the `midwinding`
// (the face that is touching the plane) should have a normal opposite the plane's normal
cs.planenum = FindPlane(brushOnFront ? -split : split, &cs.planeside);
cs.texinfo = map.skip_texinfo;
// fixme-brushbsp: configure any other settings on the face?
cs.w = brushOnFront ? midwinding.flip() : midwinding;
result[i]->faces.push_back(std::move(cs));
}
{
vec_t v1;
int i;
for (i = 0; i < 2; i++) {
v1 = BrushVolume(*result[i]);
if (v1 < 1.0) {
result[i] = nullptr;
// qprintf ("tiny volume after clip\n");
}
}
}
return result;
}
//============================================================================
/*
==================
DivideNodeBounds
==================
*/
inline void DivideNodeBounds(node_t *node, const qbsp_plane_t &split)
{
DivideBounds(node->bounds, split, node->children[0]->bounds, node->children[1]->bounds);
}
static bool AllDetail(const std::vector<std::unique_ptr<brush_t>> &brushes)
{
for (auto &brush : brushes) {
if (!brush->contents.is_any_detail(options.target_game)) {
return false;
}
}
return true;
}
/*
==================
CreateLeaf
Determines the contents of the leaf and creates the final list of
original faces that have some fragment inside this leaf.
Called in parallel.
==================
*/
static void CreateLeaf(std::vector<std::unique_ptr<brush_t>> brushes, node_t *leafnode)
{
leafnode->facelist.clear();
leafnode->planenum = PLANENUM_LEAF;
leafnode->contents = options.target_game->create_empty_contents();
for (auto &brush : brushes) {
leafnode->contents = options.target_game->combine_contents(leafnode->contents, brush->contents);
}
for (auto &brush : brushes) {
Q_assert(brush->original != nullptr);
leafnode->original_brushes.push_back(brush->original);
}
options.target_game->count_contents_in_stats(leafnode->contents, leafstats);
}
/*
==================
PartitionBrushes
Called in parallel.
==================
*/
static void PartitionBrushes(std::vector<std::unique_ptr<brush_t>> brushes, node_t *node)
{
face_t *split = const_cast<face_t *>(SelectPartition(brushes));
if (split == nullptr) { // this is a leaf node
node->planenum = PLANENUM_LEAF;
CreateLeaf(std::move(brushes), node);
return;
}
splitnodes++;
node->children[0] = new node_t{};
node->children[0]->parent = node;
node->children[1] = new node_t{};
node->children[1]->parent = node;
node->planenum = FindPositivePlane(split->planenum);
node->detail_separator = AllDetail(brushes);
const qbsp_plane_t &splitplane = map.planes.at(node->planenum);
DivideNodeBounds(node, splitplane);
// multiple surfaces, so split all the polysurfaces into front and back lists
std::vector<std::unique_ptr<brush_t>> frontlist, backlist;
for (auto &brush : brushes) {
// NOTE: we're destroying `brushes` here with the std::move()
auto frags = SplitBrush(std::move(brush), splitplane);
// mark faces which were used as a splitter
for (auto &brushMaybe : frags) {
if (brushMaybe) {
for (auto &face : brushMaybe->faces) {
if (face.planenum == split->planenum) {
face.onnode = true;
}
}
}
}
if (frags.front) {
if (frags.front->faces.empty()) {
FError("Surface with no faces");
}
frontlist.emplace_back(std::move(frags.front));
}
if (frags.back) {
if (frags.back->faces.empty()) {
FError("Surface with no faces");
}
backlist.emplace_back(std::move(frags.back));
}
}
tbb::task_group g;
g.run([&]() { PartitionBrushes(std::move(frontlist), node->children[0]); });
g.run([&]() { PartitionBrushes(std::move(backlist), node->children[1]); });
g.wait();
}
/*
==================
SolidBSP
==================
*/
tree_t *BrushBSP(mapentity_t *entity, bool midsplit)
{
tree_t *tree = new tree_t{};
if (entity->brushes.empty()) {
/*
* We allow an entity to be constructed with no visible brushes
* (i.e. all clip brushes), but need to construct a simple empty
* collision hull for the engine. Probably could be done a little
* smarter, but this works.
*/
node_t *headnode = new node_t{};
headnode->bounds = entity->bounds.grow(SIDESPACE);
headnode->children[0] = new node_t{};
headnode->children[0]->planenum = PLANENUM_LEAF;
headnode->children[0]->contents = options.target_game->create_empty_contents();
headnode->children[0]->parent = headnode;
headnode->children[1] = new node_t{};
headnode->children[1]->planenum = PLANENUM_LEAF;
headnode->children[1]->contents = options.target_game->create_empty_contents();
headnode->children[1]->parent = headnode;
tree->headnode = headnode;
tree->bounds = headnode->bounds;
return tree;
}
logging::print(logging::flag::PROGRESS, "---- {} ----\n", __func__);
// Count visible/nonvisible brush sides (this is the side effect of FillOutside)
int visible_brush_sides = 0;
int invisible_brush_sides = 0;
for (const auto &brush : entity->brushes) {
for (auto &side : brush->faces) {
if (side.visible) {
++visible_brush_sides;
} else {
++invisible_brush_sides;
}
}
}
logging::print(logging::flag::STAT, " {:8} visible brush sides\n", visible_brush_sides);
logging::print(logging::flag::STAT, " {:8} invisible brush sides\n", invisible_brush_sides);
node_t *headnode = new node_t{};
usemidsplit = midsplit;
// calculate a bounding box for the entire model
headnode->bounds = entity->bounds.grow(SIDESPACE);
// recursively partition everything
splitnodes = 0;
leafstats = options.target_game->create_content_stats();
// count map surfaces; this is used when deciding to switch between midsplit and the expensive partitioning
mapbrushes = entity->brushes.size();
// set the original pointers
std::vector<std::unique_ptr<brush_t>> brushcopies;
for (const auto &original : entity->brushes) {
auto copy = std::make_unique<brush_t>(*original);
copy->original = original.get();
brushcopies.push_back(std::move(copy));
}
PartitionBrushes(std::move(brushcopies), headnode);
//logging::percent(csgmergefaces, csgmergefaces, entity == map.pWorldEnt());
logging::print(logging::flag::STAT, " {:8} split nodes\n", splitnodes.load());
options.target_game->print_content_stats(leafstats, "leaves");
tree->bounds = headnode->bounds;
tree->headnode = headnode;
return tree;
}