Merge branch 'portalfaces' into brushbsp
This commit is contained in:
commit
40f370dbd5
|
|
@ -358,6 +358,55 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
contentflags_t visible_contents(const contentflags_t &a, const contentflags_t &b) const override
|
||||
{
|
||||
if (a.equals(this, b)) {
|
||||
if (contents_clip_same_type(a, b)) {
|
||||
return create_empty_contents();
|
||||
} else {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t a_pri = contents_priority(a);
|
||||
int32_t b_pri = contents_priority(b);
|
||||
|
||||
if (a_pri > b_pri) {
|
||||
return a;
|
||||
} else {
|
||||
return b;
|
||||
}
|
||||
// fixme-brushbsp: support detail-illusionary intersecting liquids
|
||||
}
|
||||
|
||||
bool directional_visible_contents(const contentflags_t &a, const contentflags_t &b) const override
|
||||
{
|
||||
if (contents_priority(b) > contents_priority(a)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a.is_empty(this)) {
|
||||
// empty can always see whatever is in `b`
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!a.will_clip_same_type(this) && contents_are_type_equal(a, b)) {
|
||||
// _noclipfaces
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!a.is_mirrored(this)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool contents_contains(const contentflags_t &a, const contentflags_t &b) const override
|
||||
{
|
||||
return a.equals(this, b);
|
||||
}
|
||||
|
||||
std::string get_contents_display(const contentflags_t &contents) const override
|
||||
{
|
||||
std::string base;
|
||||
|
|
@ -850,6 +899,9 @@ struct gamedef_q2_t : public gamedef_t
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the single content bit of the strongest visible content present
|
||||
*/
|
||||
constexpr int32_t visible_contents(const int32_t &contents) const
|
||||
{
|
||||
for (int32_t i = 1; i <= Q2_LAST_VISIBLE_CONTENTS; i <<= 1) {
|
||||
|
|
@ -861,6 +913,17 @@ struct gamedef_q2_t : public gamedef_t
|
|||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a portal from `a` to `b`, should the viewer on side `a` see a face?
|
||||
*/
|
||||
bool directional_visible_contents(const contentflags_t &a, const contentflags_t &b) const override
|
||||
{
|
||||
if ((a.native & Q2_CONTENTS_WINDOW) && visible_contents(a, b).native == Q2_CONTENTS_WINDOW)
|
||||
return false; // don't show insides of windows
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool portal_can_see_through(const contentflags_t &contents0, const contentflags_t &contents1, bool, bool) const override
|
||||
{
|
||||
int32_t c0 = contents0.native, c1 = contents1.native;
|
||||
|
|
@ -907,6 +970,18 @@ struct gamedef_q2_t : public gamedef_t
|
|||
return {a.native | b.native};
|
||||
}
|
||||
|
||||
contentflags_t visible_contents(const contentflags_t &a, const contentflags_t &b) const override
|
||||
{
|
||||
int viscontents = visible_contents(a.native ^ b.native);
|
||||
|
||||
return {viscontents};
|
||||
}
|
||||
|
||||
bool contents_contains(const contentflags_t &a, const contentflags_t &b) const override
|
||||
{
|
||||
return (a.native & b.native) != 0;
|
||||
}
|
||||
|
||||
std::string get_contents_display(const contentflags_t &contents) const override
|
||||
{
|
||||
if (!contents.native) {
|
||||
|
|
@ -1432,7 +1507,7 @@ std::string contentflags_t::to_string(const gamedef_t *game) const
|
|||
std::string s = game->get_contents_display(*this);
|
||||
|
||||
if (contentflags_t{native}.is_mirrored(game) != is_mirrored(game)) {
|
||||
s += fmt::format(" | MIRROR_INSIDE[{}]", mirror_inside.has_value() ? (clips_same_type.value() ? "true" : "false") : "nullopt");
|
||||
s += fmt::format(" | MIRROR_INSIDE[{}]", mirror_inside.has_value() ? (mirror_inside.value() ? "true" : "false") : "nullopt");
|
||||
}
|
||||
|
||||
if (contentflags_t{native}.will_clip_same_type(game) != will_clip_same_type(game)) {
|
||||
|
|
|
|||
|
|
@ -292,6 +292,11 @@ struct gamedef_t
|
|||
virtual bool contents_seals_map(const contentflags_t &contents) const = 0;
|
||||
virtual contentflags_t contents_remap_for_export(const contentflags_t &contents) const = 0;
|
||||
virtual contentflags_t combine_contents(const contentflags_t &a, const contentflags_t &b) const = 0;
|
||||
virtual contentflags_t visible_contents(const contentflags_t &a, const contentflags_t &b) const = 0;
|
||||
// counterpart to visible_contents. for a portal with contents from `a` to `b`, returns whether a viewer in `a`
|
||||
// should see a face
|
||||
virtual bool directional_visible_contents(const contentflags_t &a, const contentflags_t &b) const = 0;
|
||||
virtual bool contents_contains(const contentflags_t &a, const contentflags_t &b) const = 0;
|
||||
virtual std::string get_contents_display(const contentflags_t &contents) const = 0;
|
||||
virtual void contents_make_valid(contentflags_t &contents) const = 0;
|
||||
virtual const std::initializer_list<aabb3d> &get_hull_sizes() const = 0;
|
||||
|
|
|
|||
|
|
@ -27,4 +27,5 @@ struct face_t;
|
|||
struct node_t;
|
||||
|
||||
void MergeFaceToList(face_t *face, std::list<face_t *> &list);
|
||||
std::list<face_t *> MergeFaceList(std::list<face_t *> input);
|
||||
void MergeAll(node_t *headnode);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ struct portal_t
|
|||
node_t *nodes[2]; // [0] = front side of planenum
|
||||
portal_t *next[2]; // [0] = next portal in nodes[0]'s list of portals
|
||||
std::optional<winding_t> winding;
|
||||
|
||||
bool sidefound; // false if ->side hasn't been checked
|
||||
face_t *side; // NULL = non-visible // fixme-brushbsp: change to side_t
|
||||
face_t *face[2]; // output face in bsp file
|
||||
};
|
||||
|
||||
struct tree_t
|
||||
|
|
@ -53,3 +57,4 @@ void MakeTreePortals(tree_t *tree);
|
|||
void FreeTreePortals_r(node_t *node);
|
||||
void AssertNoPortals(node_t *node);
|
||||
void MakeHeadnodePortals(tree_t *tree);
|
||||
void MarkVisibleSides(tree_t *tree, mapentity_t* entity);
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ public:
|
|||
setting_int32 subdivide{this, "subdivide", 240, &common_format_group,
|
||||
"change the subdivide threshold, in luxels. 0 will disable subdivision entirely"};
|
||||
setting_bool nofill{this, "nofill", false, &debugging_group, "don't perform outside filling"};
|
||||
setting_bool nomerge{this, "nomerge", false, &debugging_group, "don't perform face merging"};
|
||||
setting_bool noclip{this, "noclip", false, &common_format_group, "don't write clip nodes (Q1-like BSP formats)"};
|
||||
setting_bool noskip{this, "noskip", false, &debugging_group, "don't remove faces with the 'skip' texture"};
|
||||
setting_bool nodetail{this, "nodetail", false, &debugging_group, "treat all detail brushes to structural"};
|
||||
|
|
@ -326,6 +327,8 @@ struct face_fragment_t
|
|||
// write surfaces
|
||||
};
|
||||
|
||||
struct portal_t;
|
||||
|
||||
struct face_t : face_fragment_t
|
||||
{
|
||||
int planenum;
|
||||
|
|
@ -345,12 +348,12 @@ struct face_t : face_fragment_t
|
|||
bool visible = true; // can any part of this side be seen from non-void parts of the level?
|
||||
// non-visible means we can discard the brush side
|
||||
// (avoiding generating a BSP spit, so expanding it outwards)
|
||||
portal_t *portal;
|
||||
};
|
||||
|
||||
// there is a node_t structure for every node and leaf in the bsp tree
|
||||
|
||||
struct brush_t;
|
||||
struct portal_t;
|
||||
|
||||
struct node_t
|
||||
{
|
||||
|
|
|
|||
|
|
@ -31,3 +31,4 @@ std::list<face_t *> SubdivideFace(face_t *f);
|
|||
void FreeNodes(node_t *node);
|
||||
void MakeVisibleFaces(mapentity_t *entity, node_t *headnode);
|
||||
void MakeMarkFaces(mapentity_t* entity, node_t* headnode);
|
||||
void MakeFaces(node_t *node);
|
||||
|
|
|
|||
393
qbsp/csg4.cc
393
qbsp/csg4.cc
|
|
@ -127,106 +127,6 @@ std::tuple<face_t *, face_t *> SplitFace(face_t *in, const qplane3d &split)
|
|||
return {new_front, new_back};
|
||||
}
|
||||
|
||||
/*
|
||||
=================
|
||||
RemoveOutsideFaces
|
||||
|
||||
Quick test before running ClipInside; move any faces that are completely
|
||||
outside the brush to the outside list, without splitting them. This saves us
|
||||
time in mergefaces later on (and sometimes a lot of memory)
|
||||
|
||||
Input is a list of faces in the param `inside`.
|
||||
On return, the ones touching `brush` remain in `inside`, the rest are added to `outside`.
|
||||
=================
|
||||
*/
|
||||
static void RemoveOutsideFaces(const brush_t &brush, std::list<face_t *> *inside, std::list<face_t *> *outside)
|
||||
{
|
||||
std::list<face_t *> oldinside;
|
||||
|
||||
// clear `inside`, transfer it to `oldinside`
|
||||
std::swap(*inside, oldinside);
|
||||
|
||||
for (face_t *face : oldinside) {
|
||||
std::optional<winding_t> w = face->w;
|
||||
for (auto &clipface : brush.faces) {
|
||||
w = w->clip(Face_Plane(&clipface), options.epsilon.value(), false)[SIDE_BACK];
|
||||
if (!w)
|
||||
break;
|
||||
}
|
||||
if (!w) {
|
||||
/* The face is completely outside this brush */
|
||||
outside->push_front(face);
|
||||
} else {
|
||||
inside->push_front(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
|
||||
=================
|
||||
*/
|
||||
static void ClipInside(
|
||||
const face_t *clipface, bool precedence, std::list<face_t *> *inside, std::list<face_t *> *outside)
|
||||
{
|
||||
std::list<face_t *> oldinside;
|
||||
|
||||
// effectively make a copy of `inside`, and clear it
|
||||
std::swap(*inside, oldinside);
|
||||
|
||||
const qbsp_plane_t &splitplane = map.planes[clipface->planenum];
|
||||
|
||||
for (face_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, options.epsilon.value());
|
||||
|
||||
if (counts[SIDE_ON] && !counts[SIDE_FRONT] && !counts[SIDE_BACK]) {
|
||||
spurious_onplane = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::array<face_t *, 2> frags;
|
||||
|
||||
/* Handle exactly on-plane faces */
|
||||
if (face->planenum == clipface->planenum || spurious_onplane) {
|
||||
const qplane3d faceplane = Face_Plane(face);
|
||||
const qplane3d clipfaceplane = Face_Plane(clipface);
|
||||
const vec_t dp = qv::dot(faceplane.normal, clipfaceplane.normal);
|
||||
const bool opposite = (dp < 0);
|
||||
|
||||
if (opposite || precedence) {
|
||||
/* always clip off opposite facing */
|
||||
frags[clipface->planeside] = {};
|
||||
frags[!clipface->planeside] = {face};
|
||||
} else {
|
||||
/* leave it on the outside */
|
||||
frags[clipface->planeside] = {face};
|
||||
frags[!clipface->planeside] = {};
|
||||
}
|
||||
} else {
|
||||
/* proper split */
|
||||
std::tie(frags[0], frags[1]) = SplitFace(face, splitplane);
|
||||
}
|
||||
|
||||
if (frags[clipface->planeside]) {
|
||||
outside->push_front(frags[clipface->planeside]);
|
||||
}
|
||||
if (frags[!clipface->planeside]) {
|
||||
inside->push_front(frags[!clipface->planeside]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
face_t *MirrorFace(const face_t *face)
|
||||
{
|
||||
face_t *newface = NewFaceFromFace(face);
|
||||
|
|
@ -237,296 +137,3 @@ face_t *MirrorFace(const face_t *face)
|
|||
|
||||
return newface;
|
||||
}
|
||||
|
||||
static void FreeFaces(std::list<face_t *> &facelist)
|
||||
{
|
||||
for (face_t *face : facelist) {
|
||||
delete face;
|
||||
}
|
||||
facelist.clear();
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
|
||||
static std::vector<std::unique_ptr<brush_t>> SingleBrush(std::unique_ptr<brush_t> a)
|
||||
{
|
||||
std::vector<std::unique_ptr<brush_t>> res;
|
||||
res.push_back(std::move(a));
|
||||
return res;
|
||||
}
|
||||
|
||||
static bool ShouldClipbrushEatBrush(const brush_t &brush, const brush_t &clipbrush)
|
||||
{
|
||||
if (clipbrush.contents.is_any_solid(options.target_game)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (clipbrush.contents.types_equal(brush.contents, options.target_game)) {
|
||||
return clipbrush.contents.will_clip_same_type(options.target_game);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static std::list<face_t *> CSGFace_ClipAgainstSingleBrush(std::list<face_t *> input, const mapentity_t *srcentity, const brush_t *srcbrush, const brush_t *clipbrush)
|
||||
{
|
||||
if (srcbrush == clipbrush) {
|
||||
//logging::print(" ignoring self-clip\n");
|
||||
return input;
|
||||
}
|
||||
|
||||
const int srcindex = srcbrush->file_order;
|
||||
const int clipindex = clipbrush->file_order;
|
||||
|
||||
if (!ShouldClipbrushEatBrush(*srcbrush, *clipbrush)) {
|
||||
return {input};
|
||||
}
|
||||
|
||||
std::list<face_t *> inside {input};
|
||||
std::list<face_t *> outside;
|
||||
RemoveOutsideFaces(*clipbrush, &inside, &outside);
|
||||
|
||||
// at this point, inside = the faces of `input` which are touching `clipbrush`
|
||||
// outside = the other faces of `input`
|
||||
|
||||
const bool overwrite = (srcindex < clipindex);
|
||||
|
||||
for (auto &clipface : clipbrush->faces)
|
||||
ClipInside(&clipface, overwrite, &inside, &outside);
|
||||
|
||||
// inside = parts of `brush` that are inside `clipbrush`
|
||||
// outside = parts of `brush` that are outside `clipbrush`
|
||||
|
||||
return outside;
|
||||
}
|
||||
|
||||
struct brush_ptr_less
|
||||
{
|
||||
constexpr bool operator()(const brush_t *a, const brush_t *b) const
|
||||
{
|
||||
return a->file_order < b->file_order;
|
||||
}
|
||||
};
|
||||
|
||||
using brush_result_set_t = std::set<const brush_t *, brush_ptr_less>;
|
||||
|
||||
// fixme-brushbsp: add bounds test
|
||||
static void GatherPossibleClippingBrushes_R(const node_t *node, const face_t *srcface, brush_result_set_t &result)
|
||||
{
|
||||
if (node->planenum == PLANENUM_LEAF) {
|
||||
for (auto *brush : node->original_brushes) {
|
||||
result.insert(brush);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
GatherPossibleClippingBrushes_R(node->children[0], srcface, result);
|
||||
GatherPossibleClippingBrushes_R(node->children[1], srcface, result);
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
GatherPossibleClippingBrushes
|
||||
|
||||
Starting a search at `node`, returns brushes that possibly intersect `srcface`.
|
||||
==================
|
||||
*/
|
||||
static brush_result_set_t GatherPossibleClippingBrushes(const mapentity_t* srcentity, const node_t *node, const face_t *srcface)
|
||||
{
|
||||
brush_result_set_t result;
|
||||
|
||||
GatherPossibleClippingBrushes_R(node, srcface, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
CSGFace
|
||||
|
||||
Given `srcface`, which was produced from `srcbrush` and lies on `srcnode`:
|
||||
|
||||
- search srcnode as well as its children for brushes which might clip
|
||||
srcface.
|
||||
|
||||
- clip srcface against all such brushes
|
||||
|
||||
Frees srcface.
|
||||
==================
|
||||
*/
|
||||
std::list<face_t *> CSGFace(face_t *srcface, const mapentity_t *srcentity, const brush_t *srcbrush, const node_t *srcnode)
|
||||
{
|
||||
const auto possible_clipbrushes = GatherPossibleClippingBrushes(srcentity, srcnode, srcface);
|
||||
|
||||
//logging::print("face {} has {} possible clipbrushes\n", (void *)srcface, possible_clipbrushes.size());
|
||||
|
||||
std::list<face_t *> result{srcface};
|
||||
|
||||
for (const brush_t *possible_clipbrush : possible_clipbrushes) {
|
||||
result = CSGFace_ClipAgainstSingleBrush(std::move(result), srcentity, srcbrush, possible_clipbrush);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
SubtractBrush
|
||||
|
||||
Returns the fragments from a - b
|
||||
==================
|
||||
*/
|
||||
static std::vector<std::unique_ptr<brush_t>> SubtractBrush(std::unique_ptr<brush_t> a, const brush_t& b)
|
||||
{
|
||||
// first, check if `a` is fully in front of _any_ of b's planes
|
||||
for (const auto &side : b.faces) {
|
||||
// is `a` fully in front of `side`?
|
||||
bool fully_infront = true;
|
||||
|
||||
// fixme-brushbsp: factor this out somewhere
|
||||
for (const auto &a_face : a->faces) {
|
||||
for (const auto &a_point : a_face.w) {
|
||||
if (Face_Plane(&side).distance_to(a_point) < 0) {
|
||||
fully_infront = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!fully_infront) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fully_infront) {
|
||||
// `a` is fully in front of this side of b, so they don't actually intersect
|
||||
return SingleBrush(std::move(a));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<brush_t>> frontlist;
|
||||
std::vector<std::unique_ptr<brush_t>> unclassified = SingleBrush(std::move(a));
|
||||
|
||||
for (const auto &side : b.faces) {
|
||||
std::vector<std::unique_ptr<brush_t>> new_unclassified;
|
||||
|
||||
for (auto &fragment : unclassified) {
|
||||
// destructively processing `unclassified` here
|
||||
auto [front, back] = SplitBrush(std::move(fragment), Face_Plane(&side));
|
||||
if (front) {
|
||||
frontlist.push_back(std::move(front));
|
||||
}
|
||||
if (back) {
|
||||
new_unclassified.push_back(std::move(back));
|
||||
}
|
||||
}
|
||||
|
||||
unclassified = std::move(new_unclassified);
|
||||
}
|
||||
|
||||
return frontlist;
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
BrushGE
|
||||
|
||||
Returns a >= b as far as brush clipping
|
||||
==================
|
||||
*/
|
||||
bool BrushGE(const brush_t& a, const brush_t& b)
|
||||
{
|
||||
// same contents clip each other
|
||||
if (a.contents.will_clip_same_type(options.target_game, b.contents)) {
|
||||
// map file order
|
||||
return a.file_order > b.file_order;
|
||||
}
|
||||
|
||||
// only chop if at least one of the two contents is
|
||||
// opaque (solid, sky, or detail)
|
||||
if (!(a.contents.chops(options.target_game) || b.contents.chops(options.target_game))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t a_pri = a.contents.priority(options.target_game);
|
||||
int32_t b_pri = b.contents.priority(options.target_game);
|
||||
|
||||
if (a_pri == b_pri) {
|
||||
// map file order
|
||||
return a.file_order > b.file_order;
|
||||
}
|
||||
|
||||
return a_pri >= b_pri;
|
||||
}
|
||||
|
||||
/*
|
||||
==================
|
||||
ChopBrushes
|
||||
|
||||
Clips off any overlapping portions of brushes
|
||||
==================
|
||||
*/
|
||||
std::vector<std::unique_ptr<brush_t>> ChopBrushes(const std::vector<std::unique_ptr<brush_t>>& input)
|
||||
{
|
||||
logging::print(logging::flag::PROGRESS, "---- {} ----\n", __func__);
|
||||
|
||||
// each inner vector corresponds to a brush in `input`
|
||||
// (set up this way for thread safety)
|
||||
std::vector<std::vector<std::unique_ptr<brush_t>>> brush_fragments;
|
||||
brush_fragments.resize(input.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"
|
||||
*/
|
||||
tbb::parallel_for(static_cast<size_t>(0), input.size(), [&](const size_t i) {
|
||||
const auto& brush = input[i];
|
||||
|
||||
// the fragments `brush` is chopped into
|
||||
std::vector<std::unique_ptr<brush_t>> brush_result = SingleBrush(
|
||||
// start with a copy of brush
|
||||
std::make_unique<brush_t>(*brush)
|
||||
);
|
||||
|
||||
for (auto &clipbrush : input) {
|
||||
if (brush == clipbrush) {
|
||||
continue;
|
||||
}
|
||||
if (brush->bounds.disjoint_or_touching(clipbrush->bounds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (BrushGE(*clipbrush, *brush)) {
|
||||
std::vector<std::unique_ptr<brush_t>> new_result;
|
||||
|
||||
// clipbrush is stronger.
|
||||
// rebuild existing fragments in brush_result, cliping them to clipbrush
|
||||
for (auto ¤t_fragment : brush_result) {
|
||||
for (auto &new_fragment : SubtractBrush(std::move(current_fragment), *clipbrush)) {
|
||||
new_result.push_back(std::move(new_fragment));
|
||||
}
|
||||
}
|
||||
|
||||
brush_result = std::move(new_result);
|
||||
}
|
||||
}
|
||||
|
||||
// save the result
|
||||
brush_fragments[i] = std::move(brush_result);
|
||||
});
|
||||
|
||||
// Non parallel part:
|
||||
std::vector<std::unique_ptr<brush_t>> result;
|
||||
for (auto &fragment_list : brush_fragments) {
|
||||
for (auto &fragment : fragment_list) {
|
||||
result.push_back(std::move(fragment));
|
||||
}
|
||||
}
|
||||
|
||||
logging::print(logging::flag::STAT, " {:8} brushes\n", input.size());
|
||||
logging::print(logging::flag::STAT, " {:8} chopped brushes\n", result.size());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ static face_t *TryMerge(face_t *f1, face_t *f2)
|
|||
bool keep1, keep2;
|
||||
|
||||
if (!f1->w.size() || !f2->w.size() || f1->planeside != f2->planeside || f1->texinfo != f2->texinfo ||
|
||||
!f1->contents[0].equals(options.target_game, f2->contents[0]) || !f1->contents[1].equals(options.target_game, f2->contents[1]) ||
|
||||
/*!f1->contents[0].equals(options.target_game, f2->contents[0]) || !f1->contents[1].equals(options.target_game, f2->contents[1]) || */
|
||||
f1->lmshift[0] != f2->lmshift[0] || f1->lmshift[1] != f2->lmshift[1])
|
||||
return NULL;
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ void MergeFaceToList(face_t *face, std::list<face_t *> &list)
|
|||
MergeFaceList
|
||||
===============
|
||||
*/
|
||||
inline std::list<face_t *> MergeFaceList(std::list<face_t *> input)
|
||||
std::list<face_t *> MergeFaceList(std::list<face_t *> input)
|
||||
{
|
||||
std::list<face_t *> result;
|
||||
|
||||
|
|
|
|||
119
qbsp/portals.cc
119
qbsp/portals.cc
|
|
@ -426,3 +426,122 @@ void FreeTreePortals_r(node_t *node)
|
|||
}
|
||||
node->portals = nullptr;
|
||||
}
|
||||
|
||||
//==============================================================
|
||||
|
||||
/*
|
||||
============
|
||||
FindPortalSide
|
||||
|
||||
Finds a brush side to use for texturing the given portal
|
||||
============
|
||||
*/
|
||||
static void FindPortalSide(portal_t *p)
|
||||
{
|
||||
// decide which content change is strongest
|
||||
// solid > lava > water, etc
|
||||
contentflags_t viscontents = options.target_game->visible_contents(p->nodes[0]->contents, p->nodes[1]->contents);
|
||||
if (viscontents.is_empty(options.target_game))
|
||||
return;
|
||||
|
||||
int planenum = p->onnode->planenum;
|
||||
face_t *bestside = nullptr;
|
||||
float bestdot = 0;
|
||||
|
||||
for (int j = 0; j < 2; j++)
|
||||
{
|
||||
node_t *n = p->nodes[j];
|
||||
auto p1 = map.planes.at(p->onnode->planenum);
|
||||
|
||||
// iterate the n->original_brushes vector in reverse order, so later brushes
|
||||
// in the map file order are prioritized
|
||||
for (auto it = n->original_brushes.rbegin(); it != n->original_brushes.rend(); ++it)
|
||||
{
|
||||
auto *brush = *it;
|
||||
if (!options.target_game->contents_contains(brush->contents, viscontents))
|
||||
continue;
|
||||
for (face_t &side : brush->faces)
|
||||
{
|
||||
// fixme-brushbsp: port these
|
||||
// if (side.bevel)
|
||||
// continue;
|
||||
// if (side.texinfo == TEXINFO_NODE)
|
||||
// continue; // non-visible
|
||||
if (side.planenum == planenum)
|
||||
{ // exact match
|
||||
bestside = &side;
|
||||
goto gotit;
|
||||
}
|
||||
// see how close the match is
|
||||
auto p2 = map.planes.at(side.planenum);
|
||||
float dot = qv::dot(p1.normal, p2.normal);
|
||||
if (dot > bestdot)
|
||||
{
|
||||
bestdot = dot;
|
||||
bestside = &side;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gotit:
|
||||
if (!bestside)
|
||||
logging::print("WARNING: side not found for portal\n");
|
||||
|
||||
p->sidefound = true;
|
||||
p->side = bestside;
|
||||
}
|
||||
|
||||
/*
|
||||
===============
|
||||
MarkVisibleSides_r
|
||||
|
||||
===============
|
||||
*/
|
||||
static void MarkVisibleSides_r(node_t *node)
|
||||
{
|
||||
if (node->planenum != PLANENUM_LEAF)
|
||||
{
|
||||
MarkVisibleSides_r(node->children[0]);
|
||||
MarkVisibleSides_r(node->children[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
// empty leafs are never boundary leafs
|
||||
if (node->contents.is_empty(options.target_game))
|
||||
return;
|
||||
|
||||
// see if there is a visible face
|
||||
int s;
|
||||
for (portal_t *p=node->portals ; p ; p = p->next[!s])
|
||||
{
|
||||
s = (p->nodes[0] == node);
|
||||
if (!p->onnode)
|
||||
continue; // edge of world
|
||||
if (!p->sidefound)
|
||||
FindPortalSide(p);
|
||||
if (p->side)
|
||||
p->side->visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
=============
|
||||
MarkVisibleSides
|
||||
|
||||
=============
|
||||
*/
|
||||
void MarkVisibleSides(tree_t *tree, mapentity_t* entity)
|
||||
{
|
||||
logging::print("--- {} ---\n", __func__);
|
||||
|
||||
// clear all the visible flags
|
||||
for (auto &brush : entity->brushes) {
|
||||
for (auto &face : brush->faces) {
|
||||
face.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// set visible flags on the sides that are used by portals
|
||||
MarkVisibleSides_r (tree->headnode);
|
||||
}
|
||||
|
|
|
|||
13
qbsp/qbsp.cc
13
qbsp/qbsp.cc
|
|
@ -896,21 +896,18 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum)
|
|||
tree = BrushBSP(entity, false);
|
||||
}
|
||||
|
||||
FreeTreePortals_r(tree->headnode);
|
||||
|
||||
PruneNodes(tree->headnode);
|
||||
|
||||
MakeTreePortals(tree);
|
||||
|
||||
MakeVisibleFaces(entity, tree->headnode);
|
||||
MarkVisibleSides(tree, entity);
|
||||
MakeFaces(tree->headnode);
|
||||
|
||||
FreeTreePortals_r(tree->headnode);
|
||||
PruneNodes(tree->headnode);
|
||||
|
||||
if (hullnum <= 0 && entity == map.world_entity() && !map.leakfile) {
|
||||
WritePortalFile(tree);
|
||||
}
|
||||
|
||||
// merge polygons
|
||||
MergeAll(tree->headnode);
|
||||
|
||||
// needs to come after any face creation
|
||||
MakeMarkFaces(entity, tree->headnode);
|
||||
|
||||
|
|
|
|||
256
qbsp/surfaces.cc
256
qbsp/surfaces.cc
|
|
@ -23,6 +23,7 @@
|
|||
#include <qbsp/portals.hh>
|
||||
#include <qbsp/csg4.hh>
|
||||
#include <qbsp/map.hh>
|
||||
#include <qbsp/merge.hh>
|
||||
#include <qbsp/solidbsp.hh>
|
||||
#include <qbsp/qbsp.hh>
|
||||
#include <qbsp/writebsp.hh>
|
||||
|
|
@ -45,6 +46,11 @@ static bool ShouldOmitFace(face_t *f)
|
|||
return false;
|
||||
}
|
||||
|
||||
static void MergeNodeFaces (node_t *node)
|
||||
{
|
||||
node->facelist = MergeFaceList(node->facelist);
|
||||
}
|
||||
|
||||
/*
|
||||
===============
|
||||
SubdivideFace
|
||||
|
|
@ -155,6 +161,18 @@ std::list<face_t *> SubdivideFace(face_t *f)
|
|||
return surfaces;
|
||||
}
|
||||
|
||||
static void SubdivideNodeFaces(node_t *node)
|
||||
{
|
||||
std::list<face_t *> result;
|
||||
|
||||
// subdivide each face and push the results onto subdivided
|
||||
for (face_t *face : node->facelist) {
|
||||
result.splice(result.end(), SubdivideFace(face));
|
||||
}
|
||||
|
||||
node->facelist = result;
|
||||
}
|
||||
|
||||
static void FreeNode(node_t *node)
|
||||
{
|
||||
FreeTreePortals_r(node);
|
||||
|
|
@ -423,7 +441,7 @@ static void GrowNodeRegion(mapentity_t *entity, node_t *node)
|
|||
node->firstface = static_cast<int>(map.bsp.dfaces.size());
|
||||
|
||||
for (face_t *face : node->facelist) {
|
||||
Q_assert(face->planenum == node->planenum);
|
||||
//Q_assert(face->planenum == node->planenum);
|
||||
|
||||
// emit a region
|
||||
EmitFace(entity, face);
|
||||
|
|
@ -561,149 +579,143 @@ void MakeMarkFaces(mapentity_t* entity, node_t* node)
|
|||
MakeMarkFaces(entity, node->children[1]);
|
||||
}
|
||||
|
||||
// the fronts of `faces` are facing `node`, determine what gets clipped away
|
||||
// (return the post-clipping result)
|
||||
static std::list<face_t *> ClipFacesToTree_r(node_t *node, const brush_t *srcbrush, std::list<face_t *> faces)
|
||||
struct makefaces_stats_t {
|
||||
int c_nodefaces;
|
||||
int c_merge;
|
||||
int c_subdivide;
|
||||
};
|
||||
|
||||
/*
|
||||
============
|
||||
FaceFromPortal
|
||||
|
||||
pside is which side of portal (equivalently, which side of the node) we're in.
|
||||
Typically, we're in an empty leaf and the other side of the portal is a solid wall.
|
||||
|
||||
see also FindPortalSide which populates p->side
|
||||
============
|
||||
*/
|
||||
static face_t *FaceFromPortal(portal_t *p, int pside)
|
||||
{
|
||||
if (node->planenum == PLANENUM_LEAF) {
|
||||
// fixme-brushbsp: move to contentflags_t?
|
||||
if (node->contents.is_solid(options.target_game)
|
||||
|| node->contents.is_detail_solid(options.target_game)
|
||||
|| node->contents.is_sky(options.target_game)) {
|
||||
// solids eat any faces that reached this point
|
||||
return {};
|
||||
}
|
||||
face_t *side = p->side;
|
||||
if (!side)
|
||||
return nullptr; // portal does not bridge different visible contents
|
||||
|
||||
// see what the game thinks about the clip
|
||||
if (srcbrush->contents.will_clip_same_type(options.target_game, node->contents)) {
|
||||
return {};
|
||||
}
|
||||
face_t *f = new face_t{};
|
||||
|
||||
// other content types let the faces thorugh
|
||||
return faces;
|
||||
f->texinfo = side->texinfo;
|
||||
f->planenum = side->planenum;
|
||||
f->planeside = static_cast<side_t>(pside);
|
||||
f->portal = p;
|
||||
f->lmshift = side->lmshift;
|
||||
|
||||
bool make_face = options.target_game->directional_visible_contents(p->nodes[pside]->contents, p->nodes[!pside]->contents);
|
||||
if (!make_face) {
|
||||
// content type / game rules requested to skip generating a face on this side
|
||||
logging::print("skipped face for {} -> {} portal\n",
|
||||
p->nodes[pside]->contents.to_string(options.target_game),
|
||||
p->nodes[!pside]->contents.to_string(options.target_game));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const qbsp_plane_t &splitplane = map.planes.at(node->planenum);
|
||||
if (!p->nodes[pside]->contents.is_empty(options.target_game)) {
|
||||
bool our_contents_mirrorinside = options.target_game->contents_are_mirrored(p->nodes[pside]->contents);
|
||||
if (!our_contents_mirrorinside) {
|
||||
if (side->planeside != pside) {
|
||||
|
||||
std::list<face_t *> front, back;
|
||||
|
||||
for (auto *face : faces) {
|
||||
auto [frontFragment, backFragment] = SplitFace(face, splitplane);
|
||||
if (frontFragment) {
|
||||
front.push_back(frontFragment);
|
||||
}
|
||||
if (backFragment) {
|
||||
back.push_back(backFragment);
|
||||
}
|
||||
}
|
||||
|
||||
front = ClipFacesToTree_r(node->children[0], srcbrush, front);
|
||||
back = ClipFacesToTree_r(node->children[1], srcbrush, back);
|
||||
|
||||
// merge lists
|
||||
front.splice(front.end(), back);
|
||||
|
||||
return front;
|
||||
}
|
||||
|
||||
static std::list<face_t *> ClipFacesToTree(node_t *node, const brush_t *srcbrush, std::list<face_t *> faces)
|
||||
{
|
||||
// handles the first level - faces are all supposed to be lying exactly on `node`
|
||||
for (auto *face : faces) {
|
||||
Q_assert(face->planenum == node->planenum);
|
||||
}
|
||||
|
||||
std::list<face_t *> front, back;
|
||||
for (auto *face : faces) {
|
||||
if (face->planeside == 0) {
|
||||
front.push_back(face);
|
||||
} else {
|
||||
back.push_back(face);
|
||||
}
|
||||
}
|
||||
|
||||
front = ClipFacesToTree_r(node->children[0], srcbrush, front);
|
||||
back = ClipFacesToTree_r(node->children[1], srcbrush, back);
|
||||
|
||||
// merge lists
|
||||
front.splice(front.end(), back);
|
||||
|
||||
return front;
|
||||
}
|
||||
|
||||
static void AddFaceToTree_r(mapentity_t* entity, face_t *face, brush_t *srcbrush, node_t* node)
|
||||
{
|
||||
if (node->planenum == PLANENUM_LEAF) {
|
||||
//FError("couldn't find node for face");
|
||||
// after outside filling, this is completely expected
|
||||
return;
|
||||
}
|
||||
|
||||
if (face->planenum == node->planenum) {
|
||||
// found the correct plane - add the face to it.
|
||||
|
||||
++c_nodefaces;
|
||||
|
||||
// csg it
|
||||
std::list<face_t *> faces = CSGFace(face, entity, srcbrush, node);
|
||||
|
||||
// clip them to the descendant parts of the BSP
|
||||
// (otherwise we could have faces floating in the void on this node)
|
||||
faces = ClipFacesToTree(node, srcbrush, faces);
|
||||
|
||||
for (face_t *part : faces) {
|
||||
node->facelist.push_back(part);
|
||||
|
||||
if (srcbrush->contents.is_mirrored(options.target_game)) {
|
||||
node->facelist.push_back(MirrorFace(part));
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// fixme-brushbsp: we need to handle the case of the face being near enough that it gets clipped away,
|
||||
// but not face->planenum == node->planenum
|
||||
auto [frontWinding, backWinding] = face->w.clip(map.planes.at(node->planenum));
|
||||
if (frontWinding) {
|
||||
auto *newFace = new face_t{*face};
|
||||
newFace->w = *frontWinding;
|
||||
AddFaceToTree_r(entity, newFace, srcbrush, node->children[0]);
|
||||
|
||||
if (pside)
|
||||
{
|
||||
f->w = p->winding->flip();
|
||||
// fixme-brushbsp: was just `f->contents` on qbsp3
|
||||
f->contents[0] = p->nodes[1]->contents;
|
||||
f->contents[1] = p->nodes[0]->contents;
|
||||
}
|
||||
if (backWinding) {
|
||||
auto *newFace = new face_t{*face};
|
||||
newFace->w = *backWinding;
|
||||
AddFaceToTree_r(entity, newFace, srcbrush, node->children[1]);
|
||||
else
|
||||
{
|
||||
f->w = *p->winding;
|
||||
f->contents[0] = p->nodes[0]->contents;
|
||||
f->contents[1] = p->nodes[1]->contents;
|
||||
}
|
||||
|
||||
delete face;
|
||||
UpdateFaceSphere(f);
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
/*
|
||||
================
|
||||
MakeVisibleFaces
|
||||
===============
|
||||
MakeFaces_r
|
||||
|
||||
Given a completed BSP tree and a list of the original brushes (in `entity`),
|
||||
If a portal will make a visible face,
|
||||
mark the side that originally created it
|
||||
|
||||
- filters the brush faces into the BSP, finding the correct nodes they end up on
|
||||
- clips the faces by other brushes.
|
||||
|
||||
first iteration, we can just do an exhaustive check against all brushes
|
||||
================
|
||||
solid / empty : solid
|
||||
solid / water : solid
|
||||
water / empty : water
|
||||
water / water : none
|
||||
===============
|
||||
*/
|
||||
void MakeVisibleFaces(mapentity_t* entity, node_t* headnode)
|
||||
static void MakeFaces_r(node_t *node, makefaces_stats_t& stats)
|
||||
{
|
||||
c_nodefaces = 0;
|
||||
// recurse down to leafs
|
||||
if (node->planenum != PLANENUM_LEAF)
|
||||
{
|
||||
MakeFaces_r(node->children[0], stats);
|
||||
MakeFaces_r(node->children[1], stats);
|
||||
|
||||
for (auto &brush : entity->brushes) {
|
||||
for (auto &face : brush->faces) {
|
||||
if (!face.visible) {
|
||||
continue;
|
||||
}
|
||||
face_t *temp = CopyFace(&face);
|
||||
// merge together all visible faces on the node
|
||||
if (!options.nomerge.value())
|
||||
MergeNodeFaces(node);
|
||||
if (options.subdivide.boolValue())
|
||||
SubdivideNodeFaces(node);
|
||||
|
||||
AddFaceToTree_r(entity, temp, brush.get(), headnode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
logging::print(logging::flag::STAT, "{} nodefaces\n", c_nodefaces);
|
||||
// solid leafs never have visible faces
|
||||
if (node->contents.is_any_solid(options.target_game))
|
||||
return;
|
||||
|
||||
// see which portals are valid
|
||||
|
||||
// (Note, this is happening per leaf, so we can potentially generate faces
|
||||
// for the same portal once from one leaf, and once from the neighbouring one)
|
||||
int s;
|
||||
for (portal_t *p = node->portals; p; p = p->next[s])
|
||||
{
|
||||
// 1 means node is on the back side of planenum
|
||||
s = (p->nodes[1] == node);
|
||||
|
||||
face_t *f = FaceFromPortal(p, s);
|
||||
if (f)
|
||||
{
|
||||
stats.c_nodefaces++;
|
||||
p->face[s] = f;
|
||||
p->onnode->facelist.push_back(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
============
|
||||
MakeFaces
|
||||
============
|
||||
*/
|
||||
void MakeFaces(node_t *node)
|
||||
{
|
||||
logging::print("--- {} ---\n", __func__);
|
||||
|
||||
makefaces_stats_t stats{};
|
||||
|
||||
MakeFaces_r(node, stats);
|
||||
|
||||
logging::print(logging::flag::STAT, "{} makefaces\n", stats.c_nodefaces);
|
||||
logging::print(logging::flag::STAT, "{} merged\n", stats.c_merge);
|
||||
logging::print(logging::flag::STAT, "{} subdivided\n", stats.c_subdivide);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -677,7 +677,11 @@ TEST_CASE("simple_worldspawn_sky", "[testmaps_q1]")
|
|||
|
||||
TEST_CASE("water_detail_illusionary", "[testmaps_q1]")
|
||||
{
|
||||
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_water_detail_illusionary.map");
|
||||
static const std::string basic_mapname = "qbsp_water_detail_illusionary.map";
|
||||
static const std::string mirrorinside_mapname = "qbsp_water_detail_illusionary_mirrorinside.map";
|
||||
|
||||
auto mapname = GENERATE_REF(basic_mapname, mirrorinside_mapname);
|
||||
const auto [bsp, bspx, prt] = LoadTestmapQ1(mapname);
|
||||
|
||||
REQUIRE(prt.has_value());
|
||||
|
||||
|
|
@ -691,8 +695,28 @@ TEST_CASE("water_detail_illusionary", "[testmaps_q1]")
|
|||
const qvec3d above_face_pos{-40, -52, 172};
|
||||
|
||||
// make sure the detail_illusionary face underwater isn't clipped away
|
||||
CHECK(nullptr != BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], underwater_face_pos, {-1, 0, 0}));
|
||||
CHECK(nullptr != BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], above_face_pos, {-1, 0, 0}));
|
||||
auto* underwater_face = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], underwater_face_pos, {-1, 0, 0});
|
||||
auto* underwater_face_inner = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], underwater_face_pos, {1, 0, 0});
|
||||
|
||||
auto* above_face = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], above_face_pos, {-1, 0, 0});
|
||||
auto* above_face_inner = BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[0], above_face_pos, {1, 0, 0});
|
||||
|
||||
REQUIRE(nullptr != underwater_face);
|
||||
REQUIRE(nullptr != above_face);
|
||||
|
||||
CHECK(std::string("{trigger") == Face_TextureName(&bsp, underwater_face));
|
||||
CHECK(std::string("{trigger") == Face_TextureName(&bsp, above_face));
|
||||
|
||||
if (mapname == mirrorinside_mapname) {
|
||||
REQUIRE(underwater_face_inner != nullptr);
|
||||
REQUIRE(above_face_inner != nullptr);
|
||||
|
||||
CHECK(std::string("{trigger") == Face_TextureName(&bsp, underwater_face_inner));
|
||||
CHECK(std::string("{trigger") == Face_TextureName(&bsp, above_face_inner));
|
||||
} else {
|
||||
CHECK(underwater_face_inner == nullptr);
|
||||
CHECK(above_face_inner == nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("noclipfaces", "[testmaps_q1]")
|
||||
|
|
@ -770,7 +794,10 @@ TEST_CASE("detail_illusionary_noclipfaces_intersecting", "[testmaps_q1]")
|
|||
}
|
||||
|
||||
// top of cross has 2 faces Z-fighting, because we disabled clipping
|
||||
CHECK(2 == BSP_FindFacesAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-58, -50, 120), qvec3d(0, 0, 1)).size());
|
||||
// (with qbsp3 method, there won't ever be z-fighting since we only ever generate 1 face per portal)
|
||||
size_t faces_at_top = BSP_FindFacesAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-58, -50, 120), qvec3d(0, 0, 1)).size();
|
||||
CHECK(faces_at_top >= 1);
|
||||
CHECK(faces_at_top <= 2);
|
||||
|
||||
// interior face not clipped away
|
||||
CHECK(1 == BSP_FindFacesAtPoint(&bsp, &bsp.dmodels[0], qvec3d(-58, -52, 116), qvec3d(0, -1, 0)).size());
|
||||
|
|
@ -908,6 +935,40 @@ TEST_CASE("simple", "[testmaps_q1]")
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Just a solid cuboid
|
||||
*/
|
||||
TEST_CASE("q1_cube", "[testmaps_q1]")
|
||||
{
|
||||
const auto [bsp, bspx, prt] = LoadTestmapQ1("qbsp_q1_cube.map");
|
||||
|
||||
REQUIRE_FALSE(prt.has_value());
|
||||
|
||||
const aabb3d cube_bounds {
|
||||
{32, -240, 80},
|
||||
{80, -144, 112}
|
||||
};
|
||||
|
||||
REQUIRE(7 == bsp.dleafs.size());
|
||||
|
||||
// check the solid leaf
|
||||
auto& solid_leaf = bsp.dleafs[0];
|
||||
// fixme-brushbsp: restore these
|
||||
// CHECK(solid_leaf.mins == cube_bounds.mins());
|
||||
// CHECK(solid_leaf.maxs == cube_bounds.maxs());
|
||||
|
||||
// check the empty leafs
|
||||
for (int i = 1; i < 7; ++i) {
|
||||
auto& leaf = bsp.dleafs[i];
|
||||
CHECK(CONTENTS_EMPTY == leaf.contents);
|
||||
|
||||
CHECK(1 == leaf.nummarksurfaces);
|
||||
}
|
||||
|
||||
REQUIRE(6 == bsp.dfaces.size());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Lots of features in one map, more for testing in game than automated testing
|
||||
*/
|
||||
|
|
@ -1320,7 +1381,7 @@ TEST_CASE("qbsp_q2_bmodel_collision", "[testmaps_q2]") {
|
|||
CHECK(Q2_CONTENTS_SOLID == BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[1], in_bmodel)->contents);
|
||||
}
|
||||
|
||||
TEST_CASE("q2_liquids", "[testmaps_q2][!mayfail]")
|
||||
TEST_CASE("q2_liquids", "[testmaps_q2]")
|
||||
{
|
||||
const auto [bsp, bspx, prt] = LoadTestmapQ2("q2_liquids.map");
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
// Game: Quake
|
||||
// Format: Valve
|
||||
// entity 0
|
||||
{
|
||||
"mapversion" "220"
|
||||
"classname" "worldspawn"
|
||||
"wad" "deprecated/free_wad.wad;deprecated/fence.wad;deprecated/origin.wad;deprecated/hintskip.wad"
|
||||
"_wateralpha" "0.5"
|
||||
"_tb_def" "builtin:Quake.fgd"
|
||||
// brush 0
|
||||
{
|
||||
( 32 -256 112 ) ( 32 -255 112 ) ( 32 -256 113 ) orangestuff8 [ 0 1 0 -16 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 64 -240 96 ) ( 63 -240 96 ) ( 64 -240 97 ) orangestuff8 [ -1 0 0 16 ] [ 0 0 -1 0 ] 180 1 1
|
||||
( 64 -576 80 ) ( 64 -575 80 ) ( 63 -576 80 ) orangestuff8 [ 1 0 0 -16 ] [ 0 -1 0 16 ] 180 1 1
|
||||
( -16 -256 112 ) ( -17 -256 112 ) ( -16 -255 112 ) orangestuff8 [ -1 0 0 16 ] [ 0 -1 0 16 ] 180 1 1
|
||||
( -16 -144 112 ) ( -16 -144 113 ) ( -17 -144 112 ) orangestuff8 [ 1 0 0 -16 ] [ 0 0 -1 0 ] 180 1 1
|
||||
( 80 -576 96 ) ( 80 -576 97 ) ( 80 -575 96 ) orangestuff8 [ 0 -1 0 16 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
}
|
||||
// entity 1
|
||||
{
|
||||
"classname" "info_player_start"
|
||||
"origin" "56 -208 136"
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
// Game: Quake
|
||||
// Format: Valve
|
||||
// entity 0
|
||||
{
|
||||
"mapversion" "220"
|
||||
"classname" "worldspawn"
|
||||
"wad" "deprecated/free_wad.wad;deprecated/fence.wad;deprecated/origin.wad;deprecated/hintskip.wad"
|
||||
"_wateralpha" "0.5"
|
||||
"_tb_def" "builtin:Quake.fgd"
|
||||
// brush 0
|
||||
{
|
||||
( -112 -112 96 ) ( -112 -111 96 ) ( -112 -112 97 ) orangestuff8 [ 0 1 0 -16 ] [ 0 0 -1 0 ] 0 2 2
|
||||
( -80 -96 80 ) ( -81 -96 80 ) ( -80 -96 81 ) orangestuff8 [ -1 0 0 16 ] [ 0 0 -1 0 ] 180 2 2
|
||||
( -80 -432 80 ) ( -80 -431 80 ) ( -81 -432 80 ) orangestuff8 [ 1 0 0 -16 ] [ 0 -1 0 16 ] 180 2 2
|
||||
( -160 -112 96 ) ( -161 -112 96 ) ( -160 -111 96 ) orangestuff8 [ -1 0 0 16 ] [ 0 -1 0 16 ] 180 2 2
|
||||
( -160 0 96 ) ( -160 0 97 ) ( -161 0 96 ) orangestuff8 [ 1 0 0 -16 ] [ 0 0 -1 0 ] 180 2 2
|
||||
( 64 -432 80 ) ( 64 -432 81 ) ( 64 -431 80 ) orangestuff8 [ 0 -1 0 16 ] [ 0 0 -1 0 ] 0 2 2
|
||||
}
|
||||
// brush 1
|
||||
{
|
||||
( -112 -96 96 ) ( -112 -95 96 ) ( -112 -96 97 ) orangestuff8 [ 0 1 0 -16 ] [ 0 0 -1 0 ] 0 2 2
|
||||
( -80 0 80 ) ( -81 0 80 ) ( -80 0 81 ) orangestuff8 [ -1 0 0 16 ] [ 0 0 -1 0 ] 180 2 2
|
||||
( -80 -416 80 ) ( -80 -415 80 ) ( -81 -416 80 ) orangestuff8 [ 1 0 0 -16 ] [ 0 -1 0 16 ] 180 2 2
|
||||
( -160 -96 224 ) ( -161 -96 224 ) ( -160 -95 224 ) orangestuff8 [ -1 0 0 16 ] [ 0 -1 0 16 ] 180 2 2
|
||||
( -160 16 96 ) ( -160 16 97 ) ( -161 16 96 ) orangestuff8 [ 1 0 0 -16 ] [ 0 0 -1 0 ] 180 2 2
|
||||
( 64 -416 80 ) ( 64 -416 81 ) ( 64 -415 80 ) orangestuff8 [ 0 -1 0 16 ] [ 0 0 -1 0 ] 0 2 2
|
||||
}
|
||||
// brush 2
|
||||
{
|
||||
( -112 -208 96 ) ( -112 -207 96 ) ( -112 -208 97 ) orangestuff8 [ 0 1 0 -16 ] [ 0 0 -1 0 ] 0 2 2
|
||||
( -80 -112 80 ) ( -81 -112 80 ) ( -80 -112 81 ) orangestuff8 [ -1 0 0 16 ] [ 0 0 -1 0 ] 180 2 2
|
||||
( -80 -528 80 ) ( -80 -527 80 ) ( -81 -528 80 ) orangestuff8 [ 1 0 0 -16 ] [ 0 -1 0 16 ] 180 2 2
|
||||
( -160 -208 224 ) ( -161 -208 224 ) ( -160 -207 224 ) orangestuff8 [ -1 0 0 16 ] [ 0 -1 0 16 ] 180 2 2
|
||||
( -160 -96 96 ) ( -160 -96 97 ) ( -161 -96 96 ) orangestuff8 [ 1 0 0 -16 ] [ 0 0 -1 0 ] 180 2 2
|
||||
( 64 -528 80 ) ( 64 -528 81 ) ( 64 -527 80 ) orangestuff8 [ 0 -1 0 16 ] [ 0 0 -1 0 ] 0 2 2
|
||||
}
|
||||
// brush 3
|
||||
{
|
||||
( -128 -112 96 ) ( -128 -111 96 ) ( -128 -112 97 ) orangestuff8 [ 0 1 0 -16 ] [ 0 0 -1 0 ] 0 2 2
|
||||
( -256 -96 80 ) ( -257 -96 80 ) ( -256 -96 81 ) orangestuff8 [ -1 0 0 16 ] [ 0 0 -1 0 ] 180 2 2
|
||||
( -256 -432 80 ) ( -256 -431 80 ) ( -257 -432 80 ) orangestuff8 [ 1 0 0 -16 ] [ 0 -1 0 16 ] 180 2 2
|
||||
( -336 -112 224 ) ( -337 -112 224 ) ( -336 -111 224 ) orangestuff8 [ -1 0 0 16 ] [ 0 -1 0 16 ] 180 2 2
|
||||
( -336 0 96 ) ( -336 0 97 ) ( -337 0 96 ) orangestuff8 [ 1 0 0 -16 ] [ 0 0 -1 0 ] 180 2 2
|
||||
( -112 -432 80 ) ( -112 -432 81 ) ( -112 -431 80 ) orangestuff8 [ 0 -1 0 16 ] [ 0 0 -1 0 ] 0 2 2
|
||||
}
|
||||
// brush 4
|
||||
{
|
||||
( 64 -112 96 ) ( 64 -111 96 ) ( 64 -112 97 ) orangestuff8 [ 0 1 0 -16 ] [ 0 0 -1 0 ] 0 2 2
|
||||
( -64 -96 80 ) ( -65 -96 80 ) ( -64 -96 81 ) orangestuff8 [ -1 0 0 16 ] [ 0 0 -1 0 ] 180 2 2
|
||||
( -64 -432 80 ) ( -64 -431 80 ) ( -65 -432 80 ) orangestuff8 [ 1 0 0 -16 ] [ 0 -1 0 16 ] 180 2 2
|
||||
( -144 -112 224 ) ( -145 -112 224 ) ( -144 -111 224 ) orangestuff8 [ -1 0 0 16 ] [ 0 -1 0 16 ] 180 2 2
|
||||
( -144 0 96 ) ( -144 0 97 ) ( -145 0 96 ) orangestuff8 [ 1 0 0 -16 ] [ 0 0 -1 0 ] 180 2 2
|
||||
( 80 -432 80 ) ( 80 -432 81 ) ( 80 -431 80 ) orangestuff8 [ 0 -1 0 16 ] [ 0 0 -1 0 ] 0 2 2
|
||||
}
|
||||
// brush 5
|
||||
{
|
||||
( -112 -112 240 ) ( -112 -111 240 ) ( -112 -112 241 ) orangestuff8 [ 0 1 0 -16 ] [ 0 0 -1 0 ] 0 2 2
|
||||
( -80 -96 224 ) ( -81 -96 224 ) ( -80 -96 225 ) orangestuff8 [ -1 0 0 16 ] [ 0 0 -1 0 ] 180 2 2
|
||||
( -80 -432 224 ) ( -80 -431 224 ) ( -81 -432 224 ) orangestuff8 [ 1 0 0 -16 ] [ 0 -1 0 16 ] 180 2 2
|
||||
( -160 -112 240 ) ( -161 -112 240 ) ( -160 -111 240 ) orangestuff8 [ -1 0 0 16 ] [ 0 -1 0 16 ] 180 2 2
|
||||
( -160 0 240 ) ( -160 0 241 ) ( -161 0 240 ) orangestuff8 [ 1 0 0 -16 ] [ 0 0 -1 0 ] 180 2 2
|
||||
( 64 -432 224 ) ( 64 -432 225 ) ( 64 -431 224 ) orangestuff8 [ 0 -1 0 16 ] [ 0 0 -1 0 ] 0 2 2
|
||||
}
|
||||
// brush 6
|
||||
{
|
||||
( -112 -112 108 ) ( -112 -111 108 ) ( -112 -112 109 ) *swater5 [ 0 1 0 -16 ] [ 0 0 -1 0 ] 0 2 2
|
||||
( -80 -96 92 ) ( -81 -96 92 ) ( -80 -96 93 ) *swater5 [ -1 0 0 16 ] [ 0 0 -1 0 ] 180 2 2
|
||||
( -80 -432 92 ) ( -80 -431 92 ) ( -81 -432 92 ) *swater5 [ 1 0 0 -16 ] [ 0 -1 0 16 ] 180 2 2
|
||||
( -160 -112 148 ) ( -161 -112 148 ) ( -160 -111 148 ) *swater5 [ -1 0 0 16 ] [ 0 -1 0 16 ] 180 2 2
|
||||
( -160 0 108 ) ( -160 0 109 ) ( -161 0 108 ) *swater5 [ 1 0 0 -16 ] [ 0 0 -1 0 ] 180 2 2
|
||||
( 64 -432 92 ) ( 64 -432 93 ) ( 64 -431 92 ) *swater5 [ 0 -1 0 16 ] [ 0 0 -1 0 ] 0 2 2
|
||||
}
|
||||
}
|
||||
// entity 1
|
||||
{
|
||||
"classname" "info_player_start"
|
||||
"origin" "-88 -64 120"
|
||||
}
|
||||
// entity 2
|
||||
{
|
||||
"classname" "func_detail_illusionary"
|
||||
"_mirrorinside" "1"
|
||||
// brush 0
|
||||
{
|
||||
( -40 -76 96 ) ( -40 -75 96 ) ( -40 -76 97 ) {trigger [ 0 -1 0 -16 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -8 -72 96 ) ( -8 -72 97 ) ( -7 -72 96 ) {trigger [ 1 0 0 16 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -8 -76 96 ) ( -7 -76 96 ) ( -8 -75 96 ) {trigger [ -1 0 0 -16 ] [ 0 -1 0 -16 ] 0 1 1
|
||||
( 56 -28 224 ) ( 56 -27 224 ) ( 57 -28 224 ) {trigger [ 1 0 0 16 ] [ 0 -1 0 -16 ] 0 1 1
|
||||
( 56 -36 112 ) ( 57 -36 112 ) ( 56 -36 113 ) {trigger [ -1 0 0 -16 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 24 -28 112 ) ( 24 -28 113 ) ( 24 -27 112 ) {trigger [ 0 1 0 16 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue