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 \