ericw-tools/qbsp/csg.cc

405 lines
12 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.
*/
// csg4.c
#include <qbsp/brush.hh>
#include <qbsp/csg.hh>
#include <qbsp/map.hh>
#include <qbsp/qbsp.hh>
#include <common/log.hh>
#include <common/parallel.hh>
#include <atomic>
#include <mutex>
/*
NOTES
-----
Brushes that touch still need to be split at the cut point to make a tjunction
*/
/*
==================
NewFaceFromFace
Duplicates the non point information of a face, used by SplitFace and
MergeFace.
==================
*/
std::unique_ptr<face_t> NewFaceFromFace(const face_t *in)
{
auto newf = std::make_unique<face_t>();
newf->planenum = in->planenum;
newf->texinfo = in->texinfo;
newf->contents = in->contents;
newf->original_side = in->original_side;
newf->origin = in->origin;
newf->radius = in->radius;
return newf;
}
std::unique_ptr<face_t> CopyFace(const face_t *in)
{
auto temp = NewFaceFromFace(in);
temp->w = in->w.clone();
return temp;
}
void UpdateFaceSphere(face_t *in)
{
in->origin = in->w.center();
in->radius = 0;
for (size_t i = 0; i < in->w.size(); i++) {
in->radius = max(in->radius, qv::distance2(in->w[i], in->origin));
}
in->radius = sqrt(in->radius);
}
/*
==================
SplitFace
Frees in. Returns {front, back}
==================
*/
std::tuple<std::unique_ptr<face_t>, std::unique_ptr<face_t>> SplitFace(
std::unique_ptr<face_t> in, const qplane3d &split)
{
if (in->w.size() < 0)
Error("Attempting to split freed face");
/* Fast test */
double dot = split.distance_to(in->origin);
if (dot > in->radius) {
// all in front
return {std::move(in), nullptr};
} else if (dot < -in->radius) {
// all behind
return {nullptr, std::move(in)};
}
auto [front_winding, back_winding] = in->w.clip(split, qbsp_options.epsilon.value(), true);
if (front_winding && !back_winding) {
// all in front
return {std::move(in), nullptr};
} else if (back_winding && !front_winding) {
// all behind
return {nullptr, std::move(in)};
}
auto new_front = NewFaceFromFace(in.get());
new_front->w = std::move(front_winding.value());
auto new_back = NewFaceFromFace(in.get());
new_back->w = std::move(back_winding.value());
return {std::move(new_front), std::move(new_back)};
}
// restored from csg4.cc from type-cleanup branch
/*
==================
SplitFace
Moves from `in`. Returns {front, back}
==================
*/
std::tuple<std::optional<side_t>, std::optional<side_t>> SplitFace(side_t &in, const qplane3d &split)
{
// fixme-brushbsp: restore fast test
#if 0
/* Fast test */
dot = split.distance_to(in->origin);
if (dot > in->radius) {
counts[SIDE_FRONT] = 1;
counts[SIDE_BACK] = 0;
} else if (dot < -in->radius) {
counts[SIDE_FRONT] = 0;
counts[SIDE_BACK] = 1;
} else
#endif
auto [front, back] = in.w.clip(split, qbsp_options.epsilon.value(), false);
// Plane doesn't split this face after all
if (!front) {
return {std::nullopt, std::move(in)};
}
if (!back) {
return {std::move(in), std::nullopt};
}
side_t front_side = in.clone_non_winding_data();
front_side.w = std::move(front.value());
side_t back_side = in.clone_non_winding_data();
back_side.w = std::move(back.value());
return {std::move(front_side), std::move(back_side)};
}
/*
=================
RemoveOutsideFaces
Quick test before running ClipInside; move any faces that are completely
outside the clipbrush to the outside list, without splitting them. This saves us
time in mergefaces later on (and sometimes a lot of memory)
inside (inout) input is the starting set of faces of `brush`, output is the faces that touch `clipbrush`
outside (out) outputs the faces of `brush` that are definitely not touching `clipbrush`
=================
*/
static void RemoveOutsideFaces(const bspbrush_t &clipbrush, std::vector<side_t> &inside, std::vector<side_t> &outside)
{
std::vector<side_t> oldinside;
// clear `inside`, transfer it to `oldinside`
std::swap(inside, oldinside);
for (side_t &face : oldinside) {
std::optional<winding_t> w = {face.w.clone()};
// clip `w` by all of `clipbrush`'s reversed planes,
// which finds intersection of `w` and `clipbrush`
for (auto &clipface : clipbrush.sides) {
qbsp_plane_t clipplane = -clipface.get_plane();
w = std::move(w->clip(clipplane, qbsp_options.epsilon.value(), true)[SIDE_FRONT]);
if (!w)
break;
}
if (!w) {
/* The face is completely outside this brush */
outside.push_back(std::move(face));
} else {
inside.push_back(std::move(face));
}
}
}
/*
=================
ClipInside
Clips all of the faces in the inside list, possibly moving them to the
outside list or spliting it into a piece in each list.
Faces exactly on the plane will stay inside unless overdrawn by later brush
clipface a face of the clipbrush
=================
*/
static void ClipInside(
const side_t &clipface, bool precedence, std::vector<side_t> &inside, std::vector<side_t> &outside)
{
std::vector<side_t> oldinside;
// effectively make a copy of `inside`, and clear it
std::swap(inside, oldinside);
const qbsp_plane_t &splitplane = clipface.get_plane();
for (side_t &face : oldinside) {
/* HACK: Check for on-plane but not the same planenum
( https://github.com/ericwa/ericw-tools/issues/174 )
*/
bool spurious_onplane = false;
{
std::array<size_t, SIDE_TOTAL> counts =
face.w.calc_sides(splitplane, nullptr, nullptr, qbsp_options.epsilon.value());
if (counts[SIDE_ON] && !counts[SIDE_FRONT] && !counts[SIDE_BACK]) {
spurious_onplane = true;
}
}
std::array<std::optional<side_t>, 2> frags;
/* Handle exactly on-plane faces (ignoring direction) */
if ((face.planenum ^ 1) == (clipface.planenum ^ 1) || spurious_onplane) {
const qplane3d faceplane = face.get_plane();
const qplane3d clipfaceplane = clipface.get_plane();
const vec_t dp = qv::dot(faceplane.normal, clipfaceplane.normal);
const bool opposite = (dp < 0);
if (opposite || precedence) {
/* always clip off opposite facing */
frags[SIDE_FRONT] = {};
frags[SIDE_BACK] = {std::move(face)};
} else {
/* leave it on the outside */
frags[SIDE_FRONT] = {std::move(face)};
frags[SIDE_BACK] = {};
}
} else {
/* proper split */
std::tie(frags[SIDE_FRONT], frags[SIDE_BACK]) = SplitFace(face, splitplane);
}
if (frags[SIDE_FRONT]) {
outside.push_back(std::move(*frags[SIDE_FRONT]));
}
if (frags[SIDE_BACK]) {
inside.push_back(std::move(*frags[SIDE_BACK]));
}
}
}
struct csg_stats
{
std::atomic<int> fullyeatenbrushes{};
std::atomic<int> postcsgfaces{};
};
/*
==================
CSGFaces
Clips overlapping areas of same-content-type brushes.
Goals:
- speed up the BSP process; there's no point in creating nodes
that we know will be deleted later in PruneNodes
- give better data for the BSP heuristic: avoid having it penalizing
splits on faces (or parts of faces - since we're clipping away overlaps here)
that won't be in the final .bsp anyway.
Note, the output brushes will be non-closed, so they can no longer be written to .map files
for debugging.
Differences from Q1 version:
- Q1 also clipped non-solids away where they touched solids,
and set the "front" content flag on the solid faces to record
that they had a liquid in front
- Q1 tools used the faces here as the final "visual" faces to write
out in the .bsp. This doesn't work for Q2 because we need the
leaf portals to decide whether a face gets output between 2 leafs
(see Q2_liquids.map).
So, we're only using these sides to construct the BSP and assign
leaf contents.
fixme-brushbsp: lots of moving of side_t, which is slow
==================
*/
bspbrush_t::container CSGFaces(bspbrush_t::container brushes)
{
logging::funcheader();
{
size_t precsgsides = 0;
for (auto &brush : brushes) {
precsgsides += brush->sides.size();
}
logging::print(logging::flag::STAT, " {:8} pre csg sides\n", precsgsides);
}
csg_stats stats{};
// output vector for the parallel_for
bspbrush_t::container brushvec_outsides;
brushvec_outsides.resize(brushes.size());
/*
* For each brush, clip away the parts that are inside other brushes.
* Solid brushes override non-solid brushes.
* brush => the brush to be clipped
* clipbrush => the brush we are clipping against
*
* The output of this is a face list for each brush called "outside"
*/
logging::parallel_for(static_cast<size_t>(0), brushes.size(), [&](size_t i) {
bspbrush_t::ptr &brush = brushes[i];
bspbrush_t::ptr brush_result = bspbrush_t::make_ptr(brush->clone());
// temporarily move brush_result's sides to the `outside` vector
std::vector<side_t> outside;
std::swap(outside, brush_result->sides);
bool overwrite = false;
for (auto &clipbrush : brushes) {
if (&brush == &clipbrush) {
/* Brushes further down the list override earlier ones.
* This is only relevant for choosing a winner when there's two
* overlapping faces.
*/
overwrite = true;
continue;
}
if (!brush->contents.equals(qbsp_options.target_game, clipbrush->contents)) {
/* Only consider clipping equal contents against each other */
continue;
}
/* check bounding box first */
// TODO: is this a disjoint check? brush->bounds.disjoint(clipbrush->bounds)?
int j;
for (j = 0; j < 3; j++) {
if (brush->bounds.mins()[j] > clipbrush->bounds.maxs()[j])
break;
if (brush->bounds.maxs()[j] < clipbrush->bounds.mins()[j])
break;
}
if (j < 3)
continue;
// divide faces by the planes of the new brush
std::vector<side_t> inside;
std::swap(inside, outside);
RemoveOutsideFaces(*clipbrush, inside, outside);
for (auto &clipface : clipbrush->sides) {
ClipInside(clipface, overwrite, inside, outside);
}
// inside = parts of `brush` that are inside `clipbrush`
// outside = parts of `brush` that are outside `clipbrush`
}
if (!outside.empty()) {
stats.postcsgfaces += outside.size();
// save the result
brush_result->sides = std::move(outside);
brushvec_outsides[i] = brush_result;
} else {
++stats.fullyeatenbrushes;
}
});
logging::print(logging::flag::STAT, " {:8} post csg sides\n", stats.postcsgfaces.load());
logging::print(logging::flag::STAT, " {:8} fully eaten brushes\n", stats.fullyeatenbrushes.load());
return brushvec_outsides;
}