forked from mirror/openmw-tes3mp
Terrain rendering
parent
10f938ff87
commit
cdd0623009
@ -1,169 +0,0 @@
|
||||
/*
|
||||
* 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 "chunk.hpp"
|
||||
|
||||
#include <OgreSceneNode.h>
|
||||
#include <OgreHardwareBufferManager.h>
|
||||
#include <OgreRenderQueue.h>
|
||||
#include <OgreMaterialManager.h>
|
||||
#include <OgreStringConverter.h>
|
||||
|
||||
namespace Terrain
|
||||
{
|
||||
|
||||
Chunk::Chunk(Ogre::HardwareVertexBufferSharedPtr uvBuffer, const Ogre::AxisAlignedBox& bounds,
|
||||
const std::vector<float>& positions, const std::vector<float>& normals, const std::vector<Ogre::uint8>& colours)
|
||||
: mBounds(bounds)
|
||||
, mOwnMaterial(false)
|
||||
{
|
||||
mVertexData = OGRE_NEW Ogre::VertexData;
|
||||
mVertexData->vertexStart = 0;
|
||||
mVertexData->vertexCount = positions.size()/3;
|
||||
|
||||
// Set up the vertex declaration, which specifies the info for each vertex (normals, colors, UVs, etc)
|
||||
Ogre::VertexDeclaration* vertexDecl = mVertexData->vertexDeclaration;
|
||||
|
||||
Ogre::HardwareBufferManager* mgr = Ogre::HardwareBufferManager::getSingletonPtr();
|
||||
size_t nextBuffer = 0;
|
||||
|
||||
// Positions
|
||||
vertexDecl->addElement(nextBuffer++, 0, Ogre::VET_FLOAT3, Ogre::VES_POSITION);
|
||||
Ogre::HardwareVertexBufferSharedPtr vertexBuffer = mgr->createVertexBuffer(Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT3),
|
||||
mVertexData->vertexCount, Ogre::HardwareBuffer::HBU_STATIC);
|
||||
|
||||
// Normals
|
||||
vertexDecl->addElement(nextBuffer++, 0, Ogre::VET_FLOAT3, Ogre::VES_NORMAL);
|
||||
Ogre::HardwareVertexBufferSharedPtr normalBuffer = mgr->createVertexBuffer(Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT3),
|
||||
mVertexData->vertexCount, Ogre::HardwareBuffer::HBU_STATIC);
|
||||
|
||||
|
||||
// UV texture coordinates
|
||||
vertexDecl->addElement(nextBuffer++, 0, Ogre::VET_FLOAT2,
|
||||
Ogre::VES_TEXTURE_COORDINATES, 0);
|
||||
|
||||
// Colours
|
||||
vertexDecl->addElement(nextBuffer++, 0, Ogre::VET_COLOUR, Ogre::VES_DIFFUSE);
|
||||
Ogre::HardwareVertexBufferSharedPtr colourBuffer = mgr->createVertexBuffer(Ogre::VertexElement::getTypeSize(Ogre::VET_COLOUR),
|
||||
mVertexData->vertexCount, Ogre::HardwareBuffer::HBU_STATIC);
|
||||
|
||||
vertexBuffer->writeData(0, vertexBuffer->getSizeInBytes(), &positions[0], true);
|
||||
normalBuffer->writeData(0, normalBuffer->getSizeInBytes(), &normals[0], true);
|
||||
colourBuffer->writeData(0, colourBuffer->getSizeInBytes(), &colours[0], true);
|
||||
|
||||
mVertexData->vertexBufferBinding->setBinding(0, vertexBuffer);
|
||||
mVertexData->vertexBufferBinding->setBinding(1, normalBuffer);
|
||||
mVertexData->vertexBufferBinding->setBinding(2, uvBuffer);
|
||||
mVertexData->vertexBufferBinding->setBinding(3, colourBuffer);
|
||||
|
||||
// Assign a default material in case terrain material fails to be created
|
||||
mMaterial = Ogre::MaterialManager::getSingleton().getByName("BaseWhite");
|
||||
|
||||
mIndexData = OGRE_NEW Ogre::IndexData();
|
||||
mIndexData->indexStart = 0;
|
||||
}
|
||||
|
||||
void Chunk::setIndexBuffer(Ogre::HardwareIndexBufferSharedPtr buffer)
|
||||
{
|
||||
mIndexData->indexBuffer = buffer;
|
||||
mIndexData->indexCount = buffer->getNumIndexes();
|
||||
}
|
||||
|
||||
Chunk::~Chunk()
|
||||
{
|
||||
if (!mMaterial.isNull() && mOwnMaterial)
|
||||
{
|
||||
#if TERRAIN_USE_SHADER
|
||||
sh::Factory::getInstance().destroyMaterialInstance(mMaterial->getName());
|
||||
#endif
|
||||
Ogre::MaterialManager::getSingleton().remove(mMaterial->getName());
|
||||
}
|
||||
OGRE_DELETE mVertexData;
|
||||
OGRE_DELETE mIndexData;
|
||||
}
|
||||
|
||||
void Chunk::setMaterial(const Ogre::MaterialPtr &material, bool own)
|
||||
{
|
||||
// Clean up the previous material, if we own it
|
||||
if (!mMaterial.isNull() && mOwnMaterial)
|
||||
{
|
||||
#if TERRAIN_USE_SHADER
|
||||
sh::Factory::getInstance().destroyMaterialInstance(mMaterial->getName());
|
||||
#endif
|
||||
Ogre::MaterialManager::getSingleton().remove(mMaterial->getName());
|
||||
}
|
||||
|
||||
mMaterial = material;
|
||||
mOwnMaterial = own;
|
||||
}
|
||||
|
||||
const Ogre::AxisAlignedBox& Chunk::getBoundingBox(void) const
|
||||
{
|
||||
return mBounds;
|
||||
}
|
||||
|
||||
Ogre::Real Chunk::getBoundingRadius(void) const
|
||||
{
|
||||
return mBounds.getHalfSize().length();
|
||||
}
|
||||
|
||||
void Chunk::_updateRenderQueue(Ogre::RenderQueue* queue)
|
||||
{
|
||||
queue->addRenderable(this, mRenderQueueID);
|
||||
}
|
||||
|
||||
void Chunk::visitRenderables(Ogre::Renderable::Visitor* visitor,
|
||||
bool debugRenderables)
|
||||
{
|
||||
visitor->visit(this, 0, false);
|
||||
}
|
||||
|
||||
const Ogre::MaterialPtr& Chunk::getMaterial(void) const
|
||||
{
|
||||
return mMaterial;
|
||||
}
|
||||
|
||||
void Chunk::getRenderOperation(Ogre::RenderOperation& op)
|
||||
{
|
||||
assert (!mIndexData->indexBuffer.isNull() && "Trying to render, but no index buffer set!");
|
||||
assert(!mMaterial.isNull() && "Trying to render, but no material set!");
|
||||
op.useIndexes = true;
|
||||
op.operationType = Ogre::RenderOperation::OT_TRIANGLE_LIST;
|
||||
op.vertexData = mVertexData;
|
||||
op.indexData = mIndexData;
|
||||
}
|
||||
|
||||
void Chunk::getWorldTransforms(Ogre::Matrix4* xform) const
|
||||
{
|
||||
*xform = getParentSceneNode()->_getFullTransform();
|
||||
}
|
||||
|
||||
Ogre::Real Chunk::getSquaredViewDepth(const Ogre::Camera* cam) const
|
||||
{
|
||||
return getParentSceneNode()->getSquaredViewDepth(cam);
|
||||
}
|
||||
|
||||
const Ogre::LightList& Chunk::getLights(void) const
|
||||
{
|
||||
return queryLights();
|
||||
}
|
||||
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
#ifndef COMPONENTS_TERRAIN_TERRAINBATCH_H
|
||||
#define COMPONENTS_TERRAIN_TERRAINBATCH_H
|
||||
|
||||
#include <OgreRenderable.h>
|
||||
#include <OgreMovableObject.h>
|
||||
|
||||
namespace Terrain
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief A movable object representing a chunk of terrain.
|
||||
*/
|
||||
class Chunk : public Ogre::Renderable, public Ogre::MovableObject
|
||||
{
|
||||
public:
|
||||
Chunk (Ogre::HardwareVertexBufferSharedPtr uvBuffer, const Ogre::AxisAlignedBox& bounds,
|
||||
const std::vector<float>& positions,
|
||||
const std::vector<float>& normals,
|
||||
const std::vector<Ogre::uint8>& colours);
|
||||
|
||||
virtual ~Chunk();
|
||||
|
||||
/// @param own Should we take ownership of the material?
|
||||
void setMaterial (const Ogre::MaterialPtr& material, bool own=true);
|
||||
|
||||
void setIndexBuffer(Ogre::HardwareIndexBufferSharedPtr buffer);
|
||||
|
||||
// Inherited from MovableObject
|
||||
virtual const Ogre::String& getMovableType(void) const { static Ogre::String t = "MW_TERRAIN"; return t; }
|
||||
virtual const Ogre::AxisAlignedBox& getBoundingBox(void) const;
|
||||
virtual Ogre::Real getBoundingRadius(void) const;
|
||||
virtual void _updateRenderQueue(Ogre::RenderQueue* queue);
|
||||
virtual void visitRenderables(Renderable::Visitor* visitor,
|
||||
bool debugRenderables = false);
|
||||
|
||||
// Inherited from Renderable
|
||||
virtual const Ogre::MaterialPtr& getMaterial(void) const;
|
||||
virtual void getRenderOperation(Ogre::RenderOperation& op);
|
||||
virtual void getWorldTransforms(Ogre::Matrix4* xform) const;
|
||||
virtual Ogre::Real getSquaredViewDepth(const Ogre::Camera* cam) const;
|
||||
virtual const Ogre::LightList& getLights(void) const;
|
||||
|
||||
private:
|
||||
Ogre::AxisAlignedBox mBounds;
|
||||
Ogre::MaterialPtr mMaterial;
|
||||
bool mOwnMaterial; // Should we remove mMaterial on destruction?
|
||||
|
||||
Ogre::VertexData* mVertexData;
|
||||
Ogre::IndexData* mIndexData;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
@ -1,336 +0,0 @@
|
||||
/*
|
||||
* 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 "defaultworld.hpp"
|
||||
|
||||
#include <OgreAxisAlignedBox.h>
|
||||
#include <OgreCamera.h>
|
||||
#include <OgreHardwarePixelBuffer.h>
|
||||
#include <OgreTextureManager.h>
|
||||
#include <OgreRenderTexture.h>
|
||||
#include <OgreSceneNode.h>
|
||||
#include <OgreRoot.h>
|
||||
|
||||
#include "storage.hpp"
|
||||
#include "quadtreenode.hpp"
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Terrain::QuadTreeNode* findNode (const Ogre::Vector2& center, Terrain::QuadTreeNode* node)
|
||||
{
|
||||
if (center == node->getCenter())
|
||||
return node;
|
||||
|
||||
if (center.x > node->getCenter().x && center.y > node->getCenter().y)
|
||||
return findNode(center, node->getChild(Terrain::NE));
|
||||
else if (center.x > node->getCenter().x && center.y < node->getCenter().y)
|
||||
return findNode(center, node->getChild(Terrain::SE));
|
||||
else if (center.x < node->getCenter().x && center.y > node->getCenter().y)
|
||||
return findNode(center, node->getChild(Terrain::NW));
|
||||
else //if (center.x < node->getCenter().x && center.y < node->getCenter().y)
|
||||
return findNode(center, node->getChild(Terrain::SW));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Terrain
|
||||
{
|
||||
|
||||
const Ogre::uint REQ_ID_CHUNK = 1;
|
||||
const Ogre::uint REQ_ID_LAYERS = 2;
|
||||
|
||||
DefaultWorld::DefaultWorld(Ogre::SceneManager* sceneMgr,
|
||||
Storage* storage, int visibilityFlags, bool shaders, Alignment align, float minBatchSize, float maxBatchSize)
|
||||
: World(sceneMgr, storage, visibilityFlags, shaders, align)
|
||||
, mWorkQueueChannel(0)
|
||||
, mVisible(true)
|
||||
, mChunksLoading(0)
|
||||
, mMinX(0)
|
||||
, mMaxX(0)
|
||||
, mMinY(0)
|
||||
, mMaxY(0)
|
||||
, mMinBatchSize(minBatchSize)
|
||||
, mMaxBatchSize(maxBatchSize)
|
||||
, mLayerLoadPending(true)
|
||||
{
|
||||
#if TERRAIN_USE_SHADER == 0
|
||||
if (mShaders)
|
||||
std::cerr << "Compiled Terrain without shader support, disabling..." << std::endl;
|
||||
mShaders = false;
|
||||
#endif
|
||||
|
||||
mCompositeMapSceneMgr = Ogre::Root::getSingleton().createSceneManager(Ogre::ST_GENERIC);
|
||||
|
||||
/// \todo make composite map size configurable
|
||||
Ogre::Camera* compositeMapCam = mCompositeMapSceneMgr->createCamera("a");
|
||||
mCompositeMapRenderTexture = Ogre::TextureManager::getSingleton().createManual(
|
||||
"terrain/comp/rt", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
|
||||
Ogre::TEX_TYPE_2D, 128, 128, 0, Ogre::PF_A8B8G8R8, Ogre::TU_RENDERTARGET);
|
||||
mCompositeMapRenderTarget = mCompositeMapRenderTexture->getBuffer()->getRenderTarget();
|
||||
mCompositeMapRenderTarget->setAutoUpdated(false);
|
||||
mCompositeMapRenderTarget->addViewport(compositeMapCam);
|
||||
|
||||
storage->getBounds(mMinX, mMaxX, mMinY, mMaxY);
|
||||
|
||||
int origSizeX = static_cast<int>(mMaxX - mMinX);
|
||||
int origSizeY = static_cast<int>(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));
|
||||
|
||||
// Adjust the center according to the new size
|
||||
float centerX = (mMinX+mMaxX)/2.f + (size-origSizeX)/2.f;
|
||||
float centerY = (mMinY+mMaxY)/2.f + (size-origSizeY)/2.f;
|
||||
|
||||
mRootSceneNode = mSceneMgr->getRootSceneNode()->createChildSceneNode();
|
||||
|
||||
// While building the quadtree, remember leaf nodes since we need to load their layers
|
||||
LayersRequestData data;
|
||||
data.mPack = getShadersEnabled();
|
||||
|
||||
mRootNode = new QuadTreeNode(this, Root, static_cast<float>(size), Ogre::Vector2(centerX, centerY), NULL);
|
||||
buildQuadTree(mRootNode, data.mNodes);
|
||||
//loadingListener->indicateProgress();
|
||||
mRootNode->initAabb();
|
||||
//loadingListener->indicateProgress();
|
||||
mRootNode->initNeighbours();
|
||||
//loadingListener->indicateProgress();
|
||||
|
||||
Ogre::WorkQueue* wq = Ogre::Root::getSingleton().getWorkQueue();
|
||||
mWorkQueueChannel = wq->getChannel("LargeTerrain");
|
||||
wq->addRequestHandler(mWorkQueueChannel, this);
|
||||
wq->addResponseHandler(mWorkQueueChannel, this);
|
||||
|
||||
// Start loading layers in the background (for leaf nodes)
|
||||
wq->addRequest(mWorkQueueChannel, REQ_ID_LAYERS, Ogre::Any(data));
|
||||
}
|
||||
|
||||
DefaultWorld::~DefaultWorld()
|
||||
{
|
||||
Ogre::WorkQueue* wq = Ogre::Root::getSingleton().getWorkQueue();
|
||||
wq->removeRequestHandler(mWorkQueueChannel, this);
|
||||
wq->removeResponseHandler(mWorkQueueChannel, this);
|
||||
|
||||
delete mRootNode;
|
||||
}
|
||||
|
||||
void DefaultWorld::buildQuadTree(QuadTreeNode *node, std::vector<QuadTreeNode*>& leafs)
|
||||
{
|
||||
float halfSize = node->getSize()/2.f;
|
||||
|
||||
if (node->getSize() <= mMinBatchSize)
|
||||
{
|
||||
// We arrived at a leaf
|
||||
float minZ,maxZ;
|
||||
Ogre::Vector2 center = node->getCenter();
|
||||
float cellWorldSize = getStorage()->getCellWorldSize();
|
||||
if (mStorage->getMinMaxHeights(static_cast<float>(node->getSize()), center, minZ, maxZ))
|
||||
{
|
||||
Ogre::AxisAlignedBox bounds(Ogre::Vector3(-halfSize*cellWorldSize, -halfSize*cellWorldSize, minZ),
|
||||
Ogre::Vector3(halfSize*cellWorldSize, halfSize*cellWorldSize, maxZ));
|
||||
convertBounds(bounds);
|
||||
node->setBoundingBox(bounds);
|
||||
leafs.push_back(node);
|
||||
}
|
||||
else
|
||||
node->markAsDummy(); // no data available for this node, skip it
|
||||
return;
|
||||
}
|
||||
|
||||
if (node->getCenter().x - halfSize > mMaxX
|
||||
|| node->getCenter().x + halfSize < mMinX
|
||||
|| node->getCenter().y - halfSize > mMaxY
|
||||
|| node->getCenter().y + halfSize < mMinY )
|
||||
// Out of bounds of the actual terrain - this will happen because
|
||||
// we rounded the size up to the next power of two
|
||||
{
|
||||
node->markAsDummy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Not a leaf, create its children
|
||||
node->createChild(SW, halfSize, node->getCenter() - halfSize/2.f);
|
||||
node->createChild(SE, halfSize, node->getCenter() + Ogre::Vector2(halfSize/2.f, -halfSize/2.f));
|
||||
node->createChild(NW, halfSize, node->getCenter() + Ogre::Vector2(-halfSize/2.f, halfSize/2.f));
|
||||
node->createChild(NE, halfSize, node->getCenter() + halfSize/2.f);
|
||||
buildQuadTree(node->getChild(SW), leafs);
|
||||
buildQuadTree(node->getChild(SE), leafs);
|
||||
buildQuadTree(node->getChild(NW), leafs);
|
||||
buildQuadTree(node->getChild(NE), leafs);
|
||||
|
||||
// if all children are dummy, we are also dummy
|
||||
for (int i=0; i<4; ++i)
|
||||
{
|
||||
if (!node->getChild((ChildDirection)i)->isDummy())
|
||||
return;
|
||||
}
|
||||
node->markAsDummy();
|
||||
}
|
||||
|
||||
void DefaultWorld::update(const Ogre::Vector3& cameraPos)
|
||||
{
|
||||
if (!mVisible)
|
||||
return;
|
||||
mRootNode->update(cameraPos);
|
||||
mRootNode->updateIndexBuffers();
|
||||
}
|
||||
|
||||
Ogre::AxisAlignedBox DefaultWorld::getWorldBoundingBox (const Ogre::Vector2& center)
|
||||
{
|
||||
if (center.x > mMaxX
|
||||
|| center.x < mMinX
|
||||
|| center.y > mMaxY
|
||||
|| center.y < mMinY)
|
||||
return Ogre::AxisAlignedBox::BOX_NULL;
|
||||
QuadTreeNode* node = findNode(center, mRootNode);
|
||||
return node->getWorldBoundingBox();
|
||||
}
|
||||
|
||||
void DefaultWorld::renderCompositeMap(Ogre::TexturePtr target)
|
||||
{
|
||||
mCompositeMapRenderTarget->update();
|
||||
target->getBuffer()->blit(mCompositeMapRenderTexture->getBuffer());
|
||||
}
|
||||
|
||||
void DefaultWorld::clearCompositeMapSceneManager()
|
||||
{
|
||||
mCompositeMapSceneMgr->destroyAllManualObjects();
|
||||
mCompositeMapSceneMgr->clearScene();
|
||||
}
|
||||
|
||||
void DefaultWorld::applyMaterials(bool shadows, bool splitShadows)
|
||||
{
|
||||
mShadows = shadows;
|
||||
mSplitShadows = splitShadows;
|
||||
mRootNode->applyMaterials();
|
||||
}
|
||||
|
||||
void DefaultWorld::setVisible(bool visible)
|
||||
{
|
||||
if (visible && !mVisible)
|
||||
mSceneMgr->getRootSceneNode()->addChild(mRootSceneNode);
|
||||
else if (!visible && mVisible)
|
||||
mSceneMgr->getRootSceneNode()->removeChild(mRootSceneNode);
|
||||
|
||||
mVisible = visible;
|
||||
}
|
||||
|
||||
bool DefaultWorld::getVisible()
|
||||
{
|
||||
return mVisible;
|
||||
}
|
||||
|
||||
void DefaultWorld::syncLoad()
|
||||
{
|
||||
while (mChunksLoading || mLayerLoadPending)
|
||||
{
|
||||
OGRE_THREAD_SLEEP(0);
|
||||
Ogre::Root::getSingleton().getWorkQueue()->processResponses();
|
||||
}
|
||||
}
|
||||
|
||||
Ogre::WorkQueue::Response* DefaultWorld::handleRequest(const Ogre::WorkQueue::Request *req, const Ogre::WorkQueue *srcQ)
|
||||
{
|
||||
if (req->getType() == REQ_ID_CHUNK)
|
||||
{
|
||||
const LoadRequestData data = Ogre::any_cast<LoadRequestData>(req->getData());
|
||||
|
||||
QuadTreeNode* node = data.mNode;
|
||||
|
||||
LoadResponseData* responseData = new LoadResponseData();
|
||||
|
||||
getStorage()->fillVertexBuffers(node->getNativeLodLevel(), static_cast<float>(node->getSize()), node->getCenter(), getAlign(),
|
||||
responseData->mPositions, responseData->mNormals, responseData->mColours);
|
||||
|
||||
return OGRE_NEW Ogre::WorkQueue::Response(req, true, Ogre::Any(responseData));
|
||||
}
|
||||
else // REQ_ID_LAYERS
|
||||
{
|
||||
const LayersRequestData data = Ogre::any_cast<LayersRequestData>(req->getData());
|
||||
|
||||
LayersResponseData* responseData = new LayersResponseData();
|
||||
|
||||
getStorage()->getBlendmaps(data.mNodes, responseData->mLayerCollections, data.mPack);
|
||||
|
||||
return OGRE_NEW Ogre::WorkQueue::Response(req, true, Ogre::Any(responseData));
|
||||
}
|
||||
}
|
||||
|
||||
void DefaultWorld::handleResponse(const Ogre::WorkQueue::Response *res, const Ogre::WorkQueue *srcQ)
|
||||
{
|
||||
assert(res->succeeded() && "Response failure not handled");
|
||||
|
||||
if (res->getRequest()->getType() == REQ_ID_CHUNK)
|
||||
{
|
||||
LoadResponseData* data = Ogre::any_cast<LoadResponseData*>(res->getData());
|
||||
|
||||
const LoadRequestData requestData = Ogre::any_cast<LoadRequestData>(res->getRequest()->getData());
|
||||
|
||||
requestData.mNode->load(*data);
|
||||
|
||||
delete data;
|
||||
|
||||
--mChunksLoading;
|
||||
}
|
||||
else // REQ_ID_LAYERS
|
||||
{
|
||||
LayersResponseData* data = Ogre::any_cast<LayersResponseData*>(res->getData());
|
||||
|
||||
for (std::vector<LayerCollection>::iterator it = data->mLayerCollections.begin(); it != data->mLayerCollections.end(); ++it)
|
||||
{
|
||||
it->mTarget->loadLayers(*it);
|
||||
}
|
||||
|
||||
delete data;
|
||||
|
||||
mRootNode->loadMaterials();
|
||||
|
||||
mLayerLoadPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
void DefaultWorld::queueLoad(QuadTreeNode *node)
|
||||
{
|
||||
LoadRequestData data;
|
||||
data.mNode = node;
|
||||
|
||||
Ogre::Root::getSingleton().getWorkQueue()->addRequest(mWorkQueueChannel, REQ_ID_CHUNK, Ogre::Any(data));
|
||||
++mChunksLoading;
|
||||
}
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
#ifndef COMPONENTS_TERRAIN_H
|
||||
#define COMPONENTS_TERRAIN_H
|
||||
|
||||
#include <OgreAxisAlignedBox.h>
|
||||
#include <OgreTexture.h>
|
||||
#include <OgreWorkQueue.h>
|
||||
|
||||
#include "world.hpp"
|
||||
|
||||
namespace Ogre
|
||||
{
|
||||
class Camera;
|
||||
}
|
||||
|
||||
namespace Terrain
|
||||
{
|
||||
|
||||
class QuadTreeNode;
|
||||
class Storage;
|
||||
|
||||
/**
|
||||
* @brief A quadtree-based terrain implementation suitable for large data sets. \n
|
||||
* Near cells are rendered with alpha splatting, distant cells are merged
|
||||
* together in batches and have their layers pre-rendered onto a composite map. \n
|
||||
* Cracks at LOD transitions are avoided using stitching.
|
||||
* @note Multiple cameras are not supported yet
|
||||
*/
|
||||
class DefaultWorld : public World, public Ogre::WorkQueue::RequestHandler, public Ogre::WorkQueue::ResponseHandler
|
||||
{
|
||||
public:
|
||||
/// @note takes ownership of \a storage
|
||||
/// @param sceneMgr scene manager to use
|
||||
/// @param storage Storage instance to get terrain data from (heights, normals, colors, textures..)
|
||||
/// @param visbilityFlags visibility flags for the created meshes
|
||||
/// @param shaders Whether to use splatting shader, or multi-pass fixed function splatting. Shader is usually
|
||||
/// faster so this is just here for compatibility.
|
||||
/// @param align The align of the terrain, see Alignment enum
|
||||
/// @param minBatchSize Minimum size of a terrain batch along one side (in cell units). Used for building the quad tree.
|
||||
/// @param maxBatchSize Maximum size of a terrain batch along one side (in cell units). Used when traversing the quad tree.
|
||||
DefaultWorld(Ogre::SceneManager* sceneMgr,
|
||||
Storage* storage, int visibilityFlags, bool shaders, Alignment align, float minBatchSize, float maxBatchSize);
|
||||
~DefaultWorld();
|
||||
|
||||
/// Update chunk LODs according to this camera position
|
||||
/// @note Calling this method might lead to composite textures being rendered, so it is best
|
||||
/// not to call it when render commands are still queued, since that would cause a flush.
|
||||
virtual void update (const Ogre::Vector3& cameraPos);
|
||||
|
||||
/// Get the world bounding box of a chunk of terrain centered at \a center
|
||||
virtual Ogre::AxisAlignedBox getWorldBoundingBox (const Ogre::Vector2& center);
|
||||
|
||||
Ogre::SceneNode* getRootSceneNode() { return mRootSceneNode; }
|
||||
|
||||
/// Show or hide the whole terrain
|
||||
/// @note this setting will be invalidated once you call Terrain::update, so do not call it while the terrain should be hidden
|
||||
virtual void setVisible(bool visible);
|
||||
virtual bool getVisible();
|
||||
|
||||
/// Recreate materials used by terrain chunks. This should be called whenever settings of
|
||||
/// the material factory are changed. (Relying on the factory to update those materials is not
|
||||
/// enough, since turning a feature on/off can change the number of texture units available for layer/blend
|
||||
/// textures, and to properly respond to this we may need to change the structure of the material, such as
|
||||
/// adding or removing passes. This can only be achieved by a full rebuild.)
|
||||
virtual void applyMaterials(bool shadows, bool splitShadows);
|
||||
|
||||
int getMaxBatchSize() { return static_cast<int>(mMaxBatchSize); }
|
||||
|
||||
/// Wait until all background loading is complete.
|
||||
void syncLoad();
|
||||
|
||||
private:
|
||||
// Called from a background worker thread
|
||||
virtual Ogre::WorkQueue::Response* handleRequest(const Ogre::WorkQueue::Request* req, const Ogre::WorkQueue* srcQ);
|
||||
// Called from the main thread
|
||||
virtual void handleResponse(const Ogre::WorkQueue::Response* res, const Ogre::WorkQueue* srcQ);
|
||||
Ogre::uint16 mWorkQueueChannel;
|
||||
|
||||
bool mVisible;
|
||||
|
||||
QuadTreeNode* mRootNode;
|
||||
Ogre::SceneNode* mRootSceneNode;
|
||||
|
||||
/// The number of chunks currently loading in a background thread. If 0, we have finished loading!
|
||||
int mChunksLoading;
|
||||
|
||||
Ogre::SceneManager* mCompositeMapSceneMgr;
|
||||
|
||||
/// Bounds in cell units
|
||||
float mMinX, mMaxX, mMinY, mMaxY;
|
||||
|
||||
/// Minimum size of a terrain batch along one side (in cell units)
|
||||
float mMinBatchSize;
|
||||
/// Maximum size of a terrain batch along one side (in cell units)
|
||||
float mMaxBatchSize;
|
||||
|
||||
void buildQuadTree(QuadTreeNode* node, std::vector<QuadTreeNode*>& leafs);
|
||||
|
||||
// Are layers for leaf nodes loaded? This is done once at startup (but in a background thread)
|
||||
bool mLayerLoadPending;
|
||||
|
||||
public:
|
||||
// ----INTERNAL----
|
||||
Ogre::SceneManager* getCompositeMapSceneManager() { return mCompositeMapSceneMgr; }
|
||||
|
||||
bool areLayersLoaded() { return !mLayerLoadPending; }
|
||||
|
||||
// Delete all quads
|
||||
void clearCompositeMapSceneManager();
|
||||
void renderCompositeMap (Ogre::TexturePtr target);
|
||||
|
||||
// Adds a WorkQueue request to load a chunk for this node in the background.
|
||||
void queueLoad (QuadTreeNode* node);
|
||||
|
||||
private:
|
||||
Ogre::RenderTarget* mCompositeMapRenderTarget;
|
||||
Ogre::TexturePtr mCompositeMapRenderTexture;
|
||||
};
|
||||
|
||||
struct LoadRequestData
|
||||
{
|
||||
QuadTreeNode* mNode;
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& o, const LoadRequestData& r)
|
||||
{ return o; }
|
||||
};
|
||||
|
||||
struct LoadResponseData
|
||||
{
|
||||
std::vector<float> mPositions;
|
||||
std::vector<float> mNormals;
|
||||
std::vector<Ogre::uint8> mColours;
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& o, const LoadResponseData& r)
|
||||
{ return o; }
|
||||
};
|
||||
|
||||
struct LayersRequestData
|
||||
{
|
||||
std::vector<QuadTreeNode*> mNodes;
|
||||
bool mPack;
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& o, const LayersRequestData& r)
|
||||
{ return o; }
|
||||
};
|
||||
|
||||
struct LayersResponseData
|
||||
{
|
||||
std::vector<LayerCollection> mLayerCollections;
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& o, const LayersResponseData& r)
|
||||
{ return o; }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
@ -1,611 +0,0 @@
|
||||
/*
|
||||
* 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 ¢er, 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 ¢er)
|
||||
{
|
||||
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();
|
||||
}
|
@ -1,189 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
#ifndef COMPONENTS_TERRAIN_QUADTREENODE_H
|
||||
#define COMPONENTS_TERRAIN_QUADTREENODE_H
|
||||
|
||||
#include <OgreAxisAlignedBox.h>
|
||||
#include <OgreVector2.h>
|
||||
#include <OgreTexture.h>
|
||||
|
||||
#include "defs.hpp"
|
||||
|
||||
namespace Ogre
|
||||
{
|
||||
class Rectangle2D;
|
||||
}
|
||||
|
||||
namespace Terrain
|
||||
{
|
||||
class DefaultWorld;
|
||||
class Chunk;
|
||||
class MaterialGenerator;
|
||||
struct LoadResponseData;
|
||||
|
||||
enum ChildDirection
|
||||
{
|
||||
NW = 0,
|
||||
NE = 1,
|
||||
SW = 2,
|
||||
SE = 3,
|
||||
Root
|
||||
};
|
||||
|
||||
enum LoadState
|
||||
{
|
||||
LS_Unloaded,
|
||||
LS_Loading,
|
||||
LS_Loaded
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A node in the quad tree for our terrain. Depending on LOD,
|
||||
* a node can either choose to render itself in one batch (merging its children),
|
||||
* or delegate the render process to its children, rendering each child in at least one batch.
|
||||
*/
|
||||
class QuadTreeNode
|
||||
{
|
||||
public:
|
||||
/// @param terrain
|
||||
/// @param dir relative to parent, or Root if we are the root node
|
||||
/// @param size size (in *cell* units!)
|
||||
/// @param center center (in *cell* units!)
|
||||
/// @param parent parent node
|
||||
QuadTreeNode (DefaultWorld* terrain, ChildDirection dir, float size, const Ogre::Vector2& center, QuadTreeNode* parent);
|
||||
~QuadTreeNode();
|
||||
|
||||
/// Rebuild all materials
|
||||
void applyMaterials();
|
||||
|
||||
/// Initialize neighbours - do this after the quadtree is built
|
||||
void initNeighbours();
|
||||
/// Initialize bounding boxes of non-leafs by merging children bounding boxes.
|
||||
/// Do this after the quadtree is built - note that leaf bounding boxes
|
||||
/// need to be set first via setBoundingBox!
|
||||
void initAabb();
|
||||
|
||||
/// @note takes ownership of \a child
|
||||
void createChild (ChildDirection id, float size, const Ogre::Vector2& center);
|
||||
|
||||
/// Mark this node as a dummy node. This can happen if the terrain size isn't a power of two.
|
||||
/// For the QuadTree to work, we need to round the size up to a power of two, which means we'll
|
||||
/// end up with empty nodes that don't actually render anything.
|
||||
void markAsDummy() { mIsDummy = true; }
|
||||
bool isDummy() { return mIsDummy; }
|
||||
|
||||
QuadTreeNode* getParent() { return mParent; }
|
||||
|
||||
Ogre::SceneNode* getSceneNode() { return mSceneNode; }
|
||||
|
||||
int getSize() { return static_cast<int>(mSize); }
|
||||
Ogre::Vector2 getCenter() { return mCenter; }
|
||||
|
||||
bool hasChildren() { return mChildren[0] != 0; }
|
||||
QuadTreeNode* getChild(ChildDirection dir) { return mChildren[dir]; }
|
||||
|
||||
/// Get neighbour node in this direction
|
||||
QuadTreeNode* getNeighbour (Direction dir);
|
||||
|
||||
/// Returns our direction relative to the parent node, or Root if we are the root node.
|
||||
ChildDirection getDirection() { return mDirection; }
|
||||
|
||||
/// Set bounding box in local coordinates. Should be done at load time for leaf nodes.
|
||||
/// Other nodes can merge AABB of child nodes.
|
||||
void setBoundingBox (const Ogre::AxisAlignedBox& box);
|
||||
|
||||
/// Get bounding box in local coordinates
|
||||
const Ogre::AxisAlignedBox& getBoundingBox();
|
||||
|
||||
const Ogre::AxisAlignedBox& getWorldBoundingBox();
|
||||
|
||||
DefaultWorld* getTerrain() { return mTerrain; }
|
||||
|
||||
/// Adjust LODs for the given camera position, possibly splitting up chunks or merging them.
|
||||
/// @return Did we (or all of our children) choose to render?
|
||||
bool update (const Ogre::Vector3& cameraPos);
|
||||
|
||||
/// Adjust index buffers of chunks to stitch together chunks of different LOD, so that cracks are avoided.
|
||||
/// Call after QuadTreeNode::update!
|
||||
void updateIndexBuffers();
|
||||
|
||||
/// Destroy chunks rendered by this node *and* its children (if param is true)
|
||||
void destroyChunks(bool children);
|
||||
|
||||
/// Get the effective LOD level if this node was rendered in one chunk
|
||||
/// with Storage::getCellVertices^2 vertices
|
||||
size_t getNativeLodLevel() { return mLodLevel; }
|
||||
|
||||
/// Get the effective current LOD level used by the chunk rendering this node
|
||||
size_t getActualLodLevel();
|
||||
|
||||
/// Is this node currently configured to render itself?
|
||||
bool hasChunk();
|
||||
|
||||
/// Add a textured quad to a specific 2d area in the composite map scenemanager.
|
||||
/// Only nodes with size <= 1 can be rendered with alpha blending, so larger nodes will simply
|
||||
/// call this method on their children.
|
||||
/// @note Do not call this before World::areLayersLoaded() == true
|
||||
/// @param area area in image space to put the quad
|
||||
void prepareForCompositeMap(Ogre::TRect<float> area);
|
||||
|
||||
/// Create a chunk for this node from the given data.
|
||||
void load (const LoadResponseData& data);
|
||||
void unload(bool recursive=false);
|
||||
void loadLayers (const LayerCollection& collection);
|
||||
/// This is recursive! Call it once on the root node after all leafs have loaded layers.
|
||||
void loadMaterials();
|
||||
|
||||
LoadState getLoadState() { return mLoadState; }
|
||||
|
||||
private:
|
||||
// Stored here for convenience in case we need layer list again
|
||||
MaterialGenerator* mMaterialGenerator;
|
||||
|
||||
LoadState mLoadState;
|
||||
|
||||
bool mIsDummy;
|
||||
float mSize;
|
||||
size_t mLodLevel; // LOD if we were to render this node in one chunk
|
||||
Ogre::AxisAlignedBox mBounds;
|
||||
Ogre::AxisAlignedBox mWorldBounds;
|
||||
ChildDirection mDirection;
|
||||
Ogre::Vector2 mCenter;
|
||||
|
||||
Ogre::SceneNode* mSceneNode;
|
||||
|
||||
QuadTreeNode* mParent;
|
||||
QuadTreeNode* mChildren[4];
|
||||
QuadTreeNode* mNeighbours[4];
|
||||
|
||||
Chunk* mChunk;
|
||||
|
||||
DefaultWorld* mTerrain;
|
||||
|
||||
Ogre::TexturePtr mCompositeMap;
|
||||
|
||||
void ensureCompositeMap();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
Loading…
Reference in New Issue