From ce6ffba986e64a62c77b630f6249a2735f2be6c1 Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 9 Aug 2023 22:21:23 +0200 Subject: [PATCH] Move blendmap sampling logic into separate function --- .../esmterrain/testgridsampling.cpp | 125 ++++++++++++++++ components/esmterrain/gridsampling.hpp | 76 ++++++++++ components/esmterrain/storage.cpp | 137 ++++++++---------- components/esmterrain/storage.hpp | 12 +- 4 files changed, 269 insertions(+), 81 deletions(-) diff --git a/apps/openmw_test_suite/esmterrain/testgridsampling.cpp b/apps/openmw_test_suite/esmterrain/testgridsampling.cpp index 5ca38b2011..048c3f9fc8 100644 --- a/apps/openmw_test_suite/esmterrain/testgridsampling.cpp +++ b/apps/openmw_test_suite/esmterrain/testgridsampling.cpp @@ -362,5 +362,130 @@ namespace ESMTerrain Sample{ .mCellX = 0, .mCellY = 3, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 1 }, Sample{ .mCellX = 3, .mCellY = 3, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 1 })); } + + auto tie(const CellSample& v) + { + return std::tie(v.mCellX, v.mCellY, v.mSrcRow, v.mSrcCol, v.mDstRow, v.mDstCol); + } + } + + static bool operator==(const CellSample& l, const CellSample& r) + { + return tie(l) == tie(r); + } + + static std::ostream& operator<<(std::ostream& stream, const CellSample& v) + { + return stream << "CellSample{.mCellX = " << v.mCellX << ", .mCellY = " << v.mCellY + << ", .mSrcRow = " << v.mSrcRow << ", .mSrcCol = " << v.mSrcCol << ", .mDstRow = " << v.mDstRow + << ", .mDstCol = " << v.mDstCol << "}"; + } + + namespace + { + struct CollectCellSamples + { + std::vector& mSamples; + + void operator()(const CellSample& value) { mSamples.push_back(value); } + }; + + TEST(ESMTerrainSampleBlendmaps, doesNotSupportNotPositiveSize) + { + const float size = 0; + EXPECT_THROW(sampleBlendmaps(size, 0, 0, 0, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleBlendmaps, doesNotSupportNotPositiveTextureSize) + { + const float size = 1; + const int textureSize = 0; + EXPECT_THROW(sampleBlendmaps(size, 0, 0, textureSize, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleBlendmaps, shouldDecrementBeginRow) + { + const float size = 0.125f; + const float minX = 0.125f; + const float minY = 0.125f; + const int textureSize = 8; + std::vector samples; + sampleBlendmaps(size, minX, minY, textureSize, CollectCellSamples{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 0, .mDstCol = 0 }, + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 1, .mSrcCol = 1, .mDstRow = 1, .mDstCol = 0 }, + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 2, .mDstRow = 0, .mDstCol = 1 }, + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 1, .mSrcCol = 2, .mDstRow = 1, .mDstCol = 1 })); + } + + TEST(ESMTerrainSampleBlendmaps, shouldDecrementBeginRowOverCellBorder) + { + const float size = 0.125f; + const float minX = 0; + const float minY = 0; + const int textureSize = 8; + std::vector samples; + sampleBlendmaps(size, minX, minY, textureSize, CollectCellSamples{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + CellSample{ .mCellX = -1, .mCellY = 0, .mSrcRow = 7, .mSrcCol = 0, .mDstRow = 0, .mDstCol = 0 }, + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 0, .mDstRow = 1, .mDstCol = 0 }, + CellSample{ .mCellX = -1, .mCellY = 0, .mSrcRow = 7, .mSrcCol = 1, .mDstRow = 0, .mDstCol = 1 }, + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 1, .mDstCol = 1 })); + } + + TEST(ESMTerrainSampleBlendmaps, shouldSupportNegativeCoordinates) + { + const float size = 0.125f; + const float minX = -0.5f; + const float minY = -0.5f; + const int textureSize = 8; + std::vector samples; + sampleBlendmaps(size, minX, minY, textureSize, CollectCellSamples{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 3, .mSrcCol = 4, .mDstRow = 0, .mDstCol = 0 }, + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 4, .mSrcCol = 4, .mDstRow = 1, .mDstCol = 0 }, + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 3, .mSrcCol = 5, .mDstRow = 0, .mDstCol = 1 }, + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 4, .mSrcCol = 5, .mDstRow = 1, .mDstCol = 1 })); + } + + TEST(ESMTerrainSampleBlendmaps, shouldCoverMultipleCells) + { + const float size = 2; + const float minX = -1.5f; + const float minY = -1.5f; + const int textureSize = 2; + std::vector samples; + sampleBlendmaps(size, minX, minY, textureSize, CollectCellSamples{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + CellSample{ .mCellX = -2, .mCellY = -2, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 0, .mDstCol = 0 }, + CellSample{ .mCellX = -2, .mCellY = -2, .mSrcRow = 1, .mSrcCol = 1, .mDstRow = 1, .mDstCol = 0 }, + CellSample{ .mCellX = -1, .mCellY = -2, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 2, .mDstCol = 0 }, + CellSample{ .mCellX = -1, .mCellY = -2, .mSrcRow = 1, .mSrcCol = 1, .mDstRow = 3, .mDstCol = 0 }, + CellSample{ .mCellX = 0, .mCellY = -2, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 4, .mDstCol = 0 }, + CellSample{ .mCellX = -2, .mCellY = -1, .mSrcRow = 0, .mSrcCol = 0, .mDstRow = 0, .mDstCol = 1 }, + CellSample{ .mCellX = -2, .mCellY = -1, .mSrcRow = 1, .mSrcCol = 0, .mDstRow = 1, .mDstCol = 1 }, + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 0, .mSrcCol = 0, .mDstRow = 2, .mDstCol = 1 }, + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 1, .mSrcCol = 0, .mDstRow = 3, .mDstCol = 1 }, + CellSample{ .mCellX = 0, .mCellY = -1, .mSrcRow = 0, .mSrcCol = 0, .mDstRow = 4, .mDstCol = 1 }, + CellSample{ .mCellX = -2, .mCellY = -1, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 0, .mDstCol = 2 }, + CellSample{ .mCellX = -2, .mCellY = -1, .mSrcRow = 1, .mSrcCol = 1, .mDstRow = 1, .mDstCol = 2 }, + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 2, .mDstCol = 2 }, + CellSample{ .mCellX = -1, .mCellY = -1, .mSrcRow = 1, .mSrcCol = 1, .mDstRow = 3, .mDstCol = 2 }, + CellSample{ .mCellX = 0, .mCellY = -1, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 4, .mDstCol = 2 }, + CellSample{ .mCellX = -2, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 0, .mDstRow = 0, .mDstCol = 3 }, + CellSample{ .mCellX = -2, .mCellY = 0, .mSrcRow = 1, .mSrcCol = 0, .mDstRow = 1, .mDstCol = 3 }, + CellSample{ .mCellX = -1, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 0, .mDstRow = 2, .mDstCol = 3 }, + CellSample{ .mCellX = -1, .mCellY = 0, .mSrcRow = 1, .mSrcCol = 0, .mDstRow = 3, .mDstCol = 3 }, + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 0, .mDstRow = 4, .mDstCol = 3 }, + CellSample{ .mCellX = -2, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 0, .mDstCol = 4 }, + CellSample{ .mCellX = -2, .mCellY = 0, .mSrcRow = 1, .mSrcCol = 1, .mDstRow = 1, .mDstCol = 4 }, + CellSample{ .mCellX = -1, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 2, .mDstCol = 4 }, + CellSample{ .mCellX = -1, .mCellY = 0, .mSrcRow = 1, .mSrcCol = 1, .mDstRow = 3, .mDstCol = 4 }, + CellSample{ .mCellX = 0, .mCellY = 0, .mSrcRow = 0, .mSrcCol = 1, .mDstRow = 4, .mDstCol = 4 })); + } } } diff --git a/components/esmterrain/gridsampling.hpp b/components/esmterrain/gridsampling.hpp index b65825d8ad..be37ad1223 100644 --- a/components/esmterrain/gridsampling.hpp +++ b/components/esmterrain/gridsampling.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -38,6 +39,16 @@ namespace ESMTerrain } } + struct CellSample + { + int mCellX; + int mCellY; + std::size_t mSrcRow; + std::size_t mSrcCol; + std::size_t mDstRow; + std::size_t mDstCol; + }; + template void sampleCellGridSimple(std::size_t cellSize, std::size_t sampleSize, std::size_t beginX, std::size_t beginY, std::size_t endX, std::size_t endY, F&& f) @@ -115,6 +126,71 @@ namespace ESMTerrain baseVertY = vertY + 1; } } + + inline int getBlendmapSize(float size, int textureSize) + { + return static_cast(textureSize * size) + 1; + } + + inline void adjustTextureCoordinates(int textureSize, int& cellX, int& cellY, int& x, int& y) + { + --x; + if (x < 0) + { + --cellX; + x += textureSize; + } + + while (x >= textureSize) + { + ++cellX; + x -= textureSize; + } + + while (y >= textureSize) + { + ++cellY; + y -= textureSize; + } + } + + template + void sampleBlendmaps(float size, float minX, float minY, int textureSize, F&& f) + { + if (size <= 0) + throw std::invalid_argument("Invalid size for blendmap sampling: " + std::to_string(size)); + + if (textureSize <= 0) + throw std::invalid_argument("Invalid texture size for blendmap sampling: " + std::to_string(textureSize)); + + const int beginCellX = static_cast(std::floor(minX)); + const int beginCellY = static_cast(std::floor(minY)); + const int beginRow = static_cast((minX - beginCellX) * (textureSize + 1)); + const int beginCol = static_cast((minY - beginCellY) * (textureSize + 1)); + const int blendmapSize = getBlendmapSize(size, textureSize); + + for (int y = 0; y < blendmapSize; y++) + { + for (int x = 0; x < blendmapSize; x++) + { + int cellX = beginCellX; + int cellY = beginCellY; + int srcX = x + beginRow; + int srcY = y + beginCol; + + adjustTextureCoordinates(textureSize, cellX, cellY, srcX, srcY); + + f(CellSample{ + .mCellX = cellX, + .mCellY = cellY, + .mSrcRow = static_cast(srcX), + .mSrcCol = static_cast(srcY), + .mDstRow = static_cast(x), + .mDstCol = static_cast(y), + }); + } + } + } } #endif diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp index bd96a3f7ce..93c569b296 100644 --- a/components/esmterrain/storage.cpp +++ b/components/esmterrain/storage.cpp @@ -17,6 +17,27 @@ 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() }; + } + } class LandCache { @@ -306,47 +327,6 @@ namespace ESMTerrain std::fill(positions.begin(), positions.end(), osg::Vec3f()); } - Storage::UniqueTextureId Storage::getVtexIndexAt( - ESM::ExteriorCellLocation cellLocation, const LandObject* land, int x, int y, LandCache& cache) - { - // For the first/last row/column, we need to get the texture from the neighbour cell - // to get consistent blending at the borders - --x; - ESM::ExteriorCellLocation cellLocationIn = cellLocation; - if (x < 0) - { - --cellLocation.mX; - x += ESM::Land::LAND_TEXTURE_SIZE; - } - while (x >= ESM::Land::LAND_TEXTURE_SIZE) - { - ++cellLocation.mX; - x -= ESM::Land::LAND_TEXTURE_SIZE; - } - while ( - y >= ESM::Land::LAND_TEXTURE_SIZE) // Y appears to be wrapped from the other side because why the hell not? - { - ++cellLocation.mY; - y -= ESM::Land::LAND_TEXTURE_SIZE; - } - - if (cellLocation != cellLocationIn) - land = getLand(cellLocation, cache); - - assert(x < ESM::Land::LAND_TEXTURE_SIZE); - assert(y < ESM::Land::LAND_TEXTURE_SIZE); - - const ESM::LandData* data = land ? land->getData(ESM::Land::DATA_VTEX) : nullptr; - if (data) - { - int tex = data->getTextures()[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->getPlugin()); - } - return std::make_pair(0, 0); - } - std::string Storage::getTextureName(UniqueTextureId id) { static constexpr char defaultTexture[] = "textures\\_land_default.dds"; @@ -371,31 +351,40 @@ namespace ESMTerrain void Storage::getBlendmaps(float chunkSize, const osg::Vec2f& chunkCenter, ImageVector& blendmaps, std::vector& layerList, ESM::RefId worldspace) { - osg::Vec2f origin = chunkCenter - osg::Vec2f(chunkSize / 2.f, chunkSize / 2.f); - int cellX = static_cast(std::floor(origin.x())); - int cellY = static_cast(std::floor(origin.y())); - - int realTextureSize = ESM::Land::LAND_TEXTURE_SIZE + 1; // add 1 to wrap around next cell - - int rowStart = (origin.x() - cellX) * realTextureSize; - int colStart = (origin.y() - cellY) * realTextureSize; - - const int blendmapSize = (realTextureSize - 1) * chunkSize + 1; + 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 - const int imageScaleFactor = 2; - const int blendmapImageSize = blendmapSize * imageScaleFactor; + constexpr std::size_t imageScaleFactor = 2; + const std::size_t blendmapImageSize = blendmapSize * imageScaleFactor; + std::vector textureIds(blendmapSize * blendmapSize); LandCache cache; - std::map textureIndicesMap; - ESM::ExteriorCellLocation cellLocation(cellX, cellY, worldspace); + std::pair lastCell{ startCellX, startCellY }; + const LandObject* land = getLand(ESM::ExteriorCellLocation(startCellX, startCellY, worldspace), cache); - const LandObject* land = getLand(cellLocation, cache); - - for (int y = 0; y < blendmapSize; y++) - { - for (int x = 0; x < blendmapSize; x++) + const auto handleSample = [&](const CellSample& sample) { + const std::pair cell{ sample.mCellX, sample.mCellY }; + if (lastCell != cell) { - UniqueTextureId id = getVtexIndexAt(cellLocation, land, x + rowStart, y + colStart, cache); + 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]; std::map::iterator found = textureIndicesMap.find(id); if (found == textureIndicesMap.end()) { @@ -417,21 +406,21 @@ namespace ESMTerrain if (layerIndex >= layerList.size()) { osg::ref_ptr image(new osg::Image); - image->allocateImage(blendmapImageSize, blendmapImageSize, 1, GL_ALPHA, GL_UNSIGNED_BYTE); - unsigned char* pData = image->data(); - memset(pData, 0, image->getTotalDataSize()); - blendmaps.emplace_back(image); - layerList.emplace_back(info); + 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)); } } - unsigned int layerIndex = found->second; - unsigned char* pData = blendmaps[layerIndex]->data(); - int realY = (blendmapSize - y - 1) * imageScaleFactor; - int realX = x * imageScaleFactor; - pData[((realY + 0) * blendmapImageSize + realX + 0)] = 255; - pData[((realY + 1) * blendmapImageSize + realX + 0)] = 255; - pData[((realY + 0) * blendmapImageSize + realX + 1)] = 255; - pData[((realY + 1) * blendmapImageSize + realX + 1)] = 255; + const unsigned int 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; } } diff --git a/components/esmterrain/storage.hpp b/components/esmterrain/storage.hpp index 291c3afae3..84979dd2d5 100644 --- a/components/esmterrain/storage.hpp +++ b/components/esmterrain/storage.hpp @@ -62,6 +62,11 @@ namespace ESMTerrain ESM::LandData mData; }; + // Since plugins can define new texture palettes, we need to know the plugin index too + // in order to retrieve the correct texture name. + // pair + using UniqueTextureId = std::pair; + /// @brief Feeds data from ESM terrain records (ESM::Land, ESM::LandTexture) /// into the terrain component, converting it on the fly as needed. class Storage : public Terrain::Storage @@ -146,13 +151,6 @@ namespace ESMTerrain virtual void adjustColor(int col, int row, const ESM::LandData* heightData, osg::Vec4ub& color) const; virtual float getAlteredHeight(int col, int row) const; - // 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; - - inline UniqueTextureId getVtexIndexAt( - ESM::ExteriorCellLocation cellLocation, const LandObject* land, int x, int y, LandCache&); std::string getTextureName(UniqueTextureId id); std::map mLayerInfoMap;