#include "storage.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gridsampling.hpp" namespace ESMTerrain { namespace { UniqueTextureId getTextureIdAt(const LandObject* land, std::size_t x, std::size_t y) { assert(x < ESM::Land::LAND_TEXTURE_SIZE); assert(y < ESM::Land::LAND_TEXTURE_SIZE); if (land == nullptr) return { 0, 0 }; const ESM::LandData* data = land->getData(ESM::Land::DATA_VTEX); if (data == nullptr) return { 0, 0 }; const std::uint16_t tex = data->getTextures()[y * ESM::Land::LAND_TEXTURE_SIZE + x]; if (tex == 0) return { 0, 0 }; // vtex 0 is always the base texture, regardless of plugin return { tex, land->getPlugin() }; } #if 0 UniqueTextureId getQuadTextureIdAt(const ESM4::Land* land, std::size_t x, std::size_t y) { assert(x < 17); assert(y < 17); if (land == nullptr) return { 0, 0 }; const ESM::LandData* data = land->getData(ESM::Land::DATA_VTEX); if (data == nullptr) return { 0, 0 }; const std::uint16_t tex = data->getTextures()[y * ESM::Land::LAND_TEXTURE_SIZE + x]; if (tex == 0) return { 0, 0 }; // vtex 0 is always the base texture, regardless of plugin return { tex, land->getPlugin() }; } #endif } class LandCache { public: explicit LandCache(int offsetX, int offsetY, std::size_t size) : mOffsetX(offsetX) , mOffsetY(offsetY) , mSize(size) , mValues(size * size) { } std::optional find(int x, int y) const { const std::size_t index = getIndex(x, y); if (const auto& value = mValues[index]) return value->get(); return std::nullopt; } void insert(int x, int y, osg::ref_ptr&& value) { const std::size_t index = getIndex(x, y); mValues[index] = std::move(value); } private: int mOffsetX; int mOffsetY; std::size_t mSize; std::vector>> mValues; std::size_t getIndex(int x, int y) const { return normalizeCoordinate(x, mOffsetX) * mSize + normalizeCoordinate(y, mOffsetY); } std::size_t normalizeCoordinate(int value, int offset) const { assert(value >= offset); assert(value < offset + static_cast(mSize)); return static_cast(value - offset); } }; LandObject::LandObject(const ESM4::Land& land, int loadFlags) : mData(land, loadFlags) { } LandObject::LandObject(const ESM::Land& land, int loadFlags) : mData(land, loadFlags) { } LandObject::LandObject(const LandObject& /*copy*/, const osg::CopyOp& /*copyOp*/) { throw std::logic_error("LandObject copy constructor is not implemented"); } const float defaultHeight = ESM::Land::DEFAULT_HEIGHT; Storage::Storage(const VFS::Manager* vfs, std::string_view normalMapPattern, std::string_view normalHeightMapPattern, bool autoUseNormalMaps, std::string_view specularMapPattern, bool autoUseSpecularMaps) : mIsEsm4Ext(false) , mVFS(vfs) , mNormalMapPattern(normalMapPattern) , mNormalHeightMapPattern(normalHeightMapPattern) , mAutoUseNormalMaps(autoUseNormalMaps) , mSpecularMapPattern(specularMapPattern) , mAutoUseSpecularMaps(autoUseSpecularMaps) { } bool Storage::getMinMaxHeights(float size, const osg::Vec2f& center, ESM::RefId worldspace, float& min, float& max) { assert(size <= 1 && "Storage::getMinMaxHeights, chunk size should be <= 1 cell"); osg::Vec2f origin = center - osg::Vec2f(size / 2.f, size / 2.f); int cellX = static_cast(std::floor(origin.x())); int cellY = static_cast(std::floor(origin.y())); osg::ref_ptr land = getLand(ESM::ExteriorCellLocation(cellX, cellY, worldspace)); const ESM::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : nullptr; const int landSize = ESM::getLandSize(worldspace); int startRow = (origin.x() - cellX) * landSize; int startColumn = (origin.y() - cellY) * landSize; int endRow = startRow + size * (landSize - 1) + 1; int endColumn = startColumn + size * (landSize - 1) + 1; if (data) { min = std::numeric_limits::max(); max = -std::numeric_limits::max(); for (int row = startRow; row < endRow; ++row) { for (int col = startColumn; col < endColumn; ++col) { float h = data->getHeights()[col * landSize + row]; if (h > max) max = h; if (h < min) min = h; } } return true; } min = defaultHeight; max = defaultHeight; return false; } void Storage::fixNormal( osg::Vec3f& normal, ESM::ExteriorCellLocation cellLocation, int col, int row, LandCache& cache) { const int landSize = ESM::getLandSize(cellLocation.mWorldspace); while (col >= landSize - 1) { ++cellLocation.mY; col -= landSize - 1; } while (row >= landSize - 1) { ++cellLocation.mX; row -= landSize - 1; } while (col < 0) { --cellLocation.mY; col += landSize - 1; } while (row < 0) { --cellLocation.mX; row += landSize - 1; } const LandObject* land = getLand(cellLocation, cache); const ESM::LandData* data = land ? land->getData(ESM::Land::DATA_VNML) : nullptr; if (data) { normal.x() = data->getNormals()[col * landSize * 3 + row * 3]; normal.y() = data->getNormals()[col * landSize * 3 + row * 3 + 1]; normal.z() = data->getNormals()[col * landSize * 3 + row * 3 + 2]; normal.normalize(); } else normal = osg::Vec3f(0, 0, 1); } void Storage::averageNormal( osg::Vec3f& normal, ESM::ExteriorCellLocation cellLocation, int col, int row, LandCache& cache) { osg::Vec3f n1, n2, n3, n4; fixNormal(n1, cellLocation, col + 1, row, cache); fixNormal(n2, cellLocation, col - 1, row, cache); fixNormal(n3, cellLocation, col, row + 1, cache); fixNormal(n4, cellLocation, col, row - 1, cache); normal = (n1 + n2 + n3 + n4); normal.normalize(); } void Storage::fixColour( osg::Vec4ub& color, ESM::ExteriorCellLocation cellLocation, int col, int row, LandCache& cache) { const int landSize = ESM::getLandSize(cellLocation.mWorldspace); if (col == landSize - 1) { ++cellLocation.mY; col = 0; } if (row == landSize - 1) { ++cellLocation.mX; row = 0; } const LandObject* land = getLand(cellLocation, cache); const ESM::LandData* data = land ? land->getData(ESM::Land::DATA_VCLR) : nullptr; if (data) { color.r() = data->getColors()[col * landSize * 3 + row * 3]; color.g() = data->getColors()[col * landSize * 3 + row * 3 + 1]; color.b() = data->getColors()[col * landSize * 3 + row * 3 + 2]; } else { color.r() = 255; color.g() = 255; color.b() = 255; } } void Storage::fillVertexBuffers(int lodLevel, float size, const osg::Vec2f& center, ESM::RefId worldspace, osg::Vec3Array& positions, osg::Vec3Array& normals, osg::Vec4ubArray& colours) { if (lodLevel < 0 || 63 < lodLevel) throw std::invalid_argument("Invalid terrain lod level: " + std::to_string(lodLevel)); if (size <= 0) throw std::invalid_argument("Invalid terrain size: " + std::to_string(size)); // LOD level n means every 2^n-th vertex is kept const std::size_t sampleSize = std::size_t{ 1 } << lodLevel; const std::size_t cellSize = static_cast(ESM::getLandSize(worldspace)); const std::size_t numVerts = static_cast(size * (cellSize - 1) / sampleSize) + 1; positions.resize(numVerts * numVerts); normals.resize(numVerts * numVerts); colours.resize(numVerts * numVerts); const bool alteration = useAlteration(); const int landSizeInUnits = ESM::getCellSize(worldspace); const osg::Vec2f origin = center - osg::Vec2f(size, size) * 0.5f; const int startCellX = static_cast(std::floor(origin.x())); const int startCellY = static_cast(std::floor(origin.y())); LandCache cache(startCellX - 1, startCellY - 1, static_cast(std::ceil(size)) + 2); std::pair lastCell{ startCellX, startCellY }; const LandObject* land = getLand(ESM::ExteriorCellLocation(startCellX, startCellY, worldspace), cache); const ESM::LandData* heightData = nullptr; const ESM::LandData* normalData = nullptr; const ESM::LandData* colourData = nullptr; bool validHeightDataExists = false; if (land != nullptr) { heightData = land->getData(ESM::Land::DATA_VHGT); normalData = land->getData(ESM::Land::DATA_VNML); colourData = land->getData(ESM::Land::DATA_VCLR); validHeightDataExists = true; } const auto handleSample = [&](std::size_t cellShiftX, std::size_t cellShiftY, std::size_t row, std::size_t col, std::size_t vertX, std::size_t vertY) { const int cellX = startCellX + cellShiftX; const int cellY = startCellY + cellShiftY; const std::pair cell{ cellX, cellY }; const ESM::ExteriorCellLocation cellLocation(cellX, cellY, worldspace); if (lastCell != cell) { land = getLand(cellLocation, cache); heightData = nullptr; normalData = nullptr; colourData = nullptr; if (land != nullptr) { heightData = land->getData(ESM::Land::DATA_VHGT); normalData = land->getData(ESM::Land::DATA_VNML); colourData = land->getData(ESM::Land::DATA_VCLR); validHeightDataExists = true; } lastCell = cell; } float height = defaultHeight; if (heightData != nullptr) height = heightData->getHeights()[col * cellSize + row]; if (alteration) height += getAlteredHeight(col, row); const std::size_t vertIndex = vertX * numVerts + vertY; positions[vertIndex] = osg::Vec3f((vertX / static_cast(numVerts - 1) - 0.5f) * size * landSizeInUnits, (vertY / static_cast(numVerts - 1) - 0.5f) * size * landSizeInUnits, height); const std::size_t srcArrayIndex = col * cellSize * 3 + row * 3; osg::Vec3f normal(0, 0, 1); if (normalData != nullptr) { for (std::size_t i = 0; i < 3; ++i) normal[i] = normalData->getNormals()[srcArrayIndex + i]; normal.normalize(); } // Normals apparently don't connect seamlessly between cells if (col == cellSize - 1 || row == cellSize - 1) fixNormal(normal, cellLocation, col, row, cache); // some corner normals appear to be complete garbage (z < 0) if ((row == 0 || row == cellSize - 1) && (col == 0 || col == cellSize - 1)) averageNormal(normal, cellLocation, col, row, cache); assert(normal.z() > 0); normals[vertIndex] = normal; osg::Vec4ub color(255, 255, 255, 255); if (colourData != nullptr) for (std::size_t i = 0; i < 3; ++i) color[i] = colourData->getColors()[srcArrayIndex + i]; // Does nothing by default, override in OpenMW-CS if (alteration) adjustColor(col, row, heightData, color); // Unlike normals, colors mostly connect seamlessly between cells, but not always... if (col == cellSize - 1 || row == cellSize - 1) fixColour(color, cellLocation, col, row, cache); colours[vertIndex] = color; }; const std::size_t beginX = static_cast((origin.x() - startCellX) * cellSize); const std::size_t beginY = static_cast((origin.y() - startCellY) * cellSize); const std::size_t distance = static_cast(size * (cellSize - 1)) + 1; sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, handleSample); if (!validHeightDataExists && ESM::isEsm4Ext(worldspace)) std::fill(positions.begin(), positions.end(), osg::Vec3f()); } // NOTE: getLandTexture() is implemented by our child class. Also note that ESM4 doesn't // want to call correctTexturePath(). We need a way of figuring out that we are in // ESM4 worldspace, either via a parameter or via a state kept as a member to Storage // or TerrainStorage (i.e. our child class). std::string Storage::getTextureName(UniqueTextureId id) { std::string_view texture = "_land_default.dds"; if (id.first != 0) { // NB: All vtex ids are +1 compared to the ltex ids const std::string* ltex = getLandTexture(id.first - 1, id.second); if (ltex) texture = *ltex; else { Log(Debug::Warning) << "Warning: Unable to find land texture index " << id.first - 1 << " in plugin " << id.second << ", using default texture instead"; } } // this is needed due to MWs messed up texture handling return Misc::ResourceHelpers::correctTexturePath(texture, mVFS); } // FIXME: for FO3/FONV/TES5 this is rather inefficient since the TextureSet indicates // whether normal map exists, etc, saving us the need to do any searching // in getLayerInfo() // // maybe if ltex->mTextureFile is empty simply return a null string and process // differently? std::string Storage::getEsm4TextureName(ESM::RefId id) { //if (mIsEsm4Ext) if (const ESM4::LandTexture *ltex = getEsm4LandTexture(id)) { if (ltex->mTextureFile.empty()) // WARN: we assume FO3/FONV/TES5 { if (const ESM4::TextureSet *txst = getEsm4TextureSet(ltex->mTexture)) { return "textures\\"+txst->mDiffuse; } } else return "textures\\landscape\\"+ltex->mTextureFile; } // FIXME: add a debug log here return ""; } // FIXME: May need some changes here to support ESM4 terrain. Not sure how to deal with many // chunks (i.e. 4 ESM4 quads). Maybe we just go with the flow here, but do // things 4 times as much? // // For now decided to create another method instead (getQuadBlendmaps). void Storage::getBlendmaps(float chunkSize, const osg::Vec2f& chunkCenter, ImageVector& blendmaps, std::vector& layerList, ESM::RefId worldspace) { const osg::Vec2f origin = chunkCenter - osg::Vec2f(chunkSize, chunkSize) * 0.5f; const int startCellX = static_cast(std::floor(origin.x())); const int startCellY = static_cast(std::floor(origin.y())); const std::size_t blendmapSize = getBlendmapSize(chunkSize, ESM::Land::LAND_TEXTURE_SIZE); // We need to upscale the blendmap 2x with nearest neighbor sampling to look like Vanilla constexpr std::size_t imageScaleFactor = 2; const std::size_t blendmapImageSize = blendmapSize * imageScaleFactor; std::vector textureIds(blendmapSize * blendmapSize); LandCache cache(startCellX - 1, startCellY - 1, static_cast(std::ceil(chunkSize)) + 2); std::pair lastCell{ startCellX, startCellY }; const LandObject* land = getLand(ESM::ExteriorCellLocation(startCellX, startCellY, worldspace), cache); const auto handleSample = [&](const CellSample& sample) { const std::pair cell{ sample.mCellX, sample.mCellY }; if (lastCell != cell) { land = getLand(ESM::ExteriorCellLocation(sample.mCellX, sample.mCellY, worldspace), cache); lastCell = cell; } textureIds[sample.mDstCol * blendmapSize + sample.mDstRow] = getTextureIdAt(land, sample.mSrcRow, sample.mSrcCol); }; sampleBlendmaps(chunkSize, origin.x(), origin.y(), ESM::Land::LAND_TEXTURE_SIZE, handleSample); std::map textureIndicesMap; for (std::size_t y = 0; y < blendmapSize; ++y) { for (std::size_t x = 0; x < blendmapSize; ++x) { const UniqueTextureId id = textureIds[y * blendmapSize + x]; auto found = textureIndicesMap.find(id); if (found == textureIndicesMap.end()) { std::size_t layerIndex = layerList.size(); Terrain::LayerInfo info = getLayerInfo(getTextureName(id)); // look for existing diffuse map, which may be present when several plugins use the same texture for (std::size_t i = 0; i < layerList.size(); ++i) { if (layerList[i].mDiffuseMap == info.mDiffuseMap) { layerIndex = i; break; } } found = textureIndicesMap.emplace(id, layerIndex).first; if (layerIndex >= layerList.size()) { osg::ref_ptr image(new osg::Image); image->allocateImage(static_cast(blendmapImageSize), static_cast(blendmapImageSize), 1, GL_ALPHA, GL_UNSIGNED_BYTE); std::memset(image->data(), 0, image->getTotalDataSize()); blendmaps.push_back(std::move(image)); layerList.push_back(std::move(info)); } } const std::size_t layerIndex = found->second; unsigned char* const data = blendmaps[layerIndex]->data(); const std::size_t realY = (blendmapSize - y - 1) * imageScaleFactor; const std::size_t realX = x * imageScaleFactor; data[((realY + 0) * blendmapImageSize + realX + 0)] = 255; data[((realY + 1) * blendmapImageSize + realX + 0)] = 255; data[((realY + 0) * blendmapImageSize + realX + 1)] = 255; data[((realY + 1) * blendmapImageSize + realX + 1)] = 255; } } if (blendmaps.size() == 1) blendmaps.clear(); // If a single texture fills the whole terrain, there is no need to blend } float Storage::getHeightAt(const osg::Vec3f& worldPos, ESM::RefId worldspace) { const float cellSize = ESM::getCellSize(worldspace); int cellX = static_cast(std::floor(worldPos.x() / cellSize)); int cellY = static_cast(std::floor(worldPos.y() / cellSize)); osg::ref_ptr land = getLand(ESM::ExteriorCellLocation(cellX, cellY, worldspace)); if (!land) return ESM::isEsm4Ext(worldspace) ? std::numeric_limits::lowest() : defaultHeight; const ESM::LandData* data = land->getData(ESM::Land::DATA_VHGT); if (!data) return defaultHeight; const int landSize = data->getLandSize(); // Mostly lifted from Ogre::Terrain::getHeightAtTerrainPosition // Normalized position in the cell float nX = (worldPos.x() - (cellX * cellSize)) / cellSize; float nY = (worldPos.y() - (cellY * cellSize)) / cellSize; // get left / bottom points (rounded down) float factor = landSize - 1.0f; float invFactor = 1.0f / factor; int startX = static_cast(nX * factor); int startY = static_cast(nY * factor); int endX = startX + 1; int endY = startY + 1; endX = std::min(endX, landSize - 1); endY = std::min(endY, landSize - 1); // now get points in terrain space (effectively rounding them to boundaries) float startXTS = startX * invFactor; float startYTS = startY * invFactor; float endXTS = endX * invFactor; float endYTS = endY * invFactor; // get parametric from start coord to next point float xParam = (nX - startXTS) * factor; float yParam = (nY - startYTS) * factor; /* For even / odd tri strip rows, triangles are this shape: even odd 3---2 3---2 | / | | \ | 0---1 0---1 */ // Build all 4 positions in normalized cell space, using point-sampled height osg::Vec3f v0(startXTS, startYTS, getVertexHeight(data, startX, startY) / cellSize); osg::Vec3f v1(endXTS, startYTS, getVertexHeight(data, endX, startY) / cellSize); osg::Vec3f v2(endXTS, endYTS, getVertexHeight(data, endX, endY) / cellSize); osg::Vec3f v3(startXTS, endYTS, getVertexHeight(data, startX, endY) / cellSize); // define this plane in terrain space osg::Plane plane; // FIXME: deal with differing triangle alignment if (true) { // odd row bool secondTri = ((1.0 - yParam) > xParam); if (secondTri) plane = osg::Plane(v0, v1, v3); else plane = osg::Plane(v1, v2, v3); } /* else { // even row bool secondTri = (yParam > xParam); if (secondTri) plane.redefine(v0, v2, v3); else plane.redefine(v0, v1, v2); } */ // Solve plane equation for z return (-plane.getNormal().x() * nX - plane.getNormal().y() * nY - plane[3]) / plane.getNormal().z() * cellSize; } const LandObject* Storage::getLand(ESM::ExteriorCellLocation cellLocation, LandCache& cache) { if (const auto land = cache.find(cellLocation.mX, cellLocation.mY)) return *land; osg::ref_ptr land = getLand(cellLocation); const LandObject* result = land.get(); cache.insert(cellLocation.mX, cellLocation.mY, std::move(land)); return result; } void Storage::adjustColor(int col, int row, const ESM::LandData* heightData, osg::Vec4ub& color) const {} float Storage::getAlteredHeight(int col, int row) const { return 0; } Terrain::LayerInfo Storage::getLayerInfo(const std::string& texture) { std::lock_guard lock(mLayerInfoMutex); // Already have this cached? std::map::iterator found = mLayerInfoMap.find(texture); if (found != mLayerInfoMap.end()) return found->second; Terrain::LayerInfo info; info.mParallax = false; info.mSpecular = false; //info.mIsEsm4 = false; // hint for Terrain::createPasses() info.mDiffuseMap = texture; if (mAutoUseNormalMaps) { std::string texture_ = texture; Misc::StringUtils::replaceLast(texture_, ".", mNormalHeightMapPattern + "."); if (mVFS->exists(texture_)) { info.mNormalMap = std::move(texture_); info.mParallax = true; } else { texture_ = texture; Misc::StringUtils::replaceLast(texture_, ".", mNormalMapPattern + "."); if (mVFS->exists(texture_)) info.mNormalMap = std::move(texture_); } } if (mAutoUseSpecularMaps) { std::string texture_ = texture; Misc::StringUtils::replaceLast(texture_, ".", mSpecularMapPattern + "."); if (mVFS->exists(texture_)) { info.mDiffuseMap = std::move(texture_); info.mSpecular = true; } } mLayerInfoMap[texture] = info; return info; } Terrain::LayerInfo Storage::getLayerInfo(const ESM4::TextureSet *txst) { Terrain::LayerInfo info; info.mDiffuseMap = ""; info.mNormalMap = ""; info.mParallax = false; info.mSpecular = false; //info.mIsEsm4 = true; // hint for Terrain::createPasses() if (txst) { assert(!txst->mDiffuseMap.empty() && "getlayerInfo: empty diffuse map"); std::string diffuse = "textures\\landscape\\"+txst->mDiffuse; std::map::iterator found = mLayerInfoMap.find(diffuse); if (found != mLayerInfoMap.end()) return found->second; info.mDiffuseMap = diffuse; if (!txst->mNormalMap.empty()) info.mNormalMap = "textures\\landscape\\"+txst->mNormalMap; // FIXME: this flag indicates height info in alpha channel of normal map // but the normal map alpha channel has specular info instead // (probably needs some flag in the terrain shader to fix) info.mParallax = false; // FIXME: this flag indicates specular info in alpha channel of diffuse // but the diffuse alpha channel has transparency data instead // (probably needs some flag in the terrain shader to fix) info.mSpecular = false; // FIXME: should support other features of ESM4::TextureSet // probably need corresponding support in the terrain shader mLayerInfoMap[diffuse] = info; } return info; } float Storage::getCellWorldSize(ESM::RefId worldspace) { return static_cast(ESM::getCellSize(worldspace)); } int Storage::getCellVertices(ESM::RefId worldspace) { return ESM::getLandSize(worldspace); } // NOTE: For now we are only conident when chunkSize is 1. Needs more testing to see // if this will work with different chunkSize values. // // This is called by ChunkManager::createPasses() which then calls // Terrain::createPasses() which ultimately calls BlendmapTexMat::value(). // I suspect that is where UV mapping is done (just a guess; LayerTexMat // may need to be looked at as well). // // WARN: the value sQuadTexturePerSide was determined empirically for TES4 only // FO3/FONV/TES5 may well have a different value - needs testing int Storage::getBlendmapScale(float chunkSize) { if (mIsEsm4Ext) { //std::cout << "blendmap scale " //<< std::to_string(ESM4::Land::sQuadTexturePerSide * chunkSize) << std::endl; return ESM4::Land::sQuadTexturePerSide;// * chunkSize; } return ESM::Land::LAND_TEXTURE_SIZE * chunkSize; } void Storage::fillQuadVertexBuffers(float size, const osg::Vec2f& center, ESM::RefId worldspace, osg::Vec3Array& positions, osg::Vec3Array& normals, osg::Vec4ubArray& colours, int quad) { // sampleSize is not used but declared here in order to keep the code as close to // fillVertexBuffers() as possible const std::size_t sampleSize = 1; // DEBUG NOTES: cellSize should be 33 for ESM4 // numVerts should be 17 for ESM4 const std::size_t cellSize = static_cast(ESM::getLandSize(worldspace)); const std::size_t numVerts = static_cast(size * (cellSize - 1) / sampleSize) + 1; positions.resize(numVerts*numVerts*3); normals.resize(numVerts*numVerts*3); colours.resize(numVerts*numVerts*4); const bool alteration = useAlteration(); // Does nothing by default, override in OpenMW-CS const int landSizeInUnits = ESM::getCellSize(worldspace); // I think the current code copied from fillVertexBuffers() works fine #if 0 // NOTE: here center is the center of the ESM4 cell (in terms of cell grid position) which // is subtly different to the way fillVertexBuffers() treats it because we don't // worry about chunk sizes or LOD // // center is wrong here due to the way TerrainGrid::buildTerrain() calculates the // new center osg::Vec2f realCenter; switch (quad) { case 3: realCenter = center - osg::Vec2f( 0.25f, 0.25f); break; case 1: realCenter = center - osg::Vec2f( 0.25f, -0.25f); break; case 2: realCenter = center - osg::Vec2f(-0.25f, 0.25f); break; case 0: realCenter = center - osg::Vec2f(-0.25f, -0.25f); break; default: realCenter = center; break; } const osg::Vec2f origin2 = realCenter - osg::Vec2f(1.f, 1.f) * 0.5f; // assumed to be bottom left corner //std::cout << origin2.x() << ", " << origin2.y() << std::endl; #endif const osg::Vec2f origin = center - osg::Vec2f(size, size) * 0.5f; //std::cout << origin.x() << ", " << origin.y() << std::endl; const int startCellX = static_cast(std::floor(origin.x())); const int startCellY = static_cast(std::floor(origin.y())); LandCache cache(startCellX - 1, startCellY - 1, static_cast(std::ceil(size)) + 2); std::pair lastCell{ startCellX, startCellY }; const LandObject* land = getLand(ESM::ExteriorCellLocation(startCellX, startCellY, worldspace), cache); const ESM::LandData* heightData = nullptr; const ESM::LandData* normalData = nullptr; const ESM::LandData* colourData = nullptr; bool validHeightDataExists = false; if (land != nullptr) { heightData = land->getData(ESM::Land::DATA_VHGT); normalData = land->getData(ESM::Land::DATA_VNML); colourData = land->getData(ESM::Land::DATA_VCLR); validHeightDataExists = true; } int rowStart = 0; int colStart = 0; int rowEnd, colEnd; // FIXME: how to ignore the repeat of left/bottom quad? switch (quad) { case 0: // bottom left { rowStart = 0; colStart = 0; rowEnd = int(cellSize / 2) + 1; // int(33 / 2) + 1 = 17 colEnd = int(cellSize / 2) + 1; break; } case 2: // bottom right { rowStart = 0; colStart = int(cellSize / 2); // 16, repeat the last of the left quad rowEnd = int(cellSize / 2) + 1; // 17 colEnd = cellSize; break; } case 1: // top left { rowStart = int(cellSize / 2); // 16, repeat the last of the bottom quad colStart = 0; rowEnd = cellSize; colEnd = int(cellSize / 2) + 1; // 17 break; } case 3: // top right { rowStart = int(cellSize / 2); // 16 colStart = int(cellSize / 2); // 16 rowEnd = cellSize; // 33 colEnd = cellSize; // 33 break; } default: std::fill(positions.begin(), positions.end(), osg::Vec3f()); return; // FIXME: throw instead? } osg::Vec3f normal(0, 0, 1); osg::Vec4ub color(255, 255, 255, 255); // ESM4::Land::mLandData.mHeights start at the bottom left hand corner // // row // | // v // 1056 ..1088 32 // 1023 ..1055 31 // .. // 99 .. 131 3 // 66 .. 98 2 // 33 .. 65 1 // 0 .. 32 0 // // 0 .. 32 <- col // // row and col represent cell space (i.e. mHeights, mVertNorm and mVertColr) // vertX and vertY represent quad space float vertY = 0; float vertX = 0; for (int col = colStart; col < colEnd; col += 1) { vertX = 0; for (int row = rowStart; row < rowEnd; row += 1) { float height = -2048; if (land && heightData) // validHeightDataExists height = heightData->getHeights()[col*cellSize + row]; // FIXME: I suspect landSizeInUnits should be 2048 const std::size_t vertIndex = vertX * numVerts + vertY; positions[vertIndex] = osg::Vec3f((vertX / static_cast(numVerts - 1) - 0.5f) * size * landSizeInUnits, (vertY / static_cast(numVerts - 1) - 0.5f) * size * landSizeInUnits, height); if (land && normalData) { normal.x() = normalData->getNormals()[col * cellSize * 3 + row * 3 + 0]; normal.y() = normalData->getNormals()[col * cellSize * 3 + row * 3 + 1]; normal.z() = normalData->getNormals()[col * cellSize * 3 + row * 3 + 2]; normal.normalize(); } else normal = osg::Vec3f(0, 0, 1); // FIXME: not sure if below normal fixes for Morrowind also applies to TES4 // TODO: needs testing #if 0 // Normals apparently don't connect seamlessly between cells if (col == cellSize - 1 || row == cellSize - 1) fixNormal(normal, cellLocation, col, row, cache); // some corner normals appear to be complete garbage (z < 0) if ((row == 0 || row == cellSize - 1) && (col == 0 || col == cellSize - 1)) averageNormal(normal, cellLocation, col, row, cache); #endif //assert(normal.z() > 0); // ToddLand triggers this if (normal.z() < 0) normal.z() = 0; normals[vertIndex] = normal; if (land && colourData) { color.r() = colourData->getColors()[col * cellSize * 3 + row * 3 + 0]; color.g() = colourData->getColors()[col * cellSize * 3 + row * 3 + 1]; color.b() = colourData->getColors()[col * cellSize * 3 + row * 3 + 2]; } else { color.r() = 1; color.g() = 1; color.b() = 1; } // FIXME: not sure if below colour fixes for Morrowind also applies to TES4 // TODO: needs testing #if 0 // Unlike normals, colors mostly connect seamlessly between cells, but not always... if (col == cellSize - 1 || row == cellSize - 1) fixColour(color, cellLocation, col, row, cache); #endif // color.a() = 1; colours[vertIndex] = color; ++vertX; } ++vertY; } } void Storage::getQuadBlendmaps(float chunkSize, const osg::Vec2f& chunkCenter, ImageVector& blendmaps, std::vector& layerList, ESM::RefId worldspace, int quad) { // VTXT info indicates texture size is 17x17 - but the cell grid is 33x33 // (cf. TES3 has 65x65 cell) do we discard one row and column or overlap? // // NOTE: each base texture does not completely "fill" a quadrant. The observations in // TES4 vanilla indicates that the texture repeats (or "wraps") 6 times each side // // ///////////////// //////////////// <-- discard texture row? // +-----------------+----------------+/ // 32 |\ \| |/ // 31 |\ \| |/ // |\ 17x16 \| 16x16 |/ // . |\ \| |/ // . |\ 2 \| 3 |/ // . |\ \| |/ // . |\ \<---------------------- overlap column instead? // 17 |\ \| |/ // +-----------------+----------------+ // 16 |\ |\\\\\\\\\\\\\\\\|<---- overlap row instead? // 15 |\ | |/ // . |\ 17x17 | 16x17 |/ // . |\ | |/ // . |\ 0 | 1 |/ // . |\ | |/ // 2 |\ | |/ // 1 |\ | |/ // 0 |\\\\\\\\\\\\\\\\\|\\\\\\\\\\\\\\\\|<---- this row of vertices is a copy of cell below // +-----------------+----------------+ // 111 1 33 ^ // 0123 ...... 456 7 ..... 12 | // ^ discard texture column? // | // this column of vertices is a copy of the cell to the left // const osg::Vec2f origin = chunkCenter - osg::Vec2f(chunkSize, chunkSize) * 0.5f; const int startCellX = static_cast(std::floor(origin.x())); const int startCellY = static_cast(std::floor(origin.y())); const int realTextureSize = 17; // FIXME: should be defined in Land record //const std::size_t blendmapSize = getBlendmapSize(chunkSize, realTextureSize); const std::size_t blendmapSize = realTextureSize; // FIXME: temp testing //if (startCellX == 12 && startCellY == 21) //std::cout << "vilverin exterior" << std::endl; // FIXME: I don't think ESM4 needs this? #if 0 // We need to upscale the blendmap 2x with nearest neighbor sampling to look like Vanilla constexpr std::size_t imageScaleFactor = 2; #else constexpr std::size_t imageScaleFactor = 1; #endif const std::size_t blendmapImageSize = blendmapSize * imageScaleFactor; std::vector textureIds(blendmapSize * blendmapSize); // NOTE: we need all the texture data which are missing in LandObject #if 0 LandCache cache(startCellX - 1, startCellY - 1, static_cast(std::ceil(chunkSize)) + 2); std::pair lastCell{ startCellX, startCellY }; const LandObject* land = getLand(ESM::ExteriorCellLocation(startCellX, startCellY, worldspace), cache); #endif // FIXME: do we need to cache this data? (already in ESMStore, why cache again?) // alternatively modify LandObject with all the extra data rather than use getEsm4Land()? const ESM4::Land* land = getEsm4Land(ESM::ExteriorCellLocation(startCellX, startCellY, worldspace)); if (!land) return; // FIXME: throw instead? // I don't think we need this? #if 0 const auto handleSample = [&](const CellSample& sample) { const std::pair cell{ sample.mCellX, sample.mCellY }; if (lastCell != cell) { land = getEsm4Land(ESM::ExteriorCellLocation(sample.mCellX, sample.mCellY, worldspace)); lastCell = cell; } textureIds[sample.mDstCol * blendmapSize + sample.mDstRow] = getQuadTextureIdAt(land, sample.mSrcRow, sample.mSrcCol); }; sampleBlendmaps(chunkSize, origin.x(), origin.y(), realTextureSize, handleSample); std::map textureIndicesMap; #endif // FIXME: debugging only //std::cout << "quad " << quad << std::endl; // base texture Terrain::LayerInfo info; ESM::FormId ltexId = ESM::FormId::fromUint32(land->mTextures[quad].base.formId); std::string texture = getEsm4TextureName(ltexId); if (texture == "") info = getLayerInfo(getEsm4TextureSet(ltexId)); // FO3/FONV/TES5 else info = getLayerInfo(texture); // TES4 // FIXME: debugging only //std::cout << "base " << info.mDiffuseMap << std::endl; osg::ref_ptr image(new osg::Image); image->allocateImage(static_cast(blendmapImageSize), static_cast(blendmapImageSize), 1, GL_ALPHA, GL_UNSIGNED_BYTE); std::memset(image->data(), 255, image->getTotalDataSize()); // fully opaque for base texture blendmaps.push_back(std::move(image)); layerList.push_back(std::move(info)); // additional textures std::size_t numLayers = land->mTextures[quad].layers.size(); for (std::size_t i = 0; i < numLayers; ++i) { Terrain::LayerInfo layerInfo; /*ESM::FormId*/ ltexId = ESM::FormId::fromUint32(land->mTextures[quad].layers[i].texture.formId); std::string layerTexture = getEsm4TextureName(ltexId); if (layerTexture == "") layerInfo = getLayerInfo(getEsm4TextureSet(ltexId)); // FO3/FONV/TES5 else layerInfo = getLayerInfo(layerTexture); // TES4 // FIXME: debugging only //std::cout << "layer " << i << ", " << layerInfo.mDiffuseMap << std::endl; osg::ref_ptr layerImage(new osg::Image); layerImage->allocateImage(static_cast(blendmapImageSize), static_cast(blendmapImageSize), 1, GL_ALPHA, GL_UNSIGNED_BYTE); std::memset(layerImage->data(), 0, layerImage->getTotalDataSize()); blendmaps.push_back(std::move(layerImage)); layerList.push_back(std::move(layerInfo)); const std::size_t layerIndex = blendmaps.size() - 1; unsigned char* const data = blendmaps[layerIndex]->data(); // osg::Image default origin is bottom left and VTXT data also starts at bottom left // corner i.e. there should be no conversion required // // FIXME: but the observed behaviour is different - either VTXT starts at top left // corner or osg::Image is being interpreted differently by the shader // // Image guessed VTXT // index position y' // // 272 ..288 0 .. 16 0 // .. .. // 51 .. 67 221 ..237 13 // 34 .. 50 238 ..254 14 // 17 .. 33 255 ..271 15 // 0 .. 16 272 ..288 16 // // y = floor(position / 17) // y' = 17 - 1 - y // x = position % 17 // // e.g. position = 275, y = 16, y' = 0, x = 3 // position = 50, y = 2, y' = 14, x = 16 const std::vector& opacityData = land->mTextures[quad].layers[i].data; for (std::size_t j = 0; j < opacityData.size(); ++j) { // NOTE: blendmapImageSize, blendmapSize and realTextureSize are all the same (17) int position = opacityData[j].position; std::size_t y = realTextureSize - 1 - std::floor(position / realTextureSize); std::size_t x = position % realTextureSize; data[y*realTextureSize + x] = unsigned char(opacityData[j].opacity * 255); } } } }