diff --git a/CHANGELOG.md b/CHANGELOG.md index bcf13ef602..80bc224155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ Bug #7450: Evading obstacles does not work for actors missing certain animations Bug #7459: Icons get stacked on the cursor when picking up multiple items simultaneously Bug #7472: Crash when enchanting last projectiles + Bug #7505: Distant terrain does not support sample size greater than cell size Feature #3537: Shader-based water ripples Feature #5492: Let rain and snow collide with statics Feature #6447: Add LOD support to Object Paging diff --git a/apps/opencs/view/render/terrainstorage.cpp b/apps/opencs/view/render/terrainstorage.cpp index 725b663649..a6a38600ad 100644 --- a/apps/opencs/view/render/terrainstorage.cpp +++ b/apps/opencs/view/render/terrainstorage.cpp @@ -1,7 +1,7 @@ #include "terrainstorage.hpp" #include -#include +#include #include #include diff --git a/apps/opencs/view/render/terrainstorage.hpp b/apps/opencs/view/render/terrainstorage.hpp index 907e63a8eb..b09de55081 100644 --- a/apps/opencs/view/render/terrainstorage.hpp +++ b/apps/opencs/view/render/terrainstorage.hpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include namespace CSMWorld diff --git a/apps/openmw/mwrender/landmanager.hpp b/apps/openmw/mwrender/landmanager.hpp index d9c36ea5d9..7166c4b111 100644 --- a/apps/openmw/mwrender/landmanager.hpp +++ b/apps/openmw/mwrender/landmanager.hpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include namespace ESM diff --git a/apps/openmw/mwrender/terrainstorage.hpp b/apps/openmw/mwrender/terrainstorage.hpp index 2526c779c6..0b41c06428 100644 --- a/apps/openmw/mwrender/terrainstorage.hpp +++ b/apps/openmw/mwrender/terrainstorage.hpp @@ -3,7 +3,7 @@ #include -#include +#include #include diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index 6b679e8653..4f93319c96 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -92,6 +92,8 @@ file(GLOB UNITTEST_SRC_FILES esm3/testinfoorder.cpp nifosg/testnifloader.cpp + + esmterrain/testgridsampling.cpp ) source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) diff --git a/apps/openmw_test_suite/esmterrain/testgridsampling.cpp b/apps/openmw_test_suite/esmterrain/testgridsampling.cpp new file mode 100644 index 0000000000..5ca38b2011 --- /dev/null +++ b/apps/openmw_test_suite/esmterrain/testgridsampling.cpp @@ -0,0 +1,366 @@ +#include + +#include +#include + +namespace ESMTerrain +{ + namespace + { + using namespace testing; + + struct Sample + { + std::size_t mCellX = 0; + std::size_t mCellY = 0; + std::size_t mLocalX = 0; + std::size_t mLocalY = 0; + std::size_t mVertexX = 0; + std::size_t mVertexY = 0; + }; + + auto tie(const Sample& v) + { + return std::tie(v.mCellX, v.mCellY, v.mLocalX, v.mLocalY, v.mVertexX, v.mVertexY); + } + + bool operator==(const Sample& l, const Sample& r) + { + return tie(l) == tie(r); + } + + std::ostream& operator<<(std::ostream& stream, const Sample& v) + { + return stream << "Sample{.mCellX = " << v.mCellX << ", .mCellY = " << v.mCellY + << ", .mLocalX = " << v.mLocalX << ", .mLocalY = " << v.mLocalY + << ", .mVertexX = " << v.mVertexX << ", .mVertexY = " << v.mVertexY << "}"; + } + + struct Collect + { + std::vector& mSamples; + + void operator()(std::size_t cellX, std::size_t cellY, std::size_t localX, std::size_t localY, + std::size_t vertexX, std::size_t vertexY) + { + mSamples.push_back(Sample{ + .mCellX = cellX, + .mCellY = cellY, + .mLocalX = localX, + .mLocalY = localY, + .mVertexX = vertexX, + .mVertexY = vertexY, + }); + } + }; + + TEST(ESMTerrainSampleCellGrid, doesNotSupportCellSizeLessThanTwo) + { + const std::size_t cellSize = 2; + EXPECT_THROW(sampleCellGrid(cellSize, 0, 0, 0, 0, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleCellGrid, doesNotSupportCellSizeMinusOneNotPowerOfTwo) + { + const std::size_t cellSize = 4; + EXPECT_THROW(sampleCellGrid(cellSize, 0, 0, 0, 0, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleCellGrid, doesNotSupportZeroSampleSize) + { + const std::size_t cellSize = 1; + const std::size_t sampleSize = 0; + EXPECT_THROW(sampleCellGrid(cellSize, sampleSize, 0, 0, 0, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleCellGrid, doesNotSupportSampleSizeNotPowerOfTwo) + { + const std::size_t cellSize = 1; + const std::size_t sampleSize = 3; + EXPECT_THROW(sampleCellGrid(cellSize, sampleSize, 0, 0, 0, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleCellGrid, doesNotSupportCountLessThanTwo) + { + const std::size_t cellSize = 1; + const std::size_t sampleSize = 1; + const std::size_t distance = 2; + EXPECT_THROW(sampleCellGrid(cellSize, sampleSize, 0, 0, distance, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleCellGrid, doesNotSupportCountMinusOneNotPowerOfTwo) + { + const std::size_t cellSize = 1; + const std::size_t sampleSize = 1; + const std::size_t distance = 4; + EXPECT_THROW(sampleCellGrid(cellSize, sampleSize, 0, 0, distance, [](auto...) {}), std::invalid_argument); + } + + TEST(ESMTerrainSampleCellGrid, sampleSizeOneShouldProduceNumberOfSamplesEqualToCellSize) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 1; + const std::size_t beginX = 0; + const std::size_t beginY = 0; + const std::size_t distance = 3; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 2, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 1, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 1, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 1, .mVertexX = 2, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 2 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 1, .mVertexY = 2 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 2, .mVertexY = 2 })); + } + + TEST(ESMTerrainSampleCellGrid, countShouldLimitScope) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 1; + const std::size_t beginX = 0; + const std::size_t beginY = 0; + const std::size_t distance = 2; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 1, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 1, .mVertexY = 1 })); + } + + TEST(ESMTerrainSampleCellGrid, beginXAndCountShouldLimitScope) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 1; + const std::size_t beginX = 1; + const std::size_t beginY = 0; + const std::size_t distance = 2; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 1, .mVertexX = 1, .mVertexY = 1 })); + } + + TEST(ESMTerrainSampleCellGrid, beginYAndCountShouldLimitScope) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 1; + const std::size_t beginX = 0; + const std::size_t beginY = 1; + const std::size_t distance = 2; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 1, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 1, .mVertexY = 1 })); + } + + TEST(ESMTerrainSampleCellGrid, beginAndCountShouldLimitScope) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 1; + const std::size_t beginX = 1; + const std::size_t beginY = 1; + const std::size_t distance = 2; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 1, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 1 })); + } + + TEST(ESMTerrainSampleCellGrid, beginAndCountShouldLimitScopeInTheMiddleOfCell) + { + const std::size_t cellSize = 5; + const std::size_t sampleSize = 1; + const std::size_t beginX = 1; + const std::size_t beginY = 1; + const std::size_t distance = 2; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 1, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 1 })); + } + + TEST(ESMTerrainSampleCellGrid, beginXWithCountLessThanCellSizeShouldLimitScopeAcrossCellBorder) + { + const std::size_t cellSize = 5; + const std::size_t sampleSize = 1; + const std::size_t beginX = 3; + const std::size_t beginY = 0; + const std::size_t distance = 3; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 3, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 4, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 2, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 3, .mLocalY = 1, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 4, .mLocalY = 1, .mVertexX = 1, .mVertexY = 1 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 2, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 3, .mLocalY = 2, .mVertexX = 0, .mVertexY = 2 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 4, .mLocalY = 2, .mVertexX = 1, .mVertexY = 2 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 2, .mVertexY = 2 })); + } + + TEST(ESMTerrainSampleCellGrid, beginXWithCountEqualToCellSizeShouldLimitScopeAcrossCellBorder) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 1; + const std::size_t beginX = 1; + const std::size_t beginY = 0; + const std::size_t distance = 3; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 1, .mVertexX = 1, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 0, .mVertexY = 2 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 2 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 2, .mVertexY = 0 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 2, .mVertexY = 1 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 2, .mVertexY = 2 })); + } + + TEST(ESMTerrainSampleCellGrid, beginXWithCountGreaterThanCellSizeShouldLimitScopeAcrossCellBorder) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 1; + const std::size_t beginX = 1; + const std::size_t beginY = 0; + const std::size_t distance = 5; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 1, .mVertexX = 1, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 0, .mVertexY = 2 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 2 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 2, .mVertexY = 0 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 3, .mVertexY = 0 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 2, .mVertexY = 1 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 2, .mLocalY = 1, .mVertexX = 3, .mVertexY = 1 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 2, .mVertexY = 2 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 3, .mVertexY = 2 }, + Sample{ .mCellX = 2, .mCellY = 0, .mLocalX = 1, .mLocalY = 0, .mVertexX = 4, .mVertexY = 0 }, + Sample{ .mCellX = 2, .mCellY = 0, .mLocalX = 1, .mLocalY = 1, .mVertexX = 4, .mVertexY = 1 }, + Sample{ .mCellX = 2, .mCellY = 0, .mLocalX = 1, .mLocalY = 2, .mVertexX = 4, .mVertexY = 2 }, + Sample{ .mCellX = 0, .mCellY = 1, .mLocalX = 1, .mLocalY = 1, .mVertexX = 0, .mVertexY = 3 }, + Sample{ .mCellX = 0, .mCellY = 1, .mLocalX = 2, .mLocalY = 1, .mVertexX = 1, .mVertexY = 3 }, + Sample{ .mCellX = 0, .mCellY = 1, .mLocalX = 1, .mLocalY = 2, .mVertexX = 0, .mVertexY = 4 }, + Sample{ .mCellX = 0, .mCellY = 1, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 4 }, + Sample{ .mCellX = 1, .mCellY = 1, .mLocalX = 1, .mLocalY = 1, .mVertexX = 2, .mVertexY = 3 }, + Sample{ .mCellX = 1, .mCellY = 1, .mLocalX = 2, .mLocalY = 1, .mVertexX = 3, .mVertexY = 3 }, + Sample{ .mCellX = 1, .mCellY = 1, .mLocalX = 1, .mLocalY = 2, .mVertexX = 2, .mVertexY = 4 }, + Sample{ .mCellX = 1, .mCellY = 1, .mLocalX = 2, .mLocalY = 2, .mVertexX = 3, .mVertexY = 4 }, + Sample{ .mCellX = 2, .mCellY = 1, .mLocalX = 1, .mLocalY = 1, .mVertexX = 4, .mVertexY = 3 }, + Sample{ .mCellX = 2, .mCellY = 1, .mLocalX = 1, .mLocalY = 2, .mVertexX = 4, .mVertexY = 4 })); + } + + TEST(ESMTerrainSampleCellGrid, sampleSizeGreaterThanOneShouldSkipPoints) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 2; + const std::size_t beginX = 0; + const std::size_t beginY = 0; + const std::size_t distance = 3; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 1 })); + } + + TEST(ESMTerrainSampleCellGrid, shouldGroupByCell) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 2; + const std::size_t beginX = 0; + const std::size_t beginY = 0; + const std::size_t distance = 5; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 1 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 2, .mVertexY = 0 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 2, .mLocalY = 2, .mVertexX = 2, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 1, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 2 }, + Sample{ .mCellX = 0, .mCellY = 1, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 2 }, + Sample{ .mCellX = 1, .mCellY = 1, .mLocalX = 2, .mLocalY = 2, .mVertexX = 2, .mVertexY = 2 })); + } + + TEST(ESMTerrainSampleCellGrid, sampleSizeGreaterThanCellSizeShouldPickSinglePointPerCell) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 4; + const std::size_t beginX = 0; + const std::size_t beginY = 0; + const std::size_t distance = 9; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 1, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + Sample{ .mCellX = 3, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 2, .mVertexY = 0 }, + Sample{ .mCellX = 0, .mCellY = 1, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 1 }, + Sample{ .mCellX = 1, .mCellY = 1, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 1 }, + Sample{ .mCellX = 3, .mCellY = 1, .mLocalX = 2, .mLocalY = 2, .mVertexX = 2, .mVertexY = 1 }, + Sample{ .mCellX = 0, .mCellY = 3, .mLocalX = 0, .mLocalY = 2, .mVertexX = 0, .mVertexY = 2 }, + Sample{ .mCellX = 1, .mCellY = 3, .mLocalX = 2, .mLocalY = 2, .mVertexX = 1, .mVertexY = 2 }, + Sample{ .mCellX = 3, .mCellY = 3, .mLocalX = 2, .mLocalY = 2, .mVertexX = 2, .mVertexY = 2 })); + } + + TEST(ESMTerrainSampleCellGrid, sampleSizeGreaterThan2CellSizeShouldSkipCells) + { + const std::size_t cellSize = 3; + const std::size_t sampleSize = 8; + const std::size_t beginX = 0; + const std::size_t beginY = 0; + const std::size_t distance = 9; + std::vector samples; + sampleCellGrid(cellSize, sampleSize, beginX, beginY, distance, Collect{ samples }); + EXPECT_THAT(samples, + ElementsAre( // + Sample{ .mCellX = 0, .mCellY = 0, .mLocalX = 0, .mLocalY = 0, .mVertexX = 0, .mVertexY = 0 }, + Sample{ .mCellX = 3, .mCellY = 0, .mLocalX = 2, .mLocalY = 0, .mVertexX = 1, .mVertexY = 0 }, + 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 })); + } + } +} diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index b085eb52d7..32482ec331 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -145,7 +145,7 @@ add_component_dir (esm3 infoorder timestamp ) -add_component_dir (esm3terrain +add_component_dir (esmterrain storage ) diff --git a/components/esmterrain/gridsampling.hpp b/components/esmterrain/gridsampling.hpp new file mode 100644 index 0000000000..b65825d8ad --- /dev/null +++ b/components/esmterrain/gridsampling.hpp @@ -0,0 +1,120 @@ +#ifndef OPENMW_COMPONENTS_ESMTERRAIN_GRIDSAMPLING_H +#define OPENMW_COMPONENTS_ESMTERRAIN_GRIDSAMPLING_H + +#include + +#include +#include +#include +#include +#include + +namespace ESMTerrain +{ + inline std::pair toCellAndLocal( + std::size_t begin, std::size_t global, std::size_t cellSize) + { + std::size_t cell = global / (cellSize - 1); + std::size_t local = global & (cellSize - 2); + if (global != begin && local == 0) + { + --cell; + local = cellSize - 1; + } + return { cell, local }; + } + + template + void sampleGrid( + std::size_t sampleSize, std::size_t beginX, std::size_t beginY, std::size_t endX, std::size_t endY, F&& f) + { + std::size_t vertY = 0; + for (std::size_t y = beginY; y < endY; y += sampleSize) + { + std::size_t vertX = 0; + for (std::size_t x = beginX; x < endX; x += sampleSize) + f(x, y, vertX++, vertY); + ++vertY; + } + } + + 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) + { + assert(cellSize > 1); + assert(Misc::isPowerOfTwo(cellSize - 1)); + assert(sampleSize != 0); + + sampleGrid(sampleSize, beginX, beginY, endX, endY, + [&](std::size_t globalX, std::size_t globalY, std::size_t vertX, std::size_t vertY) { + const auto [cellX, x] = toCellAndLocal(beginX, globalX, cellSize); + const auto [cellY, y] = toCellAndLocal(beginY, globalY, cellSize); + f(cellX, cellY, x, y, vertX, vertY); + }); + } + + template + void sampleCellGrid(std::size_t cellSize, std::size_t sampleSize, std::size_t beginX, std::size_t beginY, + std::size_t distance, F&& f) + { + if (cellSize < 2 || !Misc::isPowerOfTwo(cellSize - 1)) + throw std::invalid_argument("Invalid cell size for cell grid sampling: " + std::to_string(cellSize)); + + if (sampleSize == 0 || !Misc::isPowerOfTwo(sampleSize)) + throw std::invalid_argument("Invalid sample size for cell grid sampling: " + std::to_string(sampleSize)); + + if (distance < 2 || !Misc::isPowerOfTwo(distance - 1)) + throw std::invalid_argument("Invalid count for cell grid sampling: " + std::to_string(distance)); + + const std::size_t endX = beginX + distance; + const std::size_t endY = beginY + distance; + + if (distance < cellSize || sampleSize > cellSize - 1) + return sampleCellGridSimple(cellSize, sampleSize, beginX, beginY, endX, endY, f); + + const std::size_t beginCellX = beginX / (cellSize - 1); + const std::size_t beginCellY = beginY / (cellSize - 1); + const std::size_t endCellX = endX / (cellSize - 1); + const std::size_t endCellY = endY / (cellSize - 1); + + std::size_t baseVertY = 0; + + for (std::size_t cellY = beginCellY; cellY < endCellY; ++cellY) + { + const std::size_t offsetY = cellY * (cellSize - 1); + const std::size_t globalBeginY = offsetY <= beginY ? beginY : offsetY + sampleSize; + const std::size_t globalEndY = endY <= offsetY + cellSize ? endY : offsetY + cellSize; + + assert(globalBeginY < globalEndY); + + std::size_t baseVertX = 0; + std::size_t vertY = baseVertY; + + for (std::size_t cellX = beginCellX; cellX < endCellX; ++cellX) + { + const std::size_t offsetX = cellX * (cellSize - 1); + const std::size_t globalBeginX = offsetX <= beginX ? beginX : offsetX + sampleSize; + const std::size_t globalEndX = endX <= offsetX + cellSize ? endX : offsetX + cellSize; + + assert(globalBeginX < globalEndX); + + vertY = baseVertY; + std::size_t vertX = baseVertX; + + sampleGrid(sampleSize, globalBeginX, globalBeginY, globalEndX, globalEndY, + [&](std::size_t globalX, std::size_t globalY, std::size_t localVertX, std::size_t localVertY) { + vertX = baseVertX + localVertX; + vertY = baseVertY + localVertY; + f(cellX, cellY, globalX - offsetX, globalY - offsetY, vertX, vertY); + }); + + baseVertX = vertX + 1; + } + + baseVertY = vertY + 1; + } + } +} + +#endif diff --git a/components/esm3terrain/storage.cpp b/components/esmterrain/storage.cpp similarity index 74% rename from components/esm3terrain/storage.cpp rename to components/esmterrain/storage.cpp index 71051df847..bd96a3f7ce 100644 --- a/components/esm3terrain/storage.cpp +++ b/components/esmterrain/storage.cpp @@ -1,6 +1,7 @@ #include "storage.hpp" -#include +#include +#include #include #include @@ -12,6 +13,8 @@ #include #include +#include "gridsampling.hpp" + namespace ESMTerrain { @@ -184,47 +187,52 @@ namespace ESMTerrain } void Storage::fillVertexBuffers(int lodLevel, float size, const osg::Vec2f& center, ESM::RefId worldspace, - osg::ref_ptr positions, osg::ref_ptr normals, - osg::ref_ptr colours) + 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 - size_t increment = static_cast(1) << lodLevel; + 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; - osg::Vec2f origin = center - osg::Vec2f(size / 2.f, size / 2.f); - - int startCellX = static_cast(std::floor(origin.x())); - int startCellY = static_cast(std::floor(origin.y())); - const int landSize = ESM::getLandSize(worldspace); - const int LandSizeInUnits = ESM::getCellSize(worldspace); - - size_t numVerts = static_cast(size * (landSize - 1) / increment + 1); - - positions->resize(numVerts * numVerts); - normals->resize(numVerts * numVerts); - colours->resize(numVerts * numVerts); - - osg::Vec3f normal; - osg::Vec4ub color; - - float vertY = 0; - float vertX = 0; + positions.resize(numVerts * numVerts); + normals.resize(numVerts * numVerts); + colours.resize(numVerts * numVerts); LandCache cache; - bool alteration = useAlteration(); + 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())); + ESM::ExteriorCellLocation lastCellLocation(startCellX - 1, startCellY - 1, worldspace); + const LandObject* land = nullptr; + const ESM::LandData* heightData = nullptr; + const ESM::LandData* normalData = nullptr; + const ESM::LandData* colourData = nullptr; bool validHeightDataExists = false; - float vertY_ = 0; // of current cell corner - for (int cellY = startCellY; cellY < startCellY + std::ceil(size); ++cellY) - { - float vertX_ = 0; // of current cell corner - for (int cellX = startCellX; cellX < startCellX + std::ceil(size); ++cellX) + + 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 ESM::ExteriorCellLocation cellLocation(cellX, cellY, worldspace); + + if (lastCellLocation != cellLocation) { - ESM::ExteriorCellLocation cellLocation(cellX, cellY, worldspace); - const LandObject* land = getLand(cellLocation, cache); - const ESM::LandData* heightData = nullptr; - const ESM::LandData* normalData = nullptr; - const ESM::LandData* colourData = nullptr; - if (land) + 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); @@ -232,110 +240,70 @@ namespace ESMTerrain validHeightDataExists = true; } - 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 - // This is only relevant if we're creating a chunk spanning multiple cells - if (vertY_ != 0) - colStart += increment; - if (vertX_ != 0) - rowStart += increment; - - // Only relevant for chunks smaller than (contained in) one cell - rowStart += (origin.x() - startCellX) * landSize; - colStart += (origin.y() - startCellY) * landSize; - int rowEnd = std::min( - static_cast(rowStart + std::min(1.f, size) * (landSize - 1) + 1), static_cast(landSize)); - int colEnd = std::min( - static_cast(colStart + std::min(1.f, size) * (landSize - 1) + 1), static_cast(landSize)); - - vertY = vertY_; - for (int col = colStart; col < colEnd; col += increment) - { - vertX = vertX_; - for (int row = rowStart; row < rowEnd; row += increment) - { - int srcArrayIndex = col * landSize * 3 + row * 3; - - assert(row >= 0 && row < landSize); - assert(col >= 0 && col < landSize); - - assert(vertX < numVerts); - assert(vertY < numVerts); - - float height = defaultHeight; - if (heightData) - height = heightData->getHeights()[col * landSize + row]; - if (alteration) - height += getAlteredHeight(col, row); - (*positions)[static_cast(vertX * numVerts + vertY)] - = osg::Vec3f((vertX / float(numVerts - 1) - 0.5f) * size * LandSizeInUnits, - (vertY / float(numVerts - 1) - 0.5f) * size * LandSizeInUnits, height); - - if (normalData) - { - for (int i = 0; i < 3; ++i) - normal[i] = normalData->getNormals()[srcArrayIndex + i]; - - normal.normalize(); - } - else - normal = osg::Vec3f(0, 0, 1); - - // Normals apparently don't connect seamlessly between cells - if (col == landSize - 1 || row == landSize - 1) - fixNormal(normal, cellLocation, col, row, cache); - - // some corner normals appear to be complete garbage (z < 0) - if ((row == 0 || row == landSize - 1) && (col == 0 || col == landSize - 1)) - averageNormal(normal, cellLocation, col, row, cache); - - assert(normal.z() > 0); - - (*normals)[static_cast(vertX * numVerts + vertY)] = normal; - - if (colourData) - { - for (int i = 0; i < 3; ++i) - color[i] = colourData->getColors()[srcArrayIndex + i]; - } - else - { - color.r() = 255; - color.g() = 255; - color.b() = 255; - } - if (alteration) - adjustColor(col, row, heightData, color); // Does nothing by default, override in OpenMW-CS - - // Unlike normals, colors mostly connect seamlessly between cells, but not always... - if (col == landSize - 1 || row == landSize - 1) - fixColour(color, cellLocation, col, row, cache); - - color.a() = 255; - - (*colours)[static_cast(vertX * numVerts + vertY)] = color; - - ++vertX; - } - ++vertY; - } - vertX_ = vertX; + lastCellLocation = cellLocation; } - vertY_ = vertY; - assert(vertX_ == numVerts); // Ensure we covered whole area - } - assert(vertY_ == numVerts); // Ensure we covered whole area + 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)) - { - for (unsigned int iVert = 0; iVert < numVerts * numVerts; iVert++) - { - (*positions)[static_cast(iVert)] = osg::Vec3f(0.f, 0.f, 0.f); - } - } + std::fill(positions.begin(), positions.end(), osg::Vec3f()); } Storage::UniqueTextureId Storage::getVtexIndexAt( diff --git a/components/esm3terrain/storage.hpp b/components/esmterrain/storage.hpp similarity index 96% rename from components/esm3terrain/storage.hpp rename to components/esmterrain/storage.hpp index 5efa996c6f..291c3afae3 100644 --- a/components/esm3terrain/storage.hpp +++ b/components/esmterrain/storage.hpp @@ -1,5 +1,5 @@ -#ifndef COMPONENTS_ESM_TERRAIN_STORAGE_H -#define COMPONENTS_ESM_TERRAIN_STORAGE_H +#ifndef OPENMW_COMPONENTS_ESMTERRAIN_STORAGE_H +#define OPENMW_COMPONENTS_ESMTERRAIN_STORAGE_H #include #include @@ -99,8 +99,7 @@ namespace ESMTerrain /// @param normals buffer to write vertex normals /// @param colours buffer to write vertex colours void fillVertexBuffers(int lodLevel, float size, const osg::Vec2f& center, ESM::RefId worldspace, - osg::ref_ptr positions, osg::ref_ptr normals, - osg::ref_ptr colours) override; + osg::Vec3Array& positions, osg::Vec3Array& normals, osg::Vec4ubArray& colours) override; /// 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 diff --git a/components/misc/mathutil.hpp b/components/misc/mathutil.hpp index 8c9bff952c..f93f38e6e8 100644 --- a/components/misc/mathutil.hpp +++ b/components/misc/mathutil.hpp @@ -7,6 +7,8 @@ #include #include +#include + namespace Misc { @@ -63,8 +65,10 @@ namespace Misc return toEulerAnglesZYX(forward, up); } - inline bool isPowerOfTwo(int x) + template + bool isPowerOfTwo(T x) { + static_assert(std::is_integral_v); return ((x > 0) && ((x & (x - 1)) == 0)); } diff --git a/components/terrain/chunkmanager.cpp b/components/terrain/chunkmanager.cpp index 03643390bf..0364a0a4fd 100644 --- a/components/terrain/chunkmanager.cpp +++ b/components/terrain/chunkmanager.cpp @@ -213,7 +213,7 @@ namespace Terrain osg::ref_ptr colors(new osg::Vec4ubArray); colors->setNormalize(true); - mStorage->fillVertexBuffers(lod, chunkSize, chunkCenter, mWorldspace, positions, normals, colors); + mStorage->fillVertexBuffers(lod, chunkSize, chunkCenter, mWorldspace, *positions, *normals, *colors); osg::ref_ptr vbo(new osg::VertexBufferObject); positions->setVertexBufferObject(vbo); diff --git a/components/terrain/storage.hpp b/components/terrain/storage.hpp index c4a44f5024..7a99478929 100644 --- a/components/terrain/storage.hpp +++ b/components/terrain/storage.hpp @@ -64,8 +64,7 @@ namespace Terrain /// @param normals buffer to write vertex normals /// @param colours buffer to write vertex colours virtual void fillVertexBuffers(int lodLevel, float size, const osg::Vec2f& center, ESM::RefId worldspace, - osg::ref_ptr positions, osg::ref_ptr normals, - osg::ref_ptr colours) + osg::Vec3Array& positions, osg::Vec3Array& normals, osg::Vec4ubArray& colours) = 0; typedef std::vector> ImageVector;