Merge branch 'brushbsp-midsplit' into brushbsp

This commit is contained in:
Jonathan 2022-08-02 06:09:28 -04:00
commit dd287f30d4
6 changed files with 122 additions and 168 deletions

View File

@ -193,6 +193,11 @@ public:
constexpr value_type centroid() const { return (m_mins + m_maxs) * 0.5; }
constexpr V volume() const {
auto s = size();
return s[0] * s[1] * s[2];
}
// stream support
auto stream_data() { return std::tie(m_mins, m_maxs); }
};

View File

@ -41,4 +41,4 @@ bool WindingIsTiny(const winding_t &w, double size = EDGE_LENGTH_EPSILON);
std::unique_ptr<bspbrush_t> BrushFromBounds(const aabb3d &bounds);
// compatibility version
std::unique_ptr<tree_t> BrushBSP(mapentity_t *entity, bool use_mid_split);
std::unique_ptr<tree_t> BrushBSP(mapentity_t *entity, std::optional<bool> forced_quick_tree);

View File

@ -284,7 +284,7 @@ public:
setting_invertible_bool oldaxis{this, "oldaxis", true, &debugging_group,
"uses alternate texture alignment which was default in tyrutils-ericw v0.15.1 and older"};
setting_bool forcegoodtree{
this, "forcegoodtree", false, &debugging_group, "force use of expensive processing for SolidBSP stage"};
this, "forcegoodtree", false, &debugging_group, "force use of expensive processing for BrushBSP stage"};
setting_scalar midsplitsurffraction{this, "midsplitsurffraction", 0.f, 0.f, 1.f, &debugging_group,
"if 0 (default), use `maxnodesize` for deciding when to switch to midsplit bsp heuristic.\nif 0 < midsplitSurfFraction <= 1, switch to midsplit if the node contains more than this fraction of the model's\ntotal surfaces. Try 0.15 to 0.5. Works better than maxNodeSize for maps with a 3D skybox (e.g. +-128K unit maps)"};
setting_int32 maxnodesize{this, "maxnodesize", 1024, &debugging_group,
@ -352,6 +352,7 @@ public:
// 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"};
setting_numeric<vec_t> midsplitbrushfraction{this, "midsplitbrushfraction", 0.0, &common_format_group, "switch to cheaper partitioning if a node contains this % of brushes in the map"};
void setParameters(int argc, const char **argv) override
{

View File

@ -54,8 +54,12 @@ struct bspstats_t
std::atomic<int> c_nodes;
// number of nodes created by splitting on a side_t which had !visible
std::atomic<int> c_nonvis;
// total number of nodes created by qbsp3 method
std::atomic<int> c_qbsp3;
// total number of nodes created by block splitting
std::atomic<int> c_blocksplit;
// total number of nodes created by midsplit
std::atomic<int> c_midsplit;
// total number of leafs
std::atomic<int> c_leafs;
};
@ -622,7 +626,7 @@ static void CheckPlaneAgainstParents(const qbsp_plane_t &plane, node_t *node)
}
}
static bool CheckPlaneAgainstVolume(const qbsp_plane_t &plane, node_t *node)
static bool CheckPlaneAgainstVolume(const qbsp_plane_t &plane, const node_t *node)
{
auto [front, back] = SplitBrush(node->volume->copy_unique(), plane);
@ -631,26 +635,6 @@ static bool CheckPlaneAgainstVolume(const qbsp_plane_t &plane, node_t *node)
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
@ -710,31 +694,15 @@ inline void DivideBounds(const aabb3d &in_bounds, const qbsp_plane_t &split, aab
}
}
/*
* Calculate the split plane metric for non-axial planes
*/
inline vec_t SplitPlaneMetric_NonAxial(const qbsp_plane_t &p, const aabb3d &bounds)
inline vec_t SplitPlaneMetric(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);
}
// i.e. a good split will have equal volumes on front and back.
// a bad split will have all of the volume on one side.
return fabs(f.volume() - b.volume());
}
/*
@ -744,79 +712,49 @@ 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)
static std::optional<qbsp_plane_t> ChooseMidPlaneFromList(const std::vector<std::unique_ptr<bspbrush_t>> &brushes, const node_t *node)
{
/* 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;
for (auto &brush : brushes) {
for (auto &side : brush->sides) {
if (side.bevel) {
continue; // never use a bevel as a spliter
}
if (!(pass & 1) && brush->original->contents.is_any_detail(qbsp_options.target_game)) {
continue;
if (side.onnode) {
continue; // allready a node splitter
}
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;
const qbsp_plane_t &plane = side.plane;
/* calculate the split metric, smaller values are better */
const vec_t metric = SplitPlaneMetric(plane, bounds);
if (!CheckPlaneAgainstVolume(plane, node)) {
continue; // would produce a tiny volume
}
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;
}
/* calculate the split metric, smaller values are better */
const vec_t metric = SplitPlaneMetric(plane, node->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;
return bestaxialplane ? bestaxialplane : bestanyplane;
}
@ -828,73 +766,80 @@ Using heuristics, chooses a plane to partition the brushes with.
Returns nullopt if there are no valid planes to split with.
================
*/
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)
static std::optional<qbsp_plane_t> SelectSplitPlane(const std::vector<std::unique_ptr<bspbrush_t>> &brushes, node_t *node, std::optional<bool> forced_quick_tree, 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;
}
// if forced_quick_tree is nullopt, we will choose fast/slow based on
// certain parameters.
if (!forced_quick_tree.has_value() || forced_quick_tree.value() == true) {
// if it is crossing a block boundary, force a split;
// this is optional q3map2 mode that is disabled by default.
if (qbsp_options.blocksize.isChanged()) {
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);
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++;
if (node->bounds.maxs()[i] > dist) {
qplane3d plane{};
plane.normal[i] = 1.0;
plane.dist = dist;
qbsp_plane_t bsp_plane = plane;
for (auto &b : brushes) {
b->side = TestBrushToPlanenum(*b, bsp_plane, nullptr, nullptr, nullptr);
if (!CheckPlaneAgainstVolume(bsp_plane, node)) {
continue; // would produce a tiny volume
}
stats.c_blocksplit++;
for (auto &b : brushes) {
b->side = TestBrushToPlanenum(*b, bsp_plane, nullptr, nullptr, nullptr);
}
return bsp_plane;
}
}
}
if (!forced_quick_tree.has_value()) {
// decide if we should switch to the midsplit method
if (qbsp_options.midsplitbrushfraction.value() != 0.0) {
// new way (opt-in)
// how much of the map are we partitioning?
double fractionOfMap = brushes.size() / (double) map.brushes.size();
forced_quick_tree = (fractionOfMap > qbsp_options.midsplitbrushfraction.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();
forced_quick_tree = (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;
}
}
}
return bsp_plane;
}
}
if (forced_quick_tree.value()) {
if (auto mid_plane = ChooseMidPlaneFromList(brushes, node)) {
stats.c_midsplit++;
// 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;
for (auto &b : brushes) {
b->side = TestBrushToPlanenum(*b, mid_plane.value(), nullptr, nullptr, nullptr);
}
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;
return mid_plane;
}
}
}
// 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;
int bestvalue = -99999;
int bestsplits = 0;
@ -1019,6 +964,8 @@ static std::optional<qbsp_plane_t> SelectSplitPlane(const std::vector<std::uniqu
stats.c_nonvis++;
}
stats.c_qbsp3++;
return bestside->plane;
}
@ -1080,10 +1027,10 @@ BuildTree_r
Called in parallel.
==================
*/
static void BuildTree_r(node_t *node, std::vector<std::unique_ptr<bspbrush_t>> brushes, bool use_mid_split, bspstats_t &stats)
static void BuildTree_r(node_t *node, std::vector<std::unique_ptr<bspbrush_t>> brushes, std::optional<bool> forced_quick_tree, bspstats_t &stats)
{
// find the best plane to use as a splitter
auto bestplane = SelectSplitPlane(brushes, node, use_mid_split, stats);
auto bestplane = SelectSplitPlane(brushes, node, forced_quick_tree, stats);
if (!bestplane) {
// this is a leaf node
@ -1123,8 +1070,8 @@ static void BuildTree_r(node_t *node, std::vector<std::unique_ptr<bspbrush_t>> b
// recursively process children
tbb::task_group g;
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]), use_mid_split, stats); });
g.run([&]() { BuildTree_r(node->children[0].get(), std::move(children[0]), forced_quick_tree, stats); });
g.run([&]() { BuildTree_r(node->children[1].get(), std::move(children[1]), forced_quick_tree, stats); });
g.wait();
}
@ -1133,7 +1080,7 @@ static void BuildTree_r(node_t *node, std::vector<std::unique_ptr<bspbrush_t>> b
BrushBSP
==================
*/
static std::unique_ptr<tree_t> BrushBSP(mapentity_t *entity, std::vector<std::unique_ptr<bspbrush_t>> brushlist, bool use_mid_split)
static std::unique_ptr<tree_t> BrushBSP(mapentity_t *entity, std::vector<std::unique_ptr<bspbrush_t>> brushlist, std::optional<bool> forced_quick_tree)
{
auto tree = std::make_unique<tree_t>();
@ -1209,18 +1156,20 @@ static std::unique_ptr<tree_t> BrushBSP(mapentity_t *entity, std::vector<std::un
bspstats_t stats{};
stats.leafstats = qbsp_options.target_game->create_content_stats();
BuildTree_r(tree->headnode.get(), std::move(brushlist), use_mid_split, stats);
BuildTree_r(tree->headnode.get(), std::move(brushlist), forced_quick_tree, stats);
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} block split nodes\n", stats.c_blocksplit);
logging::print(logging::flag::STAT, " {:8} expensive split nodes\n", stats.c_qbsp3);
logging::print(logging::flag::STAT, " {:8} midsplit nodes\n", stats.c_midsplit);
logging::print(logging::flag::STAT, " {:8} leafs\n", stats.c_leafs);
qbsp_options.target_game->print_content_stats(*stats.leafstats, "leafs");
return tree;
}
std::unique_ptr<tree_t> BrushBSP(mapentity_t *entity, bool use_mid_split)
std::unique_ptr<tree_t> BrushBSP(mapentity_t *entity, std::optional<bool> forced_quick_tree)
{
return BrushBSP(entity, MakeBspBrushList(entity), use_mid_split);
return BrushBSP(entity, MakeBspBrushList(entity), forced_quick_tree);
}

View File

@ -389,18 +389,17 @@ static void CalcTreeBounds_r(tree_t *tree, node_t *node)
{
if (node->is_leaf) {
CalcNodeBounds(node);
return;
} else {
tbb::task_group g;
g.run([&]() { CalcTreeBounds_r(tree, node->children[0].get()); });
g.run([&]() { CalcTreeBounds_r(tree, node->children[1].get()); });
g.wait();
node->bounds = node->children[0]->bounds + node->children[1]->bounds;
}
tbb::task_group g;
g.run([&]() { CalcTreeBounds_r(tree, node->children[0].get()); });
g.run([&]() { CalcTreeBounds_r(tree, node->children[1].get()); });
g.wait();
node->bounds = node->children[0]->bounds + node->children[1]->bounds;
if (node->bounds.mins()[0] >= node->bounds.maxs()[0]) {
logging::print("WARNING: node without a volume\n");
logging::print("WARNING: {} without a volume\n", node->is_leaf ? "leaf" : "node");
// fixme-brushbsp: added this to work around leafs with no portals showing up in "qbspfeatures.map" among other
// test maps. Not sure if correct or there's another underlying problem.
@ -409,7 +408,7 @@ static void CalcTreeBounds_r(tree_t *tree, node_t *node)
for (auto &v : node->bounds.mins()) {
if (fabs(v) > qbsp_options.worldextent.value()) {
logging::print("WARNING: node with unbounded volume\n");
logging::print("WARNING: {} with unbounded volume\n", node->is_leaf ? "leaf" : "node");
break;
}
}

View File

@ -609,7 +609,7 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum)
if (qbsp_options.forcegoodtree.value()) {
tree = BrushBSP(entity, false);
} else {
tree = BrushBSP(entity, entity == map.world_entity());
tree = BrushBSP(entity, entity == map.world_entity() ? std::nullopt : std::optional<bool>(false));
}
// build all the portals in the bsp tree