forked from teamnwah/openmw-tes3coop
e27437f8ed
- Consistent triangle alignment, fixes a noticable crack near the census and excise office. Note that alignment is still not the same as vanilla. Vanilla uses a weird diagonal pattern. I hope there aren't more trouble spots that will force us to replicate vanilla, but at least we can do that now. - Fixes several blending issues and cell border seams - Fix map render to use the terrain bounding box instead of an arbitrary height - Different LODs are now properly connected instead of using skirts - Support self shadowing - Normals and colors are stored in the vertices instead of a texture, this enables per-vertex lighting which should improve performance, fix compatibility issues due to the PS getting too large and mimic vanilla better - Support a fixed function fallback (though the splatting shader usually performs better) - Designed for distant land support - test: https://www.youtube.com/watch?v=2wnd9EuPJIY - we can't really enable this yet due to depth precision issues when using a large view distance
359 lines
14 KiB
C++
359 lines
14 KiB
C++
#include "terrain.hpp"
|
|
|
|
#include <OgreAxisAlignedBox.h>
|
|
#include <OgreCamera.h>
|
|
#include <OgreHardwareBufferManager.h>
|
|
#include <OgreHardwarePixelBuffer.h>
|
|
#include <OgreRoot.h>
|
|
|
|
#include <components/esm/loadland.hpp>
|
|
|
|
#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
|
|
{
|
|
|
|
Terrain::Terrain(Ogre::SceneManager* sceneMgr, Storage* storage, int visibilityFlags)
|
|
: mStorage(storage)
|
|
, mMinBatchSize(1)
|
|
, mMaxBatchSize(64)
|
|
, mSceneMgr(sceneMgr)
|
|
, mVisibilityFlags(visibilityFlags)
|
|
{
|
|
mCompositeMapSceneMgr = Ogre::Root::getSingleton().createSceneManager(Ogre::ST_GENERIC);
|
|
|
|
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);
|
|
|
|
mBounds = storage->getBounds();
|
|
|
|
int origSizeX = mBounds.getSize().x;
|
|
int origSizeY = mBounds.getSize().y;
|
|
|
|
// 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
|
|
Ogre::Vector3 center = mBounds.getCenter() + Ogre::Vector3((size-origSizeX)/2.f, (size-origSizeY)/2.f, 0);
|
|
|
|
mRootNode = new QuadTreeNode(this, Root, size, Ogre::Vector2(center.x, center.y), NULL);
|
|
buildQuadTree(mRootNode);
|
|
}
|
|
|
|
Terrain::~Terrain()
|
|
{
|
|
delete mRootNode;
|
|
delete mStorage;
|
|
}
|
|
|
|
void Terrain::buildQuadTree(QuadTreeNode *node)
|
|
{
|
|
float halfSize = node->getSize()/2.f;
|
|
|
|
if (node->getSize() <= mMinBatchSize)
|
|
{
|
|
// We arrived at a leaf
|
|
float minZ,maxZ;
|
|
Ogre::Vector2 center = node->getCenter();
|
|
if (mStorage->getMinMaxHeights(node->getSize(), center, minZ, maxZ))
|
|
node->setBoundingBox(Ogre::AxisAlignedBox(Ogre::Vector3(-halfSize*8192, -halfSize*8192, minZ),
|
|
Ogre::Vector3(halfSize*8192, halfSize*8192, maxZ)));
|
|
else
|
|
node->markAsDummy(); // no data available for this node, skip it
|
|
return;
|
|
}
|
|
|
|
if (node->getCenter().x - halfSize > mBounds.getMaximum().x
|
|
|| node->getCenter().x + halfSize < mBounds.getMinimum().x
|
|
|| node->getCenter().y - halfSize > mBounds.getMaximum().y
|
|
|| node->getCenter().y + halfSize < mBounds.getMinimum().y )
|
|
// 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));
|
|
buildQuadTree(node->getChild(SE));
|
|
buildQuadTree(node->getChild(NW));
|
|
buildQuadTree(node->getChild(NE));
|
|
|
|
// 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 Terrain::update(Ogre::Camera *camera)
|
|
{
|
|
mRootNode->update(camera->getRealPosition());
|
|
mRootNode->updateIndexBuffers();
|
|
}
|
|
|
|
Ogre::AxisAlignedBox Terrain::getWorldBoundingBox (const Ogre::Vector2& center)
|
|
{
|
|
QuadTreeNode* node = findNode(center, mRootNode);
|
|
Ogre::AxisAlignedBox box = node->getBoundingBox();
|
|
box.setExtents(box.getMinimum() + Ogre::Vector3(center.x, center.y, 0) * 8192,
|
|
box.getMaximum() + Ogre::Vector3(center.x, center.y, 0) * 8192);
|
|
return box;
|
|
}
|
|
|
|
Ogre::HardwareVertexBufferSharedPtr Terrain::getVertexBuffer(int numVertsOneSide)
|
|
{
|
|
if (mUvBufferMap.find(numVertsOneSide) != mUvBufferMap.end())
|
|
{
|
|
return mUvBufferMap[numVertsOneSide];
|
|
}
|
|
|
|
int vertexCount = numVertsOneSide * numVertsOneSide;
|
|
|
|
std::vector<float> uvs;
|
|
uvs.reserve(vertexCount*2);
|
|
|
|
for (int col = 0; col < numVertsOneSide; ++col)
|
|
{
|
|
for (int row = 0; row < numVertsOneSide; ++row)
|
|
{
|
|
uvs.push_back(col / static_cast<float>(numVertsOneSide-1)); // U
|
|
uvs.push_back(row / static_cast<float>(numVertsOneSide-1)); // V
|
|
}
|
|
}
|
|
|
|
Ogre::HardwareBufferManager* mgr = Ogre::HardwareBufferManager::getSingletonPtr();
|
|
Ogre::HardwareVertexBufferSharedPtr buffer = mgr->createVertexBuffer(
|
|
Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT2),
|
|
vertexCount, Ogre::HardwareBuffer::HBU_STATIC);
|
|
|
|
buffer->writeData(0, buffer->getSizeInBytes(), &uvs[0], true);
|
|
|
|
mUvBufferMap[numVertsOneSide] = buffer;
|
|
return buffer;
|
|
}
|
|
|
|
Ogre::HardwareIndexBufferSharedPtr Terrain::getIndexBuffer(int flags, size_t& numIndices)
|
|
{
|
|
if (mIndexBufferMap.find(flags) != mIndexBufferMap.end())
|
|
{
|
|
numIndices = mIndexBufferMap[flags]->getNumIndexes();
|
|
return mIndexBufferMap[flags];
|
|
}
|
|
|
|
// LOD level n means every 2^n-th vertex is kept
|
|
size_t lodLevel = (flags >> (4*4));
|
|
|
|
size_t lodDeltas[4];
|
|
for (int i=0; i<4; ++i)
|
|
lodDeltas[i] = (flags >> (4*i)) & (0xf);
|
|
|
|
bool anyDeltas = (lodDeltas[North] || lodDeltas[South] || lodDeltas[West] || lodDeltas[East]);
|
|
|
|
size_t increment = std::pow(2, lodLevel);
|
|
assert((int)increment < ESM::Land::LAND_SIZE);
|
|
std::vector<short> indices;
|
|
indices.reserve((ESM::Land::LAND_SIZE-1)*(ESM::Land::LAND_SIZE-1)*2*3 / increment);
|
|
|
|
size_t rowStart = 0, colStart = 0, rowEnd = ESM::Land::LAND_SIZE-1, colEnd = ESM::Land::LAND_SIZE-1;
|
|
// If any edge needs stitching we'll skip all edges at this point,
|
|
// mainly because stitching one edge would have an effect on corners and on the adjacent edges
|
|
if (anyDeltas)
|
|
{
|
|
colStart += increment;
|
|
colEnd -= increment;
|
|
rowEnd -= increment;
|
|
rowStart += increment;
|
|
}
|
|
for (size_t row = rowStart; row < rowEnd; row += increment)
|
|
{
|
|
for (size_t col = colStart; col < colEnd; col += increment)
|
|
{
|
|
indices.push_back(ESM::Land::LAND_SIZE*col+row);
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+increment)+row);
|
|
indices.push_back(ESM::Land::LAND_SIZE*col+row+increment);
|
|
|
|
indices.push_back(ESM::Land::LAND_SIZE*col+row+increment);
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+increment)+row);
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+increment)+row+increment);
|
|
}
|
|
}
|
|
|
|
size_t innerStep = increment;
|
|
if (anyDeltas)
|
|
{
|
|
// Now configure LOD transitions at the edges - this is pretty tedious,
|
|
// and some very long and boring code, but it works great
|
|
|
|
// South
|
|
size_t row = 0;
|
|
size_t outerStep = std::pow(2, lodDeltas[South] + lodLevel);
|
|
for (size_t col = 0; col < ESM::Land::LAND_SIZE-1; col += outerStep)
|
|
{
|
|
indices.push_back(ESM::Land::LAND_SIZE*col+row);
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep)+row);
|
|
// Make sure not to touch the left edge
|
|
if (col == 0)
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row+innerStep);
|
|
else
|
|
indices.push_back(ESM::Land::LAND_SIZE*col+row+innerStep);
|
|
for (size_t i = 0; i < outerStep; i += innerStep)
|
|
{
|
|
// Make sure not to touch the left or right edges
|
|
if (col+i == 0 || col+i == ESM::Land::LAND_SIZE-1-innerStep)
|
|
continue;
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep)+row);
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+i+innerStep)+row+innerStep);
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+i)+row+innerStep);
|
|
}
|
|
}
|
|
|
|
// North
|
|
row = ESM::Land::LAND_SIZE-1;
|
|
outerStep = std::pow(2, lodDeltas[North] + lodLevel);
|
|
for (size_t col = 0; col < ESM::Land::LAND_SIZE-1; col += outerStep)
|
|
{
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep)+row);
|
|
indices.push_back(ESM::Land::LAND_SIZE*col+row);
|
|
// Make sure not to touch the right edge
|
|
if (col+outerStep == ESM::Land::LAND_SIZE-1)
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep-innerStep)+row-innerStep);
|
|
else
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+outerStep)+row-innerStep);
|
|
|
|
for (size_t i = 0; i < outerStep; i += innerStep)
|
|
{
|
|
// Make sure not to touch the left or right edges
|
|
if (col+i == 0 || col+i == ESM::Land::LAND_SIZE-1-innerStep)
|
|
continue;
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+i)+row-innerStep);
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+i+innerStep)+row-innerStep);
|
|
indices.push_back(ESM::Land::LAND_SIZE*col+row);
|
|
}
|
|
}
|
|
|
|
// West
|
|
size_t col = 0;
|
|
outerStep = std::pow(2, lodDeltas[West] + lodLevel);
|
|
for (size_t row = 0; row < ESM::Land::LAND_SIZE-1; row += outerStep)
|
|
{
|
|
indices.push_back(ESM::Land::LAND_SIZE*col+row+outerStep);
|
|
indices.push_back(ESM::Land::LAND_SIZE*col+row);
|
|
// Make sure not to touch the bottom edge
|
|
if (row == 0)
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row+innerStep);
|
|
else
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row);
|
|
|
|
for (size_t i = 0; i < outerStep; i += innerStep)
|
|
{
|
|
// Make sure not to touch the top or bottom edges
|
|
if (row+i == 0 || row+i == ESM::Land::LAND_SIZE-1-innerStep)
|
|
continue;
|
|
indices.push_back(ESM::Land::LAND_SIZE*col+row+outerStep);
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row+i);
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col+innerStep)+row+i+innerStep);
|
|
}
|
|
}
|
|
|
|
// East
|
|
col = ESM::Land::LAND_SIZE-1;
|
|
outerStep = std::pow(2, lodDeltas[East] + lodLevel);
|
|
for (size_t row = 0; row < ESM::Land::LAND_SIZE-1; row += outerStep)
|
|
{
|
|
indices.push_back(ESM::Land::LAND_SIZE*col+row);
|
|
indices.push_back(ESM::Land::LAND_SIZE*col+row+outerStep);
|
|
// Make sure not to touch the top edge
|
|
if (row+outerStep == ESM::Land::LAND_SIZE-1)
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col-innerStep)+row+outerStep-innerStep);
|
|
else
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col-innerStep)+row+outerStep);
|
|
|
|
for (size_t i = 0; i < outerStep; i += innerStep)
|
|
{
|
|
// Make sure not to touch the top or bottom edges
|
|
if (row+i == 0 || row+i == ESM::Land::LAND_SIZE-1-innerStep)
|
|
continue;
|
|
indices.push_back(ESM::Land::LAND_SIZE*col+row);
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col-innerStep)+row+i+innerStep);
|
|
indices.push_back(ESM::Land::LAND_SIZE*(col-innerStep)+row+i);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
numIndices = indices.size();
|
|
|
|
Ogre::HardwareBufferManager* mgr = Ogre::HardwareBufferManager::getSingletonPtr();
|
|
Ogre::HardwareIndexBufferSharedPtr buffer = mgr->createIndexBuffer(Ogre::HardwareIndexBuffer::IT_16BIT,
|
|
numIndices, Ogre::HardwareBuffer::HBU_STATIC);
|
|
buffer->writeData(0, buffer->getSizeInBytes(), &indices[0], true);
|
|
mIndexBufferMap[flags] = buffer;
|
|
return buffer;
|
|
}
|
|
|
|
void Terrain::renderCompositeMap(Ogre::TexturePtr target)
|
|
{
|
|
mCompositeMapRenderTarget->update();
|
|
target->getBuffer()->blit(mCompositeMapRenderTexture->getBuffer());
|
|
}
|
|
|
|
void Terrain::clearCompositeMapSceneManager()
|
|
{
|
|
mCompositeMapSceneMgr->destroyAllManualObjects();
|
|
mCompositeMapSceneMgr->clearScene();
|
|
}
|
|
|
|
|
|
}
|