From e27437f8ede5da7d7e58280f8f96559171e654ef Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 16 Aug 2013 13:01:52 +0200 Subject: [PATCH] New terrain renderer - improvements: - 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 --- apps/openmw/CMakeLists.txt | 3 +- apps/openmw/engine.cpp | 6 +- apps/openmw/mwrender/globalmap.cpp | 11 +- apps/openmw/mwrender/localmap.cpp | 8 +- apps/openmw/mwrender/localmap.hpp | 10 +- apps/openmw/mwrender/renderingmanager.cpp | 37 +- apps/openmw/mwrender/renderingmanager.hpp | 8 +- apps/openmw/mwrender/shadows.cpp | 1 - apps/openmw/mwrender/terrain.cpp | 531 ---------------------- apps/openmw/mwrender/terrain.hpp | 128 ------ apps/openmw/mwrender/terrainmaterial.cpp | 246 ---------- apps/openmw/mwrender/terrainmaterial.hpp | 88 ---- apps/openmw/mwrender/terrainstorage.cpp | 56 +++ apps/openmw/mwrender/terrainstorage.hpp | 22 + apps/openmw/mwrender/water.cpp | 5 +- apps/openmw/mwworld/scene.cpp | 4 +- apps/openmw/mwworld/worldimp.cpp | 6 +- components/CMakeLists.txt | 4 + components/esm/loadland.hpp | 2 +- components/terrain/chunk.cpp | 171 +++++++ components/terrain/chunk.hpp | 63 +++ components/terrain/material.cpp | 291 ++++++++++++ components/terrain/material.hpp | 54 +++ components/terrain/quadtreenode.cpp | 387 ++++++++++++++++ components/terrain/quadtreenode.hpp | 141 ++++++ components/terrain/storage.cpp | 318 +++++++++++++ components/terrain/storage.hpp | 76 ++++ components/terrain/terrain.cpp | 359 +++++++++++++++ components/terrain/terrain.hpp | 121 +++++ files/materials/objects.shader | 1 - files/materials/terrain.shader | 162 ++++--- libs/openengine/ogre/imagerotate.cpp | 4 +- 32 files changed, 2203 insertions(+), 1121 deletions(-) delete mode 100644 apps/openmw/mwrender/terrain.cpp delete mode 100644 apps/openmw/mwrender/terrain.hpp delete mode 100644 apps/openmw/mwrender/terrainmaterial.cpp delete mode 100644 apps/openmw/mwrender/terrainmaterial.hpp create mode 100644 apps/openmw/mwrender/terrainstorage.cpp create mode 100644 apps/openmw/mwrender/terrainstorage.hpp create mode 100644 components/terrain/chunk.cpp create mode 100644 components/terrain/chunk.hpp create mode 100644 components/terrain/material.cpp create mode 100644 components/terrain/material.hpp create mode 100644 components/terrain/quadtreenode.cpp create mode 100644 components/terrain/quadtreenode.hpp create mode 100644 components/terrain/storage.cpp create mode 100644 components/terrain/storage.hpp create mode 100644 components/terrain/terrain.cpp create mode 100644 components/terrain/terrain.hpp diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index eebecc09c4..8259ac8cfe 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -15,8 +15,9 @@ source_group(game FILES ${GAME} ${GAME_HEADER}) add_openmw_dir (mwrender renderingmanager debugging sky camera animation npcanimation creatureanimation activatoranimation - actors objects renderinginterface localmap occlusionquery terrain terrainmaterial water shadows + actors objects renderinginterface localmap occlusionquery water shadows compositors characterpreview externalrendering globalmap videoplayer ripplesimulation refraction + terrainstorage ) add_openmw_dir (mwinput diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 9516da5ae0..23e94cb51d 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -91,12 +91,12 @@ bool OMW::Engine::frameRenderingQueued (const Ogre::FrameEvent& evt) MWBase::Environment::get().getSoundManager()->update(frametime); // global scripts - MWBase::Environment::get().getScriptManager()->getGlobalScripts().run(); + //MWBase::Environment::get().getScriptManager()->getGlobalScripts().run(); bool changed = MWBase::Environment::get().getWorld()->hasCellChanged(); // local scripts - executeLocalScripts(); // This does not handle the case where a global script causes a cell + //executeLocalScripts(); // This does not handle the case where a global script causes a cell // change, followed by a cell change in a local script during the same // frame. @@ -597,4 +597,4 @@ void OMW::Engine::setStartupScript (const std::string& path) void OMW::Engine::setActivationDistanceOverride (int distance) { mActivationDistanceOverride = distance; -} \ No newline at end of file +} diff --git a/apps/openmw/mwrender/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp index 1ff99dda8c..78b13ec07e 100644 --- a/apps/openmw/mwrender/globalmap.cpp +++ b/apps/openmw/mwrender/globalmap.cpp @@ -58,8 +58,6 @@ namespace MWRender //if (!boost::filesystem::exists(mCacheDir + "/GlobalMap.png")) if (1) { - Ogre::Image image; - std::vector data (mWidth * mHeight * 3); for (int x = mMinX; x <= mMaxX; ++x) @@ -144,9 +142,6 @@ namespace MWRender b = waterDeepColour.b * 255; } - // uncomment this line to outline cell borders - //if (cellX == 0 || cellX == cellSize-1 || cellY == 0|| cellY == cellSize-1) r = 255; - data[texelY * mWidth * 3 + texelX * 3] = r; data[texelY * mWidth * 3 + texelX * 3+1] = g; data[texelY * mWidth * 3 + texelX * 3+2] = b; @@ -155,13 +150,11 @@ namespace MWRender } } - image.loadDynamicImage (&data[0], mWidth, mHeight, Ogre::PF_B8G8R8); - - //image.save (mCacheDir + "/GlobalMap.png"); + Ogre::DataStreamPtr stream(new Ogre::MemoryDataStream(&data[0], data.size())); tex = Ogre::TextureManager::getSingleton ().createManual ("GlobalMap.png", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, Ogre::TEX_TYPE_2D, mWidth, mHeight, 0, Ogre::PF_B8G8R8, Ogre::TU_STATIC); - tex->loadImage(image); + tex->loadRawData(stream, mWidth, mHeight, Ogre::PF_B8G8R8); } else tex = Ogre::TextureManager::getSingleton ().getByName ("GlobalMap.png"); diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 8043f8b122..c414844520 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -2,6 +2,10 @@ #include #include +#include +#include +#include +#include #include "../mwworld/esmstore.hpp" @@ -104,7 +108,7 @@ void LocalMap::saveFogOfWar(MWWorld::Ptr::CellStore* cell) } } -void LocalMap::requestMap(MWWorld::Ptr::CellStore* cell) +void LocalMap::requestMap(MWWorld::Ptr::CellStore* cell, float zMin, float zMax) { mInterior = false; @@ -118,7 +122,7 @@ void LocalMap::requestMap(MWWorld::Ptr::CellStore* cell) mCameraPosNode->setPosition(Vector3(0,0,0)); - render((x+0.5)*sSize, (y+0.5)*sSize, -10000, 10000, sSize, sSize, name); + render((x+0.5)*sSize, (y+0.5)*sSize, zMin, zMax, sSize, sSize, name); } void LocalMap::requestMap(MWWorld::Ptr::CellStore* cell, diff --git a/apps/openmw/mwrender/localmap.hpp b/apps/openmw/mwrender/localmap.hpp index 72e637d9ab..5384896407 100644 --- a/apps/openmw/mwrender/localmap.hpp +++ b/apps/openmw/mwrender/localmap.hpp @@ -28,16 +28,18 @@ namespace MWRender * Request the local map for an exterior cell. * @remarks It will either be loaded from a disk cache, * or rendered if it is not already cached. - * @param exterior cell + * @param cell exterior cell + * @param zMin min height of objects or terrain in cell + * @param zMax max height of objects or terrain in cell */ - void requestMap (MWWorld::CellStore* cell); + void requestMap (MWWorld::CellStore* cell, float zMin, float zMax); /** * Request the local map for an interior cell. * @remarks It will either be loaded from a disk cache, * or rendered if it is not already cached. - * @param interior cell - * @param bounding box of the cell + * @param cell interior cell + * @param bounds bounding box of the cell */ void requestMap (MWWorld::CellStore* cell, Ogre::AxisAlignedBox bounds); diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index f3e5188005..9f7fde10ad 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -25,6 +25,8 @@ #include #include +#include + #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" @@ -46,6 +48,7 @@ #include "externalrendering.hpp" #include "globalmap.hpp" #include "videoplayer.hpp" +#include "terrainstorage.hpp" using namespace MWRender; using namespace Ogre; @@ -63,6 +66,7 @@ RenderingManager::RenderingManager(OEngine::Render::OgreRenderer& _rend, const b , mAmbientMode(0) , mSunEnabled(0) , mPhysicsEngine(engine) + , mTerrain(NULL) { // select best shader mode bool openGL = (Ogre::Root::getSingleton ().getRenderSystem ()->getName().find("OpenGL") != std::string::npos); @@ -78,7 +82,7 @@ RenderingManager::RenderingManager(OEngine::Render::OgreRenderer& _rend, const b Settings::Manager::setString("shader mode", "General", openGL ? (glES ? "glsles" : "glsl") : "hlsl"); } - mRendering.createScene("PlayerCam", Settings::Manager::getFloat("field of view", "General"), 5); + mRendering.createScene("PlayerCam", Settings::Manager::getFloat("field of view", "General"), 50); mRendering.getWindow()->addListener(this); mRendering.setWindowListener(this); @@ -166,8 +170,6 @@ RenderingManager::RenderingManager(OEngine::Render::OgreRenderer& _rend, const b mShadows = new Shadows(&mRendering); - mTerrainManager = new TerrainManager(mRendering.getScene(), this); - mSkyManager = new SkyManager(mRootNode, mRendering.getCamera()); mOcclusionQuery = new OcclusionQuery(&mRendering, mSkyManager->getSunNode()); @@ -194,7 +196,7 @@ RenderingManager::~RenderingManager () delete mSkyManager; delete mDebugging; delete mShadows; - delete mTerrainManager; + delete mTerrain; delete mLocalMap; delete mOcclusionQuery; delete mCompositors; @@ -225,8 +227,6 @@ void RenderingManager::removeCell (MWWorld::Ptr::CellStore *store) mObjects.removeCell(store); mActors.removeCell(store); mDebugging->cellRemoved(store); - if (store->mCell->isExterior()) - mTerrainManager->cellRemoved(store); } void RenderingManager::removeWater () @@ -244,8 +244,14 @@ void RenderingManager::cellAdded (MWWorld::Ptr::CellStore *store) mObjects.buildStaticGeometry (*store); sh::Factory::getInstance().unloadUnreferencedMaterials(); mDebugging->cellAdded(store); - if (store->mCell->isExterior()) - mTerrainManager->cellAdded(store); + if (store->isExterior()) + { + if (!mTerrain) + { + mTerrain = new Terrain::Terrain(mRendering.getScene(), new MWRender::TerrainStorage(), RV_Terrain); + mTerrain->update(mRendering.getCamera()); + } + } waterAdded(store); } @@ -550,7 +556,8 @@ void RenderingManager::setAmbientMode() float RenderingManager::getTerrainHeightAt(Ogre::Vector3 worldPos) { - return mTerrainManager->getTerrainHeightAt(worldPos); + assert(mTerrain); + return mTerrain->getHeightAt(worldPos); } @@ -595,7 +602,6 @@ void RenderingManager::setSunColour(const Ogre::ColourValue& colour) if (!mSunEnabled) return; mSun->setDiffuseColour(colour); mSun->setSpecularColour(colour); - mTerrainManager->setDiffuse(colour); } void RenderingManager::setAmbientColour(const Ogre::ColourValue& colour) @@ -608,7 +614,6 @@ void RenderingManager::setAmbientColour(const Ogre::ColourValue& colour) final += Ogre::ColourValue(0.7,0.7,0.7,0) * std::min(1.f, (nightEye/100.f)); mRendering.getScene()->setAmbientLight(final); - mTerrainManager->setAmbient(final); } void RenderingManager::sunEnable(bool real) @@ -652,7 +657,12 @@ void RenderingManager::setGlare(bool glare) void RenderingManager::requestMap(MWWorld::Ptr::CellStore* cell) { if (cell->mCell->isExterior()) - mLocalMap->requestMap(cell); + { + Ogre::AxisAlignedBox dims = mObjects.getDimensions(cell); + Ogre::Vector2 center(cell->mCell->getGridX() + 0.5, -cell->mCell->getGridY() + 1 - 0.5); + dims.merge(mTerrain->getWorldBoundingBox(center)); + mLocalMap->requestMap(cell, dims.getMinimum().z, dims.getMaximum().z); + } else mLocalMap->requestMap(cell, mObjects.getDimensions(cell)); } @@ -984,6 +994,9 @@ void RenderingManager::updateWaterRippleEmitterPtr (const MWWorld::Ptr& old, con void RenderingManager::frameStarted(float dt) { + if (mTerrain) + mTerrain->update(mRendering.getCamera()); + mWater->frameStarted(dt); } diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index 17cdfff4ae..c4fb05804c 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -2,7 +2,6 @@ #define _GAME_RENDERING_MANAGER_H #include "sky.hpp" -#include "terrain.hpp" #include "debugging.hpp" #include @@ -39,6 +38,11 @@ namespace sh class Factory; } +namespace Terrain +{ + class Terrain; +} + namespace MWRender { class Shadows; @@ -228,7 +232,7 @@ private: OcclusionQuery* mOcclusionQuery; - TerrainManager* mTerrainManager; + Terrain::Terrain* mTerrain; MWRender::Water *mWater; diff --git a/apps/openmw/mwrender/shadows.cpp b/apps/openmw/mwrender/shadows.cpp index 0d066a0ecb..c28c01dccb 100644 --- a/apps/openmw/mwrender/shadows.cpp +++ b/apps/openmw/mwrender/shadows.cpp @@ -108,7 +108,6 @@ void Shadows::recreate() int visibilityMask = RV_Actors * Settings::Manager::getBool("actor shadows", "Shadows") + (RV_Statics + RV_StaticsSmall) * Settings::Manager::getBool("statics shadows", "Shadows") + RV_Misc * Settings::Manager::getBool("misc shadows", "Shadows"); - for (int i = 0; i < (split ? 3 : 1); ++i) { TexturePtr shadowTexture = mSceneMgr->getShadowTexture(i); diff --git a/apps/openmw/mwrender/terrain.cpp b/apps/openmw/mwrender/terrain.cpp deleted file mode 100644 index 1b829a2c25..0000000000 --- a/apps/openmw/mwrender/terrain.cpp +++ /dev/null @@ -1,531 +0,0 @@ -#include - -#include -#include -#include -#include - -#include "../mwworld/esmstore.hpp" - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "terrainmaterial.hpp" -#include "terrain.hpp" -#include "renderconst.hpp" -#include "shadows.hpp" -#include "renderingmanager.hpp" - -using namespace Ogre; - -namespace MWRender -{ - - //---------------------------------------------------------------------------------------------- - - TerrainManager::TerrainManager(Ogre::SceneManager* mgr, RenderingManager* rend) : - mTerrainGroup(TerrainGroup(mgr, Terrain::ALIGN_X_Y, mLandSize, mWorldSize)), mRendering(rend) - { - mTerrainGlobals = OGRE_NEW TerrainGlobalOptions(); - - TerrainMaterialGeneratorPtr matGen; - TerrainMaterial* matGenP = new TerrainMaterial(); - matGen.bind(matGenP); - mTerrainGlobals->setDefaultMaterialGenerator(matGen); - - TerrainMaterialGenerator::Profile* const activeProfile = - mTerrainGlobals->getDefaultMaterialGenerator() - ->getActiveProfile(); - mActiveProfile = static_cast(activeProfile); - - // We don't want any pixel error at all. Really, LOD makes no sense here - morrowind uses 65x65 verts in one cell, - // so applying LOD is most certainly slower than doing no LOD at all. - // Setting this to 0 seems to cause glitches though. :/ - mTerrainGlobals->setMaxPixelError(1); - - mTerrainGlobals->setLayerBlendMapSize(ESM::Land::LAND_TEXTURE_SIZE/2 + 1); - - //10 (default) didn't seem to be quite enough - mTerrainGlobals->setSkirtSize(128); - - //due to the sudden flick between composite and non composite textures, - //this seemed the distance where it wasn't too noticeable - mTerrainGlobals->setCompositeMapDistance(mWorldSize*2); - - mTerrainGroup.setOrigin(Vector3(mWorldSize/2, - mWorldSize/2, - 0)); - - Terrain::ImportData& importSettings = mTerrainGroup.getDefaultImportSettings(); - - importSettings.inputBias = 0; - importSettings.terrainSize = mLandSize; - importSettings.worldSize = mWorldSize; - importSettings.minBatchSize = 9; - importSettings.maxBatchSize = mLandSize; - - importSettings.deleteInputData = true; - } - - //---------------------------------------------------------------------------------------------- - - float TerrainManager::getTerrainHeightAt(Vector3 worldPos) - { - Ogre::Terrain* terrain = NULL; - float height = mTerrainGroup.getHeightAtWorldPosition(worldPos, &terrain); - if (terrain == NULL) - return std::numeric_limits().min(); - return height; - } - - //---------------------------------------------------------------------------------------------- - - TerrainManager::~TerrainManager() - { - OGRE_DELETE mTerrainGlobals; - } - - //---------------------------------------------------------------------------------------------- - - void TerrainManager::setDiffuse(const ColourValue& diffuse) - { - mTerrainGlobals->setCompositeMapDiffuse(diffuse); - } - - //---------------------------------------------------------------------------------------------- - - void TerrainManager::setAmbient(const ColourValue& ambient) - { - mTerrainGlobals->setCompositeMapAmbient(ambient); - } - - //---------------------------------------------------------------------------------------------- - - void TerrainManager::cellAdded(MWWorld::Ptr::CellStore *store) - { - const int cellX = store->mCell->getGridX(); - const int cellY = store->mCell->getGridY(); - - ESM::Land* land = - MWBase::Environment::get().getWorld()->getStore().get().search(cellX, cellY); - if (land == NULL) // no land data means we're not going to create any terrain. - return; - - int dataRequired = ESM::Land::DATA_VHGT | ESM::Land::DATA_VCLR; - if (!land->isDataLoaded(dataRequired)) - { - land->loadData(dataRequired); - } - - //split the cell terrain into four segments - const int numTextures = ESM::Land::LAND_TEXTURE_SIZE/2; - - for ( int x = 0; x < 2; x++ ) - { - for ( int y = 0; y < 2; y++ ) - { - Terrain::ImportData terrainData = - mTerrainGroup.getDefaultImportSettings(); - - const int terrainX = cellX * 2 + x; - const int terrainY = cellY * 2 + y; - - //it makes far more sense to reallocate the memory here, - //and let Ogre deal with it due to the issues with deleting - //it at the wrong time if using threads (Which Terrain does) - terrainData.inputFloat = OGRE_ALLOC_T(float, - mLandSize*mLandSize, - MEMCATEGORY_GEOMETRY); - - //copy the height data row by row - for ( int terrainCopyY = 0; terrainCopyY < mLandSize; terrainCopyY++ ) - { - //the offset of the current segment - const size_t yOffset = y * (mLandSize-1) * ESM::Land::LAND_SIZE + - //offset of the row - terrainCopyY * ESM::Land::LAND_SIZE; - const size_t xOffset = x * (mLandSize-1); - - memcpy(&terrainData.inputFloat[terrainCopyY*mLandSize], - &land->mLandData->mHeights[yOffset + xOffset], - mLandSize*sizeof(float)); - } - - std::map indexes; - initTerrainTextures(&terrainData, cellX, cellY, - x * numTextures, y * numTextures, - numTextures, indexes, land->mPlugin); - - if (mTerrainGroup.getTerrain(terrainX, terrainY) == NULL) - { - mTerrainGroup.defineTerrain(terrainX, terrainY, &terrainData); - - mTerrainGroup.loadTerrain(terrainX, terrainY, true); - - Terrain* terrain = mTerrainGroup.getTerrain(terrainX, terrainY); - initTerrainBlendMaps(terrain, - cellX, cellY, - x * numTextures, y * numTextures, - numTextures, - indexes); - terrain->setVisibilityFlags(RV_Terrain); - terrain->setRenderQueueGroup(RQG_Main); - - // disable or enable global colour map (depends on available vertex colours) - if ( land->mLandData->mUsingColours ) - { - TexturePtr vertex = getVertexColours(land, - cellX, cellY, - x*(mLandSize-1), - y*(mLandSize-1), - mLandSize); - - mActiveProfile->setGlobalColourMapEnabled(true); - mActiveProfile->setGlobalColourMap (terrain, vertex->getName()); - } - else - mActiveProfile->setGlobalColourMapEnabled (false); - } - } - } - - // when loading from a heightmap, Ogre::Terrain does not update the derived data (normal map, LOD) - // synchronously, even if we supply synchronous = true parameter to loadTerrain. - // the following to be the only way to make sure derived data is ready when rendering the next frame. - while (mTerrainGroup.isDerivedDataUpdateInProgress()) - { - // we need to wait for this to finish - OGRE_THREAD_SLEEP(5); - Root::getSingleton().getWorkQueue()->processResponses(); - } - - mTerrainGroup.freeTemporaryResources(); - } - - //---------------------------------------------------------------------------------------------- - - void TerrainManager::cellRemoved(MWWorld::Ptr::CellStore *store) - { - for ( int x = 0; x < 2; x++ ) - { - for ( int y = 0; y < 2; y++ ) - { - int terrainX = store->mCell->getGridX() * 2 + x; - int terrainY = store->mCell->getGridY() * 2 + y; - if (mTerrainGroup.getTerrain(terrainX, terrainY) != NULL) - mTerrainGroup.unloadTerrain(terrainX, terrainY); - } - } - } - - //---------------------------------------------------------------------------------------------- - - void TerrainManager::initTerrainTextures(Terrain::ImportData* terrainData, - int cellX, int cellY, - int fromX, int fromY, int size, - std::map& indexes, size_t plugin) - { - // FIXME: In a multiple esm configuration, we have multiple palettes. Since this code - // crosses cell boundaries, we no longer have a unique terrain palette. Instead, we need - // to adopt the following code for a dynamic palette. And this is evil - the current design - // does not work well for this task... - - assert(terrainData != NULL && "Must have valid terrain data"); - assert(fromX >= 0 && fromY >= 0 && - "Can't get a terrain texture on terrain outside the current cell"); - assert(fromX+size <= ESM::Land::LAND_TEXTURE_SIZE && - fromY+size <= ESM::Land::LAND_TEXTURE_SIZE && - "Can't get a terrain texture on terrain outside the current cell"); - - //this ensures that the ltex indexes are sorted (or retrived as sorted - //which simplifies shading between cells). - // - //If we don't sort the ltex indexes, the splatting order may differ between - //cells which may lead to inconsistent results when shading between cells - int num = MWBase::Environment::get().getWorld()->getStore().get().getSize(plugin); - std::set ltexIndexes; - for ( int y = fromY; y < fromY + size + 1; y++ ) - { - for ( int x = fromX - 1; x < fromX + size; x++ ) // NB we wrap X from the other side because Y is reversed - { - int idx = getLtexIndexAt(cellX, cellY, x, y); - // This is a quick hack to prevent the program from trying to fetch textures - // from a neighboring cell, which might originate from a different plugin, - // and use a separate texture palette. Right now, we simply cast it to the - // default texture (i.e. 0). - if (idx > num) - idx = 0; - ltexIndexes.insert(idx); - } - } - - //there is one texture that we want to use as a base (i.e. it won't have - //a blend map). This holds the ltex index of that base texture so that - //we know not to include it in the output map - int baseTexture = -1; - for ( std::set::iterator iter = ltexIndexes.begin(); - iter != ltexIndexes.end(); - ++iter ) - { - uint16_t ltexIndex = *iter; - //this is the base texture, so we can ignore this at present - if ( ltexIndex == baseTexture ) - { - continue; - } - - const std::map::const_iterator it = indexes.find(ltexIndex); - - if ( it == indexes.end() ) - { - //NB: All vtex ids are +1 compared to the ltex ids - - const MWWorld::Store <exStore = - MWBase::Environment::get().getWorld()->getStore().get(); - - // NOTE: using the quick hack above, we should no longer end up with textures indices - // that are out of bounds. However, I haven't updated the test to a multi-palette - // system yet. We probably need more work here, so we skip it for now. - //assert( (int)ltexStore.getSize() >= (int)ltexIndex - 1 && - //"LAND.VTEX must be within the bounds of the LTEX array"); - - std::string texture; - if ( ltexIndex == 0 ) - { - texture = "_land_default.dds"; - } - else - { - texture = ltexStore.search(ltexIndex-1, plugin)->mTexture; - //TODO this is needed due to MWs messed up texture handling - texture = texture.substr(0, texture.rfind(".")) + ".dds"; - } - - const size_t position = terrainData->layerList.size(); - terrainData->layerList.push_back(Terrain::LayerInstance()); - - terrainData->layerList[position].worldSize = 256; - terrainData->layerList[position].textureNames.push_back("textures\\" + texture); - - if ( baseTexture == -1 ) - { - baseTexture = ltexIndex; - } - else - { - indexes[ltexIndex] = position; - } - } - } - } - - //---------------------------------------------------------------------------------------------- - - void TerrainManager::initTerrainBlendMaps(Terrain* terrain, - int cellX, int cellY, - int fromX, int fromY, int size, - const std::map& indexes) - { - assert(terrain != NULL && "Must have valid terrain"); - assert(fromX >= 0 && fromY >= 0 && - "Can't get a terrain texture on terrain outside the current cell"); - assert(fromX+size <= ESM::Land::LAND_TEXTURE_SIZE && - fromY+size <= ESM::Land::LAND_TEXTURE_SIZE && - "Can't get a terrain texture on terrain outside the current cell"); - - //size must be a power of 2 as we do divisions with a power of 2 number - //that need to result in an integer for correct splatting - assert( (size & (size - 1)) == 0 && "Size must be a power of 2"); - - const int blendMapSize = terrain->getLayerBlendMapSize(); - - //zero out every map - std::map::const_iterator iter; - for ( iter = indexes.begin(); iter != indexes.end(); ++iter ) - { - float* pBlend = terrain->getLayerBlendMap(iter->second) - ->getBlendPointer(); - memset(pBlend, 0, sizeof(float) * blendMapSize * blendMapSize); - } - - //covert the ltex data into a set of blend maps - for ( int texY = fromY; texY < fromY + size + 1; texY++ ) - { - for ( int texX = fromX - 1; texX < fromX + size; texX++ ) // NB we wrap X from the other side because Y is reversed - { - const uint16_t ltexIndex = getLtexIndexAt(cellX, cellY, texX, texY); - - //check if it is the base texture (which isn't in the map) and - //if it is don't bother altering the blend map for it - if ( indexes.find(ltexIndex) == indexes.end() ) - { - continue; - } - - //while texX is the splat index relative to the entire cell, - //relX is relative to the current segment we are splatting - const int relX = texX - fromX + 1; - const int relY = texY - fromY; - - const int layerIndex = indexes.find(ltexIndex)->second; - - float* const pBlend = terrain->getLayerBlendMap(layerIndex) - ->getBlendPointer(); - - //Note: Y is reversed - const int splatY = blendMapSize - relY - 1; - const int splatX = relX; - - assert(splatX >= 0 && splatX < blendMapSize); - assert(splatY >= 0 && splatY < blendMapSize); - - const int index = (splatY)*blendMapSize + splatX; - pBlend[index] = 1; - } - } - - for ( int i = 1; i < terrain->getLayerCount(); i++ ) - { - TerrainLayerBlendMap* blend = terrain->getLayerBlendMap(i); - blend->dirty(); - blend->update(); - } - - } - - //---------------------------------------------------------------------------------------------- - - int TerrainManager::getLtexIndexAt(int cellX, int cellY, - int x, int y) - { - //check texture index falls within the 9 cell bounds - //as this function can't cope with anything above that - assert(x >= -ESM::Land::LAND_TEXTURE_SIZE && - y >= -ESM::Land::LAND_TEXTURE_SIZE && - "Trying to get land textures that are out of bounds"); - - assert(x < 2*ESM::Land::LAND_TEXTURE_SIZE && - y < 2*ESM::Land::LAND_TEXTURE_SIZE && - "Trying to get land textures that are out of bounds"); - - if ( x < 0 ) - { - cellX--; - x += ESM::Land::LAND_TEXTURE_SIZE; - } - else if ( x >= ESM::Land::LAND_TEXTURE_SIZE ) - { - cellX++; - x -= ESM::Land::LAND_TEXTURE_SIZE; - } - - if ( y < 0 ) - { - cellY--; - y += ESM::Land::LAND_TEXTURE_SIZE; - } - else if ( y >= ESM::Land::LAND_TEXTURE_SIZE ) - { - cellY++; - y -= ESM::Land::LAND_TEXTURE_SIZE; - } - - - ESM::Land* land = - MWBase::Environment::get().getWorld()->getStore().get().search(cellX, cellY); - if ( land != NULL ) - { - if (!land->isDataLoaded(ESM::Land::DATA_VTEX)) - { - land->loadData(ESM::Land::DATA_VTEX); - } - - return land->mLandData - ->mTextures[y * ESM::Land::LAND_TEXTURE_SIZE + x]; - } - else - { - return 0; - } - } - - //---------------------------------------------------------------------------------------------- - - TexturePtr TerrainManager::getVertexColours(ESM::Land* land, - int cellX, int cellY, - int fromX, int fromY, int size) - { - TextureManager* const texMgr = TextureManager::getSingletonPtr(); - - const std::string colourTextureName = "VtexColours_" + - boost::lexical_cast(cellX) + - "_" + - boost::lexical_cast(cellY) + - "_" + - boost::lexical_cast(fromX) + - "_" + - boost::lexical_cast(fromY); - - TexturePtr tex = texMgr->getByName(colourTextureName); - if ( !tex.isNull() ) - { - return tex; - } - - tex = texMgr->createManual(colourTextureName, - ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, - TEX_TYPE_2D, size, size, 0, PF_BYTE_BGR); - - HardwarePixelBufferSharedPtr pixelBuffer = tex->getBuffer(); - - pixelBuffer->lock(HardwareBuffer::HBL_DISCARD); - const PixelBox& pixelBox = pixelBuffer->getCurrentLock(); - - uint8* pDest = static_cast(pixelBox.data); - - if ( land != NULL ) - { - const char* const colours = land->mLandData->mColours; - for ( int y = 0; y < size; y++ ) - { - for ( int x = 0; x < size; x++ ) - { - const size_t colourOffset = (y+fromY)*3*65 + (x+fromX)*3; - - assert( colourOffset < 65*65*3 && - "Colour offset is out of the expected bounds of record" ); - - const unsigned char r = colours[colourOffset + 0]; - const unsigned char g = colours[colourOffset + 1]; - const unsigned char b = colours[colourOffset + 2]; - - //as is the case elsewhere we need to flip the y - const size_t imageOffset = (size - 1 - y)*size*4 + x*4; - pDest[imageOffset + 0] = b; - pDest[imageOffset + 1] = g; - pDest[imageOffset + 2] = r; - } - } - } - else - { - for ( int y = 0; y < size; y++ ) - { - for ( int x = 0; x < size; x++ ) - { - for ( int k = 0; k < 3; k++ ) - { - *pDest++ = 0; - } - } - } - } - - pixelBuffer->unlock(); - - return tex; - } - -} diff --git a/apps/openmw/mwrender/terrain.hpp b/apps/openmw/mwrender/terrain.hpp deleted file mode 100644 index 45c56390e8..0000000000 --- a/apps/openmw/mwrender/terrain.hpp +++ /dev/null @@ -1,128 +0,0 @@ -#ifndef _GAME_RENDER_TERRAIN_H -#define _GAME_RENDER_TERRAIN_H - -#include -#include - -#include - -#include "terrainmaterial.hpp" - -namespace Ogre{ - class SceneManager; - class TerrainGroup; - class TerrainGlobalOptions; - class Terrain; -} - -namespace MWWorld -{ - class CellStore; -} - -namespace MWRender{ - - class RenderingManager; - - /** - * Implements the Morrowind terrain using the Ogre Terrain Component - * - * Each terrain cell is split into four blocks as this leads to an increase - * in performance and means we don't hit splat limits quite as much - */ - class TerrainManager{ - public: - TerrainManager(Ogre::SceneManager* mgr, RenderingManager* rend); - virtual ~TerrainManager(); - - void setDiffuse(const Ogre::ColourValue& diffuse); - void setAmbient(const Ogre::ColourValue& ambient); - - void cellAdded(MWWorld::CellStore* store); - void cellRemoved(MWWorld::CellStore* store); - - float getTerrainHeightAt (Ogre::Vector3 worldPos); - - private: - Ogre::TerrainGlobalOptions* mTerrainGlobals; - Ogre::TerrainGroup mTerrainGroup; - - RenderingManager* mRendering; - - TerrainMaterial::Profile* mActiveProfile; - - /** - * The length in verticies of a single terrain block. - */ - static const int mLandSize = (ESM::Land::LAND_SIZE - 1)/2 + 1; - - /** - * The length in game units of a single terrain block. - */ - static const int mWorldSize = ESM::Land::REAL_SIZE/2; - - /** - * Setups up the list of textures for part of a cell, using indexes as - * an output to create a mapping of MW LtexIndex to the relevant terrain - * layer - * - * @param terrainData the terrain data to setup the textures for - * @param cellX the coord of the cell - * @param cellY the coord of the cell - * @param fromX the ltex index in the current cell to start making the texture from - * @param fromY the ltex index in the current cell to start making the texture from - * @param size the size (number of splats) to get - * @param indexes a mapping of ltex index to the terrain texture layer that - * can be used by initTerrainBlendMaps - */ - void initTerrainTextures(Ogre::Terrain::ImportData* terrainData, - int cellX, int cellY, - int fromX, int fromY, int size, - std::map& indexes, size_t plugin); - - /** - * Creates the blend (splatting maps) for the given terrain from the ltex data. - * - * @param terrain the terrain object for the current cell - * @param cellX the coord of the cell - * @param cellY the coord of the cell - * @param fromX the ltex index in the current cell to start making the texture from - * @param fromY the ltex index in the current cell to start making the texture from - * @param size the size (number of splats) to get - * @param indexes the mapping of ltex to blend map produced by initTerrainTextures - */ - void initTerrainBlendMaps(Ogre::Terrain* terrain, - int cellX, int cellY, - int fromX, int fromY, int size, - const std::map& indexes); - - /** - * Gets a LTEX index at the given point, assuming the current cell - * starts at (0,0). This supports getting values from the surrounding - * cells so negative x, y is acceptable - * - * @param cellX the coord of the cell - * @param cellY the coord of the cell - * @param x, y the splat position of the ltex index to get relative to the - * first splat of the current cell - */ - int getLtexIndexAt(int cellX, int cellY, int x, int y); - - /** - * Due to the fact that Ogre terrain doesn't support vertex colours - * we have to generate them manually - * - * @param cellX the coord of the cell - * @param cellY the coord of the cell - * @param fromX the *vertex* index in the current cell to start making texture from - * @param fromY the *vertex* index in the current cell to start making the texture from - * @param size the size (number of vertexes) to get - */ - Ogre::TexturePtr getVertexColours(ESM::Land* land, - int cellX, int cellY, - int fromX, int fromY, int size); - }; - -} - -#endif // _GAME_RENDER_TERRAIN_H diff --git a/apps/openmw/mwrender/terrainmaterial.cpp b/apps/openmw/mwrender/terrainmaterial.cpp deleted file mode 100644 index 892dab7cfc..0000000000 --- a/apps/openmw/mwrender/terrainmaterial.cpp +++ /dev/null @@ -1,246 +0,0 @@ -#include "terrainmaterial.hpp" - -#include - -#include - -#include - -namespace -{ - Ogre::String getComponent (int num) - { - if (num == 0) - return "x"; - else if (num == 1) - return "y"; - else if (num == 2) - return "z"; - else - return "w"; - } -} - - -namespace MWRender -{ - - TerrainMaterial::TerrainMaterial() - { - mLayerDecl.samplers.push_back(Ogre::TerrainLayerSampler("albedo_specular", Ogre::PF_BYTE_RGBA)); - //mLayerDecl.samplers.push_back(Ogre::TerrainLayerSampler("normal_height", Ogre::PF_BYTE_RGBA)); - - mLayerDecl.elements.push_back( - Ogre::TerrainLayerSamplerElement(0, Ogre::TLSS_ALBEDO, 0, 3)); - //mLayerDecl.elements.push_back( - // Ogre::TerrainLayerSamplerElement(0, Ogre::TLSS_SPECULAR, 3, 1)); - //mLayerDecl.elements.push_back( - // Ogre::TerrainLayerSamplerElement(1, Ogre::TLSS_NORMAL, 0, 3)); - //mLayerDecl.elements.push_back( - // Ogre::TerrainLayerSamplerElement(1, Ogre::TLSS_HEIGHT, 3, 1)); - - - mProfiles.push_back(OGRE_NEW Profile(this, "SM2", "Profile for rendering on Shader Model 2 capable cards")); - setActiveProfile("SM2"); - } - - // ----------------------------------------------------------------------------------------------------------------------- - - TerrainMaterial::Profile::Profile(Ogre::TerrainMaterialGenerator* parent, const Ogre::String& name, const Ogre::String& desc) - : Ogre::TerrainMaterialGenerator::Profile(parent, name, desc) - , mGlobalColourMap(false) - , mMaterial(0) - { - } - - TerrainMaterial::Profile::~Profile() - { - if (mMaterial) - sh::Factory::getInstance().destroyMaterialInstance(mMaterial->getName()); - } - - Ogre::MaterialPtr TerrainMaterial::Profile::generate(const Ogre::Terrain* terrain) - { - const Ogre::String& matName = terrain->getMaterialName(); - - sh::Factory::getInstance().destroyMaterialInstance (matName); - - Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().getByName(matName); - if (!mat.isNull()) - Ogre::MaterialManager::getSingleton().remove(matName); - - mMaterial = sh::Factory::getInstance().createMaterialInstance (matName); - mMaterial->setProperty ("allow_fixed_function", sh::makeProperty(new sh::BooleanValue(false))); - - int numPasses = getRequiredPasses(terrain); - int maxLayersInOnePass = getMaxLayersPerPass(terrain); - - for (int pass=0; passcreatePass (); - - p->setProperty ("vertex_program", sh::makeProperty(new sh::StringValue("terrain_vertex"))); - p->setProperty ("fragment_program", sh::makeProperty(new sh::StringValue("terrain_fragment"))); - if (pass != 0) - { - p->setProperty ("scene_blend", sh::makeProperty(new sh::StringValue("alpha_blend"))); - // Only write if depth is equal to the depth value written by the previous pass. - p->setProperty ("depth_func", sh::makeProperty(new sh::StringValue("equal"))); - } - - p->mShaderProperties.setProperty ("colour_map", sh::makeProperty(new sh::BooleanValue(mGlobalColourMap))); - p->mShaderProperties.setProperty ("is_first_pass", sh::makeProperty(new sh::BooleanValue(pass == 0))); - - // global colour map - sh::MaterialInstanceTextureUnit* colourMap = p->createTextureUnit ("colourMap"); - colourMap->setProperty ("texture_alias", sh::makeProperty (new sh::StringValue(mMaterial->getName() + "_colourMap"))); - colourMap->setProperty ("tex_address_mode", sh::makeProperty (new sh::StringValue("clamp"))); - - // global normal map - sh::MaterialInstanceTextureUnit* normalMap = p->createTextureUnit ("normalMap"); - normalMap->setProperty ("direct_texture", sh::makeProperty (new sh::StringValue(terrain->getTerrainNormalMap ()->getName()))); - normalMap->setProperty ("tex_address_mode", sh::makeProperty (new sh::StringValue("clamp"))); - - Ogre::uint numLayersInThisPass = std::min(maxLayersInOnePass, terrain->getLayerCount()-layerOffset); - - // HACK: Terrain::getLayerBlendTextureIndex should be const, but it is not. - // Remove this once ogre got fixed. - Ogre::Terrain* nonconstTerrain = const_cast(terrain); - - // a blend map might be shared between two passes - // so we can't just use terrain->getBlendTextureCount() - Ogre::uint numBlendTextures=0; - std::vector blendTextures; - for (unsigned int layer=blendmapOffset; layergetBlendTextureName(nonconstTerrain->getLayerBlendTextureIndex( - static_cast(layerOffset+layer)).first); - if (std::find(blendTextures.begin(), blendTextures.end(), blendTextureName) == blendTextures.end()) - { - blendTextures.push_back(blendTextureName); - ++numBlendTextures; - } - } - - p->mShaderProperties.setProperty ("num_layers", sh::makeProperty (new sh::StringValue(Ogre::StringConverter::toString(numLayersInThisPass)))); - p->mShaderProperties.setProperty ("num_blendmaps", sh::makeProperty (new sh::StringValue(Ogre::StringConverter::toString(numBlendTextures)))); - - // blend maps - // the index of the first blend map used in this pass - int blendmapStart; - if (terrain->getLayerCount() == 1) // special case. if there's only one layer, we don't need blend maps at all - blendmapStart = 0; - else - blendmapStart = nonconstTerrain->getLayerBlendTextureIndex(static_cast(layerOffset+blendmapOffset)).first; - for (Ogre::uint i = 0; i < numBlendTextures; ++i) - { - sh::MaterialInstanceTextureUnit* blendTex = p->createTextureUnit ("blendMap" + Ogre::StringConverter::toString(i)); - blendTex->setProperty ("direct_texture", sh::makeProperty (new sh::StringValue(terrain->getBlendTextureName(blendmapStart+i)))); - blendTex->setProperty ("tex_address_mode", sh::makeProperty (new sh::StringValue("clamp"))); - } - - // layer maps - for (Ogre::uint i = 0; i < numLayersInThisPass; ++i) - { - sh::MaterialInstanceTextureUnit* diffuseTex = p->createTextureUnit ("diffuseMap" + Ogre::StringConverter::toString(i)); - diffuseTex->setProperty ("direct_texture", sh::makeProperty (new sh::StringValue(terrain->getLayerTextureName(layerOffset+i, 0)))); - - if (i+layerOffset > 0) - { - int blendTextureIndex = nonconstTerrain->getLayerBlendTextureIndex(static_cast(layerOffset+i)).first; - int blendTextureComponent = nonconstTerrain->getLayerBlendTextureIndex(static_cast(layerOffset+i)).second; - p->mShaderProperties.setProperty ("blendmap_component_" + Ogre::StringConverter::toString(i), - sh::makeProperty (new sh::StringValue(Ogre::StringConverter::toString(blendTextureIndex-blendmapStart) + "." + getComponent(blendTextureComponent)))); - } - else - { - // just to make it shut up about blendmap_component_0 not existing in the first pass. - // it might be retrieved, but will never survive the preprocessing step. - p->mShaderProperties.setProperty ("blendmap_component_" + Ogre::StringConverter::toString(i), - sh::makeProperty (new sh::StringValue(""))); - } - } - - // shadow - for (Ogre::uint i = 0; i < 3; ++i) - { - sh::MaterialInstanceTextureUnit* shadowTex = p->createTextureUnit ("shadowMap" + Ogre::StringConverter::toString(i)); - shadowTex->setProperty ("content_type", sh::makeProperty (new sh::StringValue("shadow"))); - } - - p->mShaderProperties.setProperty ("shadowtexture_offset", sh::makeProperty (new sh::StringValue( - Ogre::StringConverter::toString(numBlendTextures + numLayersInThisPass + 2)))); - - // make sure the pass index is fed to the permutation handler, because blendmap components may be different - p->mShaderProperties.setProperty ("pass_index", sh::makeProperty(new sh::IntValue(pass))); - } - - return Ogre::MaterialManager::getSingleton().getByName(matName); - } - - void TerrainMaterial::Profile::setGlobalColourMapEnabled (bool enabled) - { - mGlobalColourMap = enabled; - mParent->_markChanged(); - } - - void TerrainMaterial::Profile::setGlobalColourMap (Ogre::Terrain* terrain, const std::string& name) - { - sh::Factory::getInstance ().setTextureAlias (terrain->getMaterialName () + "_colourMap", name); - } - - Ogre::MaterialPtr TerrainMaterial::Profile::generateForCompositeMap(const Ogre::Terrain* terrain) - { - throw std::runtime_error ("composite map not supported"); - } - - Ogre::uint8 TerrainMaterial::Profile::getMaxLayers(const Ogre::Terrain* terrain) const - { - return 255; - } - - int TerrainMaterial::Profile::getMaxLayersPerPass (const Ogre::Terrain* terrain) - { - // count the texture units free - Ogre::uint8 freeTextureUnits = 16; - // normalmap - --freeTextureUnits; - // colourmap - --freeTextureUnits; - // shadow - --freeTextureUnits; - --freeTextureUnits; - --freeTextureUnits; - - // each layer needs 1.25 units (1xdiffusespec, 0.25xblend) - return static_cast(freeTextureUnits / (1.25f)); - } - - int TerrainMaterial::Profile::getRequiredPasses (const Ogre::Terrain* terrain) - { - int maxLayersPerPass = getMaxLayersPerPass(terrain); - assert(terrain->getLayerCount()); - assert(maxLayersPerPass); - return std::ceil(static_cast(terrain->getLayerCount()) / maxLayersPerPass); - } - - void TerrainMaterial::Profile::updateParams(const Ogre::MaterialPtr& mat, const Ogre::Terrain* terrain) - { - } - - void TerrainMaterial::Profile::updateParamsForCompositeMap(const Ogre::MaterialPtr& mat, const Ogre::Terrain* terrain) - { - } - - void TerrainMaterial::Profile::requestOptions(Ogre::Terrain* terrain) - { - terrain->_setMorphRequired(true); - terrain->_setNormalMapRequired(true); // global normal map - terrain->_setLightMapRequired(false); - terrain->_setCompositeMapRequired(false); - } - -} diff --git a/apps/openmw/mwrender/terrainmaterial.hpp b/apps/openmw/mwrender/terrainmaterial.hpp deleted file mode 100644 index c90499baeb..0000000000 --- a/apps/openmw/mwrender/terrainmaterial.hpp +++ /dev/null @@ -1,88 +0,0 @@ -/* ------------------------------------------------------------------------------ -This source file is part of OGRE -(Object-oriented Graphics Rendering Engine) -For the latest info, see http://www.ogre3d.org/ - -Copyright (c) 2000-2011 Torus Knot Software Ltd - -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 MWRENDER_TERRAINMATERIAL_H -#define MWRENDER_TERRAINMATERIAL_H - -#include "OgreTerrainPrerequisites.h" -#include "OgreTerrainMaterialGenerator.h" -#include "OgreGpuProgramParams.h" - -namespace sh -{ - class MaterialInstance; -} - -namespace MWRender -{ - - class TerrainMaterial : public Ogre::TerrainMaterialGenerator - { - public: - - class Profile : public Ogre::TerrainMaterialGenerator::Profile - { - public: - Profile(Ogre::TerrainMaterialGenerator* parent, const Ogre::String& name, const Ogre::String& desc); - virtual ~Profile(); - - virtual bool isVertexCompressionSupported() const { return false; } - - virtual Ogre::MaterialPtr generate(const Ogre::Terrain* terrain); - - virtual Ogre::MaterialPtr generateForCompositeMap(const Ogre::Terrain* terrain); - - virtual Ogre::uint8 getMaxLayers(const Ogre::Terrain* terrain) const; - - virtual void updateParams(const Ogre::MaterialPtr& mat, const Ogre::Terrain* terrain); - - virtual void updateParamsForCompositeMap(const Ogre::MaterialPtr& mat, const Ogre::Terrain* terrain); - - virtual void requestOptions(Ogre::Terrain* terrain); - - void setGlobalColourMapEnabled(bool enabled); - void setGlobalColourMap (Ogre::Terrain* terrain, const std::string& name); - virtual void setLightmapEnabled(bool) {} - - private: - sh::MaterialInstance* mMaterial; - - int getRequiredPasses (const Ogre::Terrain* terrain); - int getMaxLayersPerPass (const Ogre::Terrain* terrain); - - bool mGlobalColourMap; - - }; - - TerrainMaterial(); - }; - -} - - -#endif diff --git a/apps/openmw/mwrender/terrainstorage.cpp b/apps/openmw/mwrender/terrainstorage.cpp new file mode 100644 index 0000000000..318627fc70 --- /dev/null +++ b/apps/openmw/mwrender/terrainstorage.cpp @@ -0,0 +1,56 @@ +#include "terrainstorage.hpp" + +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" +#include "../mwworld/esmstore.hpp" + +namespace MWRender +{ + + Ogre::AxisAlignedBox TerrainStorage::getBounds() + { + int minX = 0, minY = 0, maxX = 0, maxY = 0; + + const MWWorld::ESMStore &esmStore = + MWBase::Environment::get().getWorld()->getStore(); + + MWWorld::Store::iterator it = esmStore.get().extBegin(); + for (; it != esmStore.get().extEnd(); ++it) + { + if (it->getGridX() < minX) + minX = it->getGridX(); + if (it->getGridX() > maxX) + maxX = it->getGridX(); + if (it->getGridY() < minY) + minY = it->getGridY(); + if (it->getGridY() > maxY) + maxY = it->getGridY(); + } + + // since grid coords are at cell origin, we need to add 1 cell + maxX += 1; + maxY += 1; + + return Ogre::AxisAlignedBox(minX, minY, 0, maxX, maxY, 0); + } + + ESM::Land* TerrainStorage::getLand(int cellX, int cellY) + { + const MWWorld::ESMStore &esmStore = + MWBase::Environment::get().getWorld()->getStore(); + ESM::Land* land = esmStore.get().search(cellX, cellY); + // Load the data we are definitely going to need + int mask = ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | ESM::Land::DATA_VCLR | ESM::Land::DATA_VTEX; + if (land && !land->isDataLoaded(mask)) + land->loadData(mask); + return land; + } + + const ESM::LandTexture* TerrainStorage::getLandTexture(int index, short plugin) + { + const MWWorld::ESMStore &esmStore = + MWBase::Environment::get().getWorld()->getStore(); + return esmStore.get().find(index, plugin); + } + +} diff --git a/apps/openmw/mwrender/terrainstorage.hpp b/apps/openmw/mwrender/terrainstorage.hpp new file mode 100644 index 0000000000..ebf5e26ab7 --- /dev/null +++ b/apps/openmw/mwrender/terrainstorage.hpp @@ -0,0 +1,22 @@ +#ifndef MWRENDER_TERRAINSTORAGE_H +#define MWRENDER_TERRAINSTORAGE_H + +#include + +namespace MWRender +{ + + class TerrainStorage : public Terrain::Storage + { + private: + virtual ESM::Land* getLand (int cellX, int cellY); + virtual const ESM::LandTexture* getLandTexture(int index, short plugin); + public: + virtual Ogre::AxisAlignedBox getBounds(); + ///< Get bounds in cell units + }; + +} + + +#endif diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 772eaf623e..082551f371 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -202,7 +202,10 @@ Water::Water (Ogre::Camera *camera, RenderingManager* rend) : mWaterPlane = Plane(Vector3::UNIT_Z, 0); - MeshManager::getSingleton().createPlane("water", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, mWaterPlane, CELL_SIZE*5, CELL_SIZE * 5, 10, 10, true, 1, 3,3, Vector3::UNIT_Y); + int waterScale = 300; + + MeshManager::getSingleton().createPlane("water", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, mWaterPlane, + CELL_SIZE*5*waterScale, CELL_SIZE*5*waterScale, 10, 10, true, 1, 3*waterScale,3*waterScale, Vector3::UNIT_Y); mWater = mSceneMgr->createEntity("water"); mWater->setVisibilityFlags(RV_Water); diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 5fa1400b16..03ddf2aa9e 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -85,6 +85,7 @@ namespace MWWorld std::cout << "Unloading cell\n"; ListAndResetHandles functor; + /* (*iter)->forEach(functor); { // silence annoying g++ warning @@ -95,6 +96,7 @@ namespace MWWorld mPhysics->removeObject (node->getName()); } } + */ if ((*iter)->mCell->isExterior()) { @@ -148,7 +150,7 @@ namespace MWWorld // ... then references. This is important for adjustPosition to work correctly. /// \todo rescale depending on the state of a new GMST - insertCell (*cell, true); + //insertCell (*cell, true); mRendering.cellAdded (cell); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index bd234c0b6f..ced4b5faa4 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1,5 +1,7 @@ #include "worldimp.hpp" +#include + #include #include @@ -833,7 +835,7 @@ namespace MWWorld bool isPlayer = ptr == mPlayer->getPlayer(); bool haveToMove = isPlayer || mWorldScene->isCellActive(*currCell); - if (*currCell != newCell) + if (false ) //*currCell != newCell) { removeContainerScripts(ptr); @@ -1024,7 +1026,7 @@ namespace MWWorld return; } - float terrainHeight = mRendering->getTerrainHeightAt(pos); + float terrainHeight = -std::numeric_limits().max();// mRendering->getTerrainHeightAt(pos); if (pos.z < terrainHeight) pos.z = terrainHeight; diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 529891b4cb..baf905aa7f 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -65,6 +65,10 @@ add_component_dir (interpreter add_component_dir (translation translation ) + +add_component_dir (terrain + quadtreenode chunk terrain storage material + ) find_package(Qt4 COMPONENTS QtCore QtGui) diff --git a/components/esm/loadland.hpp b/components/esm/loadland.hpp index c1cce5e7ee..9c1fd1f5c6 100644 --- a/components/esm/loadland.hpp +++ b/components/esm/loadland.hpp @@ -70,7 +70,7 @@ struct Land }; #pragma pack(pop) - typedef uint8_t VNML[LAND_NUM_VERTS * 3]; + typedef signed char VNML[LAND_NUM_VERTS * 3]; struct LandData { diff --git a/components/terrain/chunk.cpp b/components/terrain/chunk.cpp new file mode 100644 index 0000000000..c9a364dc44 --- /dev/null +++ b/components/terrain/chunk.cpp @@ -0,0 +1,171 @@ +#include "chunk.hpp" + +#include +#include +#include +#include +#include + +#include "quadtreenode.hpp" +#include "terrain.hpp" +#include "storage.hpp" + +namespace Terrain +{ + + Chunk::Chunk(QuadTreeNode* node, short lodLevel) + : mNode(node) + , mVertexLod(lodLevel) + , mAdditionalLod(0) + { + mVertexData = OGRE_NEW Ogre::VertexData; + mVertexData->vertexStart = 0; + + // Set the total number of vertices + size_t numVertsOneSide = mNode->getSize() * (ESM::Land::LAND_SIZE-1); + numVertsOneSide /= std::pow(2, lodLevel); + numVertsOneSide += 1; + assert((int)numVertsOneSide == ESM::Land::LAND_SIZE); + mVertexData->vertexCount = numVertsOneSide * numVertsOneSide; + + // 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); + mVertexBuffer = 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); + mNormalBuffer = 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); + Ogre::HardwareVertexBufferSharedPtr uvBuf = mNode->getTerrain()->getVertexBuffer(numVertsOneSide); + + // Colours + vertexDecl->addElement(nextBuffer++, 0, Ogre::VET_COLOUR, Ogre::VES_DIFFUSE); + mColourBuffer = mgr->createVertexBuffer(Ogre::VertexElement::getTypeSize(Ogre::VET_COLOUR), + mVertexData->vertexCount, Ogre::HardwareBuffer::HBU_STATIC); + + + mNode->getTerrain()->getStorage()->fillVertexBuffers(lodLevel, mNode->getSize(), mNode->getCenter(), + mVertexBuffer, mNormalBuffer, mColourBuffer); + + mVertexData->vertexBufferBinding->setBinding(0, mVertexBuffer); + mVertexData->vertexBufferBinding->setBinding(1, mNormalBuffer); + mVertexData->vertexBufferBinding->setBinding(2, uvBuf); + mVertexData->vertexBufferBinding->setBinding(3, mColourBuffer); + + mIndexData = OGRE_NEW Ogre::IndexData(); + mIndexData->indexStart = 0; + } + + void Chunk::updateIndexBuffer() + { + // Fetch a suitable index buffer (which may be shared) + size_t ourLod = mVertexLod + mAdditionalLod; + + int flags = 0; + + for (int i=0; i<4; ++i) + { + QuadTreeNode* neighbour = mNode->searchNeighbour((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 < std::pow(2,4)); + flags |= int(lod - ourLod) << (4*i); + } + } + + flags |= ((int)mAdditionalLod) << (4*4); + + size_t numIndices; + mIndexBuffer = mNode->getTerrain()->getIndexBuffer(flags, numIndices); + mIndexData->indexCount = numIndices; + mIndexData->indexBuffer = mIndexBuffer; + } + + Chunk::~Chunk() + { + OGRE_DELETE mVertexData; + OGRE_DELETE mIndexData; + } + + void Chunk::setMaterial(const Ogre::MaterialPtr &material) + { + mMaterial = material; + } + + const Ogre::AxisAlignedBox& Chunk::getBoundingBox(void) const + { + return mNode->getBoundingBox(); + } + + Ogre::Real Chunk::getBoundingRadius(void) const + { + return mNode->getBoundingBox().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 (!mIndexBuffer.isNull() && "Trying to render, but no index buffer 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(); + } + +} diff --git a/components/terrain/chunk.hpp b/components/terrain/chunk.hpp new file mode 100644 index 0000000000..d74c65ba6d --- /dev/null +++ b/components/terrain/chunk.hpp @@ -0,0 +1,63 @@ +#ifndef COMPONENTS_TERRAIN_TERRAINBATCH_H +#define COMPONENTS_TERRAIN_TERRAINBATCH_H + +#include +#include + +namespace Terrain +{ + + class QuadTreeNode; + + /** + * @brief Renders a chunk of terrain, either using alpha splatting or a composite map. + */ + class Chunk : public Ogre::Renderable, public Ogre::MovableObject + { + public: + /// @param lodLevel LOD level for the vertex buffer. + Chunk (QuadTreeNode* node, short lodLevel); + virtual ~Chunk(); + + void setMaterial (const Ogre::MaterialPtr& material); + + /// Set additional LOD applied on top of vertex LOD. \n + /// This is achieved by changing the index buffer to omit vertices. + void setAdditionalLod (size_t lod) { mAdditionalLod = lod; } + size_t getAdditionalLod() { return mAdditionalLod; } + + void updateIndexBuffer(); + + // 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: + QuadTreeNode* mNode; + Ogre::MaterialPtr mMaterial; + + size_t mVertexLod; + size_t mAdditionalLod; + + Ogre::VertexData* mVertexData; + Ogre::IndexData* mIndexData; + Ogre::HardwareVertexBufferSharedPtr mVertexBuffer; + Ogre::HardwareVertexBufferSharedPtr mNormalBuffer; + Ogre::HardwareVertexBufferSharedPtr mColourBuffer; + Ogre::HardwareIndexBufferSharedPtr mIndexBuffer; + }; + +} + +#endif diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp new file mode 100644 index 0000000000..bd75015768 --- /dev/null +++ b/components/terrain/material.cpp @@ -0,0 +1,291 @@ +#include "material.hpp" + +#include +#include +#include + +#include + +namespace +{ + +int getBlendmapIndexForLayer (int layerIndex) +{ + return std::floor((layerIndex-1)/4.f); +} + +std::string getBlendmapComponentForLayer (int layerIndex) +{ + int n = (layerIndex-1)%4; + if (n == 0) + return "x"; + if (n == 1) + return "y"; + if (n == 2) + return "z"; + else + return "w"; +} + +} + +namespace Terrain +{ + + MaterialGenerator::MaterialGenerator(bool shaders) + : mShaders(shaders) + { + + } + + int MaterialGenerator::getMaxLayersPerPass () + { + // count the texture units free + Ogre::uint8 freeTextureUnits = 16; + + // first layer doesn't need blendmap + --freeTextureUnits; + + // each layer needs 1.25 units (1xdiffusespec, 0.25xblend) + return static_cast(freeTextureUnits / (1.25f)) + 1; + } + + int MaterialGenerator::getRequiredPasses () + { + int maxLayersPerPass = getMaxLayersPerPass(); + return std::max(1.f, std::ceil(static_cast(mLayerList.size()) / maxLayersPerPass)); + } + + Ogre::MaterialPtr MaterialGenerator::generate(Ogre::MaterialPtr mat) + { + return create(mat, false, false); + } + + Ogre::MaterialPtr MaterialGenerator::generateForCompositeMapRTT(Ogre::MaterialPtr mat) + { + return create(mat, true, false); + } + + Ogre::MaterialPtr MaterialGenerator::generateForCompositeMap(Ogre::MaterialPtr mat) + { + return create(mat, false, true); + } + + Ogre::MaterialPtr MaterialGenerator::create(Ogre::MaterialPtr mat, bool renderCompositeMap, bool displayCompositeMap) + { + assert(!renderCompositeMap || !displayCompositeMap); + if (!mat.isNull()) + { + sh::Factory::getInstance().destroyMaterialInstance(mat->getName()); + Ogre::MaterialManager::getSingleton().remove(mat->getName()); + } + + static int count = 0; + std::stringstream name; + name << "terrain/mat" << count++; + + if (!mShaders) + { + mat = Ogre::MaterialManager::getSingleton().create(name.str(), + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); + Ogre::Technique* technique = mat->getTechnique(0); + technique->removeAllPasses(); + + if (displayCompositeMap) + { + Ogre::Pass* pass = technique->createPass(); + pass->setVertexColourTracking(Ogre::TVC_AMBIENT|Ogre::TVC_DIFFUSE); + pass->createTextureUnitState(mCompositeMap)->setTextureAddressingMode(Ogre::TextureUnitState::TAM_CLAMP); + } + else + { + assert(mLayerList.size() == mBlendmapList.size()+1); + std::vector::iterator blend = mBlendmapList.begin(); + for (std::vector::iterator layer = mLayerList.begin(); layer != mLayerList.end(); ++layer) + { + Ogre::Pass* pass = technique->createPass(); + pass->setLightingEnabled(false); + pass->setVertexColourTracking(Ogre::TVC_NONE); + + bool first = (layer == mLayerList.begin()); + + Ogre::TextureUnitState* tus; + + if (!first) + { + pass->setSceneBlending(Ogre::SBT_TRANSPARENT_ALPHA); + pass->setDepthFunction(Ogre::CMPF_EQUAL); + + tus = pass->createTextureUnitState((*blend)->getName()); + tus->setAlphaOperation(Ogre::LBX_BLEND_TEXTURE_ALPHA, + Ogre::LBS_TEXTURE, + Ogre::LBS_TEXTURE); + tus->setColourOperationEx(Ogre::LBX_BLEND_DIFFUSE_ALPHA, + Ogre::LBS_TEXTURE, + Ogre::LBS_TEXTURE); + tus->setIsAlpha(true); + tus->setTextureAddressingMode(Ogre::TextureUnitState::TAM_CLAMP); + + float scale = (16/(16.f+1.f)); + float scroll = 1/16.f*0.5; + tus->setTextureScale(scale,scale); + tus->setTextureScroll(-scroll,-scroll); + } + + // Add the actual layer texture on top of the alpha map. + tus = pass->createTextureUnitState("textures\\" + *layer); + if (!first) + tus->setColourOperationEx(Ogre::LBX_BLEND_DIFFUSE_ALPHA, + Ogre::LBS_TEXTURE, + Ogre::LBS_CURRENT); + + tus->setTextureScale(1/16.f,1/16.f); + + if (!first) + ++blend; + } + + if (!renderCompositeMap) + { + Ogre::Pass* lightingPass = technique->createPass(); + lightingPass->setSceneBlending(Ogre::SBT_MODULATE); + lightingPass->setVertexColourTracking(Ogre::TVC_AMBIENT|Ogre::TVC_DIFFUSE); + } + } + + return mat; + } + else + { + + sh::MaterialInstance* material = sh::Factory::getInstance().createMaterialInstance (name.str()); + material->setProperty ("allow_fixed_function", sh::makeProperty(new sh::BooleanValue(false))); + + if (displayCompositeMap) + { + sh::MaterialInstancePass* p = material->createPass (); + + p->setProperty ("vertex_program", sh::makeProperty(new sh::StringValue("terrain_vertex"))); + p->setProperty ("fragment_program", sh::makeProperty(new sh::StringValue("terrain_fragment"))); + p->mShaderProperties.setProperty ("is_first_pass", sh::makeProperty(new sh::BooleanValue(true))); + p->mShaderProperties.setProperty ("render_composite_map", sh::makeProperty(new sh::BooleanValue(false))); + p->mShaderProperties.setProperty ("display_composite_map", sh::makeProperty(new sh::BooleanValue(true))); + p->mShaderProperties.setProperty ("num_layers", sh::makeProperty (new sh::StringValue("0"))); + p->mShaderProperties.setProperty ("num_blendmaps", sh::makeProperty (new sh::StringValue("0"))); + + sh::MaterialInstanceTextureUnit* tex = p->createTextureUnit ("compositeMap"); + tex->setProperty ("direct_texture", sh::makeProperty (new sh::StringValue(mCompositeMap))); + tex->setProperty ("tex_address_mode", sh::makeProperty (new sh::StringValue("clamp"))); + + // shadow. TODO: repeated, put in function + for (Ogre::uint i = 0; i < 3; ++i) + { + sh::MaterialInstanceTextureUnit* shadowTex = p->createTextureUnit ("shadowMap" + Ogre::StringConverter::toString(i)); + shadowTex->setProperty ("content_type", sh::makeProperty (new sh::StringValue("shadow"))); + } + + p->mShaderProperties.setProperty ("shadowtexture_offset", sh::makeProperty (new sh::StringValue( + Ogre::StringConverter::toString(1)))); + + p->mShaderProperties.setProperty ("pass_index", sh::makeProperty(new sh::IntValue(0))); + } + else + { + + int numPasses = getRequiredPasses(); + assert(numPasses); + int maxLayersInOnePass = getMaxLayersPerPass(); + + for (int pass=0; passcreatePass (); + + p->setProperty ("vertex_program", sh::makeProperty(new sh::StringValue("terrain_vertex"))); + p->setProperty ("fragment_program", sh::makeProperty(new sh::StringValue("terrain_fragment"))); + if (pass != 0) + { + p->setProperty ("scene_blend", sh::makeProperty(new sh::StringValue("alpha_blend"))); + // Only write if depth is equal to the depth value written by the previous pass. + p->setProperty ("depth_func", sh::makeProperty(new sh::StringValue("equal"))); + } + + p->mShaderProperties.setProperty ("is_first_pass", sh::makeProperty(new sh::BooleanValue(pass == 0))); + p->mShaderProperties.setProperty ("render_composite_map", sh::makeProperty(new sh::BooleanValue(renderCompositeMap))); + p->mShaderProperties.setProperty ("display_composite_map", sh::makeProperty(new sh::BooleanValue(displayCompositeMap))); + + Ogre::uint numLayersInThisPass = std::min(maxLayersInOnePass, (int)mLayerList.size()-layerOffset); + + // a blend map might be shared between two passes + Ogre::uint numBlendTextures=0; + std::vector blendTextures; + for (unsigned int layer=blendmapOffset; layergetName(); + if (std::find(blendTextures.begin(), blendTextures.end(), blendTextureName) == blendTextures.end()) + { + blendTextures.push_back(blendTextureName); + ++numBlendTextures; + } + } + + p->mShaderProperties.setProperty ("num_layers", sh::makeProperty (new sh::StringValue(Ogre::StringConverter::toString(numLayersInThisPass)))); + p->mShaderProperties.setProperty ("num_blendmaps", sh::makeProperty (new sh::StringValue(Ogre::StringConverter::toString(numBlendTextures)))); + + // blend maps + // the index of the first blend map used in this pass + int blendmapStart; + if (mLayerList.size() == 1) // special case. if there's only one layer, we don't need blend maps at all + blendmapStart = 0; + else + blendmapStart = getBlendmapIndexForLayer(layerOffset+blendmapOffset); + for (Ogre::uint i = 0; i < numBlendTextures; ++i) + { + sh::MaterialInstanceTextureUnit* blendTex = p->createTextureUnit ("blendMap" + Ogre::StringConverter::toString(i)); + blendTex->setProperty ("direct_texture", sh::makeProperty (new sh::StringValue(mBlendmapList[blendmapStart+i]->getName()))); + blendTex->setProperty ("tex_address_mode", sh::makeProperty (new sh::StringValue("clamp"))); + } + + // layer maps + for (Ogre::uint i = 0; i < numLayersInThisPass; ++i) + { + sh::MaterialInstanceTextureUnit* diffuseTex = p->createTextureUnit ("diffuseMap" + Ogre::StringConverter::toString(i)); + diffuseTex->setProperty ("direct_texture", sh::makeProperty (new sh::StringValue("textures\\"+mLayerList[layerOffset+i]))); + + if (i+layerOffset > 0) + { + int blendTextureIndex = getBlendmapIndexForLayer(layerOffset+i); + std::string blendTextureComponent = getBlendmapComponentForLayer(layerOffset+i); + p->mShaderProperties.setProperty ("blendmap_component_" + Ogre::StringConverter::toString(i), + sh::makeProperty (new sh::StringValue(Ogre::StringConverter::toString(blendTextureIndex-blendmapStart) + "." + blendTextureComponent))); + } + else + { + // just to make it shut up about blendmap_component_0 not existing in the first pass. + // it might be retrieved, but will never survive the preprocessing step. + p->mShaderProperties.setProperty ("blendmap_component_" + Ogre::StringConverter::toString(i), + sh::makeProperty (new sh::StringValue(""))); + } + } + + // shadow + for (Ogre::uint i = 0; i < 3; ++i) + { + sh::MaterialInstanceTextureUnit* shadowTex = p->createTextureUnit ("shadowMap" + Ogre::StringConverter::toString(i)); + shadowTex->setProperty ("content_type", sh::makeProperty (new sh::StringValue("shadow"))); + } + + p->mShaderProperties.setProperty ("shadowtexture_offset", sh::makeProperty (new sh::StringValue( + Ogre::StringConverter::toString(numBlendTextures + numLayersInThisPass)))); + + // Make sure the pass index is fed to the permutation handler, because blendmap components may be different + p->mShaderProperties.setProperty ("pass_index", sh::makeProperty(new sh::IntValue(pass))); + } + } + } + return Ogre::MaterialManager::getSingleton().getByName(name.str()); + } + +} diff --git a/components/terrain/material.hpp b/components/terrain/material.hpp new file mode 100644 index 0000000000..749e806ad7 --- /dev/null +++ b/components/terrain/material.hpp @@ -0,0 +1,54 @@ +#ifndef COMPONENTS_TERRAIN_MATERIAL_H +#define COMPONENTS_TERRAIN_MATERIAL_H + +#include + +namespace Terrain +{ + + class MaterialGenerator + { + public: + /// @param layerList layer textures + /// @param blendmapList blend textures + /// @param shaders Whether to use shaders. With a shader, blendmap packing can be used (4 channels instead of one), + /// so if this parameter is true, then the supplied blend maps are expected to be packed. + MaterialGenerator (bool shaders); + + void setLayerList (const std::vector& layerList) { mLayerList = layerList; } + bool hasLayers() { return mLayerList.size(); } + void setBlendmapList (const std::vector& blendmapList) { mBlendmapList = blendmapList; } + void setCompositeMap (const std::string& name) { mCompositeMap = name; } + + /// Creates a material suitable for displaying a chunk of terrain using alpha-blending. + /// @param mat Material that will be replaced by the generated material. May be empty as well, in which case + /// a new material is created. + Ogre::MaterialPtr generate (Ogre::MaterialPtr mat); + + /// Creates a material suitable for displaying a chunk of terrain using a ready-made composite map. + /// @param mat Material that will be replaced by the generated material. May be empty as well, in which case + /// a new material is created. + Ogre::MaterialPtr generateForCompositeMap (Ogre::MaterialPtr mat); + + /// Creates a material suitable for rendering composite maps, i.e. for "baking" several layer textures + /// into one. The main difference compared to a normal material is that no shading is applied at this point. + /// @param mat Material that will be replaced by the generated material. May be empty as well, in which case + /// a new material is created. + Ogre::MaterialPtr generateForCompositeMapRTT (Ogre::MaterialPtr mat); + + private: + Ogre::MaterialPtr create (Ogre::MaterialPtr mat, bool renderCompositeMap, bool displayCompositeMap); + + int getRequiredPasses (); + int getMaxLayersPerPass (); + + int mNumLayers; + std::vector mLayerList; + std::vector mBlendmapList; + std::string mCompositeMap; + bool mShaders; + }; + +} + +#endif diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp new file mode 100644 index 0000000000..8ebfafeb44 --- /dev/null +++ b/components/terrain/quadtreenode.cpp @@ -0,0 +1,387 @@ +#include "quadtreenode.hpp" + +#include +#include + +#include "terrain.hpp" +#include "chunk.hpp" +#include "storage.hpp" + +#include "material.hpp" + +using namespace Terrain; + +namespace +{ + + // 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 + Terrain::QuadTreeNode* searchNeighbourRecursive (Terrain::QuadTreeNode* currentNode, Terrain::Direction dir) + { + if (!currentNode->getParent()) + return NULL; // Arrived at root node, the root node does not have neighbours + + Terrain::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; + } + + + // Ogre::AxisAlignedBox::distance is broken in 1.8. + Ogre::Real distance(const Ogre::AxisAlignedBox& box, const Ogre::Vector3& v) + { + + if (box.contains(v)) + return 0; + else + { + Ogre::Vector3 maxDist(0,0,0); + const Ogre::Vector3& minimum = box.getMinimum(); + const Ogre::Vector3& maximum = box.getMaximum(); + + if (v.x < minimum.x) + maxDist.x = minimum.x - v.x; + else if (v.x > maximum.x) + maxDist.x = v.x - maximum.x; + + if (v.y < minimum.y) + maxDist.y = minimum.y - v.y; + else if (v.y > maximum.y) + maxDist.y = v.y - maximum.y; + + if (v.z < minimum.z) + maxDist.z = minimum.z - v.z; + else if (v.z > maximum.z) + maxDist.z = v.z - maximum.z; + + return maxDist.length(); + } + } + + // 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(Terrain* terrain, ChildDirection dir, float size, const Ogre::Vector2 ¢er, QuadTreeNode* parent) + : mSize(size) + , mCenter(center) + , mParent(parent) + , mDirection(dir) + , mIsDummy(false) + , mSceneNode(NULL) + , mTerrain(terrain) + , mChunk(NULL) + , mMaterialGenerator(NULL) +{ + mBounds.setNull(); + for (int i=0; i<4; ++i) + mChildren[i] = NULL; + + mSceneNode = mTerrain->getSceneManager()->getRootSceneNode()->createChildSceneNode( + Ogre::Vector3(mCenter.x*8192, mCenter.y*8192, 0)); + + mLodLevel = log2(mSize); + + mMaterialGenerator = new MaterialGenerator(true); +} + +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::searchNeighbour(Direction dir) +{ + return searchNeighbourRecursive(this, dir); +} + +const Ogre::AxisAlignedBox& QuadTreeNode::getBoundingBox() +{ + if (mIsDummy) + return Ogre::AxisAlignedBox::BOX_NULL; + if (mBounds.isNull()) + { + if (hasChildren()) + { + // X and Y are obvious, just need Z + float min = std::numeric_limits().max(); + float max = -std::numeric_limits().max(); + for (int i=0; i<4; ++i) + { + QuadTreeNode* child = getChild((ChildDirection)i); + float v = child->getBoundingBox().getMaximum().z; + if (v > max) + max = v; + v = child->getBoundingBox().getMinimum().z; + if (v < min) + min = v; + } + mBounds = Ogre::AxisAlignedBox (Ogre::Vector3(-mSize/2*8192, -mSize/2*8192, min), + Ogre::Vector3(mSize/2*8192, mSize/2*8192, max)); + } + else + throw std::runtime_error("Leaf node should have bounds set!"); + } + return mBounds; +} + +void QuadTreeNode::update(const Ogre::Vector3 &cameraPos) +{ + const Ogre::AxisAlignedBox& bounds = getBoundingBox(); + if (bounds.isNull()) + return; + + Ogre::AxisAlignedBox worldBounds (bounds.getMinimum() + Ogre::Vector3(mCenter.x*8192, mCenter.y*8192, 0), + bounds.getMaximum() + Ogre::Vector3(mCenter.x*8192, mCenter.y*8192, 0)); + + float dist = distance(worldBounds, cameraPos); + /// \todo implement error metrics or some other means of not using arbitrary values + size_t wantedLod = 0; + if (dist > 8192*1) + wantedLod = 1; + if (dist > 8192*2) + wantedLod = 2; + if (dist > 8192*5) + wantedLod = 3; + if (dist > 8192*12) + wantedLod = 4; + if (dist > 8192*32) + wantedLod = 5; + if (dist > 8192*64) + wantedLod = 6; + + if (mSize <= mTerrain->getMaxBatchSize() && mLodLevel <= wantedLod) + { + // Wanted LOD is small enough to render this node in one chunk + if (!mChunk) + { + mChunk = new Chunk(this, mLodLevel); + mChunk->setVisibilityFlags(mTerrain->getVisiblityFlags()); + mChunk->setCastShadows(true); + mSceneNode->attachObject(mChunk); + if (mSize == 1) + { + ensureLayerInfo(); + mChunk->setMaterial(mMaterialGenerator->generate(mChunk->getMaterial())); + } + else + { + ensureCompositeMap(); + mMaterialGenerator->setCompositeMap(mCompositeMap->getName()); + mChunk->setMaterial(mMaterialGenerator->generateForCompositeMap(mChunk->getMaterial())); + } + } + + + mChunk->setAdditionalLod(wantedLod - mLodLevel); + mChunk->setVisible(true); + + if (hasChildren()) + { + for (int i=0; i<4; ++i) + mChildren[i]->removeChunks(); + } + } + else + { + // Wanted LOD is too detailed to be rendered in one chunk, + // so split it up by delegating to child nodes + if (mChunk) + mChunk->setVisible(false); + assert(hasChildren() && "Leaf node's LOD needs to be 0"); + for (int i=0; i<4; ++i) + mChildren[i]->update(cameraPos); + } +} + +void QuadTreeNode::removeChunks() +{ + if (mChunk) + mChunk->setVisible(false); + if (hasChildren()) + { + for (int i=0; i<4; ++i) + mChildren[i]->removeChunks(); + } +} + +void QuadTreeNode::updateIndexBuffers() +{ + if (hasChunk()) + mChunk->updateIndexBuffer(); + else if (hasChildren()) + { + for (int i=0; i<4; ++i) + mChildren[i]->updateIndexBuffers(); + } +} + +bool QuadTreeNode::hasChunk() +{ + return 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::ensureLayerInfo() +{ + if (mMaterialGenerator->hasLayers()) + return; + + std::vector blendmaps; + std::vector layerList; + mTerrain->getStorage()->getBlendmaps(mSize, mCenter, true, blendmaps, layerList); + + mMaterialGenerator->setLayerList(layerList); + mMaterialGenerator->setBlendmapList(blendmaps); +} + +void QuadTreeNode::prepareForCompositeMap(Ogre::TRect area) +{ + Ogre::SceneManager* sceneMgr = mTerrain->getCompositeMapSceneManager(); + + if (mIsDummy) + { + MaterialGenerator matGen(true); + std::vector layer; + layer.push_back("_land_default.dds"); + matGen.setLayerList(layer); + makeQuad(sceneMgr, area.left, area.top, area.right, area.bottom, matGen.generate(Ogre::MaterialPtr())); + 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(area.left, area.top, area.right-halfW, area.bottom-halfH)); + mChildren[NE]->prepareForCompositeMap(Ogre::TRect(area.left+halfW, area.top, area.right, area.bottom-halfH)); + mChildren[SW]->prepareForCompositeMap(Ogre::TRect(area.left, area.top+halfH, area.right-halfW, area.bottom)); + mChildren[SE]->prepareForCompositeMap(Ogre::TRect(area.left+halfW, area.top+halfH, area.right, area.bottom)); + } + else + { + ensureLayerInfo(); + + Ogre::MaterialPtr material = mMaterialGenerator->generateForCompositeMapRTT(Ogre::MaterialPtr()); + makeQuad(sceneMgr, area.left, area.top, area.right, area.bottom, material); + } +} + + +bool QuadTreeNode::hasCompositeMap() +{ + return !mCompositeMap.isNull(); +} + +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(0,0,1,1)); + + mTerrain->renderCompositeMap(mCompositeMap); + + mTerrain->clearCompositeMapSceneManager(); + +} diff --git a/components/terrain/quadtreenode.hpp b/components/terrain/quadtreenode.hpp new file mode 100644 index 0000000000..b68d6ab827 --- /dev/null +++ b/components/terrain/quadtreenode.hpp @@ -0,0 +1,141 @@ +#ifndef COMPONENTS_TERRAIN_QUADTREENODE_H +#define COMPONENTS_TERRAIN_QUADTREENODE_H + +#include +#include +#include + +namespace Ogre +{ + class Rectangle2D; +} + +namespace Terrain +{ + class Terrain; + class Chunk; + class MaterialGenerator; + + enum Direction + { + North = 0, + East = 1, + South = 2, + West = 3 + }; + + enum ChildDirection + { + NW = 0, + NE = 1, + SW = 2, + SE = 3, + Root + }; + + /** + * @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 (Terrain* terrain, ChildDirection dir, float size, const Ogre::Vector2& center, QuadTreeNode* parent); + ~QuadTreeNode(); + + /// @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; } + + int getSize() { return mSize; } + Ogre::Vector2 getCenter() { return mCenter; } + + bool hasChildren() { return mChildren[0] != 0; } + QuadTreeNode* getChild(ChildDirection dir) { return mChildren[dir]; } + + /// Search for a neighbour node in this direction + QuadTreeNode* searchNeighbour (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) { mBounds = box; } + + /// Get bounding box in local coordinates + const Ogre::AxisAlignedBox& getBoundingBox(); + + Terrain* getTerrain() { return mTerrain; } + + /// Adjust LODs for the given camera position, possibly splitting up chunks or merging them. + void 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(); + + /// Remove chunks rendered by this node and all its children + void removeChunks(); + + /// Get the effective LOD level if this node was rendered in one chunk + /// with ESM::Land::LAND_SIZE^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(); + + bool hasCompositeMap(); + + /// 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. + /// @param area area in image space to put the quad + /// @param quads collect quads here so they can be deleted later + void prepareForCompositeMap(Ogre::TRect area); + + private: + // Stored here for convenience in case we need layer list again + MaterialGenerator* mMaterialGenerator; + + bool mIsDummy; + float mSize; + size_t mLodLevel; // LOD if we were to render this node in one chunk + Ogre::AxisAlignedBox mBounds; + ChildDirection mDirection; + Ogre::Vector2 mCenter; + + Ogre::SceneNode* mSceneNode; + + QuadTreeNode* mParent; + QuadTreeNode* mChildren[4]; + + Chunk* mChunk; + + Terrain* mTerrain; + + Ogre::TexturePtr mCompositeMap; + + void ensureLayerInfo(); + void ensureCompositeMap(); + }; + +} + +#endif diff --git a/components/terrain/storage.cpp b/components/terrain/storage.cpp new file mode 100644 index 0000000000..eb2763d948 --- /dev/null +++ b/components/terrain/storage.cpp @@ -0,0 +1,318 @@ +#include "storage.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace Terrain +{ + + struct VertexElement + { + Ogre::Vector3 pos; + Ogre::Vector3 normal; + Ogre::ColourValue colour; + }; + + bool Storage::getMinMaxHeights(float size, const Ogre::Vector2 ¢er, float &min, float &max) + { + assert (size <= 1 && "Storage::getMinMaxHeights, chunk size should be <= 1 cell"); + + /// \todo investigate if min/max heights should be stored at load time in ESM::Land instead + + Ogre::Vector2 origin = center - Ogre::Vector2(size/2.f, size/2.f); + + assert(origin.x == (int) origin.x); + assert(origin.y == (int) origin.y); + + int cellX = origin.x; + int cellY = origin.y; + + const ESM::Land* land = getLand(cellX, cellY); + if (!land) + return false; + + min = std::numeric_limits().max(); + max = -std::numeric_limits().max(); + for (int row=0; rowmLandData->mHeights[col*ESM::Land::LAND_SIZE+row]; + if (h > max) + max = h; + if (h < min) + min = h; + } + } + return true; + } + + void Storage::fixNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row) + { + if (col == ESM::Land::LAND_SIZE-1) + { + ++cellY; + col = 0; + } + if (row == ESM::Land::LAND_SIZE-1) + { + ++cellX; + row = 0; + } + ESM::Land* land = getLand(cellX, cellY); + if (land && land->mHasData) + { + normal.x = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3]; + normal.y = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+1]; + normal.z = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+2]; + } + } + + void Storage::fillVertexBuffers (int lodLevel, float size, const Ogre::Vector2& center, + Ogre::HardwareVertexBufferSharedPtr vertexBuffer, + Ogre::HardwareVertexBufferSharedPtr normalBuffer, + Ogre::HardwareVertexBufferSharedPtr colourBuffer) + { + // LOD level n means every 2^n-th vertex is kept + size_t increment = std::pow(2, lodLevel); + + Ogre::Vector2 origin = center - Ogre::Vector2(size/2.f, size/2.f); + assert(origin.x == (int) origin.x); + assert(origin.y == (int) origin.y); + + int startX = origin.x; + int startY = origin.y; + + size_t numVerts = size*(ESM::Land::LAND_SIZE-1)/increment + 1; + + std::vector colors; + colors.resize(numVerts*numVerts*4); + std::vector positions; + positions.resize(numVerts*numVerts*3); + std::vector normals; + normals.resize(numVerts*numVerts*3); + + Ogre::Vector3 normal; + Ogre::ColourValue color; + + float vertY; + float vertX; + + float vertY_ = 0; // of current cell corner + for (int cellY = startY; cellY < startY + std::ceil(size); ++cellY) + { + float vertX_ = 0; // of current cell corner + for (int cellX = startX; cellX < startX + std::ceil(size); ++cellX) + { + ESM::Land* land = getLand(cellX, cellY); + if (land && !land->mHasData) + land = NULL; + bool hasColors = land && land->mLandData->mUsingColours; + + int rowStart = 0; + int colStart = 0; + // Skip the first row / column unless we're at a chunk edge, + // since this row / column is already contained in a previous cell + if (colStart == 0 && vertY_ != 0) + colStart += increment; + if (rowStart == 0 && vertX_ != 0) + rowStart += increment; + + vertY = vertY_; + for (int col=colStart; colmLandData->mHeights[col*ESM::Land::LAND_SIZE+row]; + else + positions[vertX*numVerts*3 + vertY*3 + 2] = -2048; + + if (land) + { + normal.x = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3]; + normal.y = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+1]; + normal.z = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+2]; + // Normals don't connect seamlessly between cells - wtf? + if (col == ESM::Land::LAND_SIZE-1 || row == ESM::Land::LAND_SIZE-1) + fixNormal(normal, cellX, cellY, col, row); + // z < 0 should never happen, but it does - I hate this data set... + if (normal.z < 0) + normal *= -1; + normal.normalise(); + } + else + normal = Ogre::Vector3(0,0,1); + + normals[vertX*numVerts*3 + vertY*3] = normal.x; + normals[vertX*numVerts*3 + vertY*3 + 1] = normal.y; + normals[vertX*numVerts*3 + vertY*3 + 2] = normal.z; + + if (hasColors) + { + color.r = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3] / 255.f; + color.g = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+1] / 255.f; + color.b = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+2] / 255.f; + } + else + { + color.r = 1; + color.g = 1; + color.b = 1; + } + color.a = 1; + Ogre::uint32 rsColor; + Ogre::Root::getSingleton().getRenderSystem()->convertColourValue(color, &rsColor); + memcpy(&colors[vertX*numVerts*4 + vertY*4], &rsColor, sizeof(Ogre::uint32)); + + ++vertX; + } + ++vertY; + } + vertX_ = vertX; + } + vertY_ = vertY; + + assert(vertX_ == numVerts); // Ensure we covered whole area + } + assert(vertY_ == numVerts); // Ensure we covered whole area + + vertexBuffer->writeData(0, vertexBuffer->getSizeInBytes(), &positions[0], true); + normalBuffer->writeData(0, normalBuffer->getSizeInBytes(), &normals[0], true); + colourBuffer->writeData(0, colourBuffer->getSizeInBytes(), &colors[0], true); + } + + Storage::UniqueTextureId Storage::getVtexIndexAt(int cellX, int cellY, + int x, int y) + { + // If we're at the last row (or last column), we need to get the texture from the neighbour cell + // to get consistent blending at the border + if (x >= ESM::Land::LAND_TEXTURE_SIZE) + { + cellX++; + x -= ESM::Land::LAND_TEXTURE_SIZE; + } + if (y >= ESM::Land::LAND_TEXTURE_SIZE) + { + cellY++; + y -= ESM::Land::LAND_TEXTURE_SIZE; + } + assert(xisDataLoaded(ESM::Land::DATA_VTEX)) + land->loadData(ESM::Land::DATA_VTEX); + + int tex = land->mLandData->mTextures[y * ESM::Land::LAND_TEXTURE_SIZE + x]; + if (tex == 0) + return std::make_pair(0,0); // vtex 0 is always the base texture, regardless of plugin + return std::make_pair(tex, land->mPlugin); + } + else + return std::make_pair(0,0); + } + + std::string Storage::getTextureName(UniqueTextureId id) + { + if (id.first == 0) + return "_land_default.dds"; // Not sure if the default texture really is hardcoded? + + // NB: All vtex ids are +1 compared to the ltex ids + const ESM::LandTexture* ltex = getLandTexture(id.first-1, id.second); + + std::string texture = ltex->mTexture; + //TODO this is needed due to MWs messed up texture handling + texture = texture.substr(0, texture.rfind(".")) + ".dds"; + + return texture; + } + + void Storage::getBlendmaps(float chunkSize, const Ogre::Vector2 &chunkCenter, + bool pack, std::vector &blendmaps, std::vector &layerList) + { + Ogre::Vector2 origin = chunkCenter - Ogre::Vector2(chunkSize/2.f, chunkSize/2.f); + int cellX = origin.x; + int cellY = origin.y; + + // Save the used texture indices so we know the total number of textures + // and number of required blend maps + std::set textureIndices; + // Due to the way the blending works, the base layer will always shine through in between + // blend transitions (eg halfway between two texels, both blend values will be 0.5, so 25% of base layer visible). + // To get a consistent look, we need to make sure to use the same base layer in all cells. + // So we're always adding _land_default.dds as the base layer here, even if it's not referenced in this cell. + textureIndices.insert(std::make_pair(0,0)); + // NB +1 to get the last index from neighbour cell (see getVtexIndexAt) + for (int y=0; y textureIndicesMap; + for (std::set::iterator it = textureIndices.begin(); it != textureIndices.end(); ++it) + { + int size = textureIndicesMap.size(); + textureIndicesMap[*it] = size; + layerList.push_back(getTextureName(*it)); + } + + int numTextures = textureIndices.size(); + // numTextures-1 since the base layer doesn't need blending + int numBlendmaps = pack ? std::ceil((numTextures-1) / 4.f) : (numTextures-1); + + int channels = pack ? 4 : 1; + + // Second iteration - create and fill in the blend maps + const int blendmapSize = ESM::Land::LAND_TEXTURE_SIZE+1; + std::vector data; + data.resize(blendmapSize * blendmapSize * channels, 0); + + for (int i=0; isecond; + int blendIndex = (pack ? std::floor((layerIndex-1)/4.f) : layerIndex-1); + int channel = pack ? std::max(0, (layerIndex-1) % 4) : 0; + + if (blendIndex == i) + data[y*blendmapSize*channels + x*channels + channel] = 255; + else + data[y*blendmapSize*channels + x*channels + channel] = 0; + } + } + + // All done, upload to GPU + Ogre::DataStreamPtr stream(new Ogre::MemoryDataStream(&data[0], data.size())); + map->loadRawData(stream, blendmapSize, blendmapSize, format); + blendmaps.push_back(map); + } + } + + +} diff --git a/components/terrain/storage.hpp b/components/terrain/storage.hpp new file mode 100644 index 0000000000..d55403d9c4 --- /dev/null +++ b/components/terrain/storage.hpp @@ -0,0 +1,76 @@ +#ifndef COMPONENTS_TERRAIN_STORAGE_H +#define COMPONENTS_TERRAIN_STORAGE_H + +#include +#include + +#include + +#include + +namespace Terrain +{ + + /// We keep storage of terrain data abstract here since we need different implementations for game and editor + class Storage + { + private: + virtual ESM::Land* getLand (int cellX, int cellY) = 0; + virtual const ESM::LandTexture* getLandTexture(int index, short plugin) = 0; + + public: + /// Get bounds of the whole terrain in cell units + virtual Ogre::AxisAlignedBox getBounds() = 0; + + /// Get the minimum and maximum heights of a terrain chunk. + /// @note Should only be called for chunks <= 1 cell, i.e. leafs of the quad tree. + /// Larger chunks can simply merge AABB of children. + /// @param size size of the chunk in cell units + /// @param center center of the chunk in cell units + /// @param min min height will be stored here + /// @param max max height will be stored here + /// @return true if there was data available for this terrain chunk + bool getMinMaxHeights (float size, const Ogre::Vector2& center, float& min, float& max); + + /// Fill vertex buffers for a terrain chunk. + /// @param lodLevel LOD level, 0 = most detailed + /// @param size size of the terrain chunk in cell units + /// @param center center of the chunk in cell units + /// @param vertexBuffer buffer to write vertices + /// @param normalBuffer buffer to write vertex normals + /// @param colourBuffer buffer to write vertex colours + void fillVertexBuffers (int lodLevel, float size, const Ogre::Vector2& center, + Ogre::HardwareVertexBufferSharedPtr vertexBuffer, + Ogre::HardwareVertexBufferSharedPtr normalBuffer, + Ogre::HardwareVertexBufferSharedPtr colourBuffer); + + /// Create textures holding layer blend values for a terrain chunk. + /// @note The terrain chunk shouldn't be larger than one cell since otherwise we might + /// have to do a ridiculous amount of different layers. For larger chunks, composite maps should be used. + /// @param chunkSize size of the terrain chunk in cell units + /// @param chunkCenter center of the chunk in cell units + /// @param pack Whether to pack blend values for up to 4 layers into one texture (one in each channel) - + /// otherwise, each texture contains blend values for one layer only. Shader-based rendering + /// can utilize packing, FFP can't. + /// @param blendmaps created blendmaps will be written here + /// @param layerList names of the layer textures used will be written here + void getBlendmaps (float chunkSize, const Ogre::Vector2& chunkCenter, bool pack, + std::vector& blendmaps, + std::vector& layerList); + + private: + void fixNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row); + + // Since plugins can define new texture palettes, we need to know the plugin index too + // in order to retrieve the correct texture name. + // pair + typedef std::pair UniqueTextureId; + + UniqueTextureId getVtexIndexAt(int cellX, int cellY, + int x, int y); + std::string getTextureName (UniqueTextureId id); + }; + +} + +#endif diff --git a/components/terrain/terrain.cpp b/components/terrain/terrain.cpp new file mode 100644 index 0000000000..c0f6cf2eb9 --- /dev/null +++ b/components/terrain/terrain.cpp @@ -0,0 +1,359 @@ +#include "terrain.hpp" + +#include +#include +#include +#include +#include + +#include + +#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 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(numVertsOneSide-1)); // U + uvs.push_back(row / static_cast(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 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(); + } + + +} diff --git a/components/terrain/terrain.hpp b/components/terrain/terrain.hpp new file mode 100644 index 0000000000..79e35fd8e9 --- /dev/null +++ b/components/terrain/terrain.hpp @@ -0,0 +1,121 @@ +#ifndef COMPONENTS_TERRAIN_H +#define COMPONENTS_TERRAIN_H + +#include +#include +#include +#include + +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 Terrain + { + public: + /// @note takes ownership of \a storage + Terrain(Ogre::SceneManager* sceneMgr, Storage* storage, int visiblityFlags); + ~Terrain(); + + /// 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. + void update (Ogre::Camera* camera); + + /// \todo + float getHeightAt (const Ogre::Vector3& worldPos) { return 0; } + + /// Get the world bounding box of a chunk of terrain centered at \a center + Ogre::AxisAlignedBox getWorldBoundingBox (const Ogre::Vector2& center); + + Ogre::SceneManager* getSceneManager() { return mSceneMgr; } + + Storage* getStorage() { return mStorage; } + + /// Show or hide the whole terrain + void setVisible(bool visible); + + /// 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.) + void applyMaterials(); + + int getVisiblityFlags() { return mVisibilityFlags; } + + int getMaxBatchSize() { return mMaxBatchSize; } + + void enableSplattingShader(bool enabled); + + private: + QuadTreeNode* mRootNode; + Storage* mStorage; + + int mVisibilityFlags; + + Ogre::SceneManager* mSceneMgr; + Ogre::SceneManager* mCompositeMapSceneMgr; + + /// Bounds in cell units + Ogre::AxisAlignedBox mBounds; + + /// 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); + + public: + // ----INTERNAL---- + + enum IndexBufferFlags + { + IBF_North = 1 << 0, + IBF_East = 1 << 1, + IBF_South = 1 << 2, + IBF_West = 1 << 3 + }; + + /// @param flags first 4*4 bits are LOD deltas on each edge, respectively (4 bits each) + /// next 4 bits are LOD level of the index buffer (LOD 0 = don't omit any vertices) + /// @param numIndices number of indices that were used will be written here + Ogre::HardwareIndexBufferSharedPtr getIndexBuffer (int flags, size_t& numIndices); + + Ogre::HardwareVertexBufferSharedPtr getVertexBuffer (int numVertsOneSide); + + Ogre::SceneManager* getCompositeMapSceneManager() { return mCompositeMapSceneMgr; } + + // Delete all quads + void clearCompositeMapSceneManager(); + void renderCompositeMap (Ogre::TexturePtr target); + + private: + // Index buffers are shared across terrain batches where possible. There is one index buffer for each + // combination of LOD deltas and index buffer LOD we may need. + std::map mIndexBufferMap; + + std::map mUvBufferMap; + + Ogre::RenderTarget* mCompositeMapRenderTarget; + Ogre::TexturePtr mCompositeMapRenderTexture; + }; + +} + +#endif diff --git a/files/materials/objects.shader b/files/materials/objects.shader index 0c869d2cb8..36f92bfd91 100644 --- a/files/materials/objects.shader +++ b/files/materials/objects.shader @@ -80,7 +80,6 @@ #endif #if VERTEX_LIGHTING - shUniform(float, lightCount) @shAutoConstant(lightCount, light_count) shUniform(float4, lightPosition[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightPosition, light_position_view_space_array, @shGlobalSettingString(num_lights)) shUniform(float4, lightDiffuse[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightDiffuse, light_diffuse_colour_array, @shGlobalSettingString(num_lights)) shUniform(float4, lightAttenuation[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightAttenuation, light_attenuation_array, @shGlobalSettingString(num_lights)) diff --git a/files/materials/terrain.shader b/files/materials/terrain.shader index 1330229570..1f11217d23 100644 --- a/files/materials/terrain.shader +++ b/files/materials/terrain.shader @@ -2,7 +2,7 @@ #define IS_FIRST_PASS (@shPropertyString(pass_index) == 0) -#define FOG @shGlobalSettingBool(fog) +#define FOG (@shGlobalSettingBool(fog) && !@shPropertyBool(render_composite_map)) #define SHADOWS_PSSM @shGlobalSettingBool(shadows_pssm) #define SHADOWS @shGlobalSettingBool(shadows) @@ -11,8 +11,6 @@ #include "shadows.h" #endif -#define COLOUR_MAP @shPropertyBool(colour_map) - #define NUM_LAYERS @shPropertyString(num_layers) #if FOG || SHADOWS_PSSM @@ -23,9 +21,12 @@ #define VIEWPROJ_FIX @shGlobalSettingBool(viewproj_fix) -#if !IS_FIRST_PASS -// This is not the first pass. -#endif +#define RENDERCMP @shPropertyBool(render_composite_map) + +#define LIGHTING !RENDERCMP + +#define COMPOSITE_MAP @shPropertyBool(display_composite_map) + #if NEED_DEPTH @shAllocatePassthrough(1, depth) @@ -35,6 +36,11 @@ @shAllocatePassthrough(3, worldPos) +#if LIGHTING +@shAllocatePassthrough(3, lightResult) +@shAllocatePassthrough(3, directionalResult) +#endif + #if SHADOWS @shAllocatePassthrough(4, lightSpacePos0) #endif @@ -55,11 +61,19 @@ #if VIEWPROJ_FIX shUniform(float4, vpRow2Fix) @shSharedParameter(vpRow2Fix, vpRow2Fix) #endif - - shUniform(float2, lodMorph) @shAutoConstant(lodMorph, custom, 1001) shVertexInput(float2, uv0) shVertexInput(float2, uv1) // lodDelta, lodThreshold + +#if LIGHTING + shNormalInput(float4) + shColourInput(float4) + + shUniform(float, lightCount) @shAutoConstant(lightCount, light_count) + shUniform(float4, lightPosition[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightPosition, light_position_object_space_array, @shGlobalSettingString(num_lights)) + shUniform(float4, lightDiffuse[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightDiffuse, light_diffuse_colour_array, @shGlobalSettingString(num_lights)) + shUniform(float4, lightAttenuation[@shGlobalSettingString(num_lights)]) @shAutoConstant(lightAttenuation, light_attenuation_array, @shGlobalSettingString(num_lights)) + shUniform(float4, lightAmbient) @shAutoConstant(lightAmbient, ambient_light_colour) #if SHADOWS shUniform(float4x4, texViewProjMatrix0) @shAutoConstant(texViewProjMatrix0, texture_viewproj_matrix) @@ -71,31 +85,15 @@ @shEndForeach #endif +#endif + @shPassthroughVertexOutputs SH_START_PROGRAM { - - float4 worldPos = shMatrixMult(worldMatrix, shInputPosition); - // determine whether to apply the LOD morph to this vertex - // we store the deltas against all vertices so we only want to apply - // the morph to the ones which would disappear. The target LOD which is - // being morphed to is stored in lodMorph.y, and the LOD at which - // the vertex should be morphed is stored in uv.w. If we subtract - // the former from the latter, and arrange to only morph if the - // result is negative (it will only be -1 in fact, since after that - // the vertex will never be indexed), we will achieve our aim. - // sign(vertexLOD - targetLOD) == -1 is to morph - float toMorph = -min(0, sign(uv1.y - lodMorph.y)); - - // morph - // this assumes XY terrain alignment - worldPos.z += uv1.x * toMorph * lodMorph.x; - - shOutputPosition = shMatrixMult(viewProjMatrix, worldPos); #if NEED_DEPTH @@ -124,6 +122,8 @@ @shPassthroughAssign(worldPos, worldPos.xyz); +#if LIGHTING + #if SHADOWS float4 lightSpacePos = shMatrixMult(texViewProjMatrix0, shMatrixMult(worldMatrix, shInputPosition)); @shPassthroughAssign(lightSpacePos0, lightSpacePos); @@ -138,6 +138,33 @@ @shEndForeach #endif + + // Lighting + float3 lightDir; + float d; + float3 lightResult = float3(0,0,0); + float3 directionalResult = float3(0,0,0); + @shForeach(@shGlobalSettingString(num_lights)) + lightDir = lightPosition[@shIterator].xyz - (shInputPosition.xyz * lightPosition[@shIterator].w); + d = length(lightDir); + lightDir = normalize(lightDir); + + + lightResult.xyz += lightDiffuse[@shIterator].xyz + * shSaturate(1.0 / ((lightAttenuation[@shIterator].y) + (lightAttenuation[@shIterator].z * d) + (lightAttenuation[@shIterator].w * d * d))) + * max(dot(normal.xyz, lightDir), 0); + +#if @shIterator == 0 + directionalResult = lightResult.xyz; +#endif + @shEndForeach + lightResult.xyz += lightAmbient.xyz; + lightResult.xyz *= colour.xyz; + + @shPassthroughAssign(lightResult, lightResult); + @shPassthroughAssign(directionalResult, directionalResult); + +#endif } #else @@ -151,12 +178,9 @@ SH_BEGIN_PROGRAM -#if COLOUR_MAP - shSampler2D(colourMap) -#endif - - shSampler2D(normalMap) // global normal map - +#if COMPOSITE_MAP + shSampler2D(compositeMap) +#else @shForeach(@shPropertyString(num_blendmaps)) shSampler2D(blendMap@shIterator) @@ -165,6 +189,8 @@ @shForeach(@shPropertyString(num_layers)) shSampler2D(diffuseMap@shIterator) @shEndForeach + +#endif #if FOG shUniform(float3, fogColour) @shAutoConstant(fogColour, fog_colour) @@ -215,9 +241,6 @@ float2 UV = @shPassthroughReceive(UV); float3 worldPos = @shPassthroughReceive(worldPos); - - float3 normal = shSample(normalMap, UV).rgb * 2 - 1; - normal = normalize(normal); #if UNDERWATER @@ -230,17 +253,26 @@ float previousAlpha = 1.f; #endif + +shOutputColour(0) = float4(1,1,1,1); + +#if COMPOSITE_MAP + shOutputColour(0).xyz = shSample(compositeMap, UV).xyz; +#else + // Layer calculations -// rescale UV to directly map vertices to texel centers +// rescale UV to directly map edge vertices to texel centers - this is +// important to get correct blending at cell transitions // TODO: parameterize texel size -float2 blendUV = (UV - 0.5) * (8.0 / (8.0+1.0)) + 0.5; +float2 blendUV = (UV - 0.5) * (16.0 / (16.0+1.0)) + 0.5; @shForeach(@shPropertyString(num_blendmaps)) - float4 blendValues@shIterator = shSample(blendMap@shIterator, blendUV); + float4 blendValues@shIterator = shSaturate(shSample(blendMap@shIterator, blendUV)); @shEndForeach + float3 albedo = float3(0,0,0); - float2 layerUV = UV * 8; + float2 layerUV = UV * 16; @shForeach(@shPropertyString(num_layers)) @@ -262,25 +294,14 @@ float2 blendUV = (UV - 0.5) * (8.0 / (8.0+1.0)) + 0.5; #endif @shEndForeach - shOutputColour(0) = float4(1,1,1,1); - + shOutputColour(0).rgb *= albedo; -#if COLOUR_MAP - // Since we're emulating vertex colors here, - // rescale UV to directly map vertices to texel centers. TODO: parameterize texel size - const float colourmapSize = 33.f; - float2 colourUV = (UV - 0.5) * (colourmapSize / (colourmapSize+1.f)) + 0.5; - shOutputColour(0).rgb *= shSample(colourMap, colourUV).rgb; #endif - shOutputColour(0).rgb *= albedo; - - - - - - +#if LIGHTING // Lighting + float3 lightResult = @shPassthroughReceive(lightResult); + float3 directionalResult = @shPassthroughReceive(directionalResult); // shadows only for the first (directional) light #if SHADOWS @@ -305,40 +326,9 @@ float2 blendUV = (UV - 0.5) * (8.0 / (8.0+1.0)) + 0.5; float shadow = 1.0; #endif - - - float3 lightDir; - float3 diffuse = float3(0,0,0); - float d; - - @shForeach(@shGlobalSettingString(terrain_num_lights)) - - lightDir = lightPosObjSpace@shIterator.xyz - (worldPos.xyz * lightPosObjSpace@shIterator.w); - d = length(lightDir); - - - lightDir = normalize(lightDir); - -#if @shIterator == 0 - - #if (SHADOWS || SHADOWS_PSSM) - diffuse += lightDiffuse@shIterator.xyz * (1.0 / ((lightAttenuation@shIterator.y) + (lightAttenuation@shIterator.z * d) + (lightAttenuation@shIterator.w * d * d))) * max(dot(normal, lightDir), 0) * shadow; - - #else - diffuse += lightDiffuse@shIterator.xyz * (1.0 / ((lightAttenuation@shIterator.y) + (lightAttenuation@shIterator.z * d) + (lightAttenuation@shIterator.w * d * d))) * max(dot(normal, lightDir), 0); - - #endif - -#else - diffuse += lightDiffuse@shIterator.xyz * (1.0 / ((lightAttenuation@shIterator.y) + (lightAttenuation@shIterator.z * d) + (lightAttenuation@shIterator.w * d * d))) * max(dot(normal, lightDir), 0); + shOutputColour(0).xyz *= (lightResult - directionalResult * (1.0-shadow)); #endif - @shEndForeach - - shOutputColour(0).xyz *= (lightAmbient.xyz + diffuse); - - - #if FOG float fogValue = shSaturate((depth - fogParams.y) * fogParams.w); diff --git a/libs/openengine/ogre/imagerotate.cpp b/libs/openengine/ogre/imagerotate.cpp index 3dd5840785..9c32924f1f 100644 --- a/libs/openengine/ogre/imagerotate.cpp +++ b/libs/openengine/ogre/imagerotate.cpp @@ -56,7 +56,7 @@ void ImageRotate::rotate(const std::string& sourceImage, const std::string& dest TEX_TYPE_2D, width, height, 0, - PF_FLOAT16_RGBA, + PF_A8B8G8R8, TU_RENDERTARGET); RenderTarget* rtt = destTextureRot->getBuffer()->getRenderTarget(); @@ -75,7 +75,7 @@ void ImageRotate::rotate(const std::string& sourceImage, const std::string& dest TEX_TYPE_2D, width, height, 0, - PF_FLOAT16_RGBA, + PF_A8B8G8R8, Ogre::TU_STATIC); destTexture->getBuffer()->blit(destTextureRot->getBuffer());