From e85bda1ca0a89f1c44aa29a43773c77970025546 Mon Sep 17 00:00:00 2001 From: danij Date: Tue, 18 Jun 2013 18:13:00 +0100 Subject: [PATCH] Map: Cleaned up interface to the binary space partitioner As the BSP data elements are now produced using data and mechanisms provided by the map it is no longer logical to "black box" the whole build process by encapsulating behind a high-level BspBuilder class. The BspBuilder wrapper was dumped and Reporter relocated to World. Also reworked the interface to support building a BSP using only a subset of the map's lines in preparation for future work which will begin post 1.11 for Doomsday 1.12 Todo: Reconnect the Reporter to the map conversion process so that any issues or problems encountered are logged. --- doomsday/client/client.pro | 3 - doomsday/client/include/BspBuilder | 2 - .../client/include/world/bsp/partitioner.h | 45 +-- doomsday/client/include/world/bspbuilder.h | 129 ------- doomsday/client/include/world/line.h | 5 + doomsday/client/include/world/map.h | 14 + doomsday/client/src/world/bsp/partitioner.cpp | 261 ++++---------- doomsday/client/src/world/bspbuilder.cpp | 192 ---------- doomsday/client/src/world/map.cpp | 335 ++++++++++++++---- doomsday/client/src/world/world.cpp | 103 +++++- doomsday/server/server.pro | 3 - 11 files changed, 468 insertions(+), 624 deletions(-) delete mode 100644 doomsday/client/include/BspBuilder delete mode 100644 doomsday/client/include/world/bspbuilder.h delete mode 100644 doomsday/client/src/world/bspbuilder.cpp diff --git a/doomsday/client/client.pro b/doomsday/client/client.pro index d349115593..a2c1385340 100644 --- a/doomsday/client/client.pro +++ b/doomsday/client/client.pro @@ -109,7 +109,6 @@ DENG_API_HEADERS = \ # Convenience headers. DENG_HEADERS += \ - include/BspBuilder \ include/BspLeaf \ include/BspNode \ include/EntityDatabase \ @@ -364,7 +363,6 @@ DENG_HEADERS += \ include/world/bsp/partitioncost.h \ include/world/bsp/partitioner.h \ include/world/bsp/superblockmap.h \ - include/world/bspbuilder.h \ include/world/bspleaf.h \ include/world/bspnode.h \ include/world/dmuargs.h \ @@ -659,7 +657,6 @@ SOURCES += \ src/world/bsp/linesegment.cpp \ src/world/bsp/partitioner.cpp \ src/world/bsp/superblockmap.cpp \ - src/world/bspbuilder.cpp \ src/world/bspleaf.cpp \ src/world/bspnode.cpp \ src/world/dmuargs.cpp \ diff --git a/doomsday/client/include/BspBuilder b/doomsday/client/include/BspBuilder deleted file mode 100644 index 652ddc4744..0000000000 --- a/doomsday/client/include/BspBuilder +++ /dev/null @@ -1,2 +0,0 @@ -#include "world/bspbuilder.h" -#include "world/bsp/bsptreenode.h" diff --git a/doomsday/client/include/world/bsp/partitioner.h b/doomsday/client/include/world/bsp/partitioner.h index bc3b3e3ab3..d90e7c63c4 100644 --- a/doomsday/client/include/world/bsp/partitioner.h +++ b/doomsday/client/include/world/bsp/partitioner.h @@ -29,14 +29,11 @@ #include "world/bsp/bsptreenode.h" /// @todo remove me -class BspLeaf; class Line; class Sector; -class Segment; namespace de { -class Map; class Mesh; namespace bsp { @@ -51,7 +48,7 @@ static coord_t const DIST_EPSILON = 1.0 / 128.0; static coord_t const ANG_EPSILON = 1.0 / 1024.0; /** - * BSP space partitioner. + * World map binary space partitioner (BSP). * * Originally based on glBSP 2.24 (in turn, based on BSP 2.3). * @see http://sourceforge.net/projects/glbsp/ @@ -62,19 +59,20 @@ class Partitioner { public: /* - * Observers to be notified when an unclosed sector is first found. + * Observers to be notified when an unclosed sector is found. */ DENG2_DEFINE_AUDIENCE(UnclosedSectorFound, void unclosedSectorFound(Sector §or, Vector2d const &nearPoint)) - /* - * Observers to be notified when a one-way window construct is first found. - */ - DENG2_DEFINE_AUDIENCE(OneWayWindowFound, - void oneWayWindowFound(Line &line, Sector &backFacingSector)) + typedef QSet LineSet; public: - Partitioner(Map const &map, Mesh &mesh, int splitCostFactor = 7); + /** + * Construct a new binary space partitioner. + * + * @param splitCostFactor Cost factor attributed to splitting a half-edge. + */ + Partitioner(int splitCostFactor = 7); /** * Set the cost factor associated with splitting an existing half-edge. @@ -84,18 +82,21 @@ class Partitioner void setSplitCostFactor(int newFactor); /** - * Build the BSP for the given map. + * Build a new BSP for the given geometry. + * + * @param lines Set of lines to construct a BSP for. A copy of the set is + * made however the caller must ensure that line data remains + * accessible until the build process has completed (ownership + * is unaffected). + * + * @param mesh Mesh from which to assign new geometries. The caller must + * ensure that the mesh remains accessible until the build + * process has completed (ownership is unaffected). * - * High-level description (courtesy of Raphael Quinet): - * 1. Create one Seg for each Side: pick each Line in turn. If it - * has a "first" Side, then create a normal Seg. If it has a - * "second" Side, then create a flipped Seg. - * 2. Call CreateNodes with the current list of Segs. The list of Segs is - * the only argument to CreateNodes. - * 3. Save the Nodes, Segs and BspLeafs to disk. Start with the leaves of - * the Nodes tree and continue up to the root (last Node). + * @return Root tree node of the resultant BSP otherwise @c 0 if no usable + * tree data was produced. */ - void build(); + BspTreeNode *buildBsp(LineSet const &lines, Mesh &mesh); /** * Retrieve a pointer to the root BinaryTree node for the constructed BSP. @@ -146,7 +147,7 @@ class Partitioner * * @param mapElement Map data element to relinquish ownership of. */ - void release(MapElement *mapElement); + void take(MapElement *mapElement); private: DENG2_PRIVATE(d) diff --git a/doomsday/client/include/world/bspbuilder.h b/doomsday/client/include/world/bspbuilder.h deleted file mode 100644 index 637cafb5ad..0000000000 --- a/doomsday/client/include/world/bspbuilder.h +++ /dev/null @@ -1,129 +0,0 @@ -/** @file bspbuilder.h World map BSP builder. - * - * @authors Copyright © 2007-2013 Daniel Swanson - * @authors Copyright © 2000-2007 Andrew Apted - * @authors Copyright © 1998-2000 Colin Reed - * @authors Copyright © 1998-2000 Lee Killough - * - * @par License - * GPL: http://www.gnu.org/licenses/gpl.html - * - * 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., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA - */ - -#ifndef DENG_WORLD_MAP_BSPBUILDER -#define DENG_WORLD_MAP_BSPBUILDER - -#include "world/map.h" -#include "world/bsp/bsptreenode.h" - -namespace de { - -class Mesh; - -/** - * Map geometry partitioner and BSP tree builder. - * - * BspBuilder constructs a BSP object object tree for the specified map. - * - * @ingroup world - */ -class BspBuilder -{ -public: - /** - * Create and configure a new BSP builder, initializing in preparation - * for building a BSP for the specified @a map. - * - * @param map Map to construct a BSP for. The caller must ensure that - * the map remains accessible until the build process has - * completed (ownership is unaffected). - * - * @param mesh Mesh from which to assign new geometries. The caller must - * ensure that the mesh remains accessible until the build - * process has completed (ownership is unaffected). - * - * @param splitCostFactor Cost factor attributed to splitting a half-edge. - */ - BspBuilder(Map const &map, Mesh &mesh, int splitCostFactor = 7); - - /** - * Set the cost factor associated with splitting an existing half-edge. - * - * @param newFactor New split cost factor. - */ - void setSplitCostFactor(int newFactor); - - /** - * Build the BSP for the given map. - * @return @c true= iff completed successfully. - */ - bool buildBsp(); - - /** - * Retrieve a pointer to the root BinaryTree node for the constructed BSP. - * Even if construction fails this will return a valid node. - * - * The only time upon which @c NULL is returned is if called prior to calling - * BspBuilder::build() - */ - BspTreeNode *root() const; - - /** - * Retrieve the number of Segments owned by this Partitioner. When the build - * completes this number will be the total number of line segments that were - * produced during that process. Note that as BspLeaf ownership is claimed - * this number will decrease respectively. - * - * @return Current number of Segments owned by this Partitioner. - */ - int numSegments(); - - /** - * Retrieve the number of BspLeafs owned by this Partitioner. When the - * build completes this number will be the total number of BspLeafs that - * were produced during that process. Note that as BspLeaf ownership is - * claimed this number will decrease respectively. - * - * @return Current number of BspLeafs owned by this Partitioner. - */ - int numLeafs(); - - /** - * Retrieve the number of BspNodes owned by this Partitioner. When the - * build completes this number will be the total number of BspNodes that - * were produced during that process. Note that as BspNode ownership is - * claimed this number will decrease respectively. - * - * @return Current number of BspNodes owned by this Partitioner. - */ - int numNodes(); - - /** - * Retrieve the total number of Vertexes produced during the build process. - */ - int numVertexes(); - - /** - * Release ownership of the specified object. - * - * @param mapElement Map data object to relinquish ownership of. - */ - void take(MapElement *mapElement); - -private: - DENG2_PRIVATE(d) -}; - -} // namespace de - -#endif // DENG_WORLD_MAP_BSPBUILDER diff --git a/doomsday/client/include/world/line.h b/doomsday/client/include/world/line.h index 26b887fab3..cc36c7838d 100644 --- a/doomsday/client/include/world/line.h +++ b/doomsday/client/include/world/line.h @@ -611,6 +611,11 @@ class Line : public de::MapElement */ coord_t length() const; + /** + * Returns @c true iff the line has a length equivalent to zero. + */ + inline bool hasZeroLength() const { return de::abs(length()) < 1.0 / 128.0; } + /** * Returns the axis-aligned bounding box which encompases both vertex * origin points, in map coordinate space units. diff --git a/doomsday/client/include/world/map.h b/doomsday/client/include/world/map.h index e4773791ca..e138e1bd86 100644 --- a/doomsday/client/include/world/map.h +++ b/doomsday/client/include/world/map.h @@ -24,6 +24,8 @@ #include #include +#include + #include "Mesh" #include "p_particle.h" #include "Polyobj" @@ -105,6 +107,18 @@ class Map DENG2_ERROR(MissingLightGridError); #endif + /* + * Observers to be notified when a one-way window construct is first found. + */ + DENG2_DEFINE_AUDIENCE(OneWayWindowFound, + void oneWayWindowFound(Line &line, Sector &backFacingSector)) + + /* + * Observers to be notified when an unclosed sector is first found. + */ + DENG2_DEFINE_AUDIENCE(UnclosedSectorFound, + void unclosedSectorFound(Sector §or, Vector2d const &nearPoint)) + /* * Linked-element lists/sets: */ diff --git a/doomsday/client/src/world/bsp/partitioner.cpp b/doomsday/client/src/world/bsp/partitioner.cpp index dcf18cf277..2f407ad017 100644 --- a/doomsday/client/src/world/bsp/partitioner.cpp +++ b/doomsday/client/src/world/bsp/partitioner.cpp @@ -63,8 +63,8 @@ DENG2_PIMPL(Partitioner) /// Cost factor attributed to splitting a line segment. int splitCostFactor; - /// The map we are building BSP data for (not owned). - Map const *map; + /// The set of map lines we are building BSP data for (not owned). + LineSet lines; /// The mesh from which we'll assign (construct) new geometries(not owned). Mesh *mesh; @@ -95,16 +95,17 @@ DENG2_PIMPL(Partitioner) /// The "current" binary space half-plane. HPlane hplane; - Instance(Public *i, Map const &map, Mesh &mesh, int splitCostFactor) + Instance(Public *i, int splitCostFactor) : Base(i), splitCostFactor(splitCostFactor), - map(&map), - mesh(&mesh), + mesh(0), numNodes(0), numLeafs(0), numSegments(0), numVertexes(0), rootNode(0) {} - ~Instance() + ~Instance() { clear(); } + + void clear() { if(rootNode) { @@ -113,10 +114,18 @@ DENG2_PIMPL(Partitioner) clearAllBspElements(); // Destroy the internal binary tree. - delete rootNode; + delete rootNode; rootNode = 0; } + lines.clear(); + mesh = 0; qDeleteAll(lineSegments); + convexSubspaces.clear(); + edgeTipSets.clear(); + treeNodeMap.clear(); + hplane.clearIntercepts(); + + numNodes = numLeafs = numSegments = numVertexes = 0; } /** @@ -132,173 +141,6 @@ DENG2_PIMPL(Partitioner) return found.value(); } - struct testForWindowEffectParams - { - double frontDist, backDist; - Sector *frontOpen, *backOpen; - Line *frontLine, *backLine; - Line *testLine; - Vector2d testLineCenter; - bool castHorizontal; - - testForWindowEffectParams() - : frontDist(0), backDist(0), frontOpen(0), backOpen(0), - frontLine(0), backLine(0), testLine(0), castHorizontal(false) - {} - }; - - static void testForWindowEffect2(Line &line, testForWindowEffectParams &p) - { - if(&line == p.testLine) return; - if(line.isSelfReferencing()) return; - //if(line._buildData.overlap || line.length() <= 0) return; - - double dist = 0; - Sector *hitSector = 0; - bool isFront = false; - if(p.castHorizontal) - { - if(de::abs(line.direction().y) < DIST_EPSILON) - return; - - if((line.aaBox().maxY < p.testLineCenter.y - DIST_EPSILON) || - (line.aaBox().minY > p.testLineCenter.y + DIST_EPSILON)) - return; - - dist = (line.fromOrigin().x + - (p.testLineCenter.y - line.fromOrigin().y) * line.direction().x / line.direction().y) - - p.testLineCenter.x; - - isFront = ((p.testLine->direction().y > 0) != (dist > 0)); - dist = de::abs(dist); - - // Too close? (overlapping lines?) - if(dist < DIST_EPSILON) - return; - - bool dir = (p.testLine->direction().y > 0) ^ (line.direction().y > 0); - hitSector = line.side(dir ^ !isFront).sectorPtr(); - } - else // Cast vertically. - { - if(de::abs(line.direction().x) < DIST_EPSILON) - return; - - if((line.aaBox().maxX < p.testLineCenter.x - DIST_EPSILON) || - (line.aaBox().minX > p.testLineCenter.x + DIST_EPSILON)) - return; - - dist = (line.fromOrigin().y + - (p.testLineCenter.x - line.fromOrigin().x) * line.direction().y / line.direction().x) - - p.testLineCenter.y; - - isFront = ((p.testLine->direction().x > 0) == (dist > 0)); - dist = de::abs(dist); - - bool dir = (p.testLine->direction().x > 0) ^ (line.direction().x > 0); - hitSector = line.side(dir ^ !isFront).sectorPtr(); - } - - // Too close? (overlapping lines?) - if(dist < DIST_EPSILON) - return; - - if(isFront) - { - if(dist < p.frontDist) - { - p.frontDist = dist; - p.frontOpen = hitSector; - p.frontLine = &line; - } - } - else - { - if(dist < p.backDist) - { - p.backDist = dist; - p.backOpen = hitSector; - p.backLine = &line; - } - } - } - - static int testForWindowEffectWorker(Line *line, void *parms) - { - testForWindowEffect2(*line, *reinterpret_cast(parms)); - return false; // Continue iteration. - } - - bool lineMightHaveWindowEffect(Line const &line) - { - if(line.definesPolyobj()) return false; - if(line.hasFrontSector() && line.hasBackSector()) return false; - if(!line.hasFrontSector()) return false; - //if(line.hasZeroLength() || line._buildData.overlap) return false; - - // Look for window effects by checking for an odd number of one-sided - // line owners for a single vertex. Idea courtesy of Graham Jackson. - if((line.from()._onesOwnerCount % 2) == 1 && - (line.from()._onesOwnerCount + line.from()._twosOwnerCount) > 1) - return true; - - if((line.to()._onesOwnerCount % 2) == 1 && - (line.to()._onesOwnerCount + line.to()._twosOwnerCount) > 1) - return true; - - return false; - } - - void testForWindowEffect(Line &line) - { - if(!lineMightHaveWindowEffect(line)) return; - - testForWindowEffectParams p; - p.frontDist = p.backDist = DDMAXFLOAT; - p.testLine = &line; - p.testLineCenter = line.center(); - p.castHorizontal = (de::abs(line.direction().x) < de::abs(line.direction().y)? true : false); - - AABoxd scanRegion = map->bounds(); - if(p.castHorizontal) - { - scanRegion.minY = line.aaBox().minY - DIST_EPSILON; - scanRegion.maxY = line.aaBox().maxY + DIST_EPSILON; - } - else - { - scanRegion.minX = line.aaBox().minX - DIST_EPSILON; - scanRegion.maxX = line.aaBox().maxX + DIST_EPSILON; - } - validCount++; - map->linesBoxIterator(scanRegion, testForWindowEffectWorker, &p); - - if(p.backOpen && p.frontOpen && line.frontSectorPtr() == p.backOpen) - { - notifyOneWayWindowFound(line, *p.frontOpen); - - line._bspWindowSector = p.frontOpen; /// @todo Refactor away. - } - } - - void initForMap() - { - // Initialize vertex info for the initial set of vertexes. - edgeTipSets.reserve(map->vertexCount()); - foreach(Vertex *vertex, map->vertexes()) - { - // Count the total number of one and two-sided line owners for each - // vertex. (Used in the process of locating window effect lines.) - vertex->countLineOwners(); - } - - // Search for "one-way window" effects. - foreach(Line *line, map->lines()) - { - testForWindowEffect(*line); - } - } - inline void linkSegmentInSuperBlockmap(SuperBlock &block, LineSegment::Side &lineSeg) { // Associate this line segment with the subblock. @@ -310,11 +152,8 @@ DENG2_PIMPL(Partitioner) */ void createInitialLineSegments(SuperBlock &blockmap) { - foreach(Line *line, map->lines()) + foreach(Line *line, lines) { - // Polyobj lines are completely ignored. - if(line->definesPolyobj()) continue; - LineSegment *seg = 0; coord_t angle = 0; @@ -1545,20 +1384,6 @@ DENG2_PIMPL(Partitioner) return false; } - /** - * Notify interested parties of a "one-way window" in the map. - * - * @param line The window line. - * @param backFacingSector Sector that the back of the line is facing. - */ - void notifyOneWayWindowFound(Line &line, Sector &backFacingSector) - { - DENG2_FOR_PUBLIC_AUDIENCE(OneWayWindowFound, i) - { - i->oneWayWindowFound(line, backFacingSector); - } - } - /** * Notify interested parties of an unclosed sector in the map. * @@ -1588,11 +1413,8 @@ DENG2_PIMPL(Partitioner) #endif }; -Partitioner::Partitioner(Map const &map, Mesh &mesh, int splitCostFactor) - : d(new Instance(this, map, mesh, splitCostFactor)) -{ - d->initForMap(); -} +Partitioner::Partitioner(int splitCostFactor) : d(new Instance(this, splitCostFactor)) +{} void Partitioner::setSplitCostFactor(int newFactor) { @@ -1621,11 +1443,46 @@ static AABox blockmapBounds(AABoxd const &mapBounds) return blockBounds; } -void Partitioner::build() +/** + * Algorithm (description courtesy of Raphael Quinet): + * + * 1. Create one Seg for each Side: pick each Line in turn. If it + * has a "first" Side, then create a normal Seg. If it has a + * "second" Side, then create a flipped Seg. + * 2. Call CreateNodes with the current list of Segs. The list of Segs is + * the only argument to CreateNodes. + * 3. Save the Nodes, Segs and BspLeafs to disk. Start with the leaves of + * the Nodes tree and continue up to the root (last Node). + */ +BspTreeNode *Partitioner::buildBsp(LineSet const &lines, Mesh &mesh) { - d->rootNode = 0; + d->clear(); + + d->lines = lines; // make a copy. + d->mesh = &mesh; - SuperBlockmap rootBlock(blockmapBounds(d->map->bounds())); + // Initialize vertex info for the initial set of vertexes. + d->edgeTipSets.reserve(d->lines.count() * 2); + + // Determine the bounds of the line geometry. + AABoxd bounds; + bool isFirst = true; + foreach(Line *line, d->lines) + { + if(isFirst) + { + // The first line's bounds are used as is. + V2d_CopyBox(bounds.arvec2, line->aaBox().arvec2); + isFirst = false; + } + else + { + // Expand the bounding box. + V2d_UniteBox(bounds.arvec2, line->aaBox().arvec2); + } + } + + SuperBlockmap rootBlock(blockmapBounds(bounds)); d->createInitialLineSegments(rootBlock); @@ -1661,6 +1518,8 @@ void Partitioner::build() seg.mapSide().setRightSegment(right->segmentPtr()); } + + return d->rootNode; } BspTreeNode *Partitioner::root() const @@ -1688,7 +1547,7 @@ int Partitioner::numVertexes() return d->numVertexes; } -void Partitioner::release(MapElement *mapElement) +void Partitioner::take(MapElement *mapElement) { if(!d->release(mapElement)) { diff --git a/doomsday/client/src/world/bspbuilder.cpp b/doomsday/client/src/world/bspbuilder.cpp deleted file mode 100644 index d353cad2d6..0000000000 --- a/doomsday/client/src/world/bspbuilder.cpp +++ /dev/null @@ -1,192 +0,0 @@ -/** @file bspbuilder.cpp World map BSP builder. - * - * @authors Copyright © 2013 Daniel Swanson - * - * @par License - * GPL: http://www.gnu.org/licenses/gpl.html - * - * 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., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA - */ - -#include -#include - -#include -#include -#include -#include - -#include "BspLeaf" -#include "Segment" -#include "Sector" -#include "world/bsp/partitioner.h" - -#include "world/bspbuilder.h" - -using namespace de; -using namespace bsp; - -DENG2_PIMPL_NOREF(BspBuilder) -{ - /// World space partitioner. - Partitioner partitioner; - - Instance(Map const &map, Mesh &mesh) : partitioner(map, mesh) {} -}; - -BspBuilder::BspBuilder(Map const &map, Mesh &mesh, int splitCostFactor) - : d(new Instance(map, mesh)) -{ - d->partitioner.setSplitCostFactor(splitCostFactor); -} - -void BspBuilder::setSplitCostFactor(int newFactor) -{ - d->partitioner.setSplitCostFactor(newFactor); -} - -/// Maximum number of warnings to output (of each type) about any problems -/// encountered during the build process. -static int const maxWarningsPerType = 10; - -/** - * Observes the progress of the build and records any issues/problems encountered - * in the process. When asked, compiles a human-readable report intended to assist - * mod authors in debugging their maps. - * - * @todo Consolidate with the missing material reporting done elsewhere -ds - */ -class Reporter : DENG2_OBSERVES(Partitioner, UnclosedSectorFound), - DENG2_OBSERVES(Partitioner, OneWayWindowFound) -{ - /// Record "unclosed sectors". - /// Sector => world point relatively near to the problem area. - typedef std::map UnclosedSectorMap; - - /// Record "one-way window lines". - /// Line => Sector the back side faces. - typedef std::map OneWayWindowMap; - -public: - - static inline int maxWarnings(int issueCount) - { -#ifdef DENG_DEBUG - return issueCount; // No limit. -#else - return de::min(issueCount, maxWarningsPerType); -#endif - } - - inline int unclosedSectorCount() const { return (int)_unclosedSectors.size(); } - inline int oneWayWindowCount() const { return (int)_oneWayWindows.size(); } - - void writeLog() - { - if(int numToLog = maxWarnings(unclosedSectorCount())) - { - UnclosedSectorMap::const_iterator it = _unclosedSectors.begin(); - for(int i = 0; i < numToLog; ++i, ++it) - { - LOG_WARNING("Sector #%d is unclosed near %s.") - << it->first->indexInMap() << it->second.asText(); - } - - if(numToLog < unclosedSectorCount()) - LOG_INFO("(%d more like this)") << (unclosedSectorCount() - numToLog); - } - - if(int numToLog = maxWarnings(oneWayWindowCount())) - { - OneWayWindowMap::const_iterator it = _oneWayWindows.begin(); - for(int i = 0; i < numToLog; ++i, ++it) - { - LOG_VERBOSE("Line #%d seems to be a One-Way Window (back faces sector #%d).") - << it->first->indexInMap() << it->second->indexInMap(); - } - - if(numToLog < oneWayWindowCount()) - LOG_INFO("(%d more like this)") << (oneWayWindowCount() - numToLog); - } - } - -protected: - // Observes Partitioner UnclosedSectorFound. - void unclosedSectorFound(Sector §or, Vector2d const &nearPoint) - { - _unclosedSectors.insert(std::make_pair(§or, nearPoint)); - } - - // Observes Partitioner OneWayWindowFound. - void oneWayWindowFound(Line &line, Sector &backFacingSector) - { - _oneWayWindows.insert(std::make_pair(&line, &backFacingSector)); - } - -private: - UnclosedSectorMap _unclosedSectors; - OneWayWindowMap _oneWayWindows; -}; - -bool BspBuilder::buildBsp() -{ - Reporter reporter; - - d->partitioner.audienceForUnclosedSectorFound += reporter; - d->partitioner.audienceForOneWayWindowFound += reporter; - - bool builtOk = false; - try - { - d->partitioner.build(); - builtOk = true; - } - catch(Error const &er) - { - LOG_AS("BspBuilder"); - LOG_WARNING("%s.") << er.asText(); - } - - reporter.writeLog(); - - return builtOk; -} - -BspTreeNode *BspBuilder::root() const -{ - return d->partitioner.root(); -} - -int BspBuilder::numNodes() -{ - return d->partitioner.numNodes(); -} - -int BspBuilder::numLeafs() -{ - return d->partitioner.numLeafs(); -} - -int BspBuilder::numSegments() -{ - return d->partitioner.numSegments(); -} - -int BspBuilder::numVertexes() -{ - return d->partitioner.numVertexes(); -} - -void BspBuilder::take(MapElement *mapElement) -{ - d->partitioner.release(mapElement); -} diff --git a/doomsday/client/src/world/map.cpp b/doomsday/client/src/world/map.cpp index d9937b1f6b..1ad5c5d564 100644 --- a/doomsday/client/src/world/map.cpp +++ b/doomsday/client/src/world/map.cpp @@ -44,7 +44,7 @@ #include "Segment" #include "Vertex" -#include "BspBuilder" +#include "world/bsp/partitioner.h" #include "world/blockmap.h" #include "world/entitydatabase.h" @@ -103,7 +103,8 @@ struct EditableElements void clearAll(); }; -DENG2_PIMPL(Map) +DENG2_PIMPL(Map), +DENG2_OBSERVES(bsp::Partitioner, UnclosedSectorFound) { /// @c true= editing is currently enabled. bool editingEnabled; @@ -238,9 +239,236 @@ DENG2_PIMPL(Map) } } + // Observes bsp::Partitioner UnclosedSectorFound. + void unclosedSectorFound(Sector §or, Vector2d const &nearPoint) + { + // Notify interested parties that an unclosed sector was found. + DENG2_FOR_PUBLIC_AUDIENCE(UnclosedSectorFound, i) + { + i->unclosedSectorFound(sector, nearPoint); + } + } + + /** + * Notify interested parties of a "one-way window" in the map. + * + * @param line The window line. + * @param backFacingSector Sector that the back of the line is facing. + */ + void notifyOneWayWindowFound(Line &line, Sector &backFacingSector) + { + DENG2_FOR_PUBLIC_AUDIENCE(OneWayWindowFound, i) + { + i->oneWayWindowFound(line, backFacingSector); + } + } + + struct testForWindowEffectParams + { + double frontDist, backDist; + Sector *frontOpen, *backOpen; + Line *frontLine, *backLine; + Line *testLine; + Vector2d testLineCenter; + bool castHorizontal; + + testForWindowEffectParams() + : frontDist(0), backDist(0), frontOpen(0), backOpen(0), + frontLine(0), backLine(0), testLine(0), castHorizontal(false) + {} + }; + + static void testForWindowEffect2(Line &line, testForWindowEffectParams &p) + { + if(&line == p.testLine) return; + if(line.isSelfReferencing()) return; + if(line.hasZeroLength()) return; + + double dist = 0; + Sector *hitSector = 0; + bool isFront = false; + if(p.castHorizontal) + { + if(de::abs(line.direction().y) < bsp::DIST_EPSILON) + return; + + if((line.aaBox().maxY < p.testLineCenter.y - bsp::DIST_EPSILON) || + (line.aaBox().minY > p.testLineCenter.y + bsp::DIST_EPSILON)) + return; + + dist = (line.fromOrigin().x + + (p.testLineCenter.y - line.fromOrigin().y) * line.direction().x / line.direction().y) + - p.testLineCenter.x; + + isFront = ((p.testLine->direction().y > 0) != (dist > 0)); + dist = de::abs(dist); + + // Too close? (overlapping lines?) + if(dist < bsp::DIST_EPSILON) + return; + + bool dir = (p.testLine->direction().y > 0) ^ (line.direction().y > 0); + hitSector = line.side(dir ^ !isFront).sectorPtr(); + } + else // Cast vertically. + { + if(de::abs(line.direction().x) < bsp::DIST_EPSILON) + return; + + if((line.aaBox().maxX < p.testLineCenter.x - bsp::DIST_EPSILON) || + (line.aaBox().minX > p.testLineCenter.x + bsp::DIST_EPSILON)) + return; + + dist = (line.fromOrigin().y + + (p.testLineCenter.x - line.fromOrigin().x) * line.direction().y / line.direction().x) + - p.testLineCenter.y; + + isFront = ((p.testLine->direction().x > 0) == (dist > 0)); + dist = de::abs(dist); + + bool dir = (p.testLine->direction().x > 0) ^ (line.direction().x > 0); + hitSector = line.side(dir ^ !isFront).sectorPtr(); + } + + // Too close? (overlapping lines?) + if(dist < bsp::DIST_EPSILON) + return; + + if(isFront) + { + if(dist < p.frontDist) + { + p.frontDist = dist; + p.frontOpen = hitSector; + p.frontLine = &line; + } + } + else + { + if(dist < p.backDist) + { + p.backDist = dist; + p.backOpen = hitSector; + p.backLine = &line; + } + } + } + + static int testForWindowEffectWorker(Line *line, void *parms) + { + testForWindowEffect2(*line, *reinterpret_cast(parms)); + return false; // Continue iteration. + } + + bool lineMightHaveWindowEffect(Line const &line) + { + if(line.definesPolyobj()) return false; + if(line.hasFrontSector() && line.hasBackSector()) return false; + if(!line.hasFrontSector()) return false; + if(line.hasZeroLength()) return false; + + // Look for window effects by checking for an odd number of one-sided + // line owners for a single vertex. Idea courtesy of Graham Jackson. + if((line.from()._onesOwnerCount % 2) == 1 && + (line.from()._onesOwnerCount + line.from()._twosOwnerCount) > 1) + return true; + + if((line.to()._onesOwnerCount % 2) == 1 && + (line.to()._onesOwnerCount + line.to()._twosOwnerCount) > 1) + return true; + + return false; + } + + void findOneWayWindows() + { + foreach(Vertex *vertex, mesh.vertexes()) + { + // Count the total number of one and two-sided line owners for each + // vertex. (Used in the process of locating window effect lines.) + vertex->countLineOwners(); + } + + // Search for "one-way window" effects. + foreach(Line *line, lines) + { + if(!lineMightHaveWindowEffect(*line)) + continue; + + testForWindowEffectParams p; + p.frontDist = p.backDist = DDMAXFLOAT; + p.testLine = line; + p.testLineCenter = line->center(); + p.castHorizontal = (de::abs(line->direction().x) < de::abs(line->direction().y)? true : false); + + AABoxd scanRegion = bounds; + if(p.castHorizontal) + { + scanRegion.minY = line->aaBox().minY - bsp::DIST_EPSILON; + scanRegion.maxY = line->aaBox().maxY + bsp::DIST_EPSILON; + } + else + { + scanRegion.minX = line->aaBox().minX - bsp::DIST_EPSILON; + scanRegion.maxX = line->aaBox().maxX + bsp::DIST_EPSILON; + } + validCount++; + self.linesBoxIterator(scanRegion, testForWindowEffectWorker, &p); + + if(p.backOpen && p.frontOpen && line->frontSectorPtr() == p.backOpen) + { + notifyOneWayWindowFound(*line, *p.frontOpen); + + line->_bspWindowSector = p.frontOpen; /// @todo Refactor away. + } + } + } + + void collateBspElements(bsp::Partitioner &partitioner, BspTreeNode &tree) + { + if(tree.isLeaf()) + { + // Take ownership of the BspLeaf. + DENG2_ASSERT(tree.userData() != 0); + BspLeaf *leaf = tree.userData()->as(); + partitioner.take(leaf); + + // Add this BspLeaf to the LUT. + leaf->setIndexInMap(bspLeafs.count()); + bspLeafs.append(leaf); + + foreach(Segment *seg, leaf->allSegments()) + { + // Take ownership of the Segment. + partitioner.take(seg); + + // Add this segment to the LUT. + seg->setIndexInMap(segments.count()); + segments.append(seg); + } + + return; + } + // Else; a node. + + // Take ownership of this BspNode. + DENG2_ASSERT(tree.userData() != 0); + BspNode *node = tree.userData()->as(); + partitioner.take(node); + + // Add this BspNode to the LUT. + node->setIndexInMap(bspNodes.count()); + bspNodes.append(node); + } + + /** + * Build a BSP tree for the map. + * + * @pre Map line bounds have been determined and a line blockmap constructed. + */ bool buildBsp() { - /// @todo Test @em ALL preconditions! + DENG2_ASSERT(bspRoot == 0); DENG2_ASSERT(segments.isEmpty()); DENG2_ASSERT(bspLeafs.isEmpty()); DENG2_ASSERT(bspNodes.isEmpty()); @@ -252,34 +480,39 @@ DENG2_PIMPL(Map) LOG_TRACE("Building BSP for \"%s\" with split cost factor %d...") << uri << bspSplitFactor; + // First we'll scan for so-called "one-way window" constructs and mark + // them so that the space partitioner can treat them specially. + findOneWayWindows(); + + // Remember the current next vertex ordinal as we'll need to index any + // new vertexes produced during the build process. int nextVertexOrd = mesh.vertexCount(); - // Instantiate and configure a new BSP builder. - BspBuilder nodeBuilder(self, mesh, bspSplitFactor); + // Determine the set of lines for which we will build a BSP. + QSet linesToBuildBspFor = QSet::fromList(lines); - // Build the BSP. - bool builtOK = nodeBuilder.buildBsp(); - if(builtOK) + // Polyobj lines should be excluded. + foreach(Polyobj *po, polyobjs) + foreach(Line *line, po->lines()) { - BspTreeNode &treeRoot = *nodeBuilder.root(); + linesToBuildBspFor.remove(line); + } - // Determine the max depth of the two main branches. - dint32 rightBranchDpeth, leftBranchDepth; - if(!treeRoot.isLeaf()) - { - rightBranchDpeth = dint32( treeRoot.right().height() ); - leftBranchDepth = dint32( treeRoot.left().height() ); - } - else - { - rightBranchDpeth = leftBranchDepth = 0; - } + try + { + // Configure a space partitioner. + bsp::Partitioner partitioner(bspSplitFactor); + partitioner.audienceForUnclosedSectorFound += this; + + // Build a new BSP! + BspTreeNode *rootNode = partitioner.buildBsp(linesToBuildBspFor, mesh); LOG_INFO("Built %d Nodes, %d Leafs, %d Segments and %d Vertexes." "\nTree balance is %d:%d.") - << nodeBuilder.numNodes() << nodeBuilder.numLeafs() - << nodeBuilder.numSegments() << nodeBuilder.numVertexes() - << rightBranchDpeth << leftBranchDepth; + << partitioner.numNodes() << partitioner.numLeafs() + << partitioner.numSegments() << partitioner.numVertexes() + << (rootNode->isLeaf()? 0 : rootNode->right().height()) + << (rootNode->isLeaf()? 0 : rootNode->left().height()); // Attribute an index to any new vertexes. for(int i = nextVertexOrd; i < mesh.vertexCount(); ++i) @@ -291,15 +524,14 @@ DENG2_PIMPL(Map) /* * Take ownership of all the built map data elements. */ + bspRoot = rootNode->userData(); // We'll formally take ownership shortly... + #ifdef DENG2_QT_4_7_OR_NEWER - segments.reserve(nodeBuilder.numSegments()); - bspNodes.reserve(nodeBuilder.numNodes()); - bspLeafs.reserve(nodeBuilder.numLeafs()); + segments.reserve(partitioner.numSegments()); + bspNodes.reserve(partitioner.numNodes()); + bspLeafs.reserve(partitioner.numLeafs()); #endif - BspTreeNode *rootNode = nodeBuilder.root(); - bspRoot = rootNode->userData(); // We'll formally take ownership shortly... - // Iterative pre-order traversal of the BspBuilder's map element tree. BspTreeNode *cur = rootNode; BspTreeNode *prev = 0; @@ -311,7 +543,7 @@ DENG2_PIMPL(Map) { // Acquire ownership of and collate all map data elements at // this node of the tree. - collateBspElements(nodeBuilder, *cur); + collateBspElements(partitioner, *cur); } if(prev == cur->parentPtr()) @@ -342,48 +574,15 @@ DENG2_PIMPL(Map) } } } - - // How much time did we spend? - LOG_INFO(String("Completed in %1 seconds.").arg(begunAt.since(), 0, 'g', 2)); - - return builtOK; - } - - void collateBspElements(BspBuilder &builder, BspTreeNode &tree) - { - if(tree.isLeaf()) + catch(Error const &er) { - // Take ownership of the BspLeaf. - DENG2_ASSERT(tree.userData() != 0); - BspLeaf *leaf = tree.userData()->as(); - builder.take(leaf); - - // Add this BspLeaf to the LUT. - leaf->setIndexInMap(bspLeafs.count()); - bspLeafs.append(leaf); - - foreach(Segment *seg, leaf->allSegments()) - { - // Take ownership of the Segment. - builder.take(seg); - - // Add this segment to the LUT. - seg->setIndexInMap(segments.count()); - segments.append(seg); - } - - return; + LOG_WARNING("%s.") << er.asText(); } - // Else; a node. - // Take ownership of this BspNode. - DENG2_ASSERT(tree.userData() != 0); - BspNode *node = tree.userData()->as(); - builder.take(node); + // How much time did we spend? + LOG_INFO(String("Completed in %1 seconds.").arg(begunAt.since(), 0, 'g', 2)); - // Add this BspNode to the LUT. - node->setIndexInMap(bspNodes.count()); - bspNodes.append(node); + return bspRoot != 0; } void finishLines() diff --git a/doomsday/client/src/world/world.cpp b/doomsday/client/src/world/world.cpp index 43cd9f603b..d704261b1b 100644 --- a/doomsday/client/src/world/world.cpp +++ b/doomsday/client/src/world/world.cpp @@ -18,6 +18,9 @@ * 02110-1301 USA */ +#include +#include + #include #include @@ -69,6 +72,91 @@ using namespace de; +/** + * Observes the progress of a map conversion and records any issues/problems that + * are encountered in the process. When asked, compiles a human-readable report + * intended to assist mod authors in debugging their maps. + * + * @todo Consolidate with the missing material reporting done elsewhere -ds + */ +class MapConversionReporter : +DENG2_OBSERVES(Map, UnclosedSectorFound), +DENG2_OBSERVES(Map, OneWayWindowFound) +{ + /// Record "unclosed sectors". + /// Sector => world point relatively near to the problem area. + typedef std::map UnclosedSectorMap; + + /// Record "one-way window lines". + /// Line => Sector the back side faces. + typedef std::map OneWayWindowMap; + + /// Maximum number of warnings to output (of each type) about any problems + /// encountered during the build process. + static int const maxWarningsPerType = 10; + +public: + MapConversionReporter() {} + + inline int unclosedSectorCount() const { return (int)_unclosedSectors.size(); } + inline int oneWayWindowCount() const { return (int)_oneWayWindows.size(); } + + void writeLog() + { + if(int numToLog = maxWarnings(unclosedSectorCount())) + { + UnclosedSectorMap::const_iterator it = _unclosedSectors.begin(); + for(int i = 0; i < numToLog; ++i, ++it) + { + LOG_WARNING("Sector #%d is unclosed near %s.") + << it->first->indexInMap() << it->second.asText(); + } + + if(numToLog < unclosedSectorCount()) + LOG_INFO("(%d more like this)") << (unclosedSectorCount() - numToLog); + } + + if(int numToLog = maxWarnings(oneWayWindowCount())) + { + OneWayWindowMap::const_iterator it = _oneWayWindows.begin(); + for(int i = 0; i < numToLog; ++i, ++it) + { + LOG_VERBOSE("Line #%d seems to be a One-Way Window (back faces sector #%d).") + << it->first->indexInMap() << it->second->indexInMap(); + } + + if(numToLog < oneWayWindowCount()) + LOG_INFO("(%d more like this)") << (oneWayWindowCount() - numToLog); + } + } + +protected: + // Observes Partitioner UnclosedSectorFound. + void unclosedSectorFound(Sector §or, Vector2d const &nearPoint) + { + _unclosedSectors.insert(std::make_pair(§or, nearPoint)); + } + + // Observes Partitioner OneWayWindowFound. + void oneWayWindowFound(Line &line, Sector &backFacingSector) + { + _oneWayWindows.insert(std::make_pair(&line, &backFacingSector)); + } + +private: + static inline int maxWarnings(int issueCount) + { +#ifdef DENG_DEBUG + return issueCount; // No limit. +#else + return de::min(issueCount, maxWarningsPerType); +#endif + } + + UnclosedSectorMap _unclosedSectors; + OneWayWindowMap _oneWayWindows; +}; + boolean ddMapSetup; timespan_t ddMapTime; @@ -227,6 +315,11 @@ DENG2_PIMPL(World) if(markerLumpNum < 0) return 0; + /*MapConversionReporter reporter; + map->audienceForOneWayWindowFound += reporter; + map->audienceForUnclosedSectorFound += reporter; + */ + // Ask each converter in turn whether the map format is recognizable // and if so to interpret and transfer it to us via the runtime map // editing interface. @@ -239,14 +332,16 @@ DENG2_PIMPL(World) if(!MPE_GetLastBuiltMapResult()) return 0; + //reporter.writeLog(); + // Take ownership of the map. - Map *map = MPE_TakeMap(); + Map *newMap = MPE_TakeMap(); // Generate the old unique map id. File1 &markerLump = App_FileSystem().nameIndex().lump(markerLumpNum); String uniqueId = composeUniqueMapId(markerLump); QByteArray uniqueIdUtf8 = uniqueId.toUtf8(); - map->setOldUniqueId(uniqueIdUtf8.constData()); + newMap->setOldUniqueId(uniqueIdUtf8.constData()); // Are we caching this map? /*if(mapCache) @@ -255,10 +350,10 @@ DENG2_PIMPL(World) F_MakePath(rec.cachePath.toUtf8().constData()); // Cache the map! - DAM_MapWrite(map, rec.cachePath); + DAM_MapWrite(newMap, rec.cachePath); }*/ - return map; + return newMap; } #if 0 diff --git a/doomsday/server/server.pro b/doomsday/server/server.pro index 176b22b201..d13f8e4505 100644 --- a/doomsday/server/server.pro +++ b/doomsday/server/server.pro @@ -111,7 +111,6 @@ DENG_API_HEADERS = \ # Convenience headers. DENG_HEADERS += \ - $$SRC/include/BspBuilder \ $$SRC/include/EntityDatabase \ $$SRC/include/Game \ $$SRC/include/Games \ @@ -240,7 +239,6 @@ DENG_HEADERS += \ $$SRC/include/world/bsp/partitioncost.h \ $$SRC/include/world/bsp/partitioner.h \ $$SRC/include/world/bsp/superblockmap.h \ - $$SRC/include/world/bspbuilder.h \ $$SRC/include/world/bspleaf.h \ $$SRC/include/world/bspnode.h \ $$SRC/include/world/entitydatabase.h \ @@ -411,7 +409,6 @@ SOURCES += \ $$SRC/src/world/bsp/linesegment.cpp \ $$SRC/src/world/bsp/partitioner.cpp \ $$SRC/src/world/bsp/superblockmap.cpp \ - $$SRC/src/world/bspbuilder.cpp \ $$SRC/src/world/bspleaf.cpp \ $$SRC/src/world/bspnode.cpp \ $$SRC/src/world/dmuargs.cpp \