diff --git a/rts/Game/Game.cpp b/rts/Game/Game.cpp index cda7f4be9f..04151ac6c6 100644 --- a/rts/Game/Game.cpp +++ b/rts/Game/Game.cpp @@ -574,7 +574,7 @@ void CGame::PreLoadSimulation(LuaParser* defsParser) ENTER_SYNCED_CODE(); loadscreen->SetLoadMessage("Creating Smooth Height Mesh"); - smoothGround.Init(float3::maxxpos, float3::maxzpos, SQUARE_SIZE * 2, SQUARE_SIZE * 40); + smoothGround.Init(int2(mapDims.mapx, mapDims.mapy), 2, 40); loadscreen->SetLoadMessage("Creating QuadField & CEGs"); moveDefHandler.Init(defsParser); @@ -1761,6 +1761,7 @@ void CGame::SimFrame() { helper->Update(); readMap->Update(); + smoothGround.UpdateSmoothMesh(); mapDamage->Update(); pathManager->Update(); unitHandler.Update(); diff --git a/rts/Lua/LuaSyncedCtrl.cpp b/rts/Lua/LuaSyncedCtrl.cpp index 406532214d..ca9279ad4d 100644 --- a/rts/Lua/LuaSyncedCtrl.cpp +++ b/rts/Lua/LuaSyncedCtrl.cpp @@ -4214,8 +4214,10 @@ static inline void ParseSmoothMeshParams(lua_State* L, const char* caller, float& factor, int& x1, int& z1, int& x2, int& z2) { ParseParams(L, caller, factor, x1, z1, x2, z2, - smoothGround.GetResolution(), smoothGround.GetMaxX(), - smoothGround.GetMaxY()); + smoothGround.GetResolution(), + smoothGround.GetMaxX() - 1, + smoothGround.GetMaxY() - 1); + } @@ -4231,6 +4233,7 @@ int LuaSyncedCtrl::LevelSmoothMesh(lua_State* L) smoothGround.SetHeight(index, height); } } + return 0; } @@ -4298,8 +4301,8 @@ int LuaSyncedCtrl::AddSmoothMesh(lua_State* L) const int z = (int)(zl / smoothGround.GetResolution()); // discard invalid coordinates - if ((x < 0) || (x > smoothGround.GetMaxX()) || - (z < 0) || (z > smoothGround.GetMaxY())) { + if ((x < 0) || (x > smoothGround.GetMaxX() - 1) || + (z < 0) || (z > smoothGround.GetMaxY() - 1)) { return 0; } @@ -4328,8 +4331,8 @@ int LuaSyncedCtrl::SetSmoothMesh(lua_State* L) const int z = (int)(zl / smoothGround.GetResolution()); // discard invalid coordinates - if ((x < 0) || (x > smoothGround.GetMaxX()) || - (z < 0) || (z > smoothGround.GetMaxY())) { + if ((x < 0) || (x > smoothGround.GetMaxX() - 1) || + (z < 0) || (z > smoothGround.GetMaxY() - 1)) { return 0; } diff --git a/rts/Map/BasicMapDamage.cpp b/rts/Map/BasicMapDamage.cpp index ef0d129df5..349d4f7910 100644 --- a/rts/Map/BasicMapDamage.cpp +++ b/rts/Map/BasicMapDamage.cpp @@ -8,6 +8,7 @@ #include "Sim/Misc/GroundBlockingObjectMap.h" #include "Sim/Misc/LosHandler.h" #include "Sim/Misc/QuadField.h" +#include "Sim/Misc/SmoothHeightMesh.h" #include "Sim/Units/Unit.h" #include "Sim/Units/UnitHandler.h" #include "Sim/Path/IPathManager.h" @@ -227,6 +228,7 @@ void CBasicMapDamage::RecalcArea(int x1, int x2, int y1, int y2) readMap->UpdateHeightMapSynced(updRect); featureHandler.TerrainChanged(x1, y1, x2, y2); + smoothGround.MapChanged(x1, y1, x2, y2); { SCOPED_TIMER("Sim::BasicMapDamage::Los"); losHandler->UpdateHeightMapSynced(updRect); diff --git a/rts/Rendering/SmoothHeightMeshDrawer.cpp b/rts/Rendering/SmoothHeightMeshDrawer.cpp index ff5575f20b..c75e610c64 100644 --- a/rts/Rendering/SmoothHeightMeshDrawer.cpp +++ b/rts/Rendering/SmoothHeightMeshDrawer.cpp @@ -1,18 +1,86 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ #include "SmoothHeightMeshDrawer.h" -#include "Sim/Misc/SmoothHeightMesh.h" + +#include "Game/UI/MiniMap.h" +#include "Map/ReadMap.h" +#include "Rendering/GL/myGL.h" #include "Rendering/GL/VertexArray.h" +#include "Sim/Misc/GlobalSynced.h" +#include "Sim/Misc/SmoothHeightMesh.h" +#include "System/EventHandler.h" #include "System/float3.h" +#include "System/SafeUtil.h" +using namespace SmoothHeightMeshNamespace; + +static SmoothHeightMeshDrawer* smoothMeshDrawer = nullptr; SmoothHeightMeshDrawer* SmoothHeightMeshDrawer::GetInstance() { - static SmoothHeightMeshDrawer drawer; - return &drawer; + if (smoothMeshDrawer == nullptr) { + smoothMeshDrawer = new SmoothHeightMeshDrawer(); + } + return smoothMeshDrawer; +} + +void SmoothHeightMeshDrawer::FreeInstance() { + if (smoothMeshDrawer != nullptr) { + spring::SafeDelete(smoothMeshDrawer); + } +} + +SmoothHeightMeshDrawer::SmoothHeightMeshDrawer() + : CEventClient("[SmoothHeightMeshDrawer]", 300002, false) + , drawEnabled(false) +{ + eventHandler.AddClient(this); +} +SmoothHeightMeshDrawer::~SmoothHeightMeshDrawer() { + eventHandler.RemoveClient(this); +} + +void SmoothHeightMeshDrawer::DrawInMiniMap() +{ + if (!(drawEnabled && gs->cheatEnabled)) + return; + + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + glLoadIdentity(); + glOrtho(0.0f, 1.0f, 0.0f, 1.0f, 0.0, -1.0); + minimap->ApplyConstraintsMatrix(); + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + glLoadIdentity(); + glTranslatef3(UpVector); + glScalef(1.0f / mapDims.mapx, -1.0f / mapDims.mapy, 1.0f); + + glDisable(GL_TEXTURE_2D); + glColor4f(1.0f, 1.0f, 0.0f, 0.7f); + + const SmoothHeightMesh::MapChangeTrack& mapChangeTrack = smoothGround.mapChangeTrack; + const float tileSize = SAMPLES_PER_QUAD * smoothGround.resolution; + int i = 0; + for (auto changed : mapChangeTrack.damageMap) { + if (changed){ + const float x = (i % mapChangeTrack.width) * tileSize; + const float y = (i / mapChangeTrack.width) * tileSize; + glRectf(x, y, x + tileSize, y + tileSize); + } + i++; + } + + glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + glEnable(GL_TEXTURE_2D); + + glMatrixMode(GL_PROJECTION); + glPopMatrix(); + glMatrixMode(GL_MODELVIEW); + glPopMatrix(); } void SmoothHeightMeshDrawer::Draw(float yoffset) { - if (!drawEnabled) + if (!(drawEnabled && gs->cheatEnabled)) return; glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); @@ -28,10 +96,10 @@ void SmoothHeightMeshDrawer::Draw(float yoffset) { CVertexArray* va = GetVertexArray(); va->Initialize(); - va->EnlargeArrays((numQuadsX + 1) * (numQuadsZ + 1) * 4, 0, VA_SIZE_0); + va->EnlargeArrays(numQuadsX * numQuadsZ * 4, 0, VA_SIZE_0); - for (unsigned int zq = 0; zq <= numQuadsZ; zq++) { - for (unsigned int xq = 0; xq <= numQuadsX; xq++) { + for (unsigned int zq = 0; zq < numQuadsZ; zq++) { + for (unsigned int xq = 0; xq < numQuadsX; xq++) { const float x = xq * quadSize; const float z = zq * quadSize; diff --git a/rts/Rendering/SmoothHeightMeshDrawer.h b/rts/Rendering/SmoothHeightMeshDrawer.h index c75df90d97..062be10cd1 100644 --- a/rts/Rendering/SmoothHeightMeshDrawer.h +++ b/rts/Rendering/SmoothHeightMeshDrawer.h @@ -3,9 +3,22 @@ #ifndef SMOOTH_HEIGHTMESH_DRAWER_H #define SMOOTH_HEIGHTMESH_DRAWER_H -struct SmoothHeightMeshDrawer { +#include "System/EventClient.h" + +struct SmoothHeightMeshDrawer: public CEventClient { public: static SmoothHeightMeshDrawer* GetInstance(); + static void FreeInstance(); + + SmoothHeightMeshDrawer(); + ~SmoothHeightMeshDrawer(); + + // CEventClient interface + bool WantsEvent(const std::string& eventName) override { + return (eventName == "DrawInMiniMap"); + } + void DrawInMiniMap() override; + void Draw(float yoffset); bool& DrawEnabled() { return drawEnabled; } diff --git a/rts/Rendering/WorldDrawer.cpp b/rts/Rendering/WorldDrawer.cpp index 7707958549..491093d156 100644 --- a/rts/Rendering/WorldDrawer.cpp +++ b/rts/Rendering/WorldDrawer.cpp @@ -156,6 +156,7 @@ void CWorldDrawer::Kill() IGroundDecalDrawer::FreeInstance(); CShaderHandler::FreeInstance(shaderHandler); LuaObjectDrawer::Kill(); + SmoothHeightMeshDrawer::FreeInstance(); numUpdates = 0; } diff --git a/rts/Sim/Misc/ModInfo.cpp b/rts/Sim/Misc/ModInfo.cpp index 4b2bad32a7..bcef3a7aed 100644 --- a/rts/Sim/Misc/ModInfo.cpp +++ b/rts/Sim/Misc/ModInfo.cpp @@ -98,6 +98,8 @@ void CModInfo::ResetState() pfRawDistMult = 1.25f; pfUpdateRate = 0.007f; + enableSmoothMesh = true; + allowTake = true; } } @@ -137,6 +139,8 @@ void CModInfo::Init(const std::string& modFileName) pfRawDistMult = system.GetFloat("pathFinderRawDistMult", pfRawDistMult); pfUpdateRate = system.GetFloat("pathFinderUpdateRate", pfUpdateRate); + enableSmoothMesh = system.GetBool("enableSmoothMesh", enableSmoothMesh); + allowTake = system.GetBool("allowTake", allowTake); } diff --git a/rts/Sim/Misc/ModInfo.h b/rts/Sim/Misc/ModInfo.h index 02389d71ee..dc23c0a396 100644 --- a/rts/Sim/Misc/ModInfo.h +++ b/rts/Sim/Misc/ModInfo.h @@ -168,6 +168,8 @@ class CModInfo float pfRawDistMult; float pfUpdateRate; + bool enableSmoothMesh; + bool allowTake; }; diff --git a/rts/Sim/Misc/SmoothHeightMesh.cpp b/rts/Sim/Misc/SmoothHeightMesh.cpp index 0ad2ecf835..b13e50afcd 100644 --- a/rts/Sim/Misc/SmoothHeightMesh.cpp +++ b/rts/Sim/Misc/SmoothHeightMesh.cpp @@ -8,20 +8,37 @@ #include "Map/Ground.h" #include "Map/ReadMap.h" +#include "Sim/Misc/ModInfo.h" #include "System/float3.h" +#include "System/Log/ILog.h" #include "System/SpringMath.h" #include "System/TimeProfiler.h" #include "System/Threading/ThreadPool.h" + +using namespace SmoothHeightMeshNamespace; + +#if 0 +#define SMOOTH_MESH_DEBUG_MAXIMA +#endif + +#if 0 +#define SMOOTH_MESH_DEBUG_BLUR +#endif + +#if 0 +#define SMOOTH_MESH_DEBUG_GENERAL +#endif + SmoothHeightMesh smoothGround; static float Interpolate(float x, float y, const int maxx, const int maxy, const float res, const float* heightmap) { - x = Clamp(x / res, 0.0f, maxx - 1.0f); - y = Clamp(y / res, 0.0f, maxy - 1.0f); - const int sx = x; - const int sy = y; + x = Clamp(x / res, 0.0f, (float)maxx); + y = Clamp(y / res, 0.0f, (float)maxy); + const int sx = std::min((int)x, maxx - 1); + const int sy = std::min((int)y, maxy - 1); const float dx = (x - sx); const float dy = (y - sy); @@ -38,291 +55,349 @@ static float Interpolate(float x, float y, const int maxx, const int maxy, const return mix(hi1, hi2, dy); } - -void SmoothHeightMesh::Init(float mx, float my, float res, float smoothRad) +void SmoothHeightMesh::Init(int2 max, int res, int smoothRad) { - maxx = ((fmaxx = mx) / res) + 1; - maxy = ((fmaxy = my) / res) + 1; + Kill(); + + enabled = modInfo.enableSmoothMesh; + + // we use SSE in performance sensitive code, don't let the window size be too small. + if (smoothRad < 4) smoothRad = 4; + + fmaxx = max.x * SQUARE_SIZE; + fmaxy = max.y * SQUARE_SIZE; + fresolution = res * SQUARE_SIZE; resolution = res; - smoothRadius = std::max(1.0f, smoothRad); + maxx = max.x / resolution; + maxy = max.y / resolution; + + smoothRadius = std::max(1, smoothRad); + + InitMapChangeTracking(); + InitDataStructures(); + + if (enabled) MakeSmoothMesh(); +} + +void SmoothHeightMesh::InitMapChangeTracking() { + const int damageTrackWidth = maxx / SAMPLES_PER_QUAD + (maxx % SAMPLES_PER_QUAD ? 1 : 0); + const int damageTrackHeight = maxy / SAMPLES_PER_QUAD + (maxy % SAMPLES_PER_QUAD ? 1 : 0); + const int damageTrackQuads = damageTrackWidth * damageTrackHeight; + mapChangeTrack.damageMap.clear(); + mapChangeTrack.damageMap.resize(damageTrackQuads); + mapChangeTrack.width = damageTrackWidth; + mapChangeTrack.height = damageTrackHeight; +} - MakeSmoothMesh(); +void SmoothHeightMesh::InitDataStructures() { + assert(mesh.empty()); + maximaMesh.resize(maxx * maxy, 0.0f); + mesh.resize(maxx * maxy, 0.0f); + tempMesh.resize(maxx * maxy, 0.0f); + origMesh.resize(maxx * maxy, 0.0f); + colsMaxima.clear(); + colsMaxima.resize(maxx, -std::numeric_limits::max()); + maximaRows.clear(); + maximaRows.resize(maxx, -1); } void SmoothHeightMesh::Kill() { + while (!mapChangeTrack.damageQueue[0].empty()) { mapChangeTrack.damageQueue[0].pop(); } + while (!mapChangeTrack.damageQueue[1].empty()) { mapChangeTrack.damageQueue[1].pop(); } + while (!mapChangeTrack.horizontalBlurQueue.empty()) { mapChangeTrack.horizontalBlurQueue.pop(); } + while (!mapChangeTrack.verticalBlurQueue.empty()) { mapChangeTrack.verticalBlurQueue.pop(); } + mapChangeTrack.damageMap.clear(); + maximaMesh.clear(); mesh.clear(); origMesh.clear(); } - - float SmoothHeightMesh::GetHeight(float x, float y) { assert(!mesh.empty()); - return Interpolate(x, y, maxx, maxy, resolution, &mesh[0]); + return Interpolate(x, y, maxx, maxy, fresolution, &mesh[0]); } float SmoothHeightMesh::GetHeightAboveWater(float x, float y) { assert(!mesh.empty()); - return std::max(0.0f, Interpolate(x, y, maxx, maxy, resolution, &mesh[0])); + return std::max(0.0f, Interpolate(x, y, maxx, maxy, fresolution, &mesh[0])); } - - float SmoothHeightMesh::SetHeight(int index, float h) { + assert(index < maxx*maxy); return (mesh[index] = h); } float SmoothHeightMesh::AddHeight(int index, float h) { + assert(index < maxx*maxy); return (mesh[index] += h); } float SmoothHeightMesh::SetMaxHeight(int index, float h) { + assert(index < maxx*maxy); return (mesh[index] = std::max(h, mesh[index])); } +inline static float GetRealGroundHeight(int x, int y, int resolution) { + const float* heightMap = readMap->GetCornerHeightMapSynced(); + const int baseIndex = (x + y*mapDims.mapxp1)*resolution; + return heightMap[baseIndex]; +} inline static void FindMaximumColumnHeights( + const int2 map, + const int y, + const int minx, const int maxx, - const int maxy, const int winSize, - const float resolution, + const int resolution, std::vector& colsMaxima, std::vector& maximaRows ) { // initialize the algorithm: find the maximum // height per column and the corresponding row - for (int y = 0; y <= std::min(maxy, winSize); ++y) { - for (int x = 0; x <= maxx; ++x) { - const float curx = x * resolution; - const float cury = y * resolution; - const float curh = CGround::GetHeightReal(curx, cury); - if (curh > colsMaxima[x]) { - colsMaxima[x] = curh; - maximaRows[x] = y; - } - } - } -} - -inline static void AdvanceMaximaRows( - const int y, - const int maxx, - const float resolution, - const std::vector& colsMaxima, - std::vector& maximaRows -) { - const float cury = y * resolution; + const int miny = std::max(y - winSize, 0); + const int maxy = std::min(y + winSize, map.y - 1); - // try to advance rows if they're equal to current maximum but are further away - for (int x = 0; x <= maxx; ++x) { - if (maximaRows[x] == (y - 1)) { - const float curx = x * resolution; - const float curh = CGround::GetHeightReal(curx, cury); + for (int y1 = miny; y1 <= maxy; ++y1) { + for (int x = minx; x <= maxx; ++x) { + const float curh = GetRealGroundHeight(x, y1, resolution); - if (curh == colsMaxima[x]) { - maximaRows[x] = y; + if (curh >= colsMaxima[x]) { + colsMaxima[x] = curh; + maximaRows[x] = y1; } - - assert(curh <= colsMaxima[x]); } } -} +#ifdef SMOOTH_MESH_DEBUG_MAXIMA + for (int x = minx; x <= maxx; ++x) + LOG("%s: y:%d col:%d row:%d max: %f", __func__, y, x, maximaRows[x], colsMaxima[x]); +#endif +} inline static void FindRadialMaximum( + const int2 map, int y, + int minx, int maxx, int winSize, float resolution, const std::vector& colsMaxima, std::vector& mesh ) { - const float cury = y * resolution; - - for (int x = 0; x < maxx; ++x) { + for (int x = minx; x <= maxx; ++x) { float maxRowHeight = -std::numeric_limits::max(); // find current maximum within radius smoothRadius // (in every column stack) along the current row const int startx = std::max(x - winSize, 0); - const int endx = std::min(maxx - 1, x + winSize); + const int endx = std::min(x + winSize, map.x - 1); - for (int i = startx; i <= endx; ++i) { - assert(i >= 0); - assert(i <= maxx); - assert(CGround::GetHeightReal(i * resolution, cury) <= colsMaxima[i]); + // This may mean the last SSE max function compares some values have already been compared + // This is harmless and avoids needlessly messy SSE code here + const int endIdx = endx - 3; - maxRowHeight = std::max(colsMaxima[i], maxRowHeight); + // Main loop for finding maximum height values + __m128 best = _mm_loadu_ps(&colsMaxima[startx]); + for (int i = startx + 4; i < endIdx; i += 4) { + __m128 next = _mm_loadu_ps(&colsMaxima[i]); + best = _mm_max_ps(best, next); } -#ifndef NDEBUG - const float curx = x * resolution; - assert(maxRowHeight <= readMap->GetCurrMaxHeight()); - assert(maxRowHeight >= CGround::GetHeightReal(curx, cury)); - - #ifdef SMOOTHMESH_CORRECTNESS_CHECK - // naive algorithm - float maxRowHeightAlt = -std::numeric_limits::max(); + // Check the last few height values + { + __m128 next = _mm_loadu_ps(&colsMaxima[endIdx]); + best = _mm_max_ps(best, next); + } - for (float y1 = cury - smoothRadius; y1 <= cury + smoothRadius; y1 += resolution) { - for (float x1 = curx - smoothRadius; x1 <= curx + smoothRadius; x1 += resolution) { - maxRowHeightAlt = std::max(maxRowHeightAlt, CGround::GetHeightReal(x1, y1)); - } + // This is an SSE horizontal compare + { + // split the four values into sets of two and compare + __m128 bestAlt = _mm_movehl_ps(best, best); + best = _mm_max_ps(best, bestAlt); + + // split the two values and compare + bestAlt = _mm_shuffle_ps(best, best, _MM_SHUFFLE(0, 0, 0, 1)); + best = _mm_max_ss(best, bestAlt); + _mm_store_ss(&maxRowHeight, best); } - assert(maxRowHeightAlt == maxRowHeight); - #endif +#ifndef NDEBUG + assert(maxRowHeight <= readMap->GetCurrMaxHeight()); + assert(maxRowHeight >= GetRealGroundHeight(x, y, resolution)); #endif - mesh[x + y * maxx] = maxRowHeight; + mesh[x + y * map.x] = maxRowHeight; + +#ifdef SMOOTH_MESH_DEBUG_MAXIMA + LOG("%s: y:%d x:%d local max: %f", __func__, y, x, maxRowHeight); +#endif } } - -inline static void FixRemainingMaxima( +inline static void AdvanceMaximas( + const int2 map, const int y, + const int minx, const int maxx, - const int maxy, const int winSize, - const float resolution, + const int resolution, std::vector& colsMaxima, std::vector& maximaRows ) { // fix remaining maximums after a pass - const int nextrow = y + winSize + 1; - const float nextrowy = nextrow * resolution; - - for (int x = 0; x <= maxx; ++x) { -#ifdef _DEBUG - for (int y1 = std::max(0, y - winSize); y1 <= std::min(maxy, y + winSize); ++y1) { - assert(CGround::GetHeightReal(x * resolution, y1 * resolution) <= colsMaxima[x]); - } -#endif - const float curx = x * resolution; + const int miny = std::max(y - winSize, 0); + const int virtualRow = y + winSize; + const int maxy = std::min(virtualRow, map.y - 1); - if (maximaRows[x] <= (y - winSize)) { + for (int x = minx; x <= maxx; ++x) { + if (maximaRows[x] < miny) { // find a new maximum if the old one left the window colsMaxima[x] = -std::numeric_limits::max(); - for (int y1 = std::max(0, y - winSize + 1); y1 <= std::min(maxy, nextrow); ++y1) { - const float h = CGround::GetHeightReal(curx, y1 * resolution); + for (int y1 = miny; y1 <= maxy; ++y1) { + const float h = GetRealGroundHeight(x, y1, resolution); - if (h > colsMaxima[x]) { + if (h >= colsMaxima[x]) { colsMaxima[x] = h; maximaRows[x] = y1; - } else if (colsMaxima[x] == h) { - // if equal, move as far down as possible - maximaRows[x] = y1; } } - } else if (nextrow <= maxy) { + } else if (virtualRow < map.y) { // else, just check if a new maximum has entered the window - const float h = CGround::GetHeightReal(curx, nextrowy); + const float h = GetRealGroundHeight(x, maxy, resolution); - if (h > colsMaxima[x]) { + if (h >= colsMaxima[x]) { colsMaxima[x] = h; - maximaRows[x] = nextrow; + maximaRows[x] = maxy; } } - assert(maximaRows[x] <= nextrow); - assert(maximaRows[x] >= y - winSize + 1); - -#ifdef _DEBUG - for (int y1 = std::max(0, y - winSize + 1); y1 <= std::min(maxy, y + winSize + 1); ++y1) { - assert(colsMaxima[x] >= CGround::GetHeightReal(curx, y1 * resolution)); - } +#ifdef SMOOTH_MESH_DEBUG_MAXIMA + LOG("%s: y:%d x:%d column max: %f", __func__, y, x, colsMaxima[x]); #endif + + assert(maximaRows[x] <= maxy); + assert(maximaRows[x] >= y - winSize); } } - inline static void BlurHorizontal( - const int maxx, - const int maxy, + const int2 mapSize, + const int2 min, + const int2 max, const int blurSize, - const float resolution, - const std::vector& kernel, + const int resolution, const std::vector& mesh, std::vector& smoothed ) { - const int lineSize = maxx; + const int lineSize = mapSize.x; + const int mapMaxX = mapSize.x - 1; - for_mt(0, maxy, [&](const int y) + for (int y = min.y; y <= max.y; ++y) { - for (int x = 0; x < maxx; ++x) - { - float avg = 0.0f; - for (int x1 = x - blurSize; x1 <= x + blurSize; ++x1) - avg += kernel[abs(x1 - x)] * mesh[std::max(0, std::min(maxx-1, x1)) + y * lineSize]; - - const float ghaw = CGround::GetHeightReal(x * resolution, y * resolution); - - smoothed[x + y * lineSize] = std::max(ghaw, avg); - - #pragma message ("FIX ME") - smoothed[x + y * lineSize] = std::clamp( - smoothed[x + y * lineSize], - readMap->GetCurrMinHeight(), - readMap->GetCurrMaxHeight() - ); + float avg = 0.0f; + float lv = 0; + float rv = 0; + float weight = 1.f / ((float)(blurSize*2 + 1)); + int li = min.x - blurSize; + int ri = min.x + blurSize; + + // linear blending allows us to add up all the values to average for the first point + // the rest of the points can be determined by taking the average after removing the + // last oldest value and adding the next value. + for (int x1 = li; x1 <= ri; ++x1) { + avg += mesh[std::max(0, std::min(x1, mapMaxX)) + y * lineSize]; + } + // ri should point to the next value to add to the averages + ri++; - assert(smoothed[x + y * lineSize] <= readMap->GetCurrMaxHeight() ); - assert(smoothed[x + y * lineSize] >= readMap->GetCurrMinHeight() ); + for (int x = min.x; x <= max.x; ++x) + { + // Remove the oldest height value (lv) and add the newest height value (rv) + avg += (-lv) + rv; + const float gh = GetRealGroundHeight(x, y, resolution); + smoothed[x + y * lineSize] = std::max(gh, avg*weight); + + // Get the values to add/remove for next iteration + lv = mesh[std::max(0, std::min(li, mapMaxX)) + y * lineSize]; + rv = mesh[ std::min(ri, mapMaxX) + y * lineSize]; + li++; ri++; + +#ifdef SMOOTH_MESH_DEBUG_BLUR + LOG ( "%s: x: %d, y: %d, avg: %f (%f) (g: %f) (max h: %f)" + , __func__, x, y, avg, avg*weight, gh, readMap->GetCurrMaxHeight()); +#endif + assert(smoothed[x + y * lineSize] <= readMap->GetCurrMaxHeight()+1 ); + assert(smoothed[x + y * lineSize] >= readMap->GetCurrMinHeight()-1 ); } - }); + } } inline static void BlurVertical( - const int maxx, - const int maxy, + const int2 mapSize, + const int2 min, + const int2 max, const int blurSize, - const float resolution, - const std::vector& kernel, + const int resolution, const std::vector& mesh, std::vector& smoothed ) { - const int lineSize = maxx; + // See BlurHorizontal for all the detailed comments. - for_mt(0, maxx, [&](const int x) + const int lineSize = mapSize.x; + const int mapMaxY = mapSize.y - 1; + + for (int x = min.x; x <= max.x; ++x) { - for (int y = 0; y < maxy; ++y) - { - float avg = 0.0f; - for (int y1 = y - blurSize; y1 <= y + blurSize; ++y1) - avg += kernel[abs(y1 - y)] * mesh[ x + std::max(0, std::min(maxy-1, y1)) * lineSize]; + float avg = 0.0f; + float lv = 0; + float rv = 0; + float weight = 1.f / ((float)(blurSize*2 + 1)); + int li = min.y - blurSize; + int ri = min.y + blurSize; + for (int y1 = li; y1 <= ri; ++y1) { + avg += mesh[x + std::max(0, std::min(y1, mapMaxY)) * lineSize]; + } + ri++; - const float ghaw = CGround::GetHeightReal(x * resolution, y * resolution); +#ifdef SMOOTH_MESH_DEBUG_BLUR + LOG("%s: starting average is: %f (%f) (w: %f)", __func__, avg, avg*weight, weight); +#endif - smoothed[x + y * lineSize] = std::max(ghaw, avg); + for (int y = min.y; y <= max.y; ++y) + { + avg += (-lv) + rv; + const float gh = GetRealGroundHeight(x, y, resolution); + smoothed[x + y * lineSize] = std::max(gh, avg*weight); - #pragma message ("FIX ME") - smoothed[x + y * lineSize] = std::clamp( - smoothed[x + y * lineSize], - readMap->GetCurrMinHeight(), - readMap->GetCurrMaxHeight() - ); + lv = mesh[ x + std::max(0, std::min(li, mapMaxY)) * lineSize]; + rv = mesh[ x + std::min(ri, mapMaxY) * lineSize]; + li++; ri++; - assert(smoothed[x + y * lineSize] <= readMap->GetCurrMaxHeight() ); - assert(smoothed[x + y * lineSize] >= readMap->GetCurrMinHeight() ); +#ifdef SMOOTH_MESH_DEBUG_BLUR + LOG("%s: x: %d, y: %d, avg: %f (%f) (g: %f)", __func__, x, y, avg, avg*weight, gh); + LOG("%s: for next line -%f +%f", __func__, lv, rv); +#endif + assert(smoothed[x + y * lineSize] <= readMap->GetCurrMaxHeight()+1 ); + assert(smoothed[x + y * lineSize] >= readMap->GetCurrMinHeight()-1 ); } - }); + } } - inline static void CheckInvariants( int y, int maxx, @@ -349,80 +424,243 @@ inline static void CheckInvariants( } +void SmoothHeightMesh::MapChanged(int x1, int y1, int x2, int y2) { -void SmoothHeightMesh::MakeSmoothMesh() -{ - ScopedOnceTimer timer("SmoothHeightMesh::MakeSmoothMesh"); + if (!enabled) return; + + const bool queueWasEmpty = mapChangeTrack.damageQueue[mapChangeTrack.activeBuffer].empty(); + const int res = resolution*SAMPLES_PER_QUAD; + const int w = mapChangeTrack.width; + const int h = mapChangeTrack.height; + + const int2 min { std::max((x1 - smoothRadius) / res, 0) + , std::max((y1 - smoothRadius) / res, 0)}; + const int2 max { std::min((x2 + smoothRadius - 1) / res, (w-1)) + , std::min((y2 + smoothRadius - 1) / res, (h-1))}; + + for (int y = min.y; y <= max.y; ++y) { + int i = min.x + y*w; + for (int x = min.x; x <= max.x; ++x, ++i) { + if (!mapChangeTrack.damageMap[i]) { + mapChangeTrack.damageMap[i] = true; + mapChangeTrack.damageQueue[mapChangeTrack.activeBuffer].push(i); + } + } + } + + const bool queueWasUpdated = !mapChangeTrack.damageQueue[mapChangeTrack.activeBuffer].empty(); - // info: - // height-value array has size * - // and represents a grid of cols by rows - // maximum legal index is ((maxx + 1) * (maxy + 1)) - 1 - // - // row-width (number of height-value corners per row) is (maxx + 1) - // col-height (number of height-value corners per col) is (maxy + 1) - // - // 1st row has indices [maxx*( 0) + ( 0), maxx*(1) + ( 0)] inclusive - // 2nd row has indices [maxx*( 1) + ( 1), maxx*(2) + ( 1)] inclusive - // 3rd row has indices [maxx*( 2) + ( 2), maxx*(3) + ( 2)] inclusive - // ... - // Nth row has indices [maxx*(N-1) + (N-1), maxx*(N) + (N-1)] inclusive - // - // use sliding window of maximums to reduce computational complexity + if (queueWasEmpty && queueWasUpdated) + mapChangeTrack.queueReleaseOnFrame = gs->frameNum + SMOOTH_MESH_UPDATE_DELAY; +} + + +inline static void CopyMeshPart + ( int mapx + , int2 min + , int2 max + , const std::vector& src + , std::vector& dest + ) { + for (int y = min.y; y <= max.y; ++y) { + const int startIdx = min.x + y*mapx; + const int endIdx = max.x + y*mapx + 1; + std::copy(&src[startIdx], &src[endIdx], &dest[startIdx]); + } +} + + +inline static bool UpdateSmoothMeshRequired(SmoothHeightMesh::MapChangeTrack& mapChangeTrack) { + const bool flushBuffer = !mapChangeTrack.activeBuffer; + const bool activeBuffer = mapChangeTrack.activeBuffer; + const bool currentWorkloadComplete = mapChangeTrack.damageQueue[flushBuffer].empty() + && mapChangeTrack.horizontalBlurQueue.empty() + && mapChangeTrack.verticalBlurQueue.empty(); + +#ifdef SMOOTH_MESH_DEBUG_GENERAL + LOG("%s: flush buffer is %d; damage queue is %I64d; blur queue is %I64d" + , __func__, (int)flushBuffer, meshDamageTrack.damageQueue[flushBuffer].size() + , meshDamageTrack.blurQueue.size() + ); +#endif + + if (currentWorkloadComplete){ + const bool activeBufferReady = !mapChangeTrack.damageQueue[activeBuffer].empty() + && gs->frameNum >= mapChangeTrack.queueReleaseOnFrame; + if (activeBufferReady) { + mapChangeTrack.activeBuffer = !mapChangeTrack.activeBuffer; +#ifdef SMOOTH_MESH_DEBUG_GENERAL + LOG("%s: opening new queue", __func__); +#endif + } + } + + return !currentWorkloadComplete; +} + + +void SmoothHeightMesh::UpdateSmoothMeshMaximas(int2 damageMin, int2 damageMax) { const int winSize = smoothRadius / resolution; const int blurSize = std::max(1, winSize / 2); - constexpr int blurPassesCount = 2; + int2 map{maxx, maxy}; + + int2 impactRadius{winSize, winSize}; + int2 min = damageMin - impactRadius; + int2 max = damageMax + impactRadius; + + min.x = std::clamp(min.x, 0, map.x - 1); + min.y = std::clamp(min.y, 0, map.y - 1); + max.x = std::clamp(max.x, 0, map.x - 1); + max.y = std::clamp(max.y, 0, map.y - 1); + +#ifdef SMOOTH_MESH_DEBUG_GENERAL +LOG("%s: quad index %d (%d,%d)-(%d,%d) (%d,%d)-(%d,%d) updating maxima" + , __func__, damagedAreaIndex, damageMin.x, damageMin.y, damageMax.x, damageMax.y, min.x, min.y, max.x, max.y + ); + +LOG("%s: quad area in world space (%f,%f) (%f,%f)", __func__ + , (float)damageMin.x * fresolution, (float)damageMin.y * fresolution + , (float)damageMax.x * fresolution, (float)damageMax.y * fresolution + ); +#endif - const auto fillGaussianKernelFunc = [blurSize](std::vector& gaussianKernel, const float sigma) { - gaussianKernel.resize(blurSize + 1); + for (int i = min.x; i <= max.x; ++i) + colsMaxima[i] = -std::numeric_limits::max(); - const auto gaussianG = [](const int x, const float sigma) -> float { - // 0.3989422804f = 1/sqrt(2*pi) - return 0.3989422804f * math::expf(-0.5f * x * x / (sigma * sigma)) / sigma; - }; + for (int i = min.x; i <= max.x; ++i) + maximaRows[i] = -1; - float sum = (gaussianKernel[0] = gaussianG(0, sigma)); + FindMaximumColumnHeights(map, damageMin.y, min.x, max.x, winSize, resolution, colsMaxima, maximaRows); - for (int i = 1; i < blurSize + 1; ++i) { - sum += 2.0f * (gaussianKernel[i] = gaussianG(i, sigma)); - } + for (int y = damageMin.y; y <= damageMax.y; ++y) { + FindRadialMaximum(map, y, damageMin.x, damageMax.x, winSize, resolution, colsMaxima, maximaMesh); + AdvanceMaximas(map, y+1, min.x, max.x, winSize, resolution, colsMaxima, maximaRows); + } +} + + +void SmoothHeightMesh::UpdateSmoothMesh() { + if (!enabled) return; + + SCOPED_TIMER("Sim::SmoothHeightMesh::UpdateSmoothMesh"); + + if (!UpdateSmoothMeshRequired(mapChangeTrack)) return; + + const bool flushBuffer = !mapChangeTrack.activeBuffer; + const bool updateMaxima = !mapChangeTrack.damageQueue[flushBuffer].empty(); + const bool doHorizontalBlur = !mapChangeTrack.horizontalBlurQueue.empty(); + + std::queue* activeQueue = nullptr; + if (updateMaxima) + activeQueue = &mapChangeTrack.damageQueue[flushBuffer]; + else if (doHorizontalBlur) + activeQueue = &mapChangeTrack.horizontalBlurQueue; + else + activeQueue = &mapChangeTrack.verticalBlurQueue; - for (auto& gk : gaussianKernel) { - gk /= sum; + const int damagedAreaIndex = activeQueue->front(); + activeQueue->pop(); + + // area of the map which to recalculate the height values + const int damageX = damagedAreaIndex % mapChangeTrack.width; + const int damageY = damagedAreaIndex / mapChangeTrack.width; + int2 damageMin{damageX*SAMPLES_PER_QUAD, damageY*SAMPLES_PER_QUAD}; + int2 damageMax = damageMin + int2{SAMPLES_PER_QUAD - 1, SAMPLES_PER_QUAD - 1}; + + damageMin.x = std::clamp(damageMin.x, 0, maxx - 1); + damageMin.y = std::clamp(damageMin.y, 0, maxy - 1); + damageMax.x = std::clamp(damageMax.x, 0, maxx - 1); + damageMax.y = std::clamp(damageMax.y, 0, maxy - 1); + + if (updateMaxima) { + UpdateSmoothMeshMaximas(damageMin, damageMax); + + mapChangeTrack.horizontalBlurQueue.push(damagedAreaIndex); + mapChangeTrack.damageMap[damagedAreaIndex] = false; + } else { + const int winSize = smoothRadius / resolution; + const int blurSize = std::max(1, winSize / 2); + const int2 map{maxx, maxy}; + +#ifdef SMOOTH_MESH_DEBUG_GENERAL + LOG("%s: quad index %d (%d,%d)-(%d,%d) applying blur", __func__ + , damagedAreaIndex, damageMin.x, damageMin.y, damageMax.x, damageMax.y + ); + + LOG("%s: quad area in world space (%f,%f) (%f,%f)", __func__ + , (float)damageMin.x * fresolution, (float)damageMin.y * fresolution + , (float)damageMax.x * fresolution, (float)damageMax.y * fresolution + ); +#endif + + if (doHorizontalBlur) { + BlurHorizontal(map, damageMin, damageMax, blurSize, resolution, maximaMesh, tempMesh); + mapChangeTrack.verticalBlurQueue.push(damagedAreaIndex); + } + else { + BlurVertical(map, damageMin, damageMax, blurSize, resolution, tempMesh, mesh); + CopyMeshPart(map.x, damageMin, damageMax, mesh, tempMesh); } - }; + } +} - constexpr float gSigma = 5.0f; - std::vector gaussianKernel; - fillGaussianKernelFunc(gaussianKernel, gSigma); - assert(mesh.empty()); - mesh.resize((maxx + 1) * (maxy + 1), 0.0f); - origMesh.resize((maxx + 1) * (maxy + 1), 0.0f); +void SmoothHeightMesh::MakeSmoothMesh() { + ScopedOnceTimer timer("SmoothHeightMesh::MakeSmoothMesh"); - colsMaxima.clear(); - colsMaxima.resize(maxx + 1, -std::numeric_limits::max()); - maximaRows.clear(); - maximaRows.resize(maxx + 1, -1); + // A sliding window is used to reduce computational complexity + const int winSize = smoothRadius / resolution; + + // blur size is half the window size to create a wider plateau + const int blurSize = std::max(1, winSize / 2); + int2 min{0, 0}; + int2 max{maxx-1, maxy-1}; + int2 map{maxx, maxy}; - FindMaximumColumnHeights(maxx, maxy, winSize, resolution, colsMaxima, maximaRows); + FindMaximumColumnHeights(map, 0, 0, max.x, winSize, resolution, colsMaxima, maximaRows); - for (int y = 0; y <= maxy; ++y) { - AdvanceMaximaRows(y, maxx, resolution, colsMaxima, maximaRows); - FindRadialMaximum(y, maxx, winSize, resolution, colsMaxima, mesh); - FixRemainingMaxima(y, maxx, maxy, winSize, resolution, colsMaxima, maximaRows); + for (int y = 0; y <= max.y; ++y) { + FindRadialMaximum(map, y, 0, max.x, winSize, resolution, colsMaxima, maximaMesh); + AdvanceMaximas(map, y+1, 0, max.x, winSize, resolution, colsMaxima, maximaRows); #ifdef _DEBUG - CheckInvariants(y, maxx, maxy, winSize, resolution, colsMaxima, maximaRows); + CheckInvariants(y, maxx, maxy, winSize, fresolution, colsMaxima, maximaRows); #endif } - // actually smooth with approximate Gaussian blur passes - for (int numBlurs = blurPassesCount; numBlurs > 0; --numBlurs) { - BlurHorizontal(maxx, maxy, blurSize, resolution, gaussianKernel, mesh, origMesh); mesh.swap(origMesh); - BlurVertical(maxx, maxy, blurSize, resolution, gaussianKernel, mesh, origMesh); mesh.swap(origMesh); - } + BlurHorizontal(map, min, max, blurSize, resolution, maximaMesh, tempMesh); + BlurVertical(map, min, max, blurSize, resolution, tempMesh, mesh); // now contains the final smoothed heightmap, save it in origMesh std::copy(mesh.begin(), mesh.end(), origMesh.begin()); + + // tempMesh should be kept inline with mesh to avoid bluring artefacts in dynamic updates + std::copy(mesh.begin(), mesh.end(), tempMesh.begin()); } + + +// ============================================= +// Old gaussian kernel reference code kept here for future reference +// ============================================= + // const auto fillGaussianKernelFunc = [blurSize](std::vector& gaussianKernel, const float sigma) { + // gaussianKernel.resize(blurSize + 1); + + // const auto gaussianG = [](const int x, const float sigma) -> float { + // // 0.3989422804f = 1/sqrt(2*pi) + // return 0.3989422804f * math::expf(-0.5f * x * x / (sigma * sigma)) / sigma; + // }; + + // float sum = (gaussianKernel[0] = gaussianG(0, sigma)); + + // for (int i = 1; i < blurSize + 1; ++i) { + // sum += 2.0f * (gaussianKernel[i] = gaussianG(i, sigma)); + // } + + // for (auto& gk : gaussianKernel) { + // gk /= sum; + // } + // }; + + // constexpr float gSigma = 5.0f; + // fillGaussianKernelFunc(gaussianKernel, gSigma); +// ============================================= diff --git a/rts/Sim/Misc/SmoothHeightMesh.h b/rts/Sim/Misc/SmoothHeightMesh.h index b183f28b00..1ebca6540d 100644 --- a/rts/Sim/Misc/SmoothHeightMesh.h +++ b/rts/Sim/Misc/SmoothHeightMesh.h @@ -3,17 +3,41 @@ #ifndef SMOOTH_HEIGHT_MESH_H #define SMOOTH_HEIGHT_MESH_H +#include +#include #include +#include "Sim/Misc/GlobalConstants.h" +#include "System/type2.h" + class CGround; +namespace SmoothHeightMeshNamespace { + constexpr int SMOOTH_MESH_UPDATE_DELAY = GAME_SPEED; + constexpr int SAMPLES_PER_QUAD = 32; +} + /** * Provides a GetHeight(x, y) of its own that smooths the mesh. */ class SmoothHeightMesh { + friend class SmoothHeightMeshDrawer; + public: - void Init(float mx, float my, float res, float smoothRad); + + struct MapChangeTrack { + std::vector damageMap; + std::queue damageQueue[2]; + std::queue horizontalBlurQueue; + std::queue verticalBlurQueue; + int width = 0; + int height = 0; + int queueReleaseOnFrame = 0; + bool activeBuffer = 0; + }; + + void Init(int2 max, int res, int smoothRad); void Kill(); float GetHeight(float x, float y); @@ -26,26 +50,40 @@ class SmoothHeightMesh int GetMaxY() const { return maxy; } float GetFMaxX() const { return fmaxx; } float GetFMaxY() const { return fmaxy; } - float GetResolution() const { return resolution; } + float GetResolution() const { return fresolution; } const float* GetMeshData() const { return &mesh[0]; } const float* GetOriginalMeshData() const { return &origMesh[0]; } + void UpdateSmoothMesh(); + + void MapChanged(int x1, int z1, int x2, int z2); + private: + void InitMapChangeTracking(); + void InitDataStructures(); void MakeSmoothMesh(); + void UpdateSmoothMeshMaximas(int2 damageMin, int2 damageMax); + + bool enabled = true; int maxx = 0; int maxy = 0; float fmaxx = 0.0f; float fmaxy = 0.0f; - float resolution = 0.0f; - float smoothRadius = 0.0f; + float fresolution = 0.f; + int resolution = 0; + int smoothRadius = 0; + std::vector maximaMesh; std::vector mesh; + std::vector tempMesh; std::vector origMesh; std::vector colsMaxima; std::vector maximaRows; + + MapChangeTrack mapChangeTrack; }; extern SmoothHeightMesh smoothGround;