ericw-tools/qbsp/solidbsp.cc

915 lines
28 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 <climits>
#include <qbsp/qbsp.hh>
#include <atomic>
#include "tbb/task_group.h"
std::atomic<int> splitnodes;
static std::atomic<int> leaffaces;
static std::atomic<int> nodefaces;
static std::atomic<int> c_solid, c_empty, c_water, c_detail, c_detail_illusionary, c_detail_fence;
static std::atomic<int> c_illusionary_visblocker;
static bool usemidsplit;
/**
* Total number of surfaces in the map
*/
static int mapsurfaces;
//============================================================================
void ConvertNodeToLeaf(node_t *node, const contentflags_t &contents)
{
// backup the mins/maxs
aabb3d bounds = node->bounds;
// zero it
memset(node, 0, sizeof(*node));
// restore relevant fields
node->bounds = bounds;
node->planenum = PLANENUM_LEAF;
node->contents = contents;
node->markfaces = new face_t *[1] {};
Q_assert(node->markfaces[0] == nullptr);
}
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(CFLAGS_DETAIL)) {
node->contents = options.target_game->create_solid_contents();
} else if (node->contents.is_detail(CFLAGS_DETAIL_ILLUSIONARY)) {
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());
}
}
}
/*
==================
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 < 3) {
/* shortcut for axial planes */
const vec_t *p = &in->w[0][split.type];
for (i = 0; i < in->w.size(); i++, p += 3) {
if (*p > split.dist + ON_EPSILON) {
if (have_back)
return SIDE_ON;
have_front = true;
} else if (*p < split.dist - ON_EPSILON) {
if (have_front)
return SIDE_ON;
have_back = true;
}
}
} else {
/* sloping planes take longer */
const vec_t *p = &in->w[0][0];
for (i = 0; i < in->w.size(); i++, p += 3) {
const vec_t dot = DotProduct(p, split.normal) - split.dist;
if (dot > ON_EPSILON) {
if (have_back)
return SIDE_ON;
have_front = true;
} else if (dot < -ON_EPSILON) {
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 = DotProduct(in->origin, split.normal) - split.dist;
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;
vec3_t corner;
front_bounds = back_bounds = in_bounds;
if (split.type < 3) {
// CHECK: this escapes the immutability "sandbox" of aabb3d, is this a good idea?
// it'd take like 6 lines to otherwise reproduce this line.
front_bounds[0][split.type] = back_bounds[1][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 = DotProduct(corner, split.normal) - split.dist;
corner[a] = in_bounds[1][a];
dist2 = DotProduct(corner, split.normal) - split.dist;
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 (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 < 3)
return SplitPlaneMetric_Axial(p, bounds);
else
return SplitPlaneMetric_NonAxial(p, bounds);
}
/*
==================
ChooseMidPlaneFromList
The clipping hull BSP doesn't worry about avoiding splits
==================
*/
static surface_t *ChooseMidPlaneFromList(surface_t *surfaces, const aabb3d &bounds)
{
/* pick the plane that splits the least */
vec_t bestmetric = VECT_MAX;
surface_t *bestsurface = nullptr;
for (int pass = 0; pass < 2; pass++) {
for (surface_t *surf = surfaces; surf; surf = surf->next) {
if (surf->onnode)
continue;
if (surf->has_struct && pass)
continue;
if (!surf->has_struct && !pass)
continue;
/* check for axis aligned surfaces */
const qbsp_plane_t &plane = map.planes[surf->planenum];
if (!(plane.type < 3))
continue;
/* calculate the split metric, smaller values are better */
const vec_t metric = SplitPlaneMetric(plane, bounds);
if (metric < bestmetric) {
bestmetric = metric;
bestsurface = surf;
}
}
if (!bestsurface) {
/* Choose based on spatial subdivision only */
for (surface_t *surf = surfaces; surf; surf = surf->next) {
if (surf->onnode)
continue;
if (surf->has_struct && pass)
continue;
if (!surf->has_struct && !pass)
continue;
const qbsp_plane_t &plane = map.planes[surf->planenum];
const vec_t metric = SplitPlaneMetric(plane, bounds);
if (metric < bestmetric) {
bestmetric = metric;
bestsurface = surf;
}
}
}
if (bestsurface)
break;
}
if (!bestsurface)
FError("No valid planes in surface list");
// ericw -- (!usemidsplit) 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.
//
// TODO: investigate dropping the maxNodeSize feature (dynamically choosing
// between ChooseMidPlaneFromList and ChoosePlaneFromList) and use Q2's
// chopping on a uniform grid?
if (!usemidsplit && !bestsurface->has_struct) {
bestsurface->detail_separator = true;
}
return bestsurface;
}
/*
==================
ChoosePlaneFromList
The real BSP hueristic
==================
*/
static surface_t *ChoosePlaneFromList(surface_t *surfaces, const aabb3d &bounds)
{
/* pick the plane that splits the least */
int minsplits = INT_MAX - 1;
vec_t bestdistribution = VECT_MAX;
surface_t *bestsurface = nullptr;
/* Two passes - exhaust all non-detail faces before details */
for (int pass = 0; pass < 2; pass++) {
for (surface_t *surf = surfaces; surf; surf = surf->next) {
if (surf->onnode)
continue;
/*
* Check that the surface has a suitable face for the current pass
* and check whether this is a hint split.
*/
bool hintsplit = false;
for (const face_t *face = surf->faces; face; face = face->next) {
if (map.mtexinfos.at(face->texinfo).flags.extended & TEX_EXFLAG_HINT)
hintsplit = true;
}
if (surf->has_struct && pass)
continue;
if (!surf->has_struct && !pass)
continue;
const qbsp_plane_t &plane = map.planes[surf->planenum];
int splits = 0;
for (surface_t *surf2 = surfaces; surf2; surf2 = surf2->next) {
if (surf2 == surf || surf2->onnode)
continue;
const qbsp_plane_t &plane2 = map.planes[surf2->planenum];
if (plane.type < 3 && plane.type == plane2.type)
continue;
for (const face_t *face = surf2->faces; face; face = face->next) {
const surfflags_t &flags = map.mtexinfos.at(face->texinfo).flags;
/* Don't penalize for splitting skip faces */
if (flags.extended & TEX_EXFLAG_SKIP)
continue;
if (FaceSide(face, plane) == SIDE_ON) {
/* Never split a hint face except with a hint */
if (!hintsplit && (flags.extended & TEX_EXFLAG_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 < 3)) {
if (plane.type < 3) {
const vec_t distribution = SplitPlaneMetric(plane, bounds);
if (distribution > bestdistribution && splits == minsplits)
continue;
bestdistribution = distribution;
}
/* currently the best! */
minsplits = splits;
bestsurface = surf;
}
}
/* If we found a candidate on first pass, don't do a second pass */
if (bestsurface) {
bestsurface->detail_separator = (pass > 0);
break;
}
}
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 surface_t *SelectPartition(surface_t *surfaces)
{
// count onnode surfaces
int surfcount = 0;
surface_t *bestsurface = nullptr;
for (surface_t *surf = surfaces; surf; surf = surf->next)
if (!surf->onnode) {
surfcount++;
bestsurface = surf;
}
if (surfcount == 0)
return NULL;
if (surfcount == 1)
return bestsurface; // this is a final split
// calculate a bounding box of the entire surfaceset
aabb3d bounds;
for (surface_t *surf = surfaces; surf; surf = surf->next) {
bounds += surf->bounds;
}
// how much of the map are we partitioning?
double fractionOfMap = surfcount / (double)mapsurfaces;
bool largenode = false;
// decide if we should switch to the midsplit method
if (options.midsplitSurfFraction != 0.0) {
// new way (opt-in)
largenode = (fractionOfMap > options.midsplitSurfFraction);
} else {
// old way (ericw-tools 0.15.2+)
if (options.maxNodeSize >= 64) {
const vec_t maxnodesize = options.maxNodeSize - ON_EPSILON;
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(surfaces, bounds);
// do slow way to save poly splits for drawing hull
return ChoosePlaneFromList(surfaces, bounds);
}
//============================================================================
/*
=================
CalcSurfaceInfo
Calculates the bounding box
=================
*/
void CalcSurfaceInfo(surface_t *surf)
{
// calculate a bounding box
surf->bounds = {};
surf->has_detail = false;
surf->has_struct = false;
for (const face_t *f = surf->faces; f; f = f->next) {
for (auto &contents : f->contents)
if (!contents.is_valid(options.target_game, false))
FError("Bad contents in face: {}", contents.to_string(options.target_game));
surf->lmshift = (f->lmshift[0] < f->lmshift[1]) ? f->lmshift[0] : f->lmshift[1];
bool faceIsDetail = false;
if ((f->contents[0].extended | f->contents[1].extended) &
(CFLAGS_DETAIL | CFLAGS_DETAIL_ILLUSIONARY | CFLAGS_DETAIL_FENCE | CFLAGS_WAS_ILLUSIONARY))
faceIsDetail = true;
if (faceIsDetail)
surf->has_detail = true;
else
surf->has_struct = true;
for (int i = 0; i < f->w.size(); i++) {
surf->bounds += f->w[i];
}
}
}
/*
==================
DividePlane
==================
*/
static void DividePlane(surface_t *in, const qplane3d &split, surface_t **front, surface_t **back)
{
const qbsp_plane_t *inplane = &map.planes[in->planenum];
*front = *back = NULL;
// parallel case is easy
if (VectorCompare(inplane->normal, split.normal, EQUAL_EPSILON)) {
// check for exactly on node
if (inplane->dist == split.dist) {
face_t *facet = in->faces;
in->faces = NULL;
in->onnode = true;
// divide the facets to the front and back sides
surface_t *newsurf = new surface_t{};
*newsurf = *in;
// Prepend each face in facet list to either in or newsurf lists
face_t *next;
for (; facet; facet = next) {
next = facet->next;
if (facet->planeside == 1) {
facet->next = newsurf->faces;
newsurf->faces = facet;
} else {
facet->next = in->faces;
in->faces = facet;
}
}
// ericw -- added these CalcSurfaceInfo to recalculate the surf bbox.
// pretty sure their omission here was a bug.
CalcSurfaceInfo(newsurf);
CalcSurfaceInfo(in);
if (in->faces)
*front = in;
else
delete in;
if (newsurf->faces)
*back = newsurf;
else
delete newsurf;
return;
}
if (inplane->dist > split.dist)
*front = in;
else
*back = in;
return;
}
// do a real split. may still end up entirely on one side
// OPTIMIZE: use bounding box for fast test
face_t *frontlist = NULL;
face_t *backlist = NULL;
face_t *next;
for (face_t *facet = in->faces; facet; facet = next) {
next = facet->next;
face_t *frontfrag = NULL;
face_t *backfrag = NULL;
SplitFace(facet, split, &frontfrag, &backfrag);
if (frontfrag) {
frontfrag->next = frontlist;
frontlist = frontfrag;
}
if (backfrag) {
backfrag->next = backlist;
backlist = backfrag;
}
}
// if nothing actually got split, just move the in plane
if (frontlist == NULL) {
*back = in;
in->faces = backlist;
return;
}
if (backlist == NULL) {
*front = in;
in->faces = frontlist;
return;
}
// stuff got split, so allocate one new plane and reuse in
surface_t *newsurf = new surface_t(*in);
newsurf->faces = backlist;
*back = newsurf;
in->faces = frontlist;
*front = in;
// recalc bboxes and flags
CalcSurfaceInfo(newsurf);
CalcSurfaceInfo(in);
}
/*
==================
DivideNodeBounds
==================
*/
inline void DivideNodeBounds(node_t *node, const qbsp_plane_t &split)
{
DivideBounds(node->bounds, split, node->children[0]->bounds, node->children[1]->bounds);
}
/*
==================
LinkConvexFaces
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 LinkConvexFaces(surface_t *planelist, node_t *leafnode)
{
leafnode->faces = NULL;
leafnode->planenum = PLANENUM_LEAF;
int count = 0;
std::optional<contentflags_t> contents;
for (surface_t *surf = planelist; surf; surf = surf->next) {
for (face_t *f = surf->faces; f; f = f->next) {
count++;
const int currentpri = contents.has_value() ? contents->priority(options.target_game) : -1;
const int fpri = f->contents[0].priority(options.target_game);
if (fpri > currentpri) {
contents = f->contents[0];
}
// HACK: Handle structural covered by detail.
if (f->contents[0].extended & CFLAGS_STRUCTURAL_COVERED_BY_DETAIL) {
Q_assert(f->contents[0].is_empty(options.target_game));
const contentflags_t solid_detail = options.target_game->create_extended_contents(CFLAGS_DETAIL);
if (solid_detail.priority(options.target_game) > currentpri) {
contents = solid_detail;
}
}
}
}
// NOTE: This is crazy..
// Liquid leafs get assigned liquid content types because of the
// "cosmetic" mirrored faces.
leafnode->contents = contents.value_or(
options.target_game->create_solid_contents()); // FIXME: Need to create CONTENTS_DETAIL sometimes?
if (leafnode->contents.extended & CFLAGS_ILLUSIONARY_VISBLOCKER) {
c_illusionary_visblocker++;
} else if (leafnode->contents.extended & CFLAGS_DETAIL_FENCE) {
c_detail_fence++;
} else if (leafnode->contents.extended & CFLAGS_DETAIL_ILLUSIONARY) {
c_detail_illusionary++;
} else if (leafnode->contents.extended & CFLAGS_DETAIL) {
c_detail++;
} else if (leafnode->contents.is_empty(options.target_game)) {
c_empty++;
} else if (leafnode->contents.is_solid(options.target_game)) {
c_solid++;
} else if (leafnode->contents.is_liquid(options.target_game) || leafnode->contents.is_sky(options.target_game)) {
c_water++;
} else {
// FIXME: what to call here? is_valid()? this hits in Q2 a lot
// FError("Bad contents in face: {}", leafnode->contents.to_string(options.target_game));
}
// write the list of the original faces to the leaf's markfaces
// free surf and the surf->faces list.
leaffaces += count;
leafnode->markfaces = new face_t *[count + 1] {};
int i = 0;
surface_t *pnext;
for (surface_t *surf = planelist; surf; surf = pnext) {
pnext = surf->next;
face_t *next;
for (face_t *f = surf->faces; f; f = next) {
next = f->next;
leafnode->markfaces[i] = f->original;
i++;
delete f;
}
delete surf;
}
leafnode->markfaces[i] = NULL; // sentinal
}
/*
==================
LinkNodeFaces
First subdivides surface->faces.
Then, duplicates the list of subdivided faces and returns it.
For each surface->faces, ->original is set to the respective duplicate that
is returned here (why?).
Called in parallel.
==================
*/
static face_t *LinkNodeFaces(surface_t *surface)
{
face_t *list = NULL;
// subdivide large faces
face_t **prevptr = &surface->faces;
face_t *f = *prevptr;
while (f) {
SubdivideFace(f, prevptr);
prevptr = &(*prevptr)->next;
f = *prevptr;
}
// copy
for (face_t *f = surface->faces; f; f = f->next) {
nodefaces++;
face_t *newf = new face_t(*f);
f->original = newf;
newf->next = list;
list = newf;
}
return list;
}
/*
==================
PartitionSurfaces
Called in parallel.
==================
*/
static void PartitionSurfaces(surface_t *surfaces, node_t *node)
{
surface_t *split = SelectPartition(surfaces);
if (!split) { // this is a leaf node
node->planenum = PLANENUM_LEAF;
// frees `surfaces` and the faces on it.
// saves pointers to face->original in the leaf's markfaces list.
LinkConvexFaces(surfaces, node);
return;
}
splitnodes++;
LogPercent(splitnodes.load(), csgmergefaces);
node->faces = LinkNodeFaces(split);
node->children[0] = new node_t{};
node->children[1] = new node_t{};
node->planenum = split->planenum;
node->detail_separator = split->detail_separator;
const qbsp_plane_t &splitplane = map.planes[split->planenum];
DivideNodeBounds(node, splitplane);
// multiple surfaces, so split all the polysurfaces into front and back lists
surface_t *frontlist = NULL;
surface_t *backlist = NULL;
surface_t *next;
for (surface_t *surf = surfaces; surf; surf = next) {
next = surf->next;
surface_t *frontfrag, *backfrag;
DividePlane(surf, splitplane, &frontfrag, &backfrag);
if (frontfrag && backfrag) {
// the plane was split, which may expose oportunities to merge
// adjacent faces into a single face
// MergePlaneFaces (frontfrag);
// MergePlaneFaces (backfrag);
}
if (frontfrag) {
if (!frontfrag->faces)
FError("Surface with no faces");
frontfrag->next = frontlist;
frontlist = frontfrag;
}
if (backfrag) {
if (!backfrag->faces)
FError("Surface with no faces");
backfrag->next = backlist;
backlist = backfrag;
}
}
tbb::task_group g;
g.run([&]() { PartitionSurfaces(frontlist, node->children[0]); });
g.run([&]() { PartitionSurfaces(backlist, node->children[1]); });
g.wait();
}
/*
==================
SolidBSP
==================
*/
node_t *SolidBSP(const mapentity_t *entity, surface_t *surfhead, bool midsplit)
{
if (!surfhead) {
/*
* 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]->markfaces = new face_t *[1] {};
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]->markfaces = new face_t *[1] {};
return headnode;
}
LogPrint(LOG_PROGRESS, "---- {} ----\n", __func__);
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;
leaffaces = 0;
nodefaces = 0;
c_solid = 0;
c_empty = 0;
c_water = 0;
c_detail = 0;
c_detail_illusionary = 0;
c_detail_fence = 0;
c_illusionary_visblocker = 0;
// count map surfaces; this is used when deciding to switch between midsplit and the expensive partitioning
mapsurfaces = 0;
for (surface_t *surf = surfhead; surf; surf = surf->next) {
mapsurfaces++;
}
PartitionSurfaces(surfhead, headnode);
LogPrint(LOG_STAT, " {:8} split nodes\n", splitnodes.load());
LogPrint(LOG_STAT, " {:8} solid leafs\n", c_solid.load());
LogPrint(LOG_STAT, " {:8} empty leafs\n", c_empty.load());
LogPrint(LOG_STAT, " {:8} water leafs\n", c_water.load());
LogPrint(LOG_STAT, " {:8} detail leafs\n", c_detail.load());
LogPrint(LOG_STAT, " {:8} detail illusionary leafs\n", c_detail_illusionary.load());
LogPrint(LOG_STAT, " {:8} detail fence leafs\n", c_detail_fence.load());
LogPrint(LOG_STAT, " {:8} illusionary visblocker leafs\n", c_illusionary_visblocker.load());
LogPrint(LOG_STAT, " {:8} leaffaces\n", leaffaces.load());
LogPrint(LOG_STAT, " {:8} nodefaces\n", nodefaces.load());
return headnode;
}