From ce8c4ad4f55eff2ffb5b5364b885c5adad928769 Mon Sep 17 00:00:00 2001 From: scrawl Date: Wed, 8 Mar 2017 01:53:13 +0100 Subject: [PATCH] Add quad tree implementation (no rendering yet) --- components/CMakeLists.txt | 2 +- components/terrain/quadtreenode.cpp | 169 ++++++++++++++++++++ components/terrain/quadtreenode.hpp | 66 ++++++++ components/terrain/quadtreeworld.cpp | 227 +++++++++++++++++++++++++++ components/terrain/quadtreeworld.hpp | 47 ++++++ components/terrain/storage.hpp | 1 + components/terrain/terraingrid.hpp | 7 +- components/terrain/world.cpp | 5 + components/terrain/world.hpp | 14 +- 9 files changed, 529 insertions(+), 9 deletions(-) create mode 100644 components/terrain/quadtreenode.cpp create mode 100644 components/terrain/quadtreenode.hpp create mode 100644 components/terrain/quadtreeworld.cpp create mode 100644 components/terrain/quadtreeworld.hpp diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 3327caa8bc..8b019e470f 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -114,7 +114,7 @@ add_component_dir (translation ) add_component_dir (terrain - storage world buffercache defs terraingrid material terraindrawable texturemanager chunkmanager compositemaprenderer + storage world buffercache defs terraingrid material terraindrawable texturemanager chunkmanager compositemaprenderer quadtreeworld quadtreenode ) add_component_dir (loadinglistener diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp new file mode 100644 index 0000000000..518274df57 --- /dev/null +++ b/components/terrain/quadtreenode.cpp @@ -0,0 +1,169 @@ +#include "quadtreenode.hpp" + +#include + +#include + +#include "defs.hpp" + +namespace +{ + + float distance(const osg::BoundingBox& box, const osg::Vec3f& v) + { + if (box.contains(v)) + return 0; + else + { + osg::Vec3f maxDist(0,0,0); + + if (v.x() < box.xMin()) + maxDist.x() = box.xMin() - v.x(); + else if (v.x() > box.xMax()) + maxDist.x() = v.x() - box.xMax(); + + if (v.y() < box.yMin()) + maxDist.y() = box.yMin() - v.y(); + else if (v.y() > box.yMax()) + maxDist.y() = v.y() - box.yMax(); + + if (v.z() < box.zMin()) + maxDist.z() = box.zMin() - v.z(); + else if (v.z() > box.zMax()) + maxDist.z() = v.z() - box.zMax(); + + return maxDist.length(); + } + } + + int Log2( int n ) + { + assert(n > 0); + int targetlevel = 0; + while (n >>= 1) ++targetlevel; + return targetlevel; + } + +} + +namespace Terrain +{ + +ChildDirection reflect(ChildDirection dir, Direction dir2) +{ + assert(dir != Root); + const int lookupTable[4][4] = + { + // NW NE SW SE + { SW, SE, NW, NE }, // N + { NE, NW, SE, SW }, // E + { SW, SE, NW, NE }, // S + { NE, NW, SE, SW } // W + }; + return (ChildDirection)lookupTable[dir2][dir]; +} + +bool adjacent(ChildDirection dir, Direction dir2) +{ + assert(dir != Root); + const bool lookupTable[4][4] = + { + // NW NE SW SE + { true, true, false, false }, // N + { false, true, false, true }, // E + { false, false, true, true }, // S + { true, false, true, false } // W + }; + return lookupTable[dir2][dir]; +} + +QuadTreeNode* searchNeighbour (QuadTreeNode* currentNode, Direction dir) +{ + if (currentNode->getDirection() == Root) + return NULL; // Arrived at root node, the root node does not have neighbours + + QuadTreeNode* nextNode; + if (adjacent(currentNode->getDirection(), dir)) + nextNode = searchNeighbour(currentNode->getParent(), dir); + else + nextNode = currentNode->getParent(); + + if (nextNode && nextNode->getNumChildren()) + return nextNode->getChild(reflect(currentNode->getDirection(), dir)); + else + return NULL; +} + +QuadTreeNode::QuadTreeNode(QuadTreeNode* parent, ChildDirection direction, float size, const osg::Vec2f& center) + : mParent(parent) + , mDirection(direction) + , mSize(size) + , mCenter(center) +{ + for (unsigned int i=0; i<4; ++i) + mNeighbours[i] = 0; +} + +QuadTreeNode* QuadTreeNode::getParent() +{ + return mParent; +} + +QuadTreeNode *QuadTreeNode::getChild(unsigned int i) +{ + return static_cast(Group::getChild(i)); +} + +void QuadTreeNode::initNeighbours() +{ + for (int i=0; i<4; ++i) + mNeighbours[i] = searchNeighbour(this, (Direction)i); + + for (unsigned int i=0; iinitNeighbours(); +} + +void QuadTreeNode::traverse(osg::NodeVisitor &nv) +{ + if (nv.getVisitorType() != osg::NodeVisitor::CULL_VISITOR) + return; + + osgUtil::CullVisitor* cv = static_cast(&nv); + + // do another culling test against bounding box as its much more accurate than the bounding sphere. + if (cv->isCulled(mBoundingBox)) + return; + + //float dist = distance(getBoundingBox(), nv.getEyePoint()); + + osg::Group::traverse(nv); +} + +void QuadTreeNode::setBoundingBox(const osg::BoundingBox &boundingBox) +{ + mBoundingBox = boundingBox; + dirtyBound(); + getBound(); +} + +const osg::BoundingBox &QuadTreeNode::getBoundingBox() const +{ + return mBoundingBox; +} + +osg::BoundingSphere QuadTreeNode::computeBound() const +{ + return osg::BoundingSphere(mBoundingBox); +} + +float QuadTreeNode::getSize() const +{ + return mSize; +} + +const osg::Vec2f &QuadTreeNode::getCenter() const +{ + return mCenter; +} + +} diff --git a/components/terrain/quadtreenode.hpp b/components/terrain/quadtreenode.hpp new file mode 100644 index 0000000000..aab54c69c8 --- /dev/null +++ b/components/terrain/quadtreenode.hpp @@ -0,0 +1,66 @@ +#ifndef OPENMW_COMPONENTS_TERRAIN_QUADTREENODE_H +#define OPENMW_COMPONENTS_TERRAIN_QUADTREENODE_H + +#include + +#include "defs.hpp" + +namespace Terrain +{ + + enum ChildDirection + { + NW = 0, + NE = 1, + SW = 2, + SE = 3, + Root + }; + + class QuadTreeNode : public osg::Group + { + public: + QuadTreeNode(QuadTreeNode* parent, ChildDirection dir, float size, const osg::Vec2f& center); + + QuadTreeNode* getParent(); + + QuadTreeNode* getChild(unsigned int i); + using osg::Group::getNumChildren; + + /// Returns our direction relative to the parent node, or Root if we are the root node. + ChildDirection getDirection() { return mDirection; } + + /// Get neighbour node in this direction + QuadTreeNode* getNeighbour (Direction dir); + + /// Initialize neighbours - do this after the quadtree is built + void initNeighbours(); + + void setBoundingBox(const osg::BoundingBox& boundingBox); + const osg::BoundingBox& getBoundingBox() const; + + virtual osg::BoundingSphere computeBound() const; + + /// size in cell coordinates + float getSize() const; + + /// center in cell coordinates + const osg::Vec2f& getCenter() const; + + virtual void traverse(osg::NodeVisitor& nv); + + private: + QuadTreeNode* mParent; + + QuadTreeNode* mNeighbours[4]; + + ChildDirection mDirection; + + osg::BoundingBox mBoundingBox; + float mSize; + osg::Vec2f mCenter; + }; + +} + +#endif diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp new file mode 100644 index 0000000000..25aecd2a79 --- /dev/null +++ b/components/terrain/quadtreeworld.cpp @@ -0,0 +1,227 @@ +#include "quadtreeworld.hpp" + +#include + +#include "quadtreenode.hpp" +#include "storage.hpp" + + +#include +#include + +namespace +{ + + bool isPowerOfTwo(int x) + { + return ( (x > 0) && ((x & (x - 1)) == 0) ); + } + + int nextPowerOfTwo (int v) + { + if (isPowerOfTwo(v)) return v; + int depth=0; + while(v) + { + v >>= 1; + depth++; + } + return 1 << depth; + } + +} + +namespace Terrain +{ + + +class RootNode : public QuadTreeNode +{ +public: + RootNode(float size, const osg::Vec2f& center) + : QuadTreeNode(NULL, Root, size, center) + , mWorld(NULL) + { + } + + void setWorld(QuadTreeWorld* world) + { + mWorld = world; + } + + virtual void accept(osg::NodeVisitor &nv) + { + nv.pushOntoNodePath(this); + mWorld->accept(nv); + nv.popFromNodePath(); + } + +private: + QuadTreeWorld* mWorld; +}; + +class BuildQuadTreeItem : public SceneUtil::WorkItem +{ +public: + BuildQuadTreeItem(Terrain::Storage* storage, float minSize) + : mStorage(storage) + , mMinX(0.f), mMaxX(0.f), mMinY(0.f), mMaxY(0.f) + , mMinSize(minSize) + { + } + + virtual void doWork() + { + mStorage->getBounds(mMinX, mMaxX, mMinY, mMaxY); + + int origSizeX = static_cast(mMaxX - mMinX); + int origSizeY = static_cast(mMaxY - mMinY); + + // Dividing a quad tree only works well for powers of two, so round up to the nearest one + int size = nextPowerOfTwo(std::max(origSizeX, origSizeY)); + + float centerX = (mMinX+mMaxX)/2.f + (size-origSizeX)/2.f; + float centerY = (mMinY+mMaxY)/2.f + (size-origSizeY)/2.f; + + mRootNode = new RootNode(size, osg::Vec2f(centerX, centerY)); + addChildren(mRootNode); + + mRootNode->initNeighbours(); + } + + void addChildren(QuadTreeNode* parent) + { + float halfSize = parent->getSize()/2.f; + osg::BoundingBox boundingBox; + for (unsigned int i=0; i<4; ++i) + { + QuadTreeNode* child = addChild(parent, static_cast(i), halfSize); + if (child) + boundingBox.expandBy(child->getBoundingBox()); + } + + parent->setBoundingBox(boundingBox); + } + + QuadTreeNode* addChild(QuadTreeNode* parent, ChildDirection direction, float size) + { + osg::Vec2f center; + switch (direction) + { + case SW: + center = parent->getCenter() + osg::Vec2f(-size/2.f,-size/2.f); + break; + case SE: + center = parent->getCenter() + osg::Vec2f(size/2.f, -size/2.f); + break; + case NW: + center = parent->getCenter() + osg::Vec2f(-size/2.f, size/2.f); + break; + case NE: + center = parent->getCenter() + osg::Vec2f(size/2.f, size/2.f); + break; + default: + break; + } + + osg::ref_ptr node = new QuadTreeNode(parent, direction, size, center); + parent->addChild(node); + + if (center.x() - size > mMaxX + || center.x() + size < mMinX + || center.y() - size > mMaxY + || center.y() + size < mMinY ) + // Out of bounds of the actual terrain - this will happen because + // we rounded the size up to the next power of two + { + // Still create and return an empty node so as to not break the assumption that each QuadTreeNode has either 4 or 0 children. + return node; + } + + if (node->getSize() <= mMinSize) + { + // We arrived at a leaf + float minZ,maxZ; + if (mStorage->getMinMaxHeights(size, center, minZ, maxZ)) + { + float cellWorldSize = mStorage->getCellWorldSize(); + osg::BoundingBox boundingBox(osg::Vec3f((center.x()-size)*cellWorldSize, (center.y()-size)*cellWorldSize, minZ), + osg::Vec3f((center.x()+size)*cellWorldSize, (center.y()+size)*cellWorldSize, maxZ)); + node->setBoundingBox(boundingBox); + } + return node; + } + else + { + addChildren(node); + return node; + } + } + + osg::ref_ptr getRootNode() + { + return mRootNode; + } + +private: + Terrain::Storage* mStorage; + + float mMinX, mMaxX, mMinY, mMaxY; + float mMinSize; + + osg::ref_ptr mRootNode; +}; + +QuadTreeWorld::QuadTreeWorld(SceneUtil::WorkQueue* workQueue, osg::Group *parent, osg::Group *compileRoot, Resource::ResourceSystem *resourceSystem, Storage *storage, int nodeMask, int preCompileMask) + : World(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask) + , mWorkQueue(workQueue) + , mQuadTreeBuilt(false) +{ +} + +QuadTreeWorld::~QuadTreeWorld() +{ + if (mWorkItem) + { + mWorkItem->abort(); + mWorkItem->waitTillDone(); + } +} + +void QuadTreeWorld::accept(osg::NodeVisitor &nv) +{ + if (nv.getVisitorType() != osg::NodeVisitor::CULL_VISITOR && nv.getVisitorType() != osg::NodeVisitor::INTERSECTION_VISITOR) + return; + + mRootNode->traverse(nv); +} + +void QuadTreeWorld::loadCell(int x, int y) +{ + if (mQuadTreeBuilt && !mRootNode) + { + mRootNode = mWorkItem->getRootNode(); + mRootNode->setWorld(this); + mTerrainRoot->addChild(mRootNode); + mWorkItem = NULL; + } +} + +osg::ref_ptr QuadTreeWorld::cacheCell(int x, int y) +{ + if (!mQuadTreeBuilt) + { + const float minSize = 1/4.f; + mWorkItem = new BuildQuadTreeItem(mStorage, minSize); + mWorkQueue->addWorkItem(mWorkItem); + + mWorkItem->waitTillDone(); + + mQuadTreeBuilt = true; + } + return NULL; +} + + + +} diff --git a/components/terrain/quadtreeworld.hpp b/components/terrain/quadtreeworld.hpp new file mode 100644 index 0000000000..320f531202 --- /dev/null +++ b/components/terrain/quadtreeworld.hpp @@ -0,0 +1,47 @@ +#ifndef COMPONENTS_TERRAIN_QUADTREEWORLD_H +#define COMPONENTS_TERRAIN_QUADTREEWORLD_H + +#include + +#include "world.hpp" + +namespace SceneUtil +{ + class WorkQueue; +} + +namespace osg +{ + class NodeVisitor; +} + +namespace Terrain +{ + class RootNode; + class BuildQuadTreeItem; + + /// @brief Terrain implementation that loads cells into a Quad Tree, with geometry LOD and texture LOD. The entire world is displayed at all times. + class QuadTreeWorld : public Terrain::World + { + public: + QuadTreeWorld(SceneUtil::WorkQueue* workQueue, osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask=~0); + ~QuadTreeWorld(); + + void accept(osg::NodeVisitor& nv); + + virtual void loadCell(int x, int y); + virtual osg::ref_ptr cacheCell(int x, int y); + + private: + osg::ref_ptr mRootNode; + + osg::ref_ptr mWorkQueue; + + osg::ref_ptr mWorkItem; + + bool mQuadTreeBuilt; + }; + +} + +#endif diff --git a/components/terrain/storage.hpp b/components/terrain/storage.hpp index efe8fa037d..a4a8bc9fd1 100644 --- a/components/terrain/storage.hpp +++ b/components/terrain/storage.hpp @@ -18,6 +18,7 @@ namespace osg namespace Terrain { /// We keep storage of terrain data abstract here since we need different implementations for game and editor + /// @note The implementation must be thread safe. class Storage { public: diff --git a/components/terrain/terraingrid.hpp b/components/terrain/terraingrid.hpp index 2cd5b5d7a9..dc4523eefc 100644 --- a/components/terrain/terraingrid.hpp +++ b/components/terrain/terraingrid.hpp @@ -7,15 +7,10 @@ #include "world.hpp" -namespace osg -{ - class Texture2D; -} - namespace Terrain { - /// @brief Simple terrain implementation that loads cells in a grid, with no LOD + /// @brief Simple terrain implementation that loads cells in a grid, with no LOD. Only requested cells are loaded. class TerrainGrid : public Terrain::World { public: diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index 3b0a054910..b662cd314e 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -57,6 +57,11 @@ float World::getHeightAt(const osg::Vec3f &worldPos) return mStorage->getHeightAt(worldPos); } +osg::ref_ptr World::cacheCell(int x, int y) +{ + return NULL; +} + void World::updateTextureFiltering() { mTextureManager->updateTextureFiltering(); diff --git a/components/terrain/world.hpp b/components/terrain/world.hpp index 84941d0879..d4867d2ba5 100644 --- a/components/terrain/world.hpp +++ b/components/terrain/world.hpp @@ -47,10 +47,20 @@ namespace Terrain float getHeightAt (const osg::Vec3f& worldPos); - virtual osg::ref_ptr cacheCell(int x, int y) {return NULL;} + /// Load a terrain cell and store it in cache for later use. + /// @note The returned ref_ptr should be kept by the caller to ensure that the terrain stays in cache for as long as needed. + /// @note Thread safe. + /// @note May be ignored by derived implementations that don't organize the terrain into cells. + virtual osg::ref_ptr cacheCell(int x, int y); - // This is only a hint and may be ignored by the implementation. + /// Load the cell into the scene graph. + /// @note Not thread safe. + /// @note May be ignored by derived implementations that don't organize the terrain into cells. virtual void loadCell(int x, int y) {} + + /// Remove the cell from the scene graph. + /// @note Not thread safe. + /// @note May be ignored by derived implementations that don't organize the terrain into cells. virtual void unloadCell(int x, int y) {} Storage* getStorage() { return mStorage; }