diff --git a/common/bspfile.cc b/common/bspfile.cc index 27131f9d..74ddaa37 100644 --- a/common/bspfile.cc +++ b/common/bspfile.cc @@ -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)) { diff --git a/include/common/bspfile.hh b/include/common/bspfile.hh index 37ba6e1e..17a9ca96 100644 --- a/include/common/bspfile.hh +++ b/include/common/bspfile.hh @@ -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 &get_hull_sizes() const = 0; diff --git a/include/qbsp/merge.hh b/include/qbsp/merge.hh index 410c06f7..c73d9acb 100644 --- a/include/qbsp/merge.hh +++ b/include/qbsp/merge.hh @@ -27,4 +27,5 @@ struct face_t; struct node_t; void MergeFaceToList(face_t *face, std::list &list); +std::list MergeFaceList(std::list input); void MergeAll(node_t *headnode); diff --git a/include/qbsp/portals.hh b/include/qbsp/portals.hh index ba35305e..a48c5a1a 100644 --- a/include/qbsp/portals.hh +++ b/include/qbsp/portals.hh @@ -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; + + 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); diff --git a/include/qbsp/qbsp.hh b/include/qbsp/qbsp.hh index 5c4e5ce5..eeb12dad 100644 --- a/include/qbsp/qbsp.hh +++ b/include/qbsp/qbsp.hh @@ -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 { diff --git a/include/qbsp/surfaces.hh b/include/qbsp/surfaces.hh index 4e4f2e62..0b0444ca 100644 --- a/include/qbsp/surfaces.hh +++ b/include/qbsp/surfaces.hh @@ -31,3 +31,4 @@ std::list 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); diff --git a/qbsp/csg4.cc b/qbsp/csg4.cc index 69cc4d39..6e09688a 100644 --- a/qbsp/csg4.cc +++ b/qbsp/csg4.cc @@ -127,106 +127,6 @@ std::tuple 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 *inside, std::list *outside) -{ - std::list oldinside; - - // clear `inside`, transfer it to `oldinside` - std::swap(*inside, oldinside); - - for (face_t *face : oldinside) { - std::optional 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 *inside, std::list *outside) -{ - std::list 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 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 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 &facelist) -{ - for (face_t *face : facelist) { - delete face; - } - facelist.clear(); -} - -//========================================================================== - -static std::vector> SingleBrush(std::unique_ptr a) -{ - std::vector> 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 CSGFace_ClipAgainstSingleBrush(std::list 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 inside {input}; - std::list 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; - -// 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 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 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> SubtractBrush(std::unique_ptr 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> frontlist; - std::vector> unclassified = SingleBrush(std::move(a)); - - for (const auto &side : b.faces) { - std::vector> 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> ChopBrushes(const std::vector>& 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>> 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(0), input.size(), [&](const size_t i) { - const auto& brush = input[i]; - - // the fragments `brush` is chopped into - std::vector> brush_result = SingleBrush( - // start with a copy of brush - std::make_unique(*brush) - ); - - for (auto &clipbrush : input) { - if (brush == clipbrush) { - continue; - } - if (brush->bounds.disjoint_or_touching(clipbrush->bounds)) { - continue; - } - - if (BrushGE(*clipbrush, *brush)) { - std::vector> 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> 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; -} diff --git a/qbsp/merge.cc b/qbsp/merge.cc index c045d37e..af1a8f4d 100644 --- a/qbsp/merge.cc +++ b/qbsp/merge.cc @@ -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 &list) MergeFaceList =============== */ -inline std::list MergeFaceList(std::list input) +std::list MergeFaceList(std::list input) { std::list result; diff --git a/qbsp/portals.cc b/qbsp/portals.cc index 83b0167e..db5e142e 100644 --- a/qbsp/portals.cc +++ b/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); +} diff --git a/qbsp/qbsp.cc b/qbsp/qbsp.cc index 4644a469..06486583 100644 --- a/qbsp/qbsp.cc +++ b/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); diff --git a/qbsp/surfaces.cc b/qbsp/surfaces.cc index b20db449..ee50b3fe 100644 --- a/qbsp/surfaces.cc +++ b/qbsp/surfaces.cc @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -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 SubdivideFace(face_t *f) return surfaces; } +static void SubdivideNodeFaces(node_t *node) +{ + std::list 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(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 ClipFacesToTree_r(node_t *node, const brush_t *srcbrush, std::list 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(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 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 ClipFacesToTree(node_t *node, const brush_t *srcbrush, std::list 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 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 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); } diff --git a/qbsp/test_qbsp.cc b/qbsp/test_qbsp.cc index ba0836df..07c0819d 100644 --- a/qbsp/test_qbsp.cc +++ b/qbsp/test_qbsp.cc @@ -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"); diff --git a/testmaps/qbsp_q1_cube.map b/testmaps/qbsp_q1_cube.map new file mode 100644 index 00000000..3aa2baab --- /dev/null +++ b/testmaps/qbsp_q1_cube.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" +} diff --git a/testmaps/qbsp_water_detail_illusionary_mirrorinside.map b/testmaps/qbsp_water_detail_illusionary_mirrorinside.map new file mode 100644 index 00000000..d2959c84 --- /dev/null +++ b/testmaps/qbsp_water_detail_illusionary_mirrorinside.map @@ -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 +} +}