Files
UnrealEngine/Engine/Source/Runtime/Navmesh/Private/DetourTileCache/DetourTileCacheRegion.cpp
2025-05-18 13:04:45 +08:00

1595 lines
37 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
// Modified version of Recast/Detour's source file
//
// Copyright (c) 2009-2010 Mikko Mononen memon@inside.org
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.
//
#include "CoreMinimal.h"
#include "Detour/DetourCommon.h"
#include "Detour/DetourAssert.h"
#include "Detour/DetourAlloc.h"
#include "DetourTileCache/DetourTileCacheBuilder.h"
#define _USE_MATH_DEFINES
inline bool isConnected(const dtTileCacheLayer& layer, const int idx, const int dir)
{
return (layer.cons[idx] & (1 << dir)) != 0;
}
static void calculateDistanceField(const dtTileCacheLayer& layer, unsigned short* src, unsigned short& maxDist)
{
const int w = (int)layer.header->width;
const int h = (int)layer.header->height;
// Init distance and points.
memset(src, 0xff, w*h*sizeof(unsigned short));
// Mark boundary cells.
for (int y = 0; y < h; ++y)
{
for (int x = 0; x < w; ++x)
{
const int i = x+y*w;
const unsigned char area = layer.areas[i];
if (area == DT_TILECACHE_NULL_AREA)
{
src[i] = 0;
continue;
}
int nc = 0;
for (int dir = 0; dir < 4; ++dir)
{
const int ax = x + getDirOffsetX(dir);
const int ay = y + getDirOffsetY(dir);
const int ai = ax+ay*w;
if (ax >= 0 && ax < w && ay >= 0 && ay < h && isConnected(layer, i, dir))
{
if (area == layer.areas[ai])
{
nc++;
}
}
}
if (nc != 4)
{
src[i] = 0;
}
}
}
// Pass 1
for (int y = 0; y < h; ++y)
{
for (int x = 0; x < w; ++x)
{
const int i = x+y*w;
if (layer.areas[i] == DT_TILECACHE_NULL_AREA)
continue;
const int ax = x + getDirOffsetX(0);
const int ay = y + getDirOffsetY(0);
const int ai = ax+ay*w;
if (ax >= 0 && ax < w && ay >= 0 && ay < h && isConnected(layer, i, 0))
{
// (-1,0)
if (src[ai]+2 < src[i])
src[i] = src[ai]+2;
const int aax = ax + getDirOffsetX(3);
const int aay = ay + getDirOffsetY(3);
const int aai = aax+aay*w;
if (aax >= 0 && aax < w && aay >= 0 && aay < h && isConnected(layer, ai, 3))
{
// (-1,-1)
if (src[aai]+3 < src[i])
src[i] = src[aai]+3;
}
}
const int ax2 = x + getDirOffsetX(3);
const int ay2 = y + getDirOffsetY(3);
const int ai2 = ax2+ay2*w;
if (ax2 >= 0 && ax2 < w && ay2 >= 0 && ay2 < h && isConnected(layer, i, 3))
{
// (0,-1)
if (src[ai2]+2 < src[i])
src[i] = src[ai2]+2;
const int aax2 = ax2 + getDirOffsetX(2);
const int aay2 = ay2 + getDirOffsetY(2);
const int aai2 = aax2+aay2*w;
if (aax2 >= 0 && aax2 < w && aay2 >= 0 && aay2 < h && isConnected(layer, ai2, 2))
{
// (1,-1)
if (src[aai2]+3 < src[i])
src[i] = src[aai2]+3;
}
}
}
}
// Pass 2
for (int y = h-1; y >= 0; --y)
{
for (int x = w-1; x >= 0; --x)
{
const int i = x+y*w;
if (layer.areas[i] == DT_TILECACHE_NULL_AREA)
continue;
const int ax = x + getDirOffsetX(2);
const int ay = y + getDirOffsetY(2);
const int ai = ax+ay*w;
if (ax >= 0 && ax < w && ay >= 0 && ay < h && isConnected(layer, i, 2))
{
// (1,0)
if (src[ai]+2 < src[i])
src[i] = src[ai]+2;
const int aax = ax + getDirOffsetX(1);
const int aay = ay + getDirOffsetY(1);
const int aai = aax+aay*w;
if (aax >= 0 && aax < w && aay >= 0 && aay < h && isConnected(layer, ai, 1))
{
// (1,1)
if (src[aai]+3 < src[i])
src[i] = src[aai]+3;
}
}
const int ax2 = x + getDirOffsetX(1);
const int ay2 = y + getDirOffsetY(1);
const int ai2 = ax2+ay2*w;
if (ax2 >= 0 && ax2 < w && ay2 >= 0 && ay2 < h && isConnected(layer, i, 1))
{
// (0,1)
if (src[ai2]+2 < src[i])
src[i] = src[ai2]+2;
const int aax2 = ax2 + getDirOffsetX(0);
const int aay2 = ay2 + getDirOffsetY(0);
const int aai2 = aax2+aay2*w;
if (aax2 >= 0 && aax2 < w && aay2 >= 0 && aay2 < h && isConnected(layer, ai2, 0))
{
// (-1,1)
if (src[aai2]+3 < src[i])
src[i] = src[aai2]+3;
}
}
}
}
// calc max distance
maxDist = 0;
for (int i = w*h -1; i >= 0; i--)
{
maxDist = dtMax(src[i], maxDist);
}
}
static unsigned short* boxBlur(dtTileCacheLayer& layer, int thr, unsigned short* src, unsigned short* dst)
{
const int w = (int)layer.header->width;
const int h = (int)layer.header->height;
thr *= 2;
for (int y = 0; y < h; ++y)
{
for (int x = 0; x < w; ++x)
{
const int i = x+y*w;
const unsigned short cd = src[i];
if (cd <= thr)
{
dst[i] = cd;
continue;
}
int d = (int)cd;
for (int dir = 0; dir < 4; ++dir)
{
const int ax = x + getDirOffsetX(dir);
const int ay = y + getDirOffsetY(dir);
const int ni = ax+ay*w;
if (ax >= 0 && ax < w && ay >= 0 && ay < h && isConnected(layer, i, dir))
{
d += (int)src[ni];
const int dir2 = (dir+1) & 0x3;
const int ax2 = ax + getDirOffsetX(dir2);
const int ay2 = ay + getDirOffsetY(dir2);
const int ni2 = ax2+ay2*w;
if (ax2 >= 0 && ax2 < w && ay2 >= 0 && ay2 < h && isConnected(layer, ni, dir2))
{
d += (int)src[ni2];
}
else
{
d += cd;
}
}
else
{
d += cd*2;
}
}
dst[i] = (unsigned short)((d+5)/9);
}
}
return dst;
}
dtStatus dtBuildTileCacheDistanceField(dtTileCacheAlloc* alloc, dtTileCacheLayer& layer, dtTileCacheDistanceField& dfield)
{
dtAssert(alloc);
const int w = (int)layer.header->width;
const int h = (int)layer.header->height;
dfield.data = (unsigned short*)alloc->alloc(w*h*sizeof(unsigned short));
if (!dfield.data)
{
return DT_FAILURE | DT_OUT_OF_MEMORY;
}
dtTileCacheDistanceField tmpField;
tmpField.data = (unsigned short*)alloc->alloc(w*h*sizeof(unsigned short));
if (!tmpField.data)
{
return DT_FAILURE | DT_OUT_OF_MEMORY;
}
calculateDistanceField(layer, dfield.data, dfield.maxDist);
if (boxBlur(layer, 1, dfield.data, tmpField.data) != dfield.data)
{
dtSwap(dfield.data, tmpField.data);
}
alloc->free(tmpField.data);
return DT_SUCCESS;
}
static unsigned short* expandRegions(int maxIter, unsigned short level,
dtTileCacheLayer& layer, dtTileCacheDistanceField& dfield,
unsigned short* srcReg, unsigned short* srcDist,
unsigned short* dstReg, unsigned short* dstDist,
dtIntArray& stack)
{
const int w = (int)layer.header->width;
const int h = (int)layer.header->height;
// Find cells revealed by the raised level.
stack.resize(0);
for (int y = 0; y < h; ++y)
{
for (int x = 0; x < w; ++x)
{
const int i = x+y*w;
if (dfield.data[i] >= level && srcReg[i] == 0 && layer.areas[i] != DT_TILECACHE_NULL_AREA)
{
stack.push(x);
stack.push(y);
stack.push(i);
}
}
}
int iter = 0;
while (stack.size() > 0)
{
int failed = 0;
memcpy(dstReg, srcReg, sizeof(unsigned short)*w*h);
memcpy(dstDist, srcDist, sizeof(unsigned short)*w*h);
for (int j = 0; j < stack.size(); j += 3)
{
int x = stack[j+0];
int y = stack[j+1];
int i = stack[j+2];
if (i < 0)
{
failed++;
continue;
}
unsigned short r = srcReg[i];
unsigned short d2 = 0xffff;
const unsigned char area = layer.areas[i];
for (int dir = 0; dir < 4; ++dir)
{
const int ax = x + getDirOffsetX(dir);
const int ay = y + getDirOffsetY(dir);
const int ai = ax+ay*w;
if (ax >= 0 && ax < w && ay >= 0 && ay < h && isConnected(layer, i, dir))
{
if (layer.areas[ai] != area) continue;
if (srcReg[ai] > 0)
{
if ((int)srcDist[ai]+2 < (int)d2)
{
r = srcReg[ai];
d2 = srcDist[ai]+2;
}
}
}
}
if (r)
{
stack[j+2] = -1; // mark as used
dstReg[i] = r;
dstDist[i] = d2;
}
else
{
failed++;
}
}
// rcSwap source and dest.
dtSwap(srcReg, dstReg);
dtSwap(srcDist, dstDist);
if (failed*3 == stack.size())
break;
if (level > 0)
{
++iter;
if (iter >= maxIter)
break;
}
}
return srcReg;
}
static bool floodRegion(int x, int y, int i, unsigned short level, unsigned short r,
dtTileCacheLayer& layer, dtTileCacheDistanceField& dfield, unsigned short* srcReg, unsigned short* srcDist, dtIntArray& stack)
{
const int w = (int)layer.header->width;
const int h = (int)layer.header->height;
const unsigned char area = layer.areas[i];
// Flood fill mark region.
stack.resize(0);
stack.push((int)x);
stack.push((int)y);
stack.push((int)i);
srcReg[i] = r;
srcDist[i] = 0;
unsigned short lev = level >= 2 ? level-2 : 0;
int count = 0;
while (stack.size() > 0)
{
int ci = stack.pop();
int cy = stack.pop();
int cx = stack.pop();
// Check if any of the neighbours already have a valid region set.
unsigned short ar = 0;
for (int dir = 0; dir < 4; ++dir)
{
const int ax = cx + getDirOffsetX(dir);
const int ay = cy + getDirOffsetY(dir);
const int ai = ax+ay*w;
// 8 connected
if (ax >= 0 && ax < w && ay >= 0 && ay < h && isConnected(layer, i, dir))
{
if (layer.areas[ai] != area)
continue;
unsigned short nr = srcReg[ai];
if (nr != 0 && nr != r)
ar = nr;
const int dir2 = (dir+1) & 0x3;
const int ax2 = ax + getDirOffsetX(dir2);
const int ay2 = ay + getDirOffsetY(dir2);
const int ai2 = ax2+ay2*w;
if (ax2 >= 0 && ax2 < w && ay2 >= 0 && ay2 < h && isConnected(layer, ai, dir2))
{
if (layer.areas[ai2] != area)
continue;
unsigned short nr2 = srcReg[ai2];
if (nr2 != 0 && nr2 != r)
ar = nr2;
}
}
}
if (ar != 0)
{
srcReg[ci] = 0;
continue;
}
count++;
// Expand neighbours.
for (int dir = 0; dir < 4; ++dir)
{
const int ax = cx + getDirOffsetX(dir);
const int ay = cy + getDirOffsetY(dir);
const int ai = ax+ay*w;
if (ax >= 0 && ax < w && ay >= 0 && ay < h && isConnected(layer, i, dir))
{
if (layer.areas[ai] != area)
continue;
if (dfield.data[ai] >= lev && srcReg[ai] == 0)
{
srcReg[ai] = r;
srcDist[ai] = 0;
stack.push(ax);
stack.push(ay);
stack.push(ai);
}
}
}
}
return count > 0;
}
struct dtLayerRegion
{
inline dtLayerRegion(unsigned short i) :
cellCount(0),
id(i),
areaType(0),
remap(false),
visited(false),
border(false)
{}
dtIntArray connections;
int cellCount; // Number of spans belonging to this region
unsigned short id; // ID of the region
unsigned char areaType; // Are type.
unsigned char remap : 1;
unsigned char visited : 1;
unsigned char border : 1;
dtLayerRegion& operator=(const dtLayerRegion& src)
{
this->cellCount = src.cellCount;
this->id = src.id;
this->areaType = src.areaType;
this->remap = src.remap;
this->visited = src.visited;
this->border = src.border;
this->connections.copy(src.connections);
return *this;
}
};
static void removeAdjacentNeighbours(dtLayerRegion& reg)
{
// Remove adjacent duplicates.
for (int i = 0; i < reg.connections.size() && reg.connections.size() > 1; )
{
int ni = (i+1) % reg.connections.size();
if (reg.connections[i] == reg.connections[ni])
{
// Remove duplicate
for (int j = i; j < reg.connections.size()-1; ++j)
reg.connections[j] = reg.connections[j+1];
reg.connections.pop();
}
else
++i;
}
}
static void replaceNeighbour(dtLayerRegion& reg, unsigned short oldId, unsigned short newId)
{
bool neiChanged = false;
for (int i = 0; i < reg.connections.size(); ++i)
{
if (reg.connections[i] == oldId)
{
reg.connections[i] = newId;
neiChanged = true;
}
}
if (neiChanged)
removeAdjacentNeighbours(reg);
}
static bool canMergeWithRegion(const dtLayerRegion& rega, const dtLayerRegion& regb)
{
if (rega.areaType != regb.areaType)
return false;
int n = 0;
for (int i = 0; i < rega.connections.size(); ++i)
{
if (rega.connections[i] == regb.id)
n++;
}
if (n > 1)
return false;
return true;
}
static bool mergeRegions(dtLayerRegion& rega, dtLayerRegion& regb)
{
unsigned short aid = rega.id;
unsigned short bid = regb.id;
// Duplicate current neighbourhood.
dtIntArray acon;
acon.resize(rega.connections.size());
for (int i = 0; i < rega.connections.size(); ++i)
acon[i] = rega.connections[i];
dtIntArray& bcon = regb.connections;
// Find insertion point on A.
int insa = -1;
for (int i = 0; i < acon.size(); ++i)
{
if (acon[i] == bid)
{
insa = i;
break;
}
}
if (insa == -1)
return false;
// Find insertion point on B.
int insb = -1;
for (int i = 0; i < bcon.size(); ++i)
{
if (bcon[i] == aid)
{
insb = i;
break;
}
}
if (insb == -1)
return false;
// Merge neighbours.
rega.connections.resize(0);
for (int i = 0, ni = acon.size(); i < ni-1; ++i)
rega.connections.push(acon[(insa+1+i) % ni]);
for (int i = 0, ni = bcon.size(); i < ni-1; ++i)
rega.connections.push(bcon[(insb+1+i) % ni]);
removeAdjacentNeighbours(rega);
rega.cellCount += regb.cellCount;
regb.cellCount = 0;
rega.border |= regb.border;
regb.border = 0;
regb.connections.resize(0);
return true;
}
static bool isSolidEdge(dtTileCacheLayer& layer, unsigned short* srcReg,
int x, int y, int i, int dir)
{
const int w = (int)layer.header->width;
const int h = (int)layer.header->height;
const int ax = x + getDirOffsetX(dir);
const int ay = y + getDirOffsetY(dir);
const int ai = ax+ay*w;
unsigned short r = 0;
if (ax >= 0 && ax < w && ay >= 0 && ay < h && isConnected(layer, i, dir))
{
r = srcReg[ai];
}
return (r != srcReg[i]);
}
static void walkContour(int x, int y, int i, int dir,
dtTileCacheLayer& layer, unsigned short* srcReg, dtIntArray& cont)
{
int startDir = dir;
int starti = i;
const int w = (int)layer.header->width;
const int h = (int)layer.header->height;
unsigned short curReg = 0;
{
const int ax = x + getDirOffsetX(dir);
const int ay = y + getDirOffsetY(dir);
const int ai = ax+ay*w;
if (ax >= 0 && ax < w && ay >= 0 && ay < h && isConnected(layer, i, dir))
{
curReg = srcReg[ai];
}
}
cont.push(curReg);
int iter = 0;
while (++iter < 40000)
{
const int ax = x + getDirOffsetX(dir);
const int ay = y + getDirOffsetY(dir);
const int ai = ax+ay*w;
const bool bConnected = (ax >= 0 && ax < w && ay >= 0 && ay < h && isConnected(layer, i, dir));
unsigned short r = 0;
if (bConnected)
{
r = srcReg[ai];
}
if (r != srcReg[i])
{
// Choose the edge corner
if (r != curReg)
{
curReg = r;
cont.push(curReg);
}
dir = (dir+1) & 0x3; // Rotate CW
}
else
{
int ni = -1;
if (bConnected)
{
ni = ai;
}
if (ni == -1)
{
// Should not happen.
return;
}
x = ax;
y = ay;
i = ni;
dir = (dir+3) & 0x3; // Rotate CCW
}
if (starti == i && startDir == dir)
{
break;
}
}
// Remove adjacent duplicates.
if (cont.size() > 1)
{
for (int j = 0; j < cont.size(); )
{
int nj = (j+1) % cont.size();
if (cont[j] == cont[nj])
{
for (int k = j; k < cont.size()-1; ++k)
cont[k] = cont[k+1];
cont.pop();
}
else
++j;
}
}
}
static dtStatus filterSmallRegions(dtTileCacheAlloc* alloc, dtTileCacheLayer& layer, int minRegionArea, int mergeRegionSize,
unsigned short& maxRegionId, unsigned short* srcReg)
{
const int w = (int)layer.header->width;
const int h = (int)layer.header->height;
const int nreg = maxRegionId+1;
dtFixedArray<dtLayerRegion> regions(alloc, nreg);
if (!regions)
{
return DT_FAILURE | DT_OUT_OF_MEMORY;
}
// Construct regions
regions.set(0);
for (int i = 0; i < nreg; ++i)
regions[i] = dtLayerRegion((unsigned short)i);
// Find edge of a region and find connections around the contour.
for (int y = 0; y < h; ++y)
{
const bool borderY = (y == 0) || (y == (h - 1));
for (int x = 0; x < w; ++x)
{
const int i = x+y*w;
unsigned short r = srcReg[i];
if (r == DT_TILECACHE_NULL_AREA || r >= nreg)
continue;
dtLayerRegion& reg = regions[r];
reg.cellCount++;
reg.border |= borderY || (x == 0) || (x == (w - 1));
// Have found contour
if (reg.connections.size() > 0)
continue;
reg.areaType = layer.areas[i];
// Check if this cell is next to a border.
int ndir = -1;
for (int dir = 0; dir < 4; ++dir)
{
if (isSolidEdge(layer, srcReg, x, y, i, dir))
{
ndir = dir;
break;
}
}
if (ndir != -1)
{
// The cell is at border.
// Walk around the contour to find all the neighbours.
walkContour(x, y, i, ndir, layer, srcReg, reg.connections);
}
}
}
// Remove too small regions.
dtIntArray stack(32);
dtIntArray trace(32);
for (int i = 0; i < nreg; ++i)
{
dtLayerRegion& reg = regions[i];
if (reg.id == 0)
continue;
if (reg.cellCount == 0)
continue;
if (reg.visited)
continue;
// Count the total size of all the connected regions.
// Also keep track of the regions connects to a tile border.
bool connectsToBorder = false;
int cellCount = 0;
stack.resize(0);
trace.resize(0);
reg.visited = true;
stack.push(i);
while (stack.size())
{
// Pop
int ri = stack.pop();
dtLayerRegion& creg = regions[ri];
connectsToBorder |= creg.border;
cellCount += creg.cellCount;
trace.push(ri);
for (int j = 0; j < creg.connections.size(); ++j)
{
dtLayerRegion& neireg = regions[creg.connections[j]];
if (neireg.visited)
continue;
if (neireg.id == 0)
continue;
// Visit
stack.push(neireg.id);
neireg.visited = true;
}
}
// If the accumulated regions size is too small, remove it.
// Do not remove areas which connect to tile borders
// as their size cannot be estimated correctly and removing them
// can potentially remove necessary areas.
if (cellCount < minRegionArea && !connectsToBorder)
{
// Kill all visited regions.
for (int j = 0; j < trace.size(); ++j)
{
regions[trace[j]].cellCount = 0;
regions[trace[j]].id = 0;
}
}
}
// Merge too small regions to neighbour regions.
int mergeCount = 0 ;
do
{
mergeCount = 0;
for (int i = 0; i < nreg; ++i)
{
dtLayerRegion& reg = regions[i];
if (reg.id == 0)
continue;
if (reg.cellCount == 0)
continue;
// Check to see if the region should be merged.
if (reg.cellCount > mergeRegionSize && reg.border)
continue;
// Small region with more than 1 connection.
// Or region which is not connected to a border at all.
// Find smallest neighbour region that connects to this one.
int smallest = 0xfffffff;
unsigned short mergeId = reg.id;
for (int j = 0; j < reg.connections.size(); ++j)
{
dtLayerRegion& mreg = regions[reg.connections[j]];
if (mreg.id == 0) continue;
if (mreg.cellCount < smallest &&
canMergeWithRegion(reg, mreg) &&
canMergeWithRegion(mreg, reg))
{
smallest = mreg.cellCount;
mergeId = mreg.id;
}
}
// Found new id.
if (mergeId != reg.id)
{
unsigned short oldId = reg.id;
dtLayerRegion& target = regions[mergeId];
// Merge neighbours.
if (mergeRegions(target, reg))
{
// Fixup regions pointing to current region.
for (int j = 0; j < nreg; ++j)
{
if (regions[j].id == 0) continue;
// If another region was already merged into current region
// change the nid of the previous region too.
if (regions[j].id == oldId)
regions[j].id = mergeId;
// Replace the current region with the new one if the
// current regions is neighbour.
replaceNeighbour(regions[j], oldId, mergeId);
}
mergeCount++;
}
}
}
}
while (mergeCount > 0);
// Compress region Ids.
for (int i = 0; i < nreg; ++i)
{
regions[i].remap = false;
if (regions[i].id == DT_TILECACHE_NULL_AREA) continue; // Skip nil regions.
regions[i].remap = true;
}
unsigned short regIdGen = 0;
for (int i = 0; i < nreg; ++i)
{
if (!regions[i].remap)
continue;
unsigned short oldId = regions[i].id;
unsigned short newId = ++regIdGen;
for (int j = i; j < nreg; ++j)
{
if (regions[j].id == oldId)
{
regions[j].id = newId;
regions[j].remap = false;
}
}
}
maxRegionId = regIdGen;
// Remap regions.
for (int i = w*h-1; i >= 0; i--)
{
srcReg[i] = regions[srcReg[i]].id;
}
for (int i = 0; i < nreg; ++i)
regions[i].~dtLayerRegion();
return DT_SUCCESS;
}
dtStatus dtBuildTileCacheRegions(dtTileCacheAlloc* alloc,
const int minRegionArea, const int mergeRegionArea,
dtTileCacheLayer& layer, dtTileCacheDistanceField dfield)
{
dtAssert(alloc);
const int w = (int)layer.header->width;
const int h = (int)layer.header->height;
const int size = w*h;
dtFixedArray<unsigned short> buf(alloc, size*4);
if (!buf)
{
return DT_FAILURE | DT_OUT_OF_MEMORY;
}
dtIntArray stack(1024);
dtIntArray visited(1024);
unsigned short* srcReg = buf;
unsigned short* srcDist = buf+size;
unsigned short* dstReg = buf+size*2;
unsigned short* dstDist = buf+size*3;
memset(srcReg, 0, sizeof(unsigned short)*size);
memset(srcDist, 0, sizeof(unsigned short)*size);
unsigned short regionId = 1;
unsigned short level = (dfield.maxDist+1) & ~1;
// TODO: Figure better formula, expandIters defines how much the
// watershed "overflows" and simplifies the regions. Tying it to
// agent radius was usually good indication how greedy it could be.
// const int expandIters = 4 + walkableRadius * 2;
const int expandIters = 8;
while (level > 0)
{
level = level >= 2 ? level-2 : 0;
// Expand current regions until no empty connected cells found.
if (expandRegions(expandIters, level, layer, dfield, srcReg, srcDist, dstReg, dstDist, stack) != srcReg)
{
dtSwap(srcReg, dstReg);
dtSwap(srcDist, dstDist);
}
// Mark new regions with IDs.
for (int y = 0; y < h; ++y)
{
for (int x = 0; x < w; ++x)
{
const int i=x+y*w;
if (dfield.data[i] < level || srcReg[i] != 0 || layer.areas[i] == DT_TILECACHE_NULL_AREA)
continue;
if (floodRegion(x, y, i, level, regionId, layer, dfield, srcReg, srcDist, stack))
regionId++;
}
}
}
// Expand current regions until no empty connected cells found.
if (expandRegions(expandIters*8, 0, layer, dfield, srcReg, srcDist, dstReg, dstDist, stack) != srcReg)
{
dtSwap(srcReg, dstReg);
dtSwap(srcDist, dstDist);
}
dtStatus status = filterSmallRegions(alloc, layer, minRegionArea, mergeRegionArea, regionId, srcReg);
if (dtStatusFailed(status))
{
return status;
}
// Write the result out.
memcpy(layer.regs, srcReg, sizeof(unsigned short)*size);
layer.regCount = regionId;
return DT_SUCCESS;
}
struct dtLayerSweepSpan
{
unsigned short ns; // number samples
unsigned short id; // region id
unsigned short nei; // neighbour id
};
struct dtLayerMonotoneRegion
{
dtIntArray neis;
int area;
int chunkId;
unsigned short regId;
unsigned char areaId;
unsigned char remap : 1;
unsigned char border : 1;
unsigned char visited : 1;
};
static void addUniqueLast(dtIntArray& a, unsigned short v)
{
if (!a.contains(v))
{
a.push(v);
}
}
static bool canMerge(unsigned short oldRegId, unsigned short newRegId, const dtLayerMonotoneRegion* regs, const int nregs)
{
int count = 0;
for (int i = 0; i < nregs; ++i)
{
const dtLayerMonotoneRegion& reg = regs[i];
if (reg.regId != oldRegId) continue;
const int nnei = reg.neis.size();
for (int j = 0; j < nnei; ++j)
{
if (regs[reg.neis[j]].regId == newRegId)
count++;
}
}
return count == 1;
}
static dtStatus CollectRegionsMonotone(dtTileCacheAlloc* alloc, dtTileCacheLayer& layer, dtLayerMonotoneRegion*& regs, int& nregs)
{
dtAssert(alloc);
const int w = (int)layer.header->width;
const int h = (int)layer.header->height;
memset(layer.regs,0xff,sizeof(unsigned short)*w*h);
const int nsweeps = w;
dtFixedArray<dtLayerSweepSpan> sweeps(alloc, nsweeps);
if (!sweeps)
return DT_FAILURE | DT_OUT_OF_MEMORY;
memset(sweeps,0,sizeof(dtLayerSweepSpan)*nsweeps);
// Partition walkable area into monotone regions.
dtIntArray prevCount(256);
unsigned short regId = 0;
for (int y = 0; y < h; ++y)
{
prevCount.resize(regId+1);
memset(&prevCount[0],0,sizeof(int)*regId);
unsigned short sweepId = 0;
for (int x = 0; x < w; ++x)
{
const int idx = x + y*w;
if (layer.areas[idx] == DT_TILECACHE_NULL_AREA) continue;
unsigned short sid = 0xffff;
// -x
if (x > 0 && isConnected(layer, idx, 0))
{
const int xidx = (x-1)+y*w;
if (layer.regs[xidx] != 0xffff && layer.areas[xidx] == layer.areas[idx])
sid = layer.regs[xidx];
}
if (sid == 0xffff)
{
sid = sweepId++;
sweeps[sid].nei = 0xffff;
sweeps[sid].ns = 0;
}
// -y
if (y > 0 && isConnected(layer, idx, 3))
{
const int yidx = x+(y-1)*w;
const unsigned short nr = layer.regs[yidx];
if (nr != 0xffff && layer.areas[yidx] == layer.areas[idx])
{
// Set neighbour when first valid neighbour is encoutered.
if (sweeps[sid].ns == 0)
sweeps[sid].nei = nr;
if (sweeps[sid].nei == nr)
{
// Update existing neighbour
sweeps[sid].ns++;
prevCount[nr]++;
}
else
{
// This is hit if there is nore than one neighbour.
// Invalidate the neighbour.
sweeps[sid].nei = 0xffff;
}
}
}
layer.regs[idx] = sid;
}
// Create unique ID.
for (int i = 0; i < sweepId; ++i)
{
// If the neighbour is set and there is only one continuous connection to it,
// the sweep will be merged with the previous one, else new region is created.
if (sweeps[i].nei != 0xffff && prevCount[sweeps[i].nei] == sweeps[i].ns)
{
sweeps[i].id = sweeps[i].nei;
}
else
{
sweeps[i].id = regId++;
}
}
// Remap local sweep ids to region ids.
for (int x = 0; x < w; ++x)
{
const int idx = x+y*w;
if (layer.regs[idx] != 0xffff)
{
layer.regs[idx] = sweeps[layer.regs[idx]].id;
}
}
}
// Allocate and init layer regions.
nregs = (int)regId;
// @UE BEGIN: special handling of "no regions"
if (nregs == 0)
{
regs = 0;
// treating this as success because we successfully generated 0 regions,
// no issues occurred, everything was good. Just no regions.
return DT_SUCCESS;
}
// @UE END
regs = (dtLayerMonotoneRegion*)alloc->alloc(sizeof(dtLayerMonotoneRegion) * nregs);
if (!regs)
return DT_FAILURE | DT_OUT_OF_MEMORY;
memset((void*)regs, 0, sizeof(dtLayerMonotoneRegion)*nregs);
for (int i = 0; i < nregs; ++i)
{
regs[i].regId = 0xffff;
regs[i].neis.resize(16);
regs[i].neis.resize(0);
}
// Find region neighbours.
for (int y = 0; y < h; ++y)
{
const bool borderY = (y == 0) || (y == (h - 1));
for (int x = 0; x < w; ++x)
{
const int idx = x+y*w;
const unsigned short ri = layer.regs[idx];
if (ri == 0xffff)
continue;
// Update area.
regs[ri].area++;
regs[ri].areaId = layer.areas[idx];
regs[ri].border |= borderY || (x == 0) || (x == (w - 1));
// Update neighbours
if (y > 0 && isConnected(layer, idx, 3))
{
const int ymi = x+(y-1)*w;
const unsigned short rai = layer.regs[ymi];
if (rai != 0xffff && rai != ri)
{
addUniqueLast(regs[ri].neis, rai);
addUniqueLast(regs[rai].neis, ri);
}
}
}
}
return DT_SUCCESS;
}
static dtStatus CollectRegionsChunky(dtTileCacheAlloc* alloc, dtTileCacheLayer& layer, int chunkSize, dtLayerMonotoneRegion*& regs, int& nregs)
{
dtAssert(alloc);
const int w = (int)layer.header->width;
const int h = (int)layer.header->height;
memset(layer.regs,0xff,sizeof(unsigned short)*w*h);
const int nsweeps = w;
dtFixedArray<dtLayerSweepSpan> sweeps(alloc, nsweeps);
if (!sweeps)
return DT_FAILURE | DT_OUT_OF_MEMORY;
memset(sweeps,0,sizeof(dtLayerSweepSpan)*nsweeps);
// Partition walkable area into monotone regions.
dtIntArray prevCount(256);
unsigned short regId = 0;
const int numXChunks = dtMax(1, w / chunkSize); // Division floors, remainder is put to the last chunk.
const int numYChunks = dtMax(1, h / chunkSize);
for (int chunkx = 0; chunkx < numXChunks; chunkx++)
{
const bool lastXChunk = chunkx == (numXChunks - 1);
const int minx = chunkx * chunkSize;
const int maxx = lastXChunk ? w : (minx + chunkSize);
for (int chunky = 0; chunky < numYChunks; chunky++)
{
const bool lastYChunk = chunky == (numYChunks - 1);
const int miny = chunky * chunkSize;
const int maxy = lastYChunk ? h : (miny + chunkSize);
for (int y = miny; y < maxy; ++y)
{
prevCount.resize(regId+1);
memset(&prevCount[0],0,sizeof(int)*regId);
unsigned short sweepId = 0;
for (int x = minx; x < maxx; ++x)
{
const int idx = x + y*w;
if (layer.areas[idx] == DT_TILECACHE_NULL_AREA)
{
continue;
}
unsigned short sid = 0xffff;
// -x
if (x > minx && isConnected(layer, idx, 0))
{
const int xidx = (x-1)+y*w;
if (layer.regs[xidx] != 0xffff && layer.areas[xidx] == layer.areas[idx])
sid = layer.regs[xidx];
}
if (sid == 0xffff)
{
sid = sweepId++;
sweeps[sid].nei = 0xffff;
sweeps[sid].ns = 0;
}
// -y
if (y > miny && isConnected(layer, idx, 3))
{
const int yidx = x+(y-1)*w;
const unsigned short nr = layer.regs[yidx];
if (nr != 0xffff && layer.areas[yidx] == layer.areas[idx])
{
// Set neighbour when first valid neighbour is encoutered.
if (sweeps[sid].ns == 0)
sweeps[sid].nei = nr;
if (sweeps[sid].nei == nr)
{
// Update existing neighbour
sweeps[sid].ns++;
prevCount[nr]++;
}
else
{
// This is hit if there is nore than one neighbour.
// Invalidate the neighbour.
sweeps[sid].nei = 0xffff;
}
}
}
layer.regs[idx] = sid;
}
// Create unique ID.
for (int i = 0; i < sweepId; ++i)
{
// If the neighbour is set and there is only one continuous connection to it,
// the sweep will be merged with the previous one, else new region is created.
if (sweeps[i].nei != 0xffff && prevCount[sweeps[i].nei] == sweeps[i].ns)
{
sweeps[i].id = sweeps[i].nei;
}
else
{
sweeps[i].id = regId++;
}
}
// Remap local sweep ids to region ids.
for (int x = minx; x < maxx; ++x)
{
const int idx = x+y*w;
if (layer.regs[idx] != 0xffff)
{
unsigned short id = sweeps[layer.regs[idx]].id;
layer.regs[idx] = id;
}
}
}
}
}
// Allocate and init layer regions.
nregs = (int)regId;
// @UE BEGIN: special handling of "no regions"
if (nregs == 0)
{
regs = 0;
// treating this as success because we successfully generated 0 regions,
// no issues occurred, everything was good. Just no regions.
return DT_SUCCESS;
}
// @UE END
regs = (dtLayerMonotoneRegion*)alloc->alloc(sizeof(dtLayerMonotoneRegion) * nregs);
if (!regs)
return DT_FAILURE | DT_OUT_OF_MEMORY;
memset((void*)regs, 0, sizeof(dtLayerMonotoneRegion)*nregs);
for (int i = 0; i < nregs; ++i)
{
regs[i].regId = 0xffff;
regs[i].neis.resize(16);
regs[i].neis.resize(0);
}
// Find region neighbours.
for (int y = 0; y < h; ++y)
{
const int ciy = dtMin(y / chunkSize , numYChunks-1);
const bool borderY = (y == 0) || (y == (h - 1));
for (int x = 0; x < w; ++x)
{
const int idx = x+y*w;
const unsigned short ri = layer.regs[idx];
if (ri == 0xffff)
continue;
// Update area.
regs[ri].area++;
regs[ri].areaId = layer.areas[idx];
const int cix = dtMin(x / chunkSize , numXChunks-1);
regs[ri].chunkId = cix + (ciy * numXChunks);
regs[ri].border |= borderY || (x == 0) || (x == (w - 1));
// Update neighbours
if (y > 0 && isConnected(layer, idx, 3))
{
const int ymi = x+(y-1)*w;
const unsigned short rai = layer.regs[ymi];
if (rai != 0xffff && rai != ri)
{
addUniqueLast(regs[ri].neis, rai);
addUniqueLast(regs[rai].neis, ri);
}
}
}
}
return DT_SUCCESS;
}
static void MergeAndCompressRegions(dtTileCacheAlloc* alloc, dtTileCacheLayer& layer, dtLayerMonotoneRegion* regs, int nregs, const int minRegionArea, const int mergeRegionArea)
{
for (int i = 0; i < nregs; ++i)
regs[i].regId = (unsigned short)(i + 1);
// Remove too small regions.
if (minRegionArea > 0)
{
dtIntArray stack(32);
dtIntArray trace(32);
for (int i = 0; i < nregs; ++i)
{
dtLayerMonotoneRegion& reg = regs[i];
if (reg.visited || reg.area == 0)
continue;
// Count the total size of all the connected regions.
// Also keep track of the regions connects to a tile border.
bool connectsToBorder = false;
int cellCount = 0;
stack.resize(0);
trace.resize(0);
reg.visited = true;
stack.push(i);
while (stack.size())
{
// Pop
int ri = stack.pop();
dtLayerMonotoneRegion& creg = regs[ri];
connectsToBorder |= creg.border;
cellCount += creg.area;
trace.push(ri);
for (int j = 0; j < creg.neis.size(); ++j)
{
dtLayerMonotoneRegion& neireg = regs[creg.neis[j]];
if (neireg.visited)
continue;
if (neireg.regId == 0)
continue;
// Visit
stack.push(neireg.regId - 1);
neireg.visited = true;
}
}
// If the accumulated regions size is too small, remove it.
// Do not remove areas which connect to tile borders
// as their size cannot be estimated correctly and removing them
// can potentially remove necessary areas.
if (cellCount < minRegionArea && !connectsToBorder)
{
// Kill all visited regions.
for (int j = 0; j < trace.size(); ++j)
{
regs[trace[j]].area = 0;
regs[trace[j]].regId = 0;
}
}
}
}
for (int i = 0; i < nregs; ++i)
{
dtLayerMonotoneRegion& reg = regs[i];
if (reg.regId == 0)
continue;
// don't use mergeRegionArea, it doesn't work well with monotone partitioning
// (results in even more long thin polys)
int merge = -1;
int mergea = 0;
for (int j = 0; j < reg.neis.size(); ++j)
{
const unsigned short nei = (unsigned short)reg.neis[j];
dtLayerMonotoneRegion& regn = regs[nei];
if (reg.regId == regn.regId)
continue;
if (reg.areaId != regn.areaId || reg.chunkId != regn.chunkId)
continue;
if (regn.area > mergea)
{
if (canMerge(reg.regId, regn.regId, regs, nregs))
{
mergea = regn.area;
merge = (int)nei;
}
}
}
if (merge != -1)
{
const unsigned short oldId = reg.regId;
const unsigned short newId = regs[merge].regId;
for (int j = 0; j < nregs; ++j)
if (regs[j].regId == oldId)
regs[j].regId = newId;
}
}
unsigned short regId = 0;
if (nregs < 256)
{
// Compact ids.
unsigned short remap[256];
memset(remap, 0, sizeof(unsigned short)*256);
// Find number of unique regions.
for (int i = 0; i < nregs; ++i)
remap[regs[i].regId] = 1;
// skip region id 0, it's used for skipping minRegionArea
remap[0] = 0;
for (int i = 1; i < 256; ++i)
if (remap[i])
remap[i] = ++regId;
// Remap ids.
for (int i = 0; i < nregs; ++i)
regs[i].regId = remap[regs[i].regId];
}
else
{
for (int i = 0; i < nregs; ++i)
regs[i].remap = true;
for (int i = 0; i < nregs; ++i)
{
// skip region id 0, it's used for skipping minRegionArea
if (!regs[i].remap || regs[i].regId == 0)
continue;
unsigned short oldId = regs[i].regId;
unsigned short newId = ++regId;
for (int j = i; j < nregs; ++j)
{
if (regs[j].regId == oldId)
{
regs[j].regId = newId;
regs[j].remap = false;
}
}
}
}
layer.regCount = regId;
const int maxi = (int)layer.header->width * (int)layer.header->height;
for (int i = 0; i < maxi; ++i)
{
if (layer.regs[i] != 0xffff)
layer.regs[i] = regs[layer.regs[i]].regId;
}
}
static void FreeRegions(dtTileCacheAlloc* alloc, dtLayerMonotoneRegion* regs, int nregs)
{
// destroy all elements to free internal rcIntArray allocations
for (int i = 0; i < nregs; i++)
{
regs[i].~dtLayerMonotoneRegion();
}
alloc->free(regs);
}
dtStatus dtBuildTileCacheRegionsMonotone(dtTileCacheAlloc* alloc, const int minRegionArea, const int mergeRegionArea, dtTileCacheLayer& layer)
{
dtLayerMonotoneRegion* regs = NULL;
int nregs = 0;
dtStatus status = CollectRegionsMonotone(alloc, layer, regs, nregs);
// having no regions and status being successful is a valid state
// we can avoid calling MergeAndCompressRegions if that happens
if (dtStatusSucceed(status) && nregs > 0)
{
MergeAndCompressRegions(alloc, layer, regs, nregs, minRegionArea, mergeRegionArea);
}
FreeRegions(alloc, regs, nregs);
return status;
}
dtStatus dtBuildTileCacheRegionsChunky(dtTileCacheAlloc* alloc, const int minRegionArea, const int mergeRegionArea, dtTileCacheLayer& layer, int regionChunkSize)
{
dtLayerMonotoneRegion* regs = NULL;
int nregs = 0;
dtStatus status = CollectRegionsChunky(alloc, layer, regionChunkSize, regs, nregs);
// having no regions and status being successful is a valid state
// we can avoid calling MergeAndCompressRegions if that happens
if (dtStatusSucceed(status) && nregs > 0)
{
MergeAndCompressRegions(alloc, layer, regs, nregs, minRegionArea, mergeRegionArea);
}
FreeRegions(alloc, regs, nregs);
return status;
}