You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openmw-tes3mp/components/terrain/quadtreenode.cpp

612 lines
20 KiB
C++

/*
* Copyright (c) 2015 scrawl <scrawl@baseoftrash.de>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "quadtreenode.hpp"
#include <OgreSceneManager.h>
#include <OgreManualObject.h>
#include <OgreSceneNode.h>
#include <OgreMaterialManager.h>
#include <OgreTextureManager.h>
#include "defaultworld.hpp"
#include "chunk.hpp"
#include "storage.hpp"
#include "buffercache.hpp"
#include "material.hpp"
using namespace Terrain;
namespace
{
int Log2( int n )
{
assert(n > 0);
int targetlevel = 0;
while (n >>= 1) ++targetlevel;
return targetlevel;
}
// Utility functions for neighbour finding algorithm
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];
}
// Algorithm described by Hanan Samet - 'Neighbour Finding in Quadtrees'
// http://www.cs.umd.edu/~hjs/pubs/SametPRIP81.pdf
QuadTreeNode* searchNeighbourRecursive (QuadTreeNode* currentNode, Direction dir)
{
if (!currentNode->getParent())
return NULL; // Arrived at root node, the root node does not have neighbours
QuadTreeNode* nextNode;
if (adjacent(currentNode->getDirection(), dir))
nextNode = searchNeighbourRecursive(currentNode->getParent(), dir);
else
nextNode = currentNode->getParent();
if (nextNode && nextNode->hasChildren())
return nextNode->getChild(reflect(currentNode->getDirection(), dir));
else
return NULL;
}
// Create a 2D quad
void makeQuad(Ogre::SceneManager* sceneMgr, float left, float top, float right, float bottom, Ogre::MaterialPtr material)
{
Ogre::ManualObject* manual = sceneMgr->createManualObject();
// Use identity view/projection matrices to get a 2d quad
manual->setUseIdentityProjection(true);
manual->setUseIdentityView(true);
manual->begin(material->getName());
float normLeft = left*2-1;
float normTop = top*2-1;
float normRight = right*2-1;
float normBottom = bottom*2-1;
manual->position(normLeft, normTop, 0.0);
manual->textureCoord(0, 1);
manual->position(normRight, normTop, 0.0);
manual->textureCoord(1, 1);
manual->position(normRight, normBottom, 0.0);
manual->textureCoord(1, 0);
manual->position(normLeft, normBottom, 0.0);
manual->textureCoord(0, 0);
manual->quad(0,1,2,3);
manual->end();
Ogre::AxisAlignedBox aabInf;
aabInf.setInfinite();
manual->setBoundingBox(aabInf);
sceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(manual);
}
}
QuadTreeNode::QuadTreeNode(DefaultWorld* terrain, ChildDirection dir, float size, const Ogre::Vector2 &center, QuadTreeNode* parent)
: mMaterialGenerator(NULL)
, mLoadState(LS_Unloaded)
, mIsDummy(false)
, mSize(size)
, mLodLevel(Log2(static_cast<int>(mSize)))
, mBounds(Ogre::AxisAlignedBox::BOX_NULL)
, mWorldBounds(Ogre::AxisAlignedBox::BOX_NULL)
, mDirection(dir)
, mCenter(center)
, mSceneNode(NULL)
, mParent(parent)
, mChunk(NULL)
, mTerrain(terrain)
{
mBounds.setNull();
for (int i=0; i<4; ++i)
mChildren[i] = NULL;
for (int i=0; i<4; ++i)
mNeighbours[i] = NULL;
if (mDirection == Root)
mSceneNode = mTerrain->getRootSceneNode();
else
mSceneNode = mTerrain->getSceneManager()->createSceneNode();
Ogre::Vector2 pos (0,0);
if (mParent)
pos = mParent->getCenter();
pos = mCenter - pos;
float cellWorldSize = mTerrain->getStorage()->getCellWorldSize();
Ogre::Vector3 sceneNodePos (pos.x*cellWorldSize, pos.y*cellWorldSize, 0);
mTerrain->convertPosition(sceneNodePos);
mSceneNode->setPosition(sceneNodePos);
mMaterialGenerator = new MaterialGenerator();
mMaterialGenerator->enableShaders(mTerrain->getShadersEnabled());
}
void QuadTreeNode::createChild(ChildDirection id, float size, const Ogre::Vector2 &center)
{
mChildren[id] = new QuadTreeNode(mTerrain, id, size, center, this);
}
QuadTreeNode::~QuadTreeNode()
{
for (int i=0; i<4; ++i)
delete mChildren[i];
delete mChunk;
delete mMaterialGenerator;
}
QuadTreeNode* QuadTreeNode::getNeighbour(Direction dir)
{
return mNeighbours[static_cast<int>(dir)];
}
void QuadTreeNode::initNeighbours()
{
for (int i=0; i<4; ++i)
mNeighbours[i] = searchNeighbourRecursive(this, (Direction)i);
if (hasChildren())
for (int i=0; i<4; ++i)
mChildren[i]->initNeighbours();
}
void QuadTreeNode::initAabb()
{
float cellWorldSize = mTerrain->getStorage()->getCellWorldSize();
if (hasChildren())
{
for (int i=0; i<4; ++i)
{
mChildren[i]->initAabb();
mBounds.merge(mChildren[i]->getBoundingBox());
}
float minH, maxH;
switch (mTerrain->getAlign())
{
case Terrain::Align_XY:
minH = mBounds.getMinimum().z;
maxH = mBounds.getMaximum().z;
break;
case Terrain::Align_XZ:
minH = mBounds.getMinimum().y;
maxH = mBounds.getMaximum().y;
break;
case Terrain::Align_YZ:
minH = mBounds.getMinimum().x;
maxH = mBounds.getMaximum().x;
break;
}
Ogre::Vector3 min(-mSize/2*cellWorldSize, -mSize/2*cellWorldSize, minH);
Ogre::Vector3 max(Ogre::Vector3(mSize/2*cellWorldSize, mSize/2*cellWorldSize, maxH));
mBounds = Ogre::AxisAlignedBox (min, max);
mTerrain->convertBounds(mBounds);
}
Ogre::Vector3 offset(mCenter.x*cellWorldSize, mCenter.y*cellWorldSize, 0);
mTerrain->convertPosition(offset);
mWorldBounds = Ogre::AxisAlignedBox(mBounds.getMinimum() + offset,
mBounds.getMaximum() + offset);
}
void QuadTreeNode::setBoundingBox(const Ogre::AxisAlignedBox &box)
{
mBounds = box;
}
const Ogre::AxisAlignedBox& QuadTreeNode::getBoundingBox()
{
return mBounds;
}
const Ogre::AxisAlignedBox& QuadTreeNode::getWorldBoundingBox()
{
return mWorldBounds;
}
bool QuadTreeNode::update(const Ogre::Vector3 &cameraPos)
{
if (isDummy())
return true;
if (mBounds.isNull())
return true;
float dist = mWorldBounds.distance(cameraPos);
// Make sure our scene node is attached
if (!mSceneNode->isInSceneGraph())
{
mParent->getSceneNode()->addChild(mSceneNode);
}
// Simple LOD selection
/// \todo use error metrics?
size_t wantedLod = 0;
float cellWorldSize = mTerrain->getStorage()->getCellWorldSize();
if (dist > cellWorldSize*64)
wantedLod = 6;
else if (dist > cellWorldSize*32)
wantedLod = 5;
else if (dist > cellWorldSize*12)
wantedLod = 4;
else if (dist > cellWorldSize*5)
wantedLod = 3;
else if (dist > cellWorldSize*2)
wantedLod = 2;
else if (dist > cellWorldSize * 1.42) // < sqrt2 so the 3x3 grid around player is always highest lod
wantedLod = 1;
bool wantToDisplay = mSize <= mTerrain->getMaxBatchSize() && mLodLevel <= wantedLod;
if (wantToDisplay)
{
// Wanted LOD is small enough to render this node in one chunk
if (mLoadState == LS_Unloaded)
{
mLoadState = LS_Loading;
mTerrain->queueLoad(this);
return false;
}
if (mLoadState == LS_Loaded)
{
// Additional (index buffer) LOD is currently disabled.
// This is due to a problem with the LOD selection when a node splits.
// After splitting, the distance is measured from the children's bounding boxes, which are possibly
// further away than the original node's bounding box, possibly causing a child to switch to a *lower* LOD
// than the original node.
// In short, we'd sometimes get a switch to a lesser detail when actually moving closer.
// This wouldn't be so bad, but unfortunately it also breaks LOD edge connections if a neighbour
// node hasn't split yet, and has a higher LOD than our node's child:
// ----- ----- ------------
// | LOD | LOD | |
// | 1 | 1 | |
// |-----|-----| LOD 0 |
// | LOD | LOD | |
// | 0 | 0 | |
// ----- ----- ------------
// To prevent this, nodes of the same size need to always select the same LOD, which is basically what we're
// doing here.
// But this "solution" does increase triangle overhead, so eventually we need to find a more clever way.
//mChunk->setAdditionalLod(wantedLod - mLodLevel);
if (!mChunk->getVisible() && hasChildren())
{
for (int i=0; i<4; ++i)
mChildren[i]->unload(true);
}
mChunk->setVisible(true);
return true;
}
return false; // LS_Loading
}
// We do not want to display this node - delegate to children if they are already loaded
if (!wantToDisplay && hasChildren())
{
if (mChunk)
{
// Are children already loaded?
bool childrenLoaded = true;
for (int i=0; i<4; ++i)
if (!mChildren[i]->update(cameraPos))
childrenLoaded = false;
if (!childrenLoaded)
{
mChunk->setVisible(true);
// Make sure child scene nodes are detached until all children are loaded
mSceneNode->removeAllChildren();
}
else
{
// Delegation went well, we can unload now
unload();
for (int i=0; i<4; ++i)
{
if (!mChildren[i]->getSceneNode()->isInSceneGraph())
mSceneNode->addChild(mChildren[i]->getSceneNode());
}
}
return true;
}
else
{
bool success = true;
for (int i=0; i<4; ++i)
success = mChildren[i]->update(cameraPos) & success;
return success;
}
}
return false;
}
void QuadTreeNode::load(const LoadResponseData &data)
{
assert (!mChunk);
mChunk = new Chunk(mTerrain->getBufferCache().getUVBuffer(), mBounds, data.mPositions, data.mNormals, data.mColours);
mChunk->setVisibilityFlags(mTerrain->getVisibilityFlags());
mChunk->setCastShadows(true);
mSceneNode->attachObject(mChunk);
mMaterialGenerator->enableShadows(mTerrain->getShadowsEnabled());
mMaterialGenerator->enableSplitShadows(mTerrain->getSplitShadowsEnabled());
if (mTerrain->areLayersLoaded())
{
if (mSize == 1)
{
mChunk->setMaterial(mMaterialGenerator->generate());
}
else
{
ensureCompositeMap();
mMaterialGenerator->setCompositeMap(mCompositeMap->getName());
mChunk->setMaterial(mMaterialGenerator->generateForCompositeMap());
}
}
// else: will be loaded in loadMaterials() after background thread has finished loading layers
mChunk->setVisible(false);
mLoadState = LS_Loaded;
}
void QuadTreeNode::unload(bool recursive)
{
if (mChunk)
{
mSceneNode->detachObject(mChunk);
delete mChunk;
mChunk = NULL;
if (!mCompositeMap.isNull())
{
Ogre::TextureManager::getSingleton().remove(mCompositeMap->getName());
mCompositeMap.setNull();
}
// Do *not* set this when we are still loading!
mLoadState = LS_Unloaded;
}
if (recursive && hasChildren())
{
for (int i=0; i<4; ++i)
mChildren[i]->unload(true);
}
}
void QuadTreeNode::updateIndexBuffers()
{
if (hasChunk())
{
// Fetch a suitable index buffer (which may be shared)
size_t ourLod = getActualLodLevel();
unsigned int flags = 0;
for (int i=0; i<4; ++i)
{
QuadTreeNode* neighbour = getNeighbour((Direction)i);
// If the neighbour isn't currently rendering itself,
// go up until we find one. NOTE: We don't need to go down,
// because in that case neighbour's detail would be higher than
// our detail and the neighbour would handle stitching by itself.
while (neighbour && !neighbour->hasChunk())
neighbour = neighbour->getParent();
size_t lod = 0;
if (neighbour)
lod = neighbour->getActualLodLevel();
if (lod <= ourLod) // We only need to worry about neighbours less detailed than we are -
lod = 0; // neighbours with more detail will do the stitching themselves
// Use 4 bits for each LOD delta
if (lod > 0)
{
assert (lod - ourLod < (1 << 4));
flags |= static_cast<unsigned int>(lod - ourLod) << (4*i);
}
}
flags |= 0 /*((int)mAdditionalLod)*/ << (4*4);
mChunk->setIndexBuffer(mTerrain->getBufferCache().getIndexBuffer(flags));
}
else if (hasChildren())
{
for (int i=0; i<4; ++i)
mChildren[i]->updateIndexBuffers();
}
}
bool QuadTreeNode::hasChunk()
{
return mSceneNode->isInSceneGraph() && mChunk && mChunk->getVisible();
}
size_t QuadTreeNode::getActualLodLevel()
{
assert(hasChunk() && "Can't get actual LOD level if this node has no render chunk");
return mLodLevel /* + mChunk->getAdditionalLod() */;
}
void QuadTreeNode::loadLayers(const LayerCollection& collection)
{
assert (!mMaterialGenerator->hasLayers());
std::vector<Ogre::TexturePtr> blendTextures;
for (std::vector<Ogre::PixelBox>::const_iterator it = collection.mBlendmaps.begin(); it != collection.mBlendmaps.end(); ++it)
{
// TODO: clean up blend textures on destruction
static int count=0;
Ogre::TexturePtr map = Ogre::TextureManager::getSingleton().createManual("terrain/blend/"
+ Ogre::StringConverter::toString(count++), Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
Ogre::TEX_TYPE_2D, it->getWidth(), it->getHeight(), 0, it->format);
Ogre::DataStreamPtr stream(new Ogre::MemoryDataStream(it->data, it->getWidth()*it->getHeight()*Ogre::PixelUtil::getNumElemBytes(it->format), true));
map->loadRawData(stream, it->getWidth(), it->getHeight(), it->format);
blendTextures.push_back(map);
}
mMaterialGenerator->setLayerList(collection.mLayers);
mMaterialGenerator->setBlendmapList(blendTextures);
}
void QuadTreeNode::loadMaterials()
{
if (isDummy())
return;
// Load children first since we depend on them when creating a composite map
if (hasChildren())
{
for (int i=0; i<4; ++i)
mChildren[i]->loadMaterials();
}
if (mChunk)
{
if (mSize == 1)
{
mChunk->setMaterial(mMaterialGenerator->generate());
}
else
{
ensureCompositeMap();
mMaterialGenerator->setCompositeMap(mCompositeMap->getName());
mChunk->setMaterial(mMaterialGenerator->generateForCompositeMap());
}
}
}
void QuadTreeNode::prepareForCompositeMap(Ogre::TRect<float> area)
{
Ogre::SceneManager* sceneMgr = mTerrain->getCompositeMapSceneManager();
if (mIsDummy)
{
// TODO - store this default material somewhere instead of creating one for each empty cell
MaterialGenerator matGen;
matGen.enableShaders(mTerrain->getShadersEnabled());
std::vector<LayerInfo> layer;
layer.push_back(mTerrain->getStorage()->getDefaultLayer());
matGen.setLayerList(layer);
makeQuad(sceneMgr, area.left, area.top, area.right, area.bottom, matGen.generateForCompositeMapRTT());
return;
}
if (mSize > 1)
{
assert(hasChildren());
// 0,0 -------- 1,0
// | | |
// |-----|------|
// | | |
// 0,1 -------- 1,1
float halfW = area.width()/2.f;
float halfH = area.height()/2.f;
mChildren[NW]->prepareForCompositeMap(Ogre::TRect<float>(area.left, area.top, area.right-halfW, area.bottom-halfH));
mChildren[NE]->prepareForCompositeMap(Ogre::TRect<float>(area.left+halfW, area.top, area.right, area.bottom-halfH));
mChildren[SW]->prepareForCompositeMap(Ogre::TRect<float>(area.left, area.top+halfH, area.right-halfW, area.bottom));
mChildren[SE]->prepareForCompositeMap(Ogre::TRect<float>(area.left+halfW, area.top+halfH, area.right, area.bottom));
}
else
{
// TODO: when to destroy?
Ogre::MaterialPtr material = mMaterialGenerator->generateForCompositeMapRTT();
makeQuad(sceneMgr, area.left, area.top, area.right, area.bottom, material);
}
}
void QuadTreeNode::ensureCompositeMap()
{
if (!mCompositeMap.isNull())
return;
static int i=0;
std::stringstream name;
name << "terrain/comp" << i++;
const int size = 128;
mCompositeMap = Ogre::TextureManager::getSingleton().createManual(
name.str(), Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
Ogre::TEX_TYPE_2D, size, size, Ogre::MIP_DEFAULT, Ogre::PF_A8B8G8R8);
// Create quads for each cell
prepareForCompositeMap(Ogre::TRect<float>(0,0,1,1));
mTerrain->renderCompositeMap(mCompositeMap);
mTerrain->clearCompositeMapSceneManager();
}
void QuadTreeNode::applyMaterials()
{
if (mChunk)
{
mMaterialGenerator->enableShadows(mTerrain->getShadowsEnabled());
mMaterialGenerator->enableSplitShadows(mTerrain->getSplitShadowsEnabled());
if (mSize <= 1)
mChunk->setMaterial(mMaterialGenerator->generate());
else
mChunk->setMaterial(mMaterialGenerator->generateForCompositeMap());
}
if (hasChildren())
for (int i=0; i<4; ++i)
mChildren[i]->applyMaterials();
}