#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(32); //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 - 1; y < fromY + size + 1; y++ ) { for ( int x = fromX - 1; x < fromX + size + 1; x++ ) { 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(); const int splatSize = blendMapSize / size; //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 - 1; texY < fromY + size + 1; texY++ ) { for ( int texX = fromX - 1; texX < fromX + size + 1; texX++ ) { 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; const int relY = texY - fromY; const int layerIndex = indexes.find(ltexIndex)->second; float* const pBlend = terrain->getLayerBlendMap(layerIndex) ->getBlendPointer(); for ( int y = -1; y < splatSize + 1; y++ ) { for ( int x = -1; x < splatSize + 1; x++ ) { //Note: Y is reversed const int splatY = blendMapSize - 1 - relY * splatSize - y; const int splatX = relX * splatSize + x; if ( splatX >= 0 && splatX < blendMapSize && splatY >= 0 && splatY < blendMapSize ) { const int index = (splatY)*blendMapSize + splatX; if ( y >= 0 && y < splatSize && x >= 0 && x < splatSize ) { pBlend[index] = 1; } else { //this provides a transition shading but also //rounds off the corners slightly pBlend[index] = std::min(1.0f, pBlend[index] + 0.5f); } } } } } } 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; } }