Files
UnrealEngineUWP/Engine/Source/Runtime/NavigationSystem/Private/NavMesh/RecastNavMeshGenerator.cpp

5169 lines
168 KiB
C++

// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
#include "NavMesh/RecastNavMeshGenerator.h"
#include "AI/Navigation/NavRelevantInterface.h"
#include "Components/PrimitiveComponent.h"
#include "HAL/FileManager.h"
#include "Misc/Paths.h"
#include "Serialization/MemoryWriter.h"
#include "EngineGlobals.h"
#include "GameFramework/PlayerController.h"
#include "Engine/Engine.h"
#include "NavigationSystem.h"
#include "FramePro/FrameProProfiler.h"
#if WITH_RECAST
#if WITH_PHYSX
#include "PhysXPublic.h"
#endif
#include "NavMesh/PImplRecastNavMesh.h"
// recast includes
#include "Detour/DetourNavMeshBuilder.h"
#include "DetourTileCache/DetourTileCacheBuilder.h"
#include "NavMesh/RecastHelpers.h"
#include "NavAreas/NavArea_LowHeight.h"
#include "AI/NavigationSystemHelpers.h"
#include "VisualLogger/VisualLoggerTypes.h"
#include "PhysicsEngine/ConvexElem.h"
#include "PhysicsEngine/BodySetup.h"
#ifndef OUTPUT_NAV_TILE_LAYER_COMPRESSION_DATA
#define OUTPUT_NAV_TILE_LAYER_COMPRESSION_DATA 0
#endif
#ifndef FAVOR_NAV_COMPRESSION_SPEED
#define FAVOR_NAV_COMPRESSION_SPEED 1
#endif
#define SEAMLESS_REBUILDING_ENABLED 1
#define GENERATE_SEGMENT_LINKS 1
#define GENERATE_CLUSTER_LINKS 1
#define SHOW_NAV_EXPORT_PREVIEW 0
#define TEXT_WEAKOBJ_NAME(obj) (obj.IsValid(false) ? *obj->GetName() : (obj.IsValid(false, true)) ? TEXT("MT-Unreachable") : TEXT("INVALID"))
struct dtTileCacheAlloc;
FORCEINLINE bool DoesBoxContainOrOverlapVector(const FBox& BigBox, const FVector& In)
{
return (In.X >= BigBox.Min.X) && (In.X <= BigBox.Max.X)
&& (In.Y >= BigBox.Min.Y) && (In.Y <= BigBox.Max.Y)
&& (In.Z >= BigBox.Min.Z) && (In.Z <= BigBox.Max.Z);
}
/** main difference between this and FBox::ContainsBox is that this returns true also when edges overlap */
FORCEINLINE bool DoesBoxContainBox(const FBox& BigBox, const FBox& SmallBox)
{
return DoesBoxContainOrOverlapVector(BigBox, SmallBox.Min) && DoesBoxContainOrOverlapVector(BigBox, SmallBox.Max);
}
int32 GetTilesCountHelper(const dtNavMesh* DetourMesh)
{
int32 NumTiles = 0;
if (DetourMesh)
{
for (int32 i = 0; i < DetourMesh->getMaxTiles(); i++)
{
const dtMeshTile* TileData = DetourMesh->getTile(i);
if (TileData && TileData->header && TileData->dataSize > 0)
{
NumTiles++;
}
}
}
return NumTiles;
}
/**
* Exports geometry to OBJ file. Can be used to verify NavMesh generation in RecastDemo app
* @param FileName - full name of OBJ file with extension
* @param GeomVerts - list of vertices
* @param GeomFaces - list of triangles (3 vert indices for each)
*/
static void ExportGeomToOBJFile(const FString& InFileName, const TNavStatArray<float>& GeomCoords, const TNavStatArray<int32>& GeomFaces, const FString& AdditionalData)
{
#define USE_COMPRESSION 0
#if ALLOW_DEBUG_FILES
SCOPE_CYCLE_COUNTER(STAT_Navigation_TileGeometryExportToObjAsync);
FString FileName = InFileName;
#if USE_COMPRESSION
FileName += TEXT("z");
struct FDataChunk
{
TArray<uint8> UncompressedBuffer;
TArray<uint8> CompressedBuffer;
void CompressBuffer()
{
const int32 HeaderSize = sizeof(int32);
const int32 UncompressedSize = UncompressedBuffer.Num();
CompressedBuffer.Init(0, HeaderSize + FMath::Trunc(1.1f * UncompressedSize));
int32 CompressedSize = CompressedBuffer.Num() - HeaderSize;
uint8* DestBuffer = CompressedBuffer.GetData();
FMemory::Memcpy(DestBuffer, &UncompressedSize, HeaderSize);
DestBuffer += HeaderSize;
FCompression::CompressMemory(NAME_Zlib, (void*)DestBuffer, CompressedSize, (void*)UncompressedBuffer.GetData(), UncompressedSize, COMPRESS_BiasMemory);
CompressedBuffer.SetNum(CompressedSize + HeaderSize, false);
}
};
FDataChunk AllDataChunks[3];
const int32 NumberOfChunks = sizeof(AllDataChunks) / sizeof(FDataChunk);
{
FMemoryWriter ArWriter(AllDataChunks[0].UncompressedBuffer);
for (int32 i = 0; i < GeomCoords.Num(); i += 3)
{
FVector Vertex(GeomCoords[i + 0], GeomCoords[i + 1], GeomCoords[i + 2]);
ArWriter << Vertex;
}
}
{
FMemoryWriter ArWriter(AllDataChunks[1].UncompressedBuffer);
for (int32 i = 0; i < GeomFaces.Num(); i += 3)
{
FVector Face(GeomFaces[i + 0] + 1, GeomFaces[i + 1] + 1, GeomFaces[i + 2] + 1);
ArWriter << Face;
}
}
{
auto AnsiAdditionalData = StringCast<ANSICHAR>(*AdditionalData);
FMemoryWriter ArWriter(AllDataChunks[2].UncompressedBuffer);
ArWriter.Serialize((ANSICHAR*)AnsiAdditionalData.Get(), AnsiAdditionalData.Length());
}
FArchive* FileAr = IFileManager::Get().CreateDebugFileWriter(*FileName);
if (FileAr != NULL)
{
for (int32 Index = 0; Index < NumberOfChunks; ++Index)
{
AllDataChunks[Index].CompressBuffer();
int32 BufferSize = AllDataChunks[Index].CompressedBuffer.Num();
FileAr->Serialize(&BufferSize, sizeof(int32));
FileAr->Serialize((void*)AllDataChunks[Index].CompressedBuffer.GetData(), AllDataChunks[Index].CompressedBuffer.Num());
}
UE_LOG(LogNavigation, Error, TEXT("UncompressedBuffer size:: %d "), AllDataChunks[0].UncompressedBuffer.Num() + AllDataChunks[1].UncompressedBuffer.Num() + AllDataChunks[2].UncompressedBuffer.Num());
FileAr->Close();
}
#else
FArchive* FileAr = IFileManager::Get().CreateDebugFileWriter(*FileName);
if (FileAr != NULL)
{
for (int32 Index = 0; Index < GeomCoords.Num(); Index += 3)
{
FString LineToSave = FString::Printf(TEXT("v %f %f %f\n"), GeomCoords[Index + 0], GeomCoords[Index + 1], GeomCoords[Index + 2]);
auto AnsiLineToSave = StringCast<ANSICHAR>(*LineToSave);
FileAr->Serialize((ANSICHAR*)AnsiLineToSave.Get(), AnsiLineToSave.Length());
}
for (int32 Index = 0; Index < GeomFaces.Num(); Index += 3)
{
FString LineToSave = FString::Printf(TEXT("f %d %d %d\n"), GeomFaces[Index + 0] + 1, GeomFaces[Index + 1] + 1, GeomFaces[Index + 2] + 1);
auto AnsiLineToSave = StringCast<ANSICHAR>(*LineToSave);
FileAr->Serialize((ANSICHAR*)AnsiLineToSave.Get(), AnsiLineToSave.Length());
}
auto AnsiAdditionalData = StringCast<ANSICHAR>(*AdditionalData);
FileAr->Serialize((ANSICHAR*)AnsiAdditionalData.Get(), AnsiAdditionalData.Length());
FileAr->Close();
}
#endif
#undef USE_COMPRESSION
#endif
}
//----------------------------------------------------------------------//
//
//
struct FRecastGeometryExport : public FNavigableGeometryExport
{
FRecastGeometryExport(FNavigationRelevantData& InData) : Data(&InData)
{
Data->Bounds = FBox(ForceInit);
}
FNavigationRelevantData* Data;
TNavStatArray<float> VertexBuffer;
TNavStatArray<int32> IndexBuffer;
FWalkableSlopeOverride SlopeOverride;
#if WITH_PHYSX
virtual void ExportPxTriMesh16Bit(physx::PxTriangleMesh const * const TriMesh, const FTransform& LocalToWorld) override;
virtual void ExportPxTriMesh32Bit(physx::PxTriangleMesh const * const TriMesh, const FTransform& LocalToWorld) override;
virtual void ExportPxConvexMesh(physx::PxConvexMesh const * const ConvexMesh, const FTransform& LocalToWorld) override;
virtual void ExportPxHeightField(physx::PxHeightField const * const HeightField, const FTransform& LocalToWorld) override;
#endif // WITH_PHYSX
virtual void ExportHeightFieldSlice(const FNavHeightfieldSamples& PrefetchedHeightfieldSamples, const int32 NumRows, const int32 NumCols, const FTransform& LocalToWorld, const FBox& SliceBox) override;
virtual void ExportCustomMesh(const FVector* InVertices, int32 NumVerts, const int32* InIndices, int32 NumIndices, const FTransform& LocalToWorld) override;
virtual void ExportRigidBodySetup(UBodySetup& BodySetup, const FTransform& LocalToWorld) override;
virtual void AddNavModifiers(const FCompositeNavModifier& Modifiers) override;
virtual void SetNavDataPerInstanceTransformDelegate(const FNavDataPerInstanceTransformDelegate& InDelegate) override;
};
FRecastVoxelCache::FRecastVoxelCache(const uint8* Memory)
{
uint8* BytesArr = (uint8*)Memory;
if (Memory)
{
NumTiles = *((int32*)BytesArr); BytesArr += sizeof(int32);
Tiles = (FTileInfo*)BytesArr;
}
else
{
NumTiles = 0;
}
FTileInfo* iTile = Tiles;
for (int i = 0; i < NumTiles; i++)
{
iTile = (FTileInfo*)BytesArr; BytesArr += sizeof(FTileInfo);
if (iTile->NumSpans)
{
iTile->SpanData = (rcSpanCache*)BytesArr; BytesArr += sizeof(rcSpanCache) * iTile->NumSpans;
}
else
{
iTile->SpanData = 0;
}
iTile->NextTile = (FTileInfo*)BytesArr;
}
if (NumTiles > 0)
{
iTile->NextTile = 0;
}
else
{
Tiles = 0;
}
}
FRecastGeometryCache::FRecastGeometryCache(const uint8* Memory)
{
Header = *((FHeader*)Memory);
Verts = (float*)(Memory + sizeof(FRecastGeometryCache));
Indices = (int32*)(Memory + sizeof(FRecastGeometryCache) + (sizeof(float) * Header.NumVerts * 3));
}
namespace RecastGeometryExport {
static UWorld* FindEditorWorld()
{
if (GEngine)
{
for (const FWorldContext& Context : GEngine->GetWorldContexts())
{
if (Context.WorldType == EWorldType::Editor)
{
return Context.World();
}
}
}
return NULL;
}
static void StoreCollisionCache(FRecastGeometryExport& GeomExport)
{
const int32 NumFaces = GeomExport.IndexBuffer.Num() / 3;
const int32 NumVerts = GeomExport.VertexBuffer.Num() / 3;
if (NumFaces == 0 || NumVerts == 0)
{
GeomExport.Data->CollisionData.Empty();
return;
}
FRecastGeometryCache::FHeader HeaderInfo;
HeaderInfo.NumFaces = NumFaces;
HeaderInfo.NumVerts = NumVerts;
HeaderInfo.SlopeOverride = GeomExport.SlopeOverride;
// allocate memory
const int32 HeaderSize = sizeof(FRecastGeometryCache);
const int32 CoordsSize = sizeof(float) * 3 * NumVerts;
const int32 IndicesSize = sizeof(int32) * 3 * NumFaces;
const int32 CacheSize = HeaderSize + CoordsSize + IndicesSize;
HeaderInfo.Validation.DataSize = CacheSize;
// empty + add combo to allocate exact amount (without any overhead/slack)
GeomExport.Data->CollisionData.Empty(CacheSize);
GeomExport.Data->CollisionData.AddUninitialized(CacheSize);
// store collisions
uint8* RawMemory = GeomExport.Data->CollisionData.GetData();
FRecastGeometryCache* CacheMemory = (FRecastGeometryCache*)RawMemory;
CacheMemory->Header = HeaderInfo;
CacheMemory->Verts = 0;
CacheMemory->Indices = 0;
FMemory::Memcpy(RawMemory + HeaderSize, GeomExport.VertexBuffer.GetData(), CoordsSize);
FMemory::Memcpy(RawMemory + HeaderSize + CoordsSize, GeomExport.IndexBuffer.GetData(), IndicesSize);
}
#if WITH_PHYSX
/** exports PxConvexMesh as trimesh */
void ExportPxConvexMesh(PxConvexMesh const * const ConvexMesh, const FTransform& LocalToWorld,
TNavStatArray<float>& VertexBuffer, TNavStatArray<int32>& IndexBuffer,
FBox& UnrealBounds)
{
// after FKConvexElem::AddCachedSolidConvexGeom
if(ConvexMesh == NULL)
{
return;
}
int32 StartVertOffset = VertexBuffer.Num() / 3;
const bool bNegX = LocalToWorld.GetDeterminant() < 0;
// get PhysX data
const PxVec3* PVertices = ConvexMesh->getVertices();
const PxU8* PIndexBuffer = ConvexMesh->getIndexBuffer();
const PxU32 NbPolygons = ConvexMesh->getNbPolygons();
#if SHOW_NAV_EXPORT_PREVIEW
UWorld* DebugWorld = FindEditorWorld();
#endif // SHOW_NAV_EXPORT_PREVIEW
for(PxU32 i = 0; i < NbPolygons; ++i)
{
PxHullPolygon Data;
bool bStatus = ConvexMesh->getPolygonData(i, Data);
check(bStatus);
const PxU8* indices = PIndexBuffer + Data.mIndexBase;
// add vertices
for(PxU32 j = 0; j < Data.mNbVerts; ++j)
{
const int32 VertIndex = indices[j];
const FVector UnrealCoords = LocalToWorld.TransformPosition( P2UVector(PVertices[VertIndex]) );
UnrealBounds += UnrealCoords;
VertexBuffer.Add(UnrealCoords.X);
VertexBuffer.Add(UnrealCoords.Y);
VertexBuffer.Add(UnrealCoords.Z);
}
// Add indices
const PxU32 nbTris = Data.mNbVerts - 2;
for(PxU32 j = 0; j < nbTris; ++j)
{
IndexBuffer.Add(StartVertOffset + 0 );
IndexBuffer.Add(StartVertOffset + j + 2);
IndexBuffer.Add(StartVertOffset + j + 1);
#if SHOW_NAV_EXPORT_PREVIEW
if (DebugWorld)
{
FVector V0(VertexBuffer[(StartVertOffset + 0) * 3+0], VertexBuffer[(StartVertOffset + 0) * 3+1], VertexBuffer[(StartVertOffset + 0) * 3+2]);
FVector V1(VertexBuffer[(StartVertOffset + j + 2) * 3+0], VertexBuffer[(StartVertOffset + j + 2) * 3+1], VertexBuffer[(StartVertOffset + j + 2) * 3+2]);
FVector V2(VertexBuffer[(StartVertOffset + j + 1) * 3+0], VertexBuffer[(StartVertOffset + j + 1) * 3+1], VertexBuffer[(StartVertOffset + j + 1) * 3+2]);
DrawDebugLine(DebugWorld, V0, V1, bNegX ? FColor::Red : FColor::Blue, true);
DrawDebugLine(DebugWorld, V1, V2, bNegX ? FColor::Red : FColor::Blue, true);
DrawDebugLine(DebugWorld, V2, V0, bNegX ? FColor::Red : FColor::Blue, true);
}
#endif // SHOW_NAV_EXPORT_PREVIEW
}
StartVertOffset += Data.mNbVerts;
}
}
template<typename TIndicesType>
FORCEINLINE_DEBUGGABLE void ExportPxTriMesh(PxTriangleMesh const * const TriMesh, const FTransform& LocalToWorld,
TNavStatArray<float>& VertexBuffer, TNavStatArray<int32>& IndexBuffer,
FBox& UnrealBounds)
{
if (TriMesh == NULL)
{
return;
}
int32 VertOffset = VertexBuffer.Num() / 3;
const PxVec3* PVerts = TriMesh->getVertices();
const PxU32 NumTris = TriMesh->getNbTriangles();
const TIndicesType* Indices = (TIndicesType*)TriMesh->getTriangles();;
VertexBuffer.Reserve(VertexBuffer.Num() + NumTris*3);
IndexBuffer.Reserve(IndexBuffer.Num() + NumTris*3);
const bool bFlipCullMode = (LocalToWorld.GetDeterminant() < 0.f);
const int32 IndexOrder[3] = { bFlipCullMode ? 0 : 2, 1, bFlipCullMode ? 2 : 0 };
#if SHOW_NAV_EXPORT_PREVIEW
UWorld* DebugWorld = FindEditorWorld();
#endif // SHOW_NAV_EXPORT_PREVIEW
for(PxU32 TriIdx = 0; TriIdx < NumTris; ++TriIdx)
{
for (int32 i = 0; i < 3; i++)
{
const FVector UnrealCoords = LocalToWorld.TransformPosition(P2UVector(PVerts[Indices[i]]));
UnrealBounds += UnrealCoords;
VertexBuffer.Add(UnrealCoords.X);
VertexBuffer.Add(UnrealCoords.Y);
VertexBuffer.Add(UnrealCoords.Z);
}
Indices += 3;
IndexBuffer.Add(VertOffset + IndexOrder[0]);
IndexBuffer.Add(VertOffset + IndexOrder[1]);
IndexBuffer.Add(VertOffset + IndexOrder[2]);
#if SHOW_NAV_EXPORT_PREVIEW
if (DebugWorld)
{
FVector V0(VertexBuffer[(VertOffset + IndexOrder[0]) * 3+0], VertexBuffer[(VertOffset + IndexOrder[0]) * 3+1], VertexBuffer[(VertOffset + IndexOrder[0]) * 3+2]);
FVector V1(VertexBuffer[(VertOffset + IndexOrder[1]) * 3+0], VertexBuffer[(VertOffset + IndexOrder[1]) * 3+1], VertexBuffer[(VertOffset + IndexOrder[1]) * 3+2]);
FVector V2(VertexBuffer[(VertOffset + IndexOrder[2]) * 3+0], VertexBuffer[(VertOffset + IndexOrder[2]) * 3+1], VertexBuffer[(VertOffset + IndexOrder[2]) * 3+2]);
DrawDebugLine(DebugWorld, V0, V1, bFlipCullMode ? FColor::Red : FColor::Blue, true);
DrawDebugLine(DebugWorld, V1, V2, bFlipCullMode ? FColor::Red : FColor::Blue, true);
DrawDebugLine(DebugWorld, V2, V0, bFlipCullMode ? FColor::Red : FColor::Blue, true);
}
#endif // SHOW_NAV_EXPORT_PREVIEW
VertOffset += 3;
}
}
void ExportPxHeightField(PxHeightField const * const HeightField, const FTransform& LocalToWorld
, TNavStatArray<float>& VertexBuffer, TNavStatArray<int32>& IndexBuffer
, FBox& UnrealBounds)
{
if (HeightField == NULL)
{
return;
}
QUICK_SCOPE_CYCLE_COUNTER(STAT_NavMesh_ExportPxHeightField);
const int32 NumRows = HeightField->getNbRows();
const int32 NumCols = HeightField->getNbColumns();
const int32 VertexCount = NumRows * NumCols;
// Unfortunately we have to use PxHeightField::saveCells instead PxHeightField::getHeight here
// because current PxHeightField interface does not provide an access to a triangle material index by HF 2D coordinates
// PxHeightField::getTriangleMaterialIndex uses some internal adressing which does not match HF 2D coordinates
TArray<PxHeightFieldSample> HFSamples;
HFSamples.SetNumUninitialized(VertexCount);
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_NavMesh_ExportPxHeightField_saveCells);
HeightField->saveCells(HFSamples.GetData(), VertexCount*HFSamples.GetTypeSize());
}
//
const int32 VertOffset = VertexBuffer.Num() / 3;
const int32 NumQuads = (NumRows - 1)*(NumCols - 1);
VertexBuffer.Reserve(VertexBuffer.Num() + VertexCount * 3);
IndexBuffer.Reserve(IndexBuffer.Num() + NumQuads * 6);
const bool bMirrored = (LocalToWorld.GetDeterminant() < 0.f);
for (int32 Y = 0; Y < NumRows; Y++)
{
for (int32 X = 0; X < NumCols; X++)
{
const int32 SampleIdx = (bMirrored ? X : (NumCols - X - 1))*NumCols + Y;
const PxHeightFieldSample& Sample = HFSamples[SampleIdx];
const FVector UnrealCoords = LocalToWorld.TransformPosition(FVector(X, Y, Sample.height));
UnrealBounds += UnrealCoords;
VertexBuffer.Add(UnrealCoords.X);
VertexBuffer.Add(UnrealCoords.Y);
VertexBuffer.Add(UnrealCoords.Z);
}
}
for (int32 Y = 0; Y < NumRows - 1; Y++)
{
for (int32 X = 0; X < NumCols - 1; X++)
{
const int32 SampleIdx = (bMirrored ? X : (NumCols - X - 1 - 1))*NumCols + Y;
const PxHeightFieldSample& Sample = HFSamples[SampleIdx];
const bool bIsHole = (Sample.materialIndex0 == PxHeightFieldMaterial::eHOLE);
if (bIsHole)
{
continue;
}
const int32 I00 = X + 0 + (Y + 0)*NumCols;
int32 I01 = X + 0 + (Y + 1)*NumCols;
int32 I10 = X + 1 + (Y + 0)*NumCols;
const int32 I11 = X + 1 + (Y + 1)*NumCols;
if (bMirrored)
{
Swap(I01, I10);
}
IndexBuffer.Add(VertOffset + I00);
IndexBuffer.Add(VertOffset + I11);
IndexBuffer.Add(VertOffset + I10);
IndexBuffer.Add(VertOffset + I00);
IndexBuffer.Add(VertOffset + I01);
IndexBuffer.Add(VertOffset + I11);
}
}
}
#endif // WITH_PHYSX
void ExportHeightFieldSlice(const FNavHeightfieldSamples& PrefetchedHeightfieldSamples, const int32 NumRows, const int32 NumCols, const FTransform& LocalToWorld
, TNavStatArray<float>& VertexBuffer, TNavStatArray<int32>& IndexBuffer, const FBox& SliceBox
, FBox& UnrealBounds)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_NavMesh_ExportHeightFieldSlice);
#if WITH_PHYSX
static const uint32 SizeOfPx = sizeof(physx::PxI16);
static const uint32 SizeOfHeight = PrefetchedHeightfieldSamples.Heights.GetTypeSize();
ensure(SizeOfPx == SizeOfHeight);
#endif // WITH_PHYSX
// calculate the actual start and number of columns we want
const FBox LocalBox = SliceBox.TransformBy(LocalToWorld.Inverse());
const bool bMirrored = (LocalToWorld.GetDeterminant() < 0.f);
const int32 MinX = FMath::Clamp(FMath::FloorToInt(LocalBox.Min.X) - 1, 0, NumCols);
const int32 MinY = FMath::Clamp(FMath::FloorToInt(LocalBox.Min.Y) - 1, 0, NumRows);
const int32 MaxX = FMath::Clamp(FMath::CeilToInt(LocalBox.Max.X) + 1, 0, NumCols);
const int32 MaxY = FMath::Clamp(FMath::CeilToInt(LocalBox.Max.Y) + 1, 0, NumRows);
const int32 SizeX = MaxX - MinX;
const int32 SizeY = MaxY - MinY;
if (SizeX <= 0 || SizeY <= 0)
{
// slice is outside bounds, skip
return;
}
const int32 VertOffset = VertexBuffer.Num() / 3;
const int32 NumVerts = SizeX * SizeY;
const int32 NumQuads = (SizeX - 1) * (SizeY - 1);
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastGeometryExport_AllocatingMemory);
VertexBuffer.Reserve(VertexBuffer.Num() + NumVerts * 3);
IndexBuffer.Reserve(IndexBuffer.Num() + NumQuads * 3 * 2);
}
for (int32 IdxY = 0; IdxY < SizeY; IdxY++)
{
for (int32 IdxX = 0; IdxX < SizeX; IdxX++)
{
const int32 CoordX = IdxX + MinX;
const int32 CoordY = IdxY + MinY;
const int32 SampleIdx = ((bMirrored ? CoordX : (NumCols - CoordX - 1)) * NumCols) + CoordY;
const FVector UnrealCoords = LocalToWorld.TransformPosition(FVector(CoordX, CoordY, PrefetchedHeightfieldSamples.Heights[SampleIdx]));
VertexBuffer.Add(UnrealCoords.X);
VertexBuffer.Add(UnrealCoords.Y);
VertexBuffer.Add(UnrealCoords.Z);
}
}
for (int32 IdxY = 0; IdxY < SizeY - 1; IdxY++)
{
for (int32 IdxX = 0; IdxX < SizeX - 1; IdxX++)
{
const int32 CoordX = IdxX + MinX;
const int32 CoordY = IdxY + MinY;
const int32 SampleIdx = ((bMirrored ? CoordX : (NumCols - CoordX - 1)) * NumCols) + CoordY;
const bool bIsHole = PrefetchedHeightfieldSamples.Holes[SampleIdx];
if (bIsHole)
{
continue;
}
const int32 I00 = (IdxX + 0) + (IdxY + 0)*SizeX;
int32 I01 = (IdxX + 0) + (IdxY + 1)*SizeX;
int32 I10 = (IdxX + 1) + (IdxY + 0)*SizeX;
const int32 I11 = (IdxX + 1) + (IdxY + 1)*SizeX;
if (bMirrored)
{
Swap(I01, I10);
}
IndexBuffer.Add(VertOffset + I00);
IndexBuffer.Add(VertOffset + I11);
IndexBuffer.Add(VertOffset + I10);
IndexBuffer.Add(VertOffset + I00);
IndexBuffer.Add(VertOffset + I01);
IndexBuffer.Add(VertOffset + I11);
}
}
}
void ExportCustomMesh(const FVector* InVertices, int32 NumVerts, const int32* InIndices, int32 NumIndices, const FTransform& LocalToWorld,
TNavStatArray<float>& VertexBuffer, TNavStatArray<int32>& IndexBuffer, FBox& UnrealBounds)
{
if (NumVerts <= 0 || NumIndices <= 0)
{
return;
}
int32 VertOffset = VertexBuffer.Num() / 3;
VertexBuffer.Reserve(VertexBuffer.Num() + NumVerts*3);
IndexBuffer.Reserve(IndexBuffer.Num() + NumIndices);
const bool bFlipCullMode = (LocalToWorld.GetDeterminant() < 0.f);
const int32 IndexOrder[3] = { bFlipCullMode ? 2 : 0, 1, bFlipCullMode ? 0 : 2 };
#if SHOW_NAV_EXPORT_PREVIEW
UWorld* DebugWorld = FindEditorWorld();
#endif // SHOW_NAV_EXPORT_PREVIEW
// Add vertices
for (int32 i = 0; i < NumVerts; ++i)
{
const FVector UnrealCoords = LocalToWorld.TransformPosition(InVertices[i]);
UnrealBounds += UnrealCoords;
VertexBuffer.Add(UnrealCoords.X);
VertexBuffer.Add(UnrealCoords.Y);
VertexBuffer.Add(UnrealCoords.Z);
}
// Add indices
for (int32 i = 0; i < NumIndices; i += 3)
{
IndexBuffer.Add(InIndices[i + IndexOrder[0]] + VertOffset);
IndexBuffer.Add(InIndices[i + IndexOrder[1]] + VertOffset);
IndexBuffer.Add(InIndices[i + IndexOrder[2]] + VertOffset);
#if SHOW_NAV_EXPORT_PREVIEW
if (DebugWorld)
{
FVector V0(VertexBuffer[(VertOffset + InIndices[i + IndexOrder[0]]) * 3+0], VertexBuffer[(VertOffset + InIndices[i + IndexOrder[0]]) * 3+1], VertexBuffer[(VertOffset + InIndices[i + IndexOrder[0]]) * 3+2]);
FVector V1(VertexBuffer[(VertOffset + InIndices[i + IndexOrder[1]]) * 3+0], VertexBuffer[(VertOffset + InIndices[i + IndexOrder[1]]) * 3+1], VertexBuffer[(VertOffset + InIndices[i + IndexOrder[1]]) * 3+2]);
FVector V2(VertexBuffer[(VertOffset + InIndices[i + IndexOrder[2]]) * 3+0], VertexBuffer[(VertOffset + InIndices[i + IndexOrder[2]]) * 3+1], VertexBuffer[(VertOffset + InIndices[i + IndexOrder[2]]) * 3+2]);
DrawDebugLine(DebugWorld, V0, V1, bFlipCullMode ? FColor::Red : FColor::Blue, true);
DrawDebugLine(DebugWorld, V1, V2, bFlipCullMode ? FColor::Red : FColor::Blue, true);
DrawDebugLine(DebugWorld, V2, V0, bFlipCullMode ? FColor::Red : FColor::Blue, true);
}
#endif // SHOW_NAV_EXPORT_PREVIEW
}
}
template<typename OtherAllocator>
FORCEINLINE_DEBUGGABLE void AddFacesToRecast(TArray<FVector, OtherAllocator>& InVerts, TArray<int32, OtherAllocator>& InFaces,
TNavStatArray<float>& OutVerts, TNavStatArray<int32>& OutIndices, FBox& UnrealBounds)
{
// Add indices
int32 StartVertOffset = OutVerts.Num();
if (StartVertOffset > 0)
{
const int32 FirstIndex = OutIndices.AddUninitialized(InFaces.Num());
for (int32 Idx=0; Idx < InFaces.Num(); ++Idx)
{
OutIndices[FirstIndex + Idx] = InFaces[Idx]+StartVertOffset;
}
}
else
{
OutIndices.Append(InFaces);
}
// Add vertices
for (int32 i = 0; i < InVerts.Num(); i++)
{
const FVector& RecastCoords = InVerts[i];
OutVerts.Add(RecastCoords.X);
OutVerts.Add(RecastCoords.Y);
OutVerts.Add(RecastCoords.Z);
UnrealBounds += Recast2UnrealPoint(RecastCoords);
}
}
FORCEINLINE_DEBUGGABLE void ExportRigidBodyConvexElements(UBodySetup& BodySetup, TNavStatArray<float>& VertexBuffer, TNavStatArray<int32>& IndexBuffer,
TNavStatArray<int32>& ShapeBuffer, FBox& UnrealBounds, const FTransform& LocalToWorld)
{
#if WITH_PHYSX
const int32 ConvexCount = BodySetup.AggGeom.ConvexElems.Num();
FKConvexElem const * ConvexElem = BodySetup.AggGeom.ConvexElems.GetData();
const FTransform NegXScale(FQuat::Identity, FVector::ZeroVector, FVector(-1, 1, 1));
for(int32 i=0; i< ConvexCount; ++i, ++ConvexElem)
{
// Store index of first vertex in shape buffer
ShapeBuffer.Add(VertexBuffer.Num() / 3);
// Get verts/triangles from this hull.
if (!ConvexElem->GetConvexMesh() && ConvexElem->GetMirroredConvexMesh())
{
// If there is only a NegX mesh (e.g. a mirrored volume), use it
ExportPxConvexMesh(ConvexElem->GetMirroredConvexMesh(), NegXScale * LocalToWorld, VertexBuffer, IndexBuffer, UnrealBounds);
}
else
{
// Otherwise use the regular mesh in the case that both exist
ExportPxConvexMesh(ConvexElem->GetConvexMesh(), LocalToWorld, VertexBuffer, IndexBuffer, UnrealBounds);
}
}
#endif // WITH_PHYSX
}
FORCEINLINE_DEBUGGABLE void ExportRigidBodyTriMesh(UBodySetup& BodySetup, TNavStatArray<float>& VertexBuffer, TNavStatArray<int32>& IndexBuffer,
FBox& UnrealBounds, const FTransform& LocalToWorld)
{
#if WITH_PHYSX
if (BodySetup.GetCollisionTraceFlag() == CTF_UseComplexAsSimple)
{
for(PxTriangleMesh* TriMesh : BodySetup.TriMeshes)
{
if (TriMesh->getTriangleMeshFlags() & PxTriangleMeshFlag::e16_BIT_INDICES)
{
ExportPxTriMesh<PxU16>(TriMesh, LocalToWorld, VertexBuffer, IndexBuffer, UnrealBounds);
}
else
{
ExportPxTriMesh<PxU32>(TriMesh, LocalToWorld, VertexBuffer, IndexBuffer, UnrealBounds);
}
}
}
#endif // WITH_PHYSX
}
void ExportRigidBodyBoxElements(const FKAggregateGeom& AggGeom, TNavStatArray<float>& VertexBuffer, TNavStatArray<int32>& IndexBuffer,
TNavStatArray<int32>& ShapeBuffer, FBox& UnrealBounds, const FTransform& LocalToWorld, const int32 NumExistingVerts = 0)
{
for (int32 i = 0; i < AggGeom.BoxElems.Num(); i++)
{
const FKBoxElem& BoxInfo = AggGeom.BoxElems[i];
const FMatrix ElemTM = BoxInfo.GetTransform().ToMatrixWithScale() * LocalToWorld.ToMatrixWithScale();
const FVector Extent(BoxInfo.X * 0.5f, BoxInfo.Y * 0.5f, BoxInfo.Z * 0.5f);
const int32 VertBase = NumExistingVerts + (VertexBuffer.Num() / 3);
// Store index of first vertex in shape buffer
ShapeBuffer.Add(VertBase);
// add box vertices
FVector UnrealVerts[] = {
ElemTM.TransformPosition(FVector(-Extent.X, -Extent.Y, Extent.Z)),
ElemTM.TransformPosition(FVector( Extent.X, -Extent.Y, Extent.Z)),
ElemTM.TransformPosition(FVector(-Extent.X, -Extent.Y, -Extent.Z)),
ElemTM.TransformPosition(FVector( Extent.X, -Extent.Y, -Extent.Z)),
ElemTM.TransformPosition(FVector(-Extent.X, Extent.Y, Extent.Z)),
ElemTM.TransformPosition(FVector( Extent.X, Extent.Y, Extent.Z)),
ElemTM.TransformPosition(FVector(-Extent.X, Extent.Y, -Extent.Z)),
ElemTM.TransformPosition(FVector( Extent.X, Extent.Y, -Extent.Z))
};
for (int32 iv = 0; iv < ARRAY_COUNT(UnrealVerts); iv++)
{
UnrealBounds += UnrealVerts[iv];
VertexBuffer.Add(UnrealVerts[iv].X);
VertexBuffer.Add(UnrealVerts[iv].Y);
VertexBuffer.Add(UnrealVerts[iv].Z);
}
IndexBuffer.Add(VertBase + 3); IndexBuffer.Add(VertBase + 2); IndexBuffer.Add(VertBase + 0);
IndexBuffer.Add(VertBase + 3); IndexBuffer.Add(VertBase + 0); IndexBuffer.Add(VertBase + 1);
IndexBuffer.Add(VertBase + 7); IndexBuffer.Add(VertBase + 3); IndexBuffer.Add(VertBase + 1);
IndexBuffer.Add(VertBase + 7); IndexBuffer.Add(VertBase + 1); IndexBuffer.Add(VertBase + 5);
IndexBuffer.Add(VertBase + 6); IndexBuffer.Add(VertBase + 7); IndexBuffer.Add(VertBase + 5);
IndexBuffer.Add(VertBase + 6); IndexBuffer.Add(VertBase + 5); IndexBuffer.Add(VertBase + 4);
IndexBuffer.Add(VertBase + 2); IndexBuffer.Add(VertBase + 6); IndexBuffer.Add(VertBase + 4);
IndexBuffer.Add(VertBase + 2); IndexBuffer.Add(VertBase + 4); IndexBuffer.Add(VertBase + 0);
IndexBuffer.Add(VertBase + 1); IndexBuffer.Add(VertBase + 0); IndexBuffer.Add(VertBase + 4);
IndexBuffer.Add(VertBase + 1); IndexBuffer.Add(VertBase + 4); IndexBuffer.Add(VertBase + 5);
IndexBuffer.Add(VertBase + 7); IndexBuffer.Add(VertBase + 6); IndexBuffer.Add(VertBase + 2);
IndexBuffer.Add(VertBase + 7); IndexBuffer.Add(VertBase + 2); IndexBuffer.Add(VertBase + 3);
}
}
void ExportRigidBodySphylElements(const FKAggregateGeom& AggGeom, TNavStatArray<float>& VertexBuffer, TNavStatArray<int32>& IndexBuffer,
TNavStatArray<int32>& ShapeBuffer, FBox& UnrealBounds, const FTransform& LocalToWorld, const int32 NumExistingVerts = 0)
{
TArray<FVector> ArcVerts;
for (int32 i = 0; i < AggGeom.SphylElems.Num(); i++)
{
const FKSphylElem& SphylInfo = AggGeom.SphylElems[i];
const FMatrix ElemTM = SphylInfo.GetTransform().ToMatrixWithScale() * LocalToWorld.ToMatrixWithScale();
const int32 VertBase = NumExistingVerts + (VertexBuffer.Num() / 3);
// Store index of first vertex in shape buffer
ShapeBuffer.Add(VertBase);
const int32 NumSides = 16;
const int32 NumRings = (NumSides/2) + 1;
// The first/last arc are on top of each other.
const int32 NumVerts = (NumSides+1) * (NumRings+1);
ArcVerts.Reset();
ArcVerts.AddZeroed(NumRings+1);
for (int32 RingIdx=0; RingIdx<NumRings+1; RingIdx++)
{
float Angle;
float ZOffset;
if (RingIdx <= NumSides/4)
{
Angle = ((float)RingIdx/(NumRings-1)) * PI;
ZOffset = 0.5 * SphylInfo.Length;
}
else
{
Angle = ((float)(RingIdx-1)/(NumRings-1)) * PI;
ZOffset = -0.5 * SphylInfo.Length;
}
// Note- unit sphere, so position always has mag of one. We can just use it for normal!
FVector SpherePos;
SpherePos.X = 0.0f;
SpherePos.Y = SphylInfo.Radius * FMath::Sin(Angle);
SpherePos.Z = SphylInfo.Radius * FMath::Cos(Angle);
ArcVerts[RingIdx] = SpherePos + FVector(0,0,ZOffset);
}
// Then rotate this arc NumSides+1 times.
for (int32 SideIdx=0; SideIdx<NumSides+1; SideIdx++)
{
const FRotator ArcRotator(0, 360.f * ((float)SideIdx/NumSides), 0);
const FRotationMatrix ArcRot( ArcRotator );
const FMatrix ArcTM = ArcRot * ElemTM;
for(int32 VertIdx=0; VertIdx<NumRings+1; VertIdx++)
{
const FVector UnrealVert = ArcTM.TransformPosition(ArcVerts[VertIdx]);
UnrealBounds += UnrealVert;
VertexBuffer.Add(UnrealVert.X);
VertexBuffer.Add(UnrealVert.Y);
VertexBuffer.Add(UnrealVert.Z);
}
}
// Add all of the triangles to the mesh.
for (int32 SideIdx=0; SideIdx<NumSides; SideIdx++)
{
const int32 a0start = VertBase + ((SideIdx+0) * (NumRings+1));
const int32 a1start = VertBase + ((SideIdx+1) * (NumRings+1));
for (int32 RingIdx=0; RingIdx<NumRings; RingIdx++)
{
IndexBuffer.Add(a0start + RingIdx + 0); IndexBuffer.Add(a1start + RingIdx + 0); IndexBuffer.Add(a0start + RingIdx + 1);
IndexBuffer.Add(a1start + RingIdx + 0); IndexBuffer.Add(a1start + RingIdx + 1); IndexBuffer.Add(a0start + RingIdx + 1);
}
}
}
}
void ExportRigidBodySphereElements(const FKAggregateGeom& AggGeom, TNavStatArray<float>& VertexBuffer, TNavStatArray<int32>& IndexBuffer,
TNavStatArray<int32>& ShapeBuffer, FBox& UnrealBounds, const FTransform& LocalToWorld, const int32 NumExistingVerts = 0)
{
TArray<FVector> ArcVerts;
for (int32 i = 0; i < AggGeom.SphereElems.Num(); i++)
{
const FKSphereElem& SphereInfo = AggGeom.SphereElems[i];
const FMatrix ElemTM = SphereInfo.GetTransform().ToMatrixWithScale() * LocalToWorld.ToMatrixWithScale();
const int32 VertBase = NumExistingVerts + (VertexBuffer.Num() / 3);
// Store index of first vertex in shape buffer
ShapeBuffer.Add(VertBase);
const int32 NumSides = 16;
const int32 NumRings = (NumSides/2) + 1;
// The first/last arc are on top of each other.
const int32 NumVerts = (NumSides+1) * (NumRings+1);
ArcVerts.Reset();
ArcVerts.AddZeroed(NumRings+1);
for (int32 RingIdx=0; RingIdx<NumRings+1; RingIdx++)
{
float Angle = ((float)RingIdx/NumRings) * PI;
// Note- unit sphere, so position always has mag of one. We can just use it for normal!
FVector& ArcVert = ArcVerts[RingIdx];
ArcVert.X = 0.0f;
ArcVert.Y = SphereInfo.Radius * FMath::Sin(Angle);
ArcVert.Z = SphereInfo.Radius * FMath::Cos(Angle);
}
// Then rotate this arc NumSides+1 times.
for (int32 SideIdx=0; SideIdx<NumSides+1; SideIdx++)
{
const FRotator ArcRotator(0, 360.f * ((float)SideIdx/NumSides), 0);
const FRotationMatrix ArcRot( ArcRotator );
const FMatrix ArcTM = ArcRot * ElemTM;
for(int32 VertIdx=0; VertIdx<NumRings+1; VertIdx++)
{
const FVector UnrealVert = ArcTM.TransformPosition(ArcVerts[VertIdx]);
UnrealBounds += UnrealVert;
VertexBuffer.Add(UnrealVert.X);
VertexBuffer.Add(UnrealVert.Y);
VertexBuffer.Add(UnrealVert.Z);
}
}
// Add all of the triangles to the mesh.
for (int32 SideIdx=0; SideIdx<NumSides; SideIdx++)
{
const int32 a0start = VertBase + ((SideIdx+0) * (NumRings+1));
const int32 a1start = VertBase + ((SideIdx+1) * (NumRings+1));
for (int32 RingIdx=0; RingIdx<NumRings; RingIdx++)
{
IndexBuffer.Add(a0start + RingIdx + 0); IndexBuffer.Add(a1start + RingIdx + 0); IndexBuffer.Add(a0start + RingIdx + 1);
IndexBuffer.Add(a1start + RingIdx + 0); IndexBuffer.Add(a1start + RingIdx + 1); IndexBuffer.Add(a0start + RingIdx + 1);
}
}
}
}
FORCEINLINE_DEBUGGABLE void ExportRigidBodySetup(UBodySetup& BodySetup, TNavStatArray<float>& VertexBuffer, TNavStatArray<int32>& IndexBuffer,
FBox& UnrealBounds, const FTransform& LocalToWorld)
{
// Make sure meshes are created before we try and export them
BodySetup.CreatePhysicsMeshes();
static TNavStatArray<int32> TemporaryShapeBuffer;
ExportRigidBodyTriMesh(BodySetup, VertexBuffer, IndexBuffer, UnrealBounds, LocalToWorld);
ExportRigidBodyConvexElements(BodySetup, VertexBuffer, IndexBuffer, TemporaryShapeBuffer, UnrealBounds, LocalToWorld);
ExportRigidBodyBoxElements(BodySetup.AggGeom, VertexBuffer, IndexBuffer, TemporaryShapeBuffer, UnrealBounds, LocalToWorld);
ExportRigidBodySphylElements(BodySetup.AggGeom, VertexBuffer, IndexBuffer, TemporaryShapeBuffer, UnrealBounds, LocalToWorld);
ExportRigidBodySphereElements(BodySetup.AggGeom, VertexBuffer, IndexBuffer, TemporaryShapeBuffer, UnrealBounds, LocalToWorld);
TemporaryShapeBuffer.Reset();
}
FORCEINLINE_DEBUGGABLE void ExportComponent(UActorComponent* Component, FRecastGeometryExport& GeomExport, const FBox* ClipBounds=NULL)
{
#if WITH_PHYSX
bool bHasData = false;
UPrimitiveComponent* PrimComp = Cast<UPrimitiveComponent>(Component);
if (PrimComp && PrimComp->IsNavigationRelevant() && (PrimComp->HasCustomNavigableGeometry() != EHasCustomNavigableGeometry::DontExport))
{
if ((PrimComp->HasCustomNavigableGeometry() != EHasCustomNavigableGeometry::Type::No) && !PrimComp->DoCustomNavigableGeometryExport(GeomExport))
{
bHasData = true;
}
UBodySetup* BodySetup = PrimComp->GetBodySetup();
if (BodySetup)
{
if (!bHasData)
{
ExportRigidBodySetup(*BodySetup, GeomExport.VertexBuffer, GeomExport.IndexBuffer, GeomExport.Data->Bounds, PrimComp->GetComponentTransform());
bHasData = true;
}
GeomExport.SlopeOverride = BodySetup->WalkableSlopeOverride;
}
}
#endif // WITH_PHYSX
}
FORCEINLINE void TransformVertexSoupToRecast(const TArray<FVector>& VertexSoup, TNavStatArray<FVector>& Verts, TNavStatArray<int32>& Faces)
{
if (VertexSoup.Num() == 0)
{
return;
}
check(VertexSoup.Num() % 3 == 0);
const int32 StaticFacesCount = VertexSoup.Num() / 3;
int32 VertsCount = Verts.Num();
const FVector* Vertex = VertexSoup.GetData();
for (int32 k = 0; k < StaticFacesCount; ++k, Vertex += 3)
{
Verts.Add(Unreal2RecastPoint(Vertex[0]));
Verts.Add(Unreal2RecastPoint(Vertex[1]));
Verts.Add(Unreal2RecastPoint(Vertex[2]));
Faces.Add(VertsCount + 2);
Faces.Add(VertsCount + 1);
Faces.Add(VertsCount + 0);
VertsCount += 3;
}
}
FORCEINLINE void CovertCoordDataToRecast(TNavStatArray<float>& Coords)
{
float* CoordPtr = Coords.GetData();
const int32 MaxIt = Coords.Num() / 3;
for (int32 i = 0; i < MaxIt; i++)
{
CoordPtr[0] = -CoordPtr[0];
const float TmpV = -CoordPtr[1];
CoordPtr[1] = CoordPtr[2];
CoordPtr[2] = TmpV;
CoordPtr += 3;
}
}
void ExportVertexSoup(const TArray<FVector>& VertexSoup, TNavStatArray<float>& VertexBuffer, TNavStatArray<int32>& IndexBuffer, FBox& UnrealBounds)
{
if (VertexSoup.Num())
{
check(VertexSoup.Num() % 3 == 0);
int32 VertBase = VertexBuffer.Num() / 3;
VertexBuffer.Reserve(VertexSoup.Num() * 3);
IndexBuffer.Reserve(VertexSoup.Num() / 3);
const int32 NumVerts = VertexSoup.Num();
for (int32 i = 0; i < NumVerts; i++)
{
const FVector& UnrealCoords = VertexSoup[i];
UnrealBounds += UnrealCoords;
const FVector RecastCoords = Unreal2RecastPoint(UnrealCoords);
VertexBuffer.Add(RecastCoords.X);
VertexBuffer.Add(RecastCoords.Y);
VertexBuffer.Add(RecastCoords.Z);
}
const int32 NumFaces = VertexSoup.Num() / 3;
for (int32 i = 0; i < NumFaces; i++)
{
IndexBuffer.Add(VertBase + 2);
IndexBuffer.Add(VertBase + 1);
IndexBuffer.Add(VertBase + 0);
VertBase += 3;
}
}
}
} // namespace RecastGeometryExport
#if WITH_PHYSX
void FRecastGeometryExport::ExportPxTriMesh16Bit(physx::PxTriangleMesh const * const TriMesh, const FTransform& LocalToWorld)
{
RecastGeometryExport::ExportPxTriMesh<PxU16>(TriMesh, LocalToWorld, VertexBuffer, IndexBuffer, Data->Bounds);
}
void FRecastGeometryExport::ExportPxTriMesh32Bit(physx::PxTriangleMesh const * const TriMesh, const FTransform& LocalToWorld)
{
RecastGeometryExport::ExportPxTriMesh<PxU32>(TriMesh, LocalToWorld, VertexBuffer, IndexBuffer, Data->Bounds);
}
void FRecastGeometryExport::ExportPxConvexMesh(physx::PxConvexMesh const * const ConvexMesh, const FTransform& LocalToWorld)
{
RecastGeometryExport::ExportPxConvexMesh(ConvexMesh, LocalToWorld, VertexBuffer, IndexBuffer, Data->Bounds);
}
void FRecastGeometryExport::ExportPxHeightField(physx::PxHeightField const * const HeightField, const FTransform& LocalToWorld)
{
RecastGeometryExport::ExportPxHeightField(HeightField, LocalToWorld, VertexBuffer, IndexBuffer, Data->Bounds);
}
#endif // WITH_PHYSX
void FRecastGeometryExport::ExportHeightFieldSlice(const FNavHeightfieldSamples& PrefetchedHeightfieldSamples, const int32 NumRows, const int32 NumCols, const FTransform& LocalToWorld, const FBox& SliceBox)
{
RecastGeometryExport::ExportHeightFieldSlice(PrefetchedHeightfieldSamples, NumRows, NumCols, LocalToWorld, VertexBuffer, IndexBuffer, SliceBox, Data->Bounds);
}
void FRecastGeometryExport::ExportCustomMesh(const FVector* InVertices, int32 NumVerts, const int32* InIndices, int32 NumIndices, const FTransform& LocalToWorld)
{
RecastGeometryExport::ExportCustomMesh(InVertices, NumVerts, InIndices, NumIndices, LocalToWorld, VertexBuffer, IndexBuffer, Data->Bounds);
}
void FRecastGeometryExport::ExportRigidBodySetup(UBodySetup& BodySetup, const FTransform& LocalToWorld)
{
RecastGeometryExport::ExportRigidBodySetup(BodySetup, VertexBuffer, IndexBuffer, Data->Bounds, LocalToWorld);
}
void FRecastGeometryExport::AddNavModifiers(const FCompositeNavModifier& Modifiers)
{
Data->Modifiers.Add(Modifiers);
}
void FRecastGeometryExport::SetNavDataPerInstanceTransformDelegate(const FNavDataPerInstanceTransformDelegate& InDelegate)
{
Data->NavDataPerInstanceTransformDelegate = InDelegate;
}
static void PartialTransformConvexHull(FConvexNavAreaData& ConvexData, const FTransform& LocalToWorld)
{
FVector ScaleXY = LocalToWorld.GetScale3D().GetAbs();
ScaleXY.Z = 1.f;
FVector TranslationXY = LocalToWorld.GetLocation();
TranslationXY.Z = 0.f;
for (FVector& Point : ConvexData.Points)
{
Point = Point*ScaleXY + TranslationXY;
}
ConvexData.MaxZ+= LocalToWorld.GetLocation().Z;
ConvexData.MinZ+= LocalToWorld.GetLocation().Z;
}
FORCEINLINE void GrowConvexHull(const float ExpandBy, const TArray<FVector>& Verts, TArray<FVector>& OutResult)
{
if (Verts.Num() < 3)
{
return;
}
struct FSimpleLine
{
FVector P1, P2;
FSimpleLine() {}
FSimpleLine(FVector Point1, FVector Point2)
: P1(Point1), P2(Point2)
{
}
static FVector Intersection(const FSimpleLine& Line1, const FSimpleLine& Line2)
{
const float A1 = Line1.P2.X - Line1.P1.X;
const float B1 = Line2.P1.X - Line2.P2.X;
const float C1 = Line2.P1.X - Line1.P1.X;
const float A2 = Line1.P2.Y - Line1.P1.Y;
const float B2 = Line2.P1.Y - Line2.P2.Y;
const float C2 = Line2.P1.Y - Line1.P1.Y;
const float Denominator = A2*B1 - A1*B2;
if (Denominator != 0)
{
const float t = (B1*C2 - B2*C1) / Denominator;
return Line1.P1 + t * (Line1.P2 - Line1.P1);
}
return FVector::ZeroVector;
}
};
TArray<FVector> AllVerts(Verts);
AllVerts.Add(Verts[0]);
AllVerts.Add(Verts[1]);
const int32 VertsCount = AllVerts.Num();
const FQuat Rotation90(FVector(0, 0, 1), FMath::DegreesToRadians(90));
float RotationAngle = MAX_FLT;
for (int32 Index = 0; Index < VertsCount - 2; ++Index)
{
const FVector& V1 = AllVerts[Index + 0];
const FVector& V2 = AllVerts[Index + 1];
const FVector& V3 = AllVerts[Index + 2];
const FVector V01 = (V1 - V2).GetSafeNormal();
const FVector V12 = (V2 - V3).GetSafeNormal();
const FVector NV1 = Rotation90.RotateVector(V01);
const float d = FVector::DotProduct(NV1, V12);
if (d < 0)
{
// CW
RotationAngle = -90;
break;
}
else if (d > 0)
{
//CCW
RotationAngle = 90;
break;
}
}
// check if we detected CW or CCW direction
if (RotationAngle >= BIG_NUMBER)
{
return;
}
const float ExpansionThreshold = 2 * ExpandBy;
const float ExpansionThresholdSQ = ExpansionThreshold * ExpansionThreshold;
const FQuat Rotation(FVector(0, 0, 1), FMath::DegreesToRadians(RotationAngle));
FSimpleLine PreviousLine;
OutResult.Reserve(Verts.Num());
for (int32 Index = 0; Index < VertsCount-2; ++Index)
{
const FVector& V1 = AllVerts[Index + 0];
const FVector& V2 = AllVerts[Index + 1];
const FVector& V3 = AllVerts[Index + 2];
FSimpleLine Line1;
if (Index > 0)
{
Line1 = PreviousLine;
}
else
{
const FVector V01 = (V1 - V2).GetSafeNormal();
const FVector N1 = Rotation.RotateVector(V01).GetSafeNormal();
const FVector MoveDir1 = N1 * ExpandBy;
Line1 = FSimpleLine(V1 + MoveDir1, V2 + MoveDir1);
}
const FVector V12 = (V2 - V3).GetSafeNormal();
const FVector N2 = Rotation.RotateVector(V12).GetSafeNormal();
const FVector MoveDir2 = N2 * ExpandBy;
const FSimpleLine Line2(V2 + MoveDir2, V3 + MoveDir2);
const FVector NewPoint = FSimpleLine::Intersection(Line1, Line2);
if (NewPoint == FVector::ZeroVector)
{
// both lines are parallel so just move our point by expansion distance
OutResult.Add(V2 + MoveDir2);
}
else
{
const FVector VectorToNewPoint = NewPoint - V2;
const float DistToNewVector = VectorToNewPoint.SizeSquared2D();
if (DistToNewVector > ExpansionThresholdSQ)
{
//clamp our point to not move to far from original location
const FVector HelpPos = V2 + VectorToNewPoint.GetSafeNormal2D() * ExpandBy * 1.4142;
OutResult.Add(HelpPos);
}
else
{
OutResult.Add(NewPoint);
}
}
PreviousLine = Line2;
}
}
//----------------------------------------------------------------------//
struct FOffMeshData
{
TArray<dtOffMeshLinkCreateParams> LinkParams;
const TMap<const UClass*, int32>* AreaClassToIdMap;
const ARecastNavMesh::FNavPolyFlags* FlagsPerArea;
FOffMeshData() : AreaClassToIdMap(NULL), FlagsPerArea(NULL) {}
FORCEINLINE void Reserve(const uint32 ElementsCount)
{
LinkParams.Reserve(ElementsCount);
}
void AddLinks(const TArray<FNavigationLink>& Links, const FTransform& LocalToWorld, int32 AgentIndex, float DefaultSnapHeight)
{
for (int32 LinkIndex = 0; LinkIndex < Links.Num(); ++LinkIndex)
{
const FNavigationLink& Link = Links[LinkIndex];
if (!Link.SupportedAgents.Contains(AgentIndex))
{
continue;
}
dtOffMeshLinkCreateParams NewInfo;
FMemory::Memzero(NewInfo);
// not doing anything to link's points order - should be already ordered properly by link processor
StoreUnrealPoint(NewInfo.vertsA0, LocalToWorld.TransformPosition(Link.Left));
StoreUnrealPoint(NewInfo.vertsB0, LocalToWorld.TransformPosition(Link.Right));
NewInfo.type = DT_OFFMESH_CON_POINT |
(Link.Direction == ENavLinkDirection::BothWays ? DT_OFFMESH_CON_BIDIR : 0) |
(Link.bSnapToCheapestArea ? DT_OFFMESH_CON_CHEAPAREA : 0);
NewInfo.snapRadius = Link.SnapRadius;
NewInfo.snapHeight = Link.bUseSnapHeight ? Link.SnapHeight : DefaultSnapHeight;
NewInfo.userID = Link.UserId;
UClass* AreaClass = Link.GetAreaClass();
const int32* AreaID = AreaClassToIdMap->Find(AreaClass);
if (AreaID != NULL)
{
NewInfo.area = *AreaID;
NewInfo.polyFlag = FlagsPerArea[*AreaID];
}
else
{
UE_LOG(LogNavigation, Warning, TEXT("FRecastTileGenerator: Trying to use undefined area class while defining Off-Mesh links! (%s)"), *GetNameSafe(AreaClass));
}
// snap area is currently not supported for regular (point-point) offmesh links
LinkParams.Add(NewInfo);
}
}
void AddSegmentLinks(const TArray<FNavigationSegmentLink>& Links, const FTransform& LocalToWorld, int32 AgentIndex, float DefaultSnapHeight)
{
for (int32 LinkIndex = 0; LinkIndex < Links.Num(); ++LinkIndex)
{
const FNavigationSegmentLink& Link = Links[LinkIndex];
if (!Link.SupportedAgents.Contains(AgentIndex))
{
continue;
}
dtOffMeshLinkCreateParams NewInfo;
FMemory::Memzero(NewInfo);
// not doing anything to link's points order - should be already ordered properly by link processor
StoreUnrealPoint(NewInfo.vertsA0, LocalToWorld.TransformPosition(Link.LeftStart));
StoreUnrealPoint(NewInfo.vertsA1, LocalToWorld.TransformPosition(Link.LeftEnd));
StoreUnrealPoint(NewInfo.vertsB0, LocalToWorld.TransformPosition(Link.RightStart));
StoreUnrealPoint(NewInfo.vertsB1, LocalToWorld.TransformPosition(Link.RightEnd));
NewInfo.type = DT_OFFMESH_CON_SEGMENT | (Link.Direction == ENavLinkDirection::BothWays ? DT_OFFMESH_CON_BIDIR : 0);
NewInfo.snapRadius = Link.SnapRadius;
NewInfo.snapHeight = Link.bUseSnapHeight ? Link.SnapHeight : DefaultSnapHeight;
NewInfo.userID = Link.UserId;
UClass* AreaClass = Link.GetAreaClass();
const int32* AreaID = AreaClassToIdMap->Find(AreaClass);
if (AreaID != NULL)
{
NewInfo.area = *AreaID;
NewInfo.polyFlag = FlagsPerArea[*AreaID];
}
else
{
UE_LOG(LogNavigation, Warning, TEXT("FRecastTileGenerator: Trying to use undefined area class while defining Off-Mesh links! (%s)"), *GetNameSafe(AreaClass));
}
LinkParams.Add(NewInfo);
}
}
protected:
void StoreUnrealPoint(float* dest, const FVector& UnrealPt)
{
const FVector RecastPt = Unreal2RecastPoint(UnrealPt);
dest[0] = RecastPt.X;
dest[1] = RecastPt.Y;
dest[2] = RecastPt.Z;
}
};
//----------------------------------------------------------------------//
// FNavMeshBuildContext
// A navmesh building reporting helper
//----------------------------------------------------------------------//
class FNavMeshBuildContext : public rcContext, public dtTileCacheLogContext
{
public:
FNavMeshBuildContext()
: rcContext(true)
{
}
protected:
/// Logs a message.
/// @param[in] category The category of the message.
/// @param[in] msg The formatted message.
/// @param[in] len The length of the formatted message.
virtual void doLog(const rcLogCategory category, const char* Msg, const int32 /*len*/)
{
switch (category)
{
case RC_LOG_ERROR:
UE_LOG(LogNavigation, Error, TEXT("Recast: %s"), ANSI_TO_TCHAR( Msg ) );
break;
case RC_LOG_WARNING:
UE_LOG(LogNavigation, Log, TEXT("Recast: %s"), ANSI_TO_TCHAR( Msg ) );
break;
default:
UE_LOG(LogNavigation, Verbose, TEXT("Recast: %s"), ANSI_TO_TCHAR( Msg ) );
break;
}
}
virtual void doDtLog(const char* Msg, const int32 /*len*/)
{
UE_LOG(LogNavigation, Error, TEXT("Recast: %s"), ANSI_TO_TCHAR(Msg));
}
};
//----------------------------------------------------------------------//
struct FTileCacheCompressor : public dtTileCacheCompressor
{
struct FCompressedCacheHeader
{
int32 UncompressedSize;
};
virtual int32 maxCompressedSize(const int32 bufferSize)
{
return FMath::TruncToInt(bufferSize * 1.1f) + sizeof(FCompressedCacheHeader);
}
virtual dtStatus compress(const uint8* buffer, const int32 bufferSize,
uint8* compressed, const int32 maxCompressedSize, int32* compressedSize)
{
const int32 HeaderSize = sizeof(FCompressedCacheHeader);
FCompressedCacheHeader DataHeader;
DataHeader.UncompressedSize = bufferSize;
FMemory::Memcpy((void*)compressed, &DataHeader, HeaderSize);
uint8* DataPtr = compressed + HeaderSize;
int32 DataSize = maxCompressedSize - HeaderSize;
FCompression::CompressMemory(NAME_Zlib, (void*)DataPtr, DataSize, (const void*)buffer, bufferSize, COMPRESS_BiasMemory);
*compressedSize = DataSize + HeaderSize;
return DT_SUCCESS;
}
virtual dtStatus decompress(const uint8* compressed, const int32 compressedSize,
uint8* buffer, const int32 maxBufferSize, int32* bufferSize)
{
const int32 HeaderSize = sizeof(FCompressedCacheHeader);
FCompressedCacheHeader DataHeader;
FMemory::Memcpy(&DataHeader, (void*)compressed, HeaderSize);
const uint8* DataPtr = compressed + HeaderSize;
const int32 DataSize = compressedSize - HeaderSize;
FCompression::UncompressMemory(NAME_Zlib, (void*)buffer, DataHeader.UncompressedSize, (const void*)DataPtr, DataSize);
*bufferSize = DataHeader.UncompressedSize;
return DT_SUCCESS;
}
};
struct FTileCacheAllocator : public dtTileCacheAlloc
{
virtual void reset()
{
check(0 && "dtTileCacheAlloc.reset() is not supported!");
}
virtual void* alloc(const int32 Size)
{
return dtAlloc(Size, DT_ALLOC_TEMP);
}
virtual void free(void* Data)
{
dtFree(Data);
}
};
//----------------------------------------------------------------------//
// FVoxelCacheRasterizeContext
//----------------------------------------------------------------------//
struct FVoxelCacheRasterizeContext
{
FVoxelCacheRasterizeContext()
{
RasterizeHF = NULL;
}
~FVoxelCacheRasterizeContext()
{
rcFreeHeightField(RasterizeHF);
RasterizeHF = 0;
}
void Create(int32 FieldSize, float CellSize, float CellHeight)
{
if (RasterizeHF == NULL)
{
const float DummyBounds[3] = { 0 };
RasterizeHF = rcAllocHeightfield();
rcCreateHeightfield(NULL, *RasterizeHF, FieldSize, FieldSize, DummyBounds, DummyBounds, CellSize, CellHeight);
}
}
void Reset()
{
rcResetHeightfield(*RasterizeHF);
}
void SetupForTile(const float* TileBMin, const float* TileBMax, const float RasterizationPadding)
{
Reset();
rcVcopy(RasterizeHF->bmin, TileBMin);
rcVcopy(RasterizeHF->bmax, TileBMax);
RasterizeHF->bmin[0] -= RasterizationPadding;
RasterizeHF->bmin[2] -= RasterizationPadding;
RasterizeHF->bmax[0] += RasterizationPadding;
RasterizeHF->bmax[2] += RasterizationPadding;
}
rcHeightfield* RasterizeHF;
};
static FVoxelCacheRasterizeContext VoxelCacheContext;
uint32 GetTileCacheSizeHelper(TArray<FNavMeshTileData>& CompressedTiles)
{
uint32 TotalMemory = 0;
for (int32 i = 0; i < CompressedTiles.Num(); i++)
{
TotalMemory += CompressedTiles[i].DataSize;
}
return TotalMemory;
}
static FBox CalculateTileBounds(int32 X, int32 Y, const FVector& RcNavMeshOrigin, const FBox& TotalNavBounds, float TileSizeInWorldUnits)
{
FBox TileBox(
RcNavMeshOrigin + (FVector(X + 0, 0, Y + 0) * TileSizeInWorldUnits),
RcNavMeshOrigin + (FVector(X + 1, 0, Y + 1) * TileSizeInWorldUnits)
);
TileBox = Recast2UnrealBox(TileBox);
TileBox.Min.Z = TotalNavBounds.Min.Z;
TileBox.Max.Z = TotalNavBounds.Max.Z;
// unreal coord space
return TileBox;
}
//----------------------------------------------------------------------//
// FRecastTileGenerator
//----------------------------------------------------------------------//
FRecastTileGenerator::FRecastTileGenerator(FRecastNavMeshGenerator& ParentGenerator, const FIntPoint& Location)
{
bSucceeded = false;
bUpdateGeometry = true;
bHasLowAreaModifiers = false;
TileX = Location.X;
TileY = Location.Y;
TileConfig = ParentGenerator.GetConfig();
Version = ParentGenerator.GetVersion();
AdditionalCachedData = ParentGenerator.GetAdditionalCachedData();
ParentGeneratorWeakPtr = ParentGenerator.AsShared();
#if TIME_SLICE_NAV_REGEN
bDoneAsyncDataGathering = false;
bDoneRegenerateCompressedLayers = false;
bDoneWork = false;
#endif
}
FRecastTileGenerator::~FRecastTileGenerator()
{
}
void FRecastTileGenerator::Setup(const FRecastNavMeshGenerator& ParentGenerator, const TArray<FBox>& DirtyAreas)
{
const FVector RcNavMeshOrigin = ParentGenerator.GetRcNavMeshOrigin();
const FBox NavTotalBounds = ParentGenerator.GetTotalBounds();
const float TileCellSize = (TileConfig.tileSize * TileConfig.cs);
NavDataConfig = ParentGenerator.GetOwner()->GetConfig();
TileBB = CalculateTileBounds(TileX, TileY, RcNavMeshOrigin, NavTotalBounds, TileCellSize);
TileBBExpandedForAgent = TileBB.ExpandBy(NavDataConfig.AgentRadius * 2 + TileConfig.cs);
const FBox RCBox = Unreal2RecastBox(TileBB);
rcVcopy(TileConfig.bmin, &RCBox.Min.X);
rcVcopy(TileConfig.bmax, &RCBox.Max.X);
// from passed in boxes pick the ones overlapping with tile bounds
bFullyEncapsulatedByInclusionBounds = true;
const TNavStatArray<FBox>& ParentBounds = ParentGenerator.GetInclusionBounds();
if (ParentBounds.Num() > 0)
{
bFullyEncapsulatedByInclusionBounds = false;
InclusionBounds.Reserve(ParentBounds.Num());
for (const FBox& Bounds : ParentBounds)
{
if (Bounds.Intersect(TileBB))
{
InclusionBounds.Add(Bounds);
bFullyEncapsulatedByInclusionBounds = DoesBoxContainBox(Bounds, TileBB);
}
}
}
const bool bGeometryChanged = (DirtyAreas.Num() == 0);
if (!bGeometryChanged)
{
// Get compressed tile cache layers if they exist for this location
CompressedLayers = ParentGenerator.GetOwner()->GetTileCacheLayers(TileX, TileY);
for (FNavMeshTileData& LayerData : CompressedLayers)
{
// we don't want to modify shared state inside async task, so make sure we are unique owner
LayerData.MakeUnique();
}
}
// We have to regenerate layers data in case geometry is changed or tile cache is missing
bRegenerateCompressedLayers = (bGeometryChanged || CompressedLayers.Num() == 0);
// Gather geometry for tile if it inside navigable bounds
if (InclusionBounds.Num())
{
if (!bRegenerateCompressedLayers)
{
// Mark layers that needs to be updated
DirtyLayers.Init(false, CompressedLayers.Num());
for (const FNavMeshTileData& LayerData : CompressedLayers)
{
for (FBox DirtyBox : DirtyAreas)
{
if (DirtyBox.Intersect(LayerData.LayerBBox))
{
DirtyLayers[LayerData.LayerIndex] = true;
}
}
}
}
if (ParentGenerator.GatherGeometryOnGameThread())
{
GatherGeometry(ParentGenerator, bRegenerateCompressedLayers);
}
else
{
PrepareGeometrySources(ParentGenerator, bRegenerateCompressedLayers);
}
}
//
UsedMemoryOnStartup = GetUsedMemCount() + sizeof(FRecastTileGenerator);
}
bool FRecastTileGenerator::HasDataToBuild() const
{
return
CompressedLayers.Num()
|| Modifiers.Num()
|| OffmeshLinks.Num()
|| RawGeometry.Num()
|| (InclusionBounds.Num() && NavigationRelevantData.Num() > 0);
}
ETimeSliceWorkResult FRecastTileGenerator::DoWork()
{
TSharedPtr<FNavDataGenerator, ESPMode::ThreadSafe> ParentGenerator = ParentGeneratorWeakPtr.Pin();
ETimeSliceWorkResult TimeSliceWorkResult = ETimeSliceWorkResult::Succeeded;
if (ParentGenerator.IsValid())
{
if (InclusionBounds.Num()
#if TIME_SLICE_NAV_REGEN
&& !bDoneAsyncDataGathering
#endif
)
{
DoAsyncGeometryGathering();
}
TimeSliceWorkResult = GenerateTile();
//bSucceeded is set false in the constructor anyway
bSucceeded = (TimeSliceWorkResult == ETimeSliceWorkResult::Succeeded);
}
#if TIME_SLICE_NAV_REGEN
if (TimeSliceWorkResult != ETimeSliceWorkResult::CallAgain)
#endif
{
DumpAsyncData();
if (!bSucceeded)
{
TimeSliceWorkResult = ETimeSliceWorkResult::Failed;
}
#if TIME_SLICE_NAV_REGEN
bDoneWork = true;
#endif
}
return TimeSliceWorkResult;
}
void FRecastTileGenerator::DumpAsyncData()
{
RawGeometry.Empty();
Modifiers.Empty();
OffmeshLinks.Empty();
NavigationRelevantData.Empty();
NavOctree = nullptr;
}
void FRecastTileGenerator::DoAsyncGeometryGathering()
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_PrepareGeometrySources);
#if TIME_SLICE_NAV_REGEN
bDoneAsyncDataGathering = true;
#endif
for (auto& ElementData : NavigationRelevantData)
{
if (ElementData->GetOwner() == nullptr)
{
UE_LOG(LogNavigation, Warning, TEXT("DoAsyncGeometryGathering: skipping an element with no longer valid Owner"));
continue;
}
bool bDumpGeometryData = false;
if (ElementData->IsPendingLazyGeometryGathering() && ElementData->SupportsGatheringGeometrySlices())
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_LandscapeSlicesExporting);
FRecastGeometryExport GeomExport(*ElementData);
INavRelevantInterface* NavRelevant = Cast<INavRelevantInterface>(ElementData->GetOwner());
if(NavRelevant)
{
NavRelevant->PrepareGeometryExportSync();
// adding a small bump to avoid special case of zero-expansion when tile bounds
// overlap landscape's tile bounds
NavRelevant->GatherGeometrySlice(GeomExport, TileBBExpandedForAgent);
RecastGeometryExport::CovertCoordDataToRecast(GeomExport.VertexBuffer);
RecastGeometryExport::StoreCollisionCache(GeomExport);
bDumpGeometryData = true;
}
else
{
UE_LOG(LogNavigation, Error, TEXT("DoAsyncGeometryGathering: got an invalid NavRelevant instance!"));
}
}
if (ElementData->IsPendingLazyGeometryGathering() || ElementData->IsPendingLazyModifiersGathering())
{
NavOctree->DemandLazyDataGathering(*ElementData);
}
const bool bExportGeometry = bUpdateGeometry && ElementData->HasGeometry();
if (bExportGeometry)
{
if (ARecastNavMesh::IsVoxelCacheEnabled())
{
TNavStatArray<rcSpanCache> SpanData;
rcSpanCache* CachedVoxels = 0;
int32 NumCachedVoxels = 0;
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("Rasterization: prepare voxel cache"), Stat_RecastRasterCachePrep, STATGROUP_Navigation);
if (!HasVoxelCache(ElementData->VoxelData, CachedVoxels, NumCachedVoxels))
{
// rasterize
PrepareVoxelCache(ElementData->CollisionData, SpanData);
CachedVoxels = SpanData.GetData();
NumCachedVoxels = SpanData.Num();
// encode
const int32 PrevElementMemory = ElementData->GetAllocatedSize();
FNavigationRelevantData* ModData = (FNavigationRelevantData*)&ElementData;
AddVoxelCache(ModData->VoxelData, CachedVoxels, NumCachedVoxels);
const int32 NewElementMemory = ElementData->GetAllocatedSize();
const int32 ElementMemoryDelta = NewElementMemory - PrevElementMemory;
INC_MEMORY_STAT_BY(STAT_Navigation_CollisionTreeMemory, ElementMemoryDelta);
}
}
else
{
ValidateAndAppendGeometry(ElementData);
}
if (bDumpGeometryData)
{
const_cast<FNavigationRelevantData&>(*ElementData).CollisionData.Empty();
}
}
const FCompositeNavModifier ModifierInstance = ElementData->Modifiers.HasMetaAreas() ? ElementData->Modifiers.GetInstantiatedMetaModifier(&NavDataConfig, ElementData->SourceObject) : ElementData->Modifiers;
if (ModifierInstance.IsEmpty() == false)
{
AppendModifier(ModifierInstance, ElementData->NavDataPerInstanceTransformDelegate);
}
}
}
void FRecastTileGenerator::PrepareGeometrySources(const FRecastNavMeshGenerator& ParentGenerator, bool bGeometryChanged)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_PrepareGeometrySources);
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(ParentGenerator.GetWorld());
FNavigationOctree* NavOctreeInstance = NavSys ? NavSys->GetMutableNavOctree() : nullptr;
check(NavOctreeInstance);
NavigationRelevantData.Reset();
NavOctree = NavOctreeInstance->AsShared();
bUpdateGeometry = bGeometryChanged;
for (FNavigationOctree::TConstElementBoxIterator<FNavigationOctree::DefaultStackAllocator> It(*NavOctreeInstance, ParentGenerator.GrowBoundingBox(TileBB, /*bIncludeAgentHeight*/ false));
It.HasPendingElements();
It.Advance())
{
const FNavigationOctreeElement& Element = It.GetCurrentElement();
const bool bShouldUse = Element.ShouldUseGeometry(NavDataConfig);
if (bShouldUse)
{
const bool bExportGeometry = bGeometryChanged && (Element.Data->HasGeometry() || Element.Data->IsPendingLazyGeometryGathering());
if (bExportGeometry
|| (Element.Data->IsPendingLazyModifiersGathering() || Element.Data->Modifiers.HasMetaAreas() == true || Element.Data->Modifiers.IsEmpty() == false))
{
NavigationRelevantData.Add(Element.Data);
}
}
}
}
void FRecastTileGenerator::GatherGeometry(const FRecastNavMeshGenerator& ParentGenerator, bool bGeometryChanged)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_GatherGeometry);
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(ParentGenerator.GetWorld());
FNavigationOctree* NavigationOctree = NavSys ? NavSys->GetMutableNavOctree() : nullptr;
if (NavigationOctree == nullptr)
{
return;
}
const FNavDataConfig& OwnerNavDataConfig = ParentGenerator.GetOwner()->GetConfig();
for (FNavigationOctree::TConstElementBoxIterator<FNavigationOctree::DefaultStackAllocator> It(*NavigationOctree, ParentGenerator.GrowBoundingBox(TileBB, /*bIncludeAgentHeight*/ false));
It.HasPendingElements();
It.Advance())
{
const FNavigationOctreeElement& Element = It.GetCurrentElement();
const bool bShouldUse = Element.ShouldUseGeometry(OwnerNavDataConfig);
if (bShouldUse)
{
bool bDumpGeometryData = false;
if (Element.Data->IsPendingLazyGeometryGathering() || Element.Data->IsPendingLazyModifiersGathering())
{
const bool bSupportsSlices = Element.Data->SupportsGatheringGeometrySlices();
if (bSupportsSlices == false || Element.Data->IsPendingLazyModifiersGathering() == true)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_LazyGeometryExport);
NavigationOctree->DemandLazyDataGathering(Element);
}
if (bSupportsSlices == true)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_LandscapeSlicesExporting);
FRecastGeometryExport GeomExport(const_cast<FNavigationRelevantData&>(*Element.Data));
INavRelevantInterface* NavRelevant = const_cast<INavRelevantInterface*>(Cast<const INavRelevantInterface>(Element.GetOwner()));
if (NavRelevant)
{
NavRelevant->PrepareGeometryExportSync();
// adding a small bump to avoid special case of zero-expansion when tile bounds
// overlap landscape's tile bounds
NavRelevant->GatherGeometrySlice(GeomExport, TileBBExpandedForAgent);
RecastGeometryExport::CovertCoordDataToRecast(GeomExport.VertexBuffer);
RecastGeometryExport::StoreCollisionCache(GeomExport);
bDumpGeometryData = true;
}
else
{
UE_LOG(LogNavigation, Error, TEXT("GatherGeometry: got an invalid NavRelevant instance!"));
}
}
}
const bool bExportGeometry = bGeometryChanged && Element.Data->HasGeometry();
if (bExportGeometry)
{
if (ARecastNavMesh::IsVoxelCacheEnabled())
{
TNavStatArray<rcSpanCache> SpanData;
rcSpanCache* CachedVoxels = 0;
int32 NumCachedVoxels = 0;
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("Rasterization: prepare voxel cache"), Stat_RecastRasterCachePrep, STATGROUP_Navigation);
if (!HasVoxelCache(Element.Data->VoxelData, CachedVoxels, NumCachedVoxels))
{
// rasterize
PrepareVoxelCache(Element.Data->CollisionData, SpanData);
CachedVoxels = SpanData.GetData();
NumCachedVoxels = SpanData.Num();
// encode
const int32 PrevElementMemory = Element.Data->GetAllocatedSize();
FNavigationRelevantData* ModData = (FNavigationRelevantData*)&Element.Data;
AddVoxelCache(ModData->VoxelData, CachedVoxels, NumCachedVoxels);
const int32 NewElementMemory = Element.Data->GetAllocatedSize();
const int32 ElementMemoryDelta = NewElementMemory - PrevElementMemory;
INC_MEMORY_STAT_BY(STAT_Navigation_CollisionTreeMemory, ElementMemoryDelta);
}
}
else
{
ValidateAndAppendGeometry(Element.Data);
}
if (bDumpGeometryData)
{
const_cast<FNavigationRelevantData&>(*Element.Data).CollisionData.Empty();
}
}
const FCompositeNavModifier ModifierInstance = Element.GetModifierForAgent(&OwnerNavDataConfig);
if (ModifierInstance.IsEmpty() == false)
{
AppendModifier(ModifierInstance, Element.Data->NavDataPerInstanceTransformDelegate);
}
}
}
}
void FRecastTileGenerator::ApplyVoxelFilter(rcHeightfield* HF, float WalkableRadius)
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_TileVoxelFilteringAsync);
if (HF != NULL)
{
const int32 Width = HF->width;
const int32 Height = HF->height;
const float CellSize = HF->cs;
const float CellHeight = HF->ch;
const float BottomX = HF->bmin[0];
const float BottomZ = HF->bmin[1];
const float BottomY = HF->bmin[2];
const int32 SpansCount = Width*Height;
// we need to expand considered bounding boxes so that
// it doesn't create "fake cliffs"
const float ExpandBBBy = WalkableRadius*CellSize;
const FBox* BBox = InclusionBounds.GetData();
// optimized common case of single box
if (InclusionBounds.Num() == 1)
{
const FBox BB = BBox->ExpandBy(ExpandBBBy);
rcSpan** Span = HF->spans;
for (int32 y = 0; y < Height; ++y)
{
for (int32 x = 0; x < Width; ++x)
{
const float SpanX = -(BottomX + x * CellSize);
const float SpanY = -(BottomY + y * CellSize);
// mark all spans outside of InclusionBounds as unwalkable
for (rcSpan* s = *Span; s; s = s->next)
{
if (s->data.area == RC_WALKABLE_AREA)
{
const float SpanMin = CellHeight * s->data.smin + BottomZ;
const float SpanMax = CellHeight * s->data.smax + BottomZ;
const FVector SpanMinV(SpanX-CellSize, SpanY-CellSize, SpanMin);
const FVector SpanMaxV(SpanX, SpanY, SpanMax);
if (BB.IsInside(SpanMinV) == false && BB.IsInside(SpanMaxV) == false)
{
s->data.area = RC_NULL_AREA;
}
}
}
++Span;
}
}
}
else
{
TArray<FBox> Bounds;
Bounds.Reserve(InclusionBounds.Num());
for (int32 i = 0; i < InclusionBounds.Num(); ++i, ++BBox)
{
Bounds.Add(BBox->ExpandBy(ExpandBBBy));
}
const int32 BoundsCount = Bounds.Num();
rcSpan** Span = HF->spans;
for (int32 y = 0; y < Height; ++y)
{
for (int32 x = 0; x < Width; ++x)
{
const float SpanX = -(BottomX + x * CellSize);
const float SpanY = -(BottomY + y * CellSize);
// mark all spans outside of InclusionBounds as unwalkable
for (rcSpan* s = *Span; s; s = s->next)
{
if (s->data.area == RC_WALKABLE_AREA)
{
const float SpanMin = CellHeight * s->data.smin + BottomZ;
const float SpanMax = CellHeight * s->data.smax + BottomZ;
const FVector SpanMinV(SpanX-CellSize, SpanY-CellSize, SpanMin);
const FVector SpanMaxV(SpanX, SpanY, SpanMax);
bool bIsInsideAnyBB = false;
const FBox* BB = Bounds.GetData();
for (int32 BoundIndex = 0; BoundIndex < BoundsCount; ++BoundIndex, ++BB)
{
if (BB->IsInside(SpanMinV) || BB->IsInside(SpanMaxV))
{
bIsInsideAnyBB = true;
break;
}
}
if (bIsInsideAnyBB == false)
{
s->data.area = RC_NULL_AREA;
}
}
}
++Span;
}
}
}
}
}
void FRecastTileGenerator::PrepareVoxelCache(const TNavStatArray<uint8>& RawCollisionCache, TNavStatArray<rcSpanCache>& SpanData)
{
// tile's geometry: voxel cache (only for synchronous rebuilds)
const int32 WalkableClimbVX = TileConfig.walkableClimb;
const float WalkableSlopeCos = FMath::Cos(FMath::DegreesToRadians(TileConfig.walkableSlopeAngle));
const float RasterizationPadding = TileConfig.borderSize * TileConfig.cs;
FRecastGeometryCache CachedCollisions(RawCollisionCache.GetData());
VoxelCacheContext.SetupForTile(TileConfig.bmin, TileConfig.bmax, RasterizationPadding);
float SlopeCosPerActor = WalkableSlopeCos;
CachedCollisions.Header.SlopeOverride.ModifyWalkableFloorZ(SlopeCosPerActor);
// rasterize triangle soup
TNavStatArray<uint8> TriAreas;
TriAreas.AddZeroed(CachedCollisions.Header.NumFaces);
rcMarkWalkableTrianglesCos(0, SlopeCosPerActor,
CachedCollisions.Verts, CachedCollisions.Header.NumVerts,
CachedCollisions.Indices, CachedCollisions.Header.NumFaces,
TriAreas.GetData());
rcRasterizeTriangles(0, CachedCollisions.Verts, CachedCollisions.Header.NumVerts,
CachedCollisions.Indices, TriAreas.GetData(), CachedCollisions.Header.NumFaces,
*VoxelCacheContext.RasterizeHF, WalkableClimbVX);
const int32 NumSpans = rcCountSpans(0, *VoxelCacheContext.RasterizeHF);
if (NumSpans > 0)
{
SpanData.AddZeroed(NumSpans);
rcCacheSpans(0, *VoxelCacheContext.RasterizeHF, SpanData.GetData());
}
}
bool FRecastTileGenerator::HasVoxelCache(const TNavStatArray<uint8>& RawVoxelCache, rcSpanCache*& CachedVoxels, int32& NumCachedVoxels) const
{
FRecastVoxelCache VoxelCache(RawVoxelCache.GetData());
for (FRecastVoxelCache::FTileInfo* iTile = VoxelCache.Tiles; iTile; iTile = iTile->NextTile)
{
if (iTile->TileX == TileX && iTile->TileY == TileY)
{
CachedVoxels = iTile->SpanData;
NumCachedVoxels = iTile->NumSpans;
return true;
}
}
return false;
}
void FRecastTileGenerator::AddVoxelCache(TNavStatArray<uint8>& RawVoxelCache, const rcSpanCache* CachedVoxels, const int32 NumCachedVoxels) const
{
if (RawVoxelCache.Num() == 0)
{
RawVoxelCache.AddZeroed(sizeof(int32));
}
int32* NumTiles = (int32*)RawVoxelCache.GetData();
*NumTiles = *NumTiles + 1;
const int32 NewCacheIdx = RawVoxelCache.Num();
const int32 HeaderSize = sizeof(FRecastVoxelCache::FTileInfo);
const int32 VoxelsSize = sizeof(rcSpanCache) * NumCachedVoxels;
const int32 EntrySize = HeaderSize + VoxelsSize;
RawVoxelCache.AddZeroed(EntrySize);
FRecastVoxelCache::FTileInfo* TileInfo = (FRecastVoxelCache::FTileInfo*)(RawVoxelCache.GetData() + NewCacheIdx);
TileInfo->TileX = TileX;
TileInfo->TileY = TileY;
TileInfo->NumSpans = NumCachedVoxels;
FMemory::Memcpy(RawVoxelCache.GetData() + NewCacheIdx + HeaderSize, CachedVoxels, VoxelsSize);
}
void FRecastTileGenerator::AppendModifier(const FCompositeNavModifier& Modifier, const FNavDataPerInstanceTransformDelegate& InTransformsDelegate)
{
// append all offmesh links (not included in compress layers)
OffmeshLinks.Append(Modifier.GetSimpleLinks());
// evaluate custom links
const FCustomLinkNavModifier* LinkModifier = Modifier.GetCustomLinks().GetData();
for (int32 i = 0; i < Modifier.GetCustomLinks().Num(); i++, LinkModifier++)
{
FSimpleLinkNavModifier SimpleLinkCollection(UNavLinkDefinition::GetLinksDefinition(LinkModifier->GetNavLinkClass()), LinkModifier->LocalToWorld);
OffmeshLinks.Add(SimpleLinkCollection);
}
if (Modifier.GetAreas().Num() == 0)
{
return;
}
bHasLowAreaModifiers = bHasLowAreaModifiers || Modifier.HasLowAreaModifiers();
FRecastAreaNavModifierElement ModifierElement;
// Gather per instance transforms if any
if (InTransformsDelegate.IsBound())
{
InTransformsDelegate.Execute(TileBBExpandedForAgent, ModifierElement.PerInstanceTransform);
// skip this modifier in case there is no instances for this tile
if (ModifierElement.PerInstanceTransform.Num() == 0)
{
return;
}
}
ModifierElement.Areas = Modifier.GetAreas();
Modifiers.Add(MoveTemp(ModifierElement));
}
void FRecastTileGenerator::ValidateAndAppendGeometry(TSharedRef<FNavigationRelevantData, ESPMode::ThreadSafe> ElementData)
{
const FNavigationRelevantData& DataRef = ElementData.Get();
if (DataRef.IsCollisionDataValid())
{
AppendGeometry(DataRef.CollisionData, DataRef.NavDataPerInstanceTransformDelegate);
}
}
void FRecastTileGenerator::AppendGeometry(const TNavStatArray<uint8>& RawCollisionCache, const FNavDataPerInstanceTransformDelegate& InTransformsDelegate)
{
if (RawCollisionCache.Num() == 0)
{
return;
}
FRecastRawGeometryElement GeometryElement;
FRecastGeometryCache CollisionCache(RawCollisionCache.GetData());
// Gather per instance transforms
if (InTransformsDelegate.IsBound())
{
InTransformsDelegate.Execute(TileBBExpandedForAgent, GeometryElement.PerInstanceTransform);
if (GeometryElement.PerInstanceTransform.Num() == 0)
{
return;
}
}
const int32 NumCoords = CollisionCache.Header.NumVerts * 3;
const int32 NumIndices = CollisionCache.Header.NumFaces * 3;
if (NumIndices > 0)
{
GeometryElement.GeomCoords.SetNumUninitialized(NumCoords);
GeometryElement.GeomIndices.SetNumUninitialized(NumIndices);
FMemory::Memcpy(GeometryElement.GeomCoords.GetData(), CollisionCache.Verts, sizeof(float) * NumCoords);
FMemory::Memcpy(GeometryElement.GeomIndices.GetData(), CollisionCache.Indices, sizeof(int32) * NumIndices);
RawGeometry.Add(MoveTemp(GeometryElement));
}
}
ETimeSliceWorkResult FRecastTileGenerator::GenerateTile()
{
ETimeSliceWorkResult TimeSliceWorkResult = ETimeSliceWorkResult::Succeeded;
FNavMeshBuildContext BuildContext;
if (bRegenerateCompressedLayers
#if TIME_SLICE_NAV_REGEN
&& !bDoneRegenerateCompressedLayers
#endif
)
{
CompressedLayers.Reset();
const bool bSuccess = GenerateCompressedLayers(BuildContext);
#if TIME_SLICE_NAV_REGEN
bDoneRegenerateCompressedLayers = true;
#endif
if (bSuccess)
{
// Mark all layers as dirty
DirtyLayers.Init(true, CompressedLayers.Num());
#if TIME_SLICE_NAV_REGEN
TimeSliceWorkResult = ETimeSliceWorkResult::CallAgain;
#else
TimeSliceWorkResult = ETimeSliceWorkResult::Succeeded;
#endif
}
else
{
TimeSliceWorkResult = ETimeSliceWorkResult::Failed;
}
}
if (TimeSliceWorkResult == ETimeSliceWorkResult::Succeeded)
{
const bool bSuccess = GenerateNavigationData(BuildContext);
if (!bSuccess)
{
TimeSliceWorkResult = ETimeSliceWorkResult::Failed;
}
}
// it's possible to have valid generation with empty resulting tile (no navigable geometry in tile)
return TimeSliceWorkResult;
}
struct FTileRasterizationContext
{
FTileRasterizationContext() : SolidHF(0), LayerSet(0), CompactHF(0)
{
}
~FTileRasterizationContext()
{
rcFreeHeightField(SolidHF);
rcFreeHeightfieldLayerSet(LayerSet);
rcFreeCompactHeightfield(CompactHF);
}
struct rcHeightfield* SolidHF;
struct rcHeightfieldLayerSet* LayerSet;
struct rcCompactHeightfield* CompactHF;
TArray<FNavMeshTileData> Layers;
};
static void RasterizeGeometry(
FNavMeshBuildContext& BuildContext, const FRecastBuildConfig& TileConfig,
const TArray<float>& Coords, const TArray<int32>& Indices, FTileRasterizationContext& RasterContext)
{
const int32 NumFaces = Indices.Num() / 3;
const int32 NumVerts = Coords.Num() / 3;
TNavStatArray<uint8> TriAreas;
TriAreas.Reserve(NumFaces);
TriAreas.AddZeroed(NumFaces);
rcMarkWalkableTriangles(&BuildContext, TileConfig.walkableSlopeAngle,
Coords.GetData(), NumVerts, Indices.GetData(), NumFaces,
TriAreas.GetData());
rcRasterizeTriangles(&BuildContext,
Coords.GetData(), NumVerts,
Indices.GetData(), TriAreas.GetData(), NumFaces,
*RasterContext.SolidHF, TileConfig.walkableClimb);
}
static void RasterizeGeometry(
FNavMeshBuildContext& BuildContext,const FRecastBuildConfig& TileConfig,
const TArray<float>& Coords, const TArray<int32>& Indices, const FTransform& LocalToWorld, FTileRasterizationContext& RasterContext)
{
TArray<float> WorldRecastCoords;
WorldRecastCoords.SetNumUninitialized(Coords.Num());
FMatrix LocalToRecastWorld = LocalToWorld.ToMatrixWithScale()*Unreal2RecastMatrix();
// Convert geometry to recast world space
for (int32 i = 0; i < Coords.Num(); i+=3)
{
// collision cache stores coordinates in recast space, convert them to unreal and transform to recast world space
FVector WorldRecastCoord = LocalToRecastWorld.TransformPosition(Recast2UnrealPoint(&Coords[i]));
WorldRecastCoords[i+0] = WorldRecastCoord.X;
WorldRecastCoords[i+1] = WorldRecastCoord.Y;
WorldRecastCoords[i+2] = WorldRecastCoord.Z;
}
RasterizeGeometry(BuildContext, TileConfig, WorldRecastCoords, Indices, RasterContext);
}
bool FRecastTileGenerator::GenerateCompressedLayers(FNavMeshBuildContext& BuildContext)
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildCompressedLayers);
TileConfig.width = TileConfig.tileSize + TileConfig.borderSize*2;
TileConfig.height = TileConfig.tileSize + TileConfig.borderSize*2;
const float BBoxPadding = TileConfig.borderSize * TileConfig.cs;
TileConfig.bmin[0] -= BBoxPadding;
TileConfig.bmin[2] -= BBoxPadding;
TileConfig.bmax[0] += BBoxPadding;
TileConfig.bmax[2] += BBoxPadding;
BuildContext.log(RC_LOG_PROGRESS, "GenerateCompressedLayers:");
BuildContext.log(RC_LOG_PROGRESS, " - %d x %d cells", TileConfig.width, TileConfig.height);
FTileRasterizationContext RasterContext;
const bool bHasGeometry = RawGeometry.Num() > 0;
// Allocate voxel heightfield where we rasterize our input data to.
if (bHasGeometry)
{
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastCreateHeightField);
RasterContext.SolidHF = rcAllocHeightfield();
if (RasterContext.SolidHF == NULL)
{
BuildContext.log(RC_LOG_ERROR, "GenerateCompressedLayers: Out of memory 'SolidHF'.");
return false;
}
if (!rcCreateHeightfield(&BuildContext, *RasterContext.SolidHF, TileConfig.width, TileConfig.height, TileConfig.bmin, TileConfig.bmax, TileConfig.cs, TileConfig.ch))
{
BuildContext.log(RC_LOG_ERROR, "GenerateCompressedLayers: Could not create solid heightfield.");
return false;
}
}
{
// Rasterize geometry
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastRasterizeTriangles)
for (const FRecastRawGeometryElement& Element : RawGeometry)
{
for (const FTransform& InstanceTransform : Element.PerInstanceTransform)
{
RasterizeGeometry(BuildContext, TileConfig, Element.GeomCoords, Element.GeomIndices, InstanceTransform, RasterContext);
}
if (Element.PerInstanceTransform.Num() == 0)
{
RasterizeGeometry(BuildContext, TileConfig, Element.GeomCoords, Element.GeomIndices, RasterContext);
}
}
}
}
if (!RasterContext.SolidHF || RasterContext.SolidHF->pools == 0)
{
BuildContext.log(RC_LOG_WARNING, "GenerateCompressedLayers: empty tile - aborting");
return true;
}
// Reject voxels outside generation boundaries
if (TileConfig.bPerformVoxelFiltering && !bFullyEncapsulatedByInclusionBounds)
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastVoxelFilter)
ApplyVoxelFilter(RasterContext.SolidHF, TileConfig.walkableRadius);
}
// TileConfig.walkableHeight is set to 1 when marking low spans, calculate real value for filtering
const int32 FilterWalkableHeight = FMath::CeilToInt(TileConfig.AgentHeight / TileConfig.ch);
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastFilter)
// Once all geometry is rasterized, we do initial pass of filtering to
// remove unwanted overhangs caused by the conservative rasterization
// as well as filter spans where the character cannot possibly stand.
rcFilterLowHangingWalkableObstacles(&BuildContext, TileConfig.walkableClimb, *RasterContext.SolidHF);
rcFilterLedgeSpans(&BuildContext, TileConfig.walkableHeight, TileConfig.walkableClimb, *RasterContext.SolidHF);
if (!TileConfig.bMarkLowHeightAreas)
{
rcFilterWalkableLowHeightSpans(&BuildContext, TileConfig.walkableHeight, *RasterContext.SolidHF);
}
else if (TileConfig.bFilterLowSpanFromTileCache)
{
// TODO: investigate if creating detailed 2D map from active modifiers is cheap enough
// for now, switch on presence of those modifiers, will save memory as long as they are sparse (should be)
if (TileConfig.bFilterLowSpanSequences && bHasLowAreaModifiers)
{
rcFilterWalkableLowHeightSpansSequences(&BuildContext, FilterWalkableHeight, *RasterContext.SolidHF);
}
else
{
rcFilterWalkableLowHeightSpans(&BuildContext, FilterWalkableHeight, *RasterContext.SolidHF);
}
}
}
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildCompactHeightField);
// Compact the heightfield so that it is faster to handle from now on.
// This will result more cache coherent data as well as the neighbors
// between walkable cells will be calculated.
RasterContext.CompactHF = rcAllocCompactHeightfield();
if (RasterContext.CompactHF == NULL)
{
BuildContext.log(RC_LOG_ERROR, "GenerateCompressedLayers: Out of memory 'CompactHF'.");
return false;
}
if (!rcBuildCompactHeightfield(&BuildContext, TileConfig.walkableHeight, TileConfig.walkableClimb, *RasterContext.SolidHF, *RasterContext.CompactHF))
{
const int SpanCount = rcGetHeightFieldSpanCount(&BuildContext, *RasterContext.SolidHF);
if (SpanCount > 0)
{
BuildContext.log(RC_LOG_ERROR, "GenerateCompressedLayers: Could not build compact data.");
}
// else there's just no spans to walk on (no spans at all or too small/sparse)
else
{
BuildContext.log(RC_LOG_WARNING, "GenerateCompressedLayers: no walkable spans - aborting");
}
return false;
}
}
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastErodeWalkable);
if (TileConfig.walkableRadius > RECAST_VERY_SMALL_AGENT_RADIUS)
{
uint8 FilterFlags = 0;
if (TileConfig.bFilterLowSpanSequences)
{
FilterFlags = RC_LOW_FILTER_POST_PROCESS | (TileConfig.bFilterLowSpanFromTileCache ? 0 : RC_LOW_FILTER_SEED_SPANS);
}
const bool bEroded = TileConfig.bMarkLowHeightAreas ?
rcErodeWalkableAndLowAreas(&BuildContext, TileConfig.walkableRadius, FilterWalkableHeight, RECAST_LOW_AREA, FilterFlags, *RasterContext.CompactHF) :
rcErodeWalkableArea(&BuildContext, TileConfig.walkableRadius, *RasterContext.CompactHF);
if (!bEroded)
{
BuildContext.log(RC_LOG_ERROR, "GenerateCompressedLayers: Could not erode.");
return false;
}
}
else if (TileConfig.bMarkLowHeightAreas)
{
rcMarkLowAreas(&BuildContext, FilterWalkableHeight, RECAST_LOW_AREA, *RasterContext.CompactHF);
}
}
// Build layers
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildLayers);
RasterContext.LayerSet = rcAllocHeightfieldLayerSet();
if (RasterContext.LayerSet == NULL)
{
BuildContext.log(RC_LOG_ERROR, "GenerateCompressedLayers: Out of memory 'LayerSet'.");
return false;
}
if (TileConfig.regionPartitioning == RC_REGION_MONOTONE)
{
if (!rcBuildHeightfieldLayersMonotone(&BuildContext, *RasterContext.CompactHF, TileConfig.borderSize, TileConfig.walkableHeight, *RasterContext.LayerSet))
{
BuildContext.log(RC_LOG_ERROR, "GenerateCompressedLayers: Could not build heightfield layers.");
return 0;
}
}
else if (TileConfig.regionPartitioning == RC_REGION_WATERSHED)
{
if (!rcBuildDistanceField(&BuildContext, *RasterContext.CompactHF))
{
BuildContext.log(RC_LOG_ERROR, "GenerateCompressedLayers: Could not build distance field.");
return 0;
}
if (!rcBuildHeightfieldLayers(&BuildContext, *RasterContext.CompactHF, TileConfig.borderSize, TileConfig.walkableHeight, *RasterContext.LayerSet))
{
BuildContext.log(RC_LOG_ERROR, "GenerateCompressedLayers: Could not build heightfield layers.");
return 0;
}
}
else
{
if (!rcBuildHeightfieldLayersChunky(&BuildContext, *RasterContext.CompactHF, TileConfig.borderSize, TileConfig.walkableHeight, TileConfig.regionChunkSize, *RasterContext.LayerSet))
{
BuildContext.log(RC_LOG_ERROR, "GenerateCompressedLayers: Could not build heightfield layers.");
return 0;
}
}
}
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildTileCache);
const int32 NumLayers = RasterContext.LayerSet->nlayers;
// use this to expand vertically layer's bounds
// this is needed to allow off-mesh connections that are not quite
// touching tile layer still connect with it.
const float StepHeights = TileConfig.AgentMaxClimb;
FTileCacheCompressor TileCompressor;
for (int32 i = 0; i < NumLayers; i++)
{
const rcHeightfieldLayer* layer = &RasterContext.LayerSet->layers[i];
// Store header
dtTileCacheLayerHeader header;
header.magic = DT_TILECACHE_MAGIC;
header.version = DT_TILECACHE_VERSION;
// Tile layer location in the navmesh.
header.tx = TileX;
header.ty = TileY;
header.tlayer = i;
dtVcopy(header.bmin, layer->bmin);
dtVcopy(header.bmax, layer->bmax);
// Tile info.
header.width = (unsigned short)layer->width;
header.height = (unsigned short)layer->height;
header.minx = (unsigned short)layer->minx;
header.maxx = (unsigned short)layer->maxx;
header.miny = (unsigned short)layer->miny;
header.maxy = (unsigned short)layer->maxy;
header.hmin = (unsigned short)layer->hmin;
header.hmax = (unsigned short)layer->hmax;
// Layer bounds in unreal coords
FBox LayerBBox = Recast2UnrealBox(header.bmin, header.bmax);
LayerBBox.Min.Z -= StepHeights;
LayerBBox.Max.Z += StepHeights;
// Compress tile layer
uint8* TileData = NULL;
int32 TileDataSize = 0;
const dtStatus status = dtBuildTileCacheLayer(&TileCompressor, &header, layer->heights, layer->areas, layer->cons, &TileData, &TileDataSize);
if (dtStatusFailed(status))
{
dtFree(TileData);
BuildContext.log(RC_LOG_ERROR, "GenerateCompressedLayers: failed to build layer.");
return false;
}
#if !UE_BUILD_SHIPPING && OUTPUT_NAV_TILE_LAYER_COMPRESSION_DATA
else
{
const int gridSize = (int)header.width * (int)header.height;
const int bufferSize = gridSize * 4;
FPlatformMisc::CustomNamedStat("NavTileLayerUncompSize", static_cast<float>(bufferSize), "NavMesh", "Bytes");
FPlatformMisc::CustomNamedStat("NavTileLayerCompSize", static_cast<float>(TileDataSize), "NavMesh", "Bytes");
}
#endif
// copy compressed data to new buffer in rasterization context
// (TileData allocates a lots of space, but only first TileDataSize bytes hold compressed data)
uint8* CompressedData = (uint8*)dtAlloc(TileDataSize * sizeof(uint8), DT_ALLOC_PERM);
if (CompressedData == NULL)
{
dtFree(TileData);
BuildContext.log(RC_LOG_ERROR, "GenerateCompressedLayers: Out of memory 'CompressedData'.");
return false;
}
FMemory::Memcpy(CompressedData, TileData, TileDataSize);
RasterContext.Layers.Add(FNavMeshTileData(CompressedData, TileDataSize, i, LayerBBox));
dtFree(TileData);
const int32 UncompressedSize = ((sizeof(dtTileCacheLayerHeader)+3) & ~3) + (3 * header.width * header.height);
const float Inv1kB = 1.0f / 1024.0f;
BuildContext.log(RC_LOG_PROGRESS, ">> Cache[%d,%d:%d] = %.2fkB (full:%.2fkB rate:%.2f%%)", TileX, TileY, i,
TileDataSize * Inv1kB, UncompressedSize * Inv1kB, 1.0f * TileDataSize / UncompressedSize);
}
}
// Transfer final data
CompressedLayers = RasterContext.Layers;
return true;
}
struct FTileGenerationContext
{
FTileGenerationContext(dtTileCacheAlloc* MyAllocator) :
Allocator(MyAllocator), Layer(0), DistanceField(0), ContourSet(0), ClusterSet(0), PolyMesh(0), DetailMesh(0)
{
}
~FTileGenerationContext()
{
ResetIntermediateData();
}
void ResetIntermediateData()
{
dtFreeTileCacheLayer(Allocator, Layer);
Layer = 0;
dtFreeTileCacheDistanceField(Allocator, DistanceField);
DistanceField = 0;
dtFreeTileCacheContourSet(Allocator, ContourSet);
ContourSet = 0;
dtFreeTileCacheClusterSet(Allocator, ClusterSet);
ClusterSet = 0;
dtFreeTileCachePolyMesh(Allocator, PolyMesh);
PolyMesh = 0;
dtFreeTileCachePolyMeshDetail(Allocator, DetailMesh);
DetailMesh = 0;
// don't clear NavigationData here!
}
struct dtTileCacheAlloc* Allocator;
struct dtTileCacheLayer* Layer;
struct dtTileCacheDistanceField* DistanceField;
struct dtTileCacheContourSet* ContourSet;
struct dtTileCacheClusterSet* ClusterSet;
struct dtTileCachePolyMesh* PolyMesh;
struct dtTileCachePolyMeshDetail* DetailMesh;
TArray<FNavMeshTileData> NavigationData;
};
bool FRecastTileGenerator::GenerateNavigationData(FNavMeshBuildContext& BuildContext)
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildNavigation);
FTileCacheAllocator MyAllocator;
FTileCacheCompressor TileCompressor;
FTileGenerationContext GenerationContext(&MyAllocator);
GenerationContext.NavigationData.Reserve(CompressedLayers.Num());
dtStatus status = DT_SUCCESS;
for (int32 iLayer = 0; iLayer < CompressedLayers.Num(); iLayer++)
{
if (DirtyLayers[iLayer] == false || !CompressedLayers[iLayer].IsValid())
{
// skip layers not marked for rebuild
continue;
}
FNavMeshTileData& CompressedData = CompressedLayers[iLayer];
const dtTileCacheLayerHeader* TileHeader = (const dtTileCacheLayerHeader*)CompressedData.GetData();
GenerationContext.ResetIntermediateData();
// Decompress tile layer data.
status = dtDecompressTileCacheLayer(&MyAllocator, &TileCompressor, (unsigned char*)CompressedData.GetData(), CompressedData.DataSize, &GenerationContext.Layer);
if (dtStatusFailed(status))
{
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: failed to decompress layer.");
return false;
}
// Rasterize obstacles.
MarkDynamicAreas(*GenerationContext.Layer);
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildRegions)
// Build regions
if (TileConfig.TileCachePartitionType == RC_REGION_MONOTONE)
{
status = dtBuildTileCacheRegionsMonotone(&MyAllocator, TileConfig.minRegionArea, TileConfig.mergeRegionArea, *GenerationContext.Layer);
}
else if (TileConfig.TileCachePartitionType == RC_REGION_WATERSHED)
{
GenerationContext.DistanceField = dtAllocTileCacheDistanceField(&MyAllocator);
if (GenerationContext.DistanceField == NULL)
{
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: Out of memory 'DistanceField'.");
return false;
}
status = dtBuildTileCacheDistanceField(&MyAllocator, *GenerationContext.Layer, *GenerationContext.DistanceField);
if (dtStatusFailed(status))
{
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: Failed to build distance field.");
return false;
}
status = dtBuildTileCacheRegions(&MyAllocator, TileConfig.minRegionArea, TileConfig.mergeRegionArea, *GenerationContext.Layer, *GenerationContext.DistanceField);
}
else
{
status = dtBuildTileCacheRegionsChunky(&MyAllocator, TileConfig.minRegionArea, TileConfig.mergeRegionArea, *GenerationContext.Layer, TileConfig.TileCacheChunkSize);
}
if (dtStatusFailed(status))
{
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: Failed to build regions.");
return false;
}
// skip empty layer
if (GenerationContext.Layer->regCount <= 0)
{
continue;
}
}
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildContours);
// Build contour set
GenerationContext.ContourSet = dtAllocTileCacheContourSet(&MyAllocator);
if (GenerationContext.ContourSet == NULL)
{
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: Out of memory 'ContourSet'.");
return false;
}
GenerationContext.ClusterSet = dtAllocTileCacheClusterSet(&MyAllocator);
if (GenerationContext.ClusterSet == NULL)
{
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: Out of memory 'ClusterSet'.");
return false;
}
status = dtBuildTileCacheContours(&MyAllocator, *GenerationContext.Layer,
TileConfig.walkableClimb, TileConfig.maxSimplificationError, TileConfig.cs, TileConfig.ch,
*GenerationContext.ContourSet, *GenerationContext.ClusterSet);
if (dtStatusFailed(status))
{
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: Failed to generate contour set (0x%08X).", status);
return false;
}
// skip empty layer, sometimes there are regions assigned but all flagged as empty (id=0)
if (GenerationContext.ContourSet->nconts <= 0)
{
continue;
}
}
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildPolyMesh);
// Build poly mesh
GenerationContext.PolyMesh = dtAllocTileCachePolyMesh(&MyAllocator);
if (GenerationContext.PolyMesh == NULL)
{
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: Out of memory 'PolyMesh'.");
return false;
}
status = dtBuildTileCachePolyMesh(&MyAllocator, &BuildContext, *GenerationContext.ContourSet, *GenerationContext.PolyMesh);
if (dtStatusFailed(status))
{
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: Failed to generate poly mesh.");
return false;
}
status = dtBuildTileCacheClusters(&MyAllocator, *GenerationContext.ClusterSet, *GenerationContext.PolyMesh);
if (dtStatusFailed(status))
{
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: Failed to update cluster set.");
return false;
}
}
// Build detail mesh
if (TileConfig.bGenerateDetailedMesh)
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildPolyDetail);
// Build detail mesh.
GenerationContext.DetailMesh = dtAllocTileCachePolyMeshDetail(&MyAllocator);
if (GenerationContext.DetailMesh == NULL)
{
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: Out of memory 'DetailMesh'.");
return false;
}
status = dtBuildTileCachePolyMeshDetail(&MyAllocator, TileConfig.cs, TileConfig.ch, TileConfig.detailSampleDist, TileConfig.detailSampleMaxError,
*GenerationContext.Layer, *GenerationContext.PolyMesh, *GenerationContext.DetailMesh);
if (dtStatusFailed(status))
{
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: Failed to generate poly detail mesh.");
return false;
}
}
unsigned char* NavData = 0;
int32 NavDataSize = 0;
if (TileConfig.maxVertsPerPoly <= DT_VERTS_PER_POLYGON &&
GenerationContext.PolyMesh->npolys > 0 && GenerationContext.PolyMesh->nverts > 0)
{
ensure(GenerationContext.PolyMesh->npolys <= TileConfig.MaxPolysPerTile && "Polys per Tile limit exceeded!");
if (GenerationContext.PolyMesh->nverts >= 0xffff)
{
// The vertex indices are ushorts, and cannot point to more than 0xffff vertices.
BuildContext.log(RC_LOG_ERROR, "Too many vertices per tile %d (max: %d).", GenerationContext.PolyMesh->nverts, 0xffff);
return false;
}
// if we didn't fail already then it's high time we created data for off-mesh links
FOffMeshData OffMeshData;
if (OffmeshLinks.Num() > 0)
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastGatherOffMeshData);
OffMeshData.Reserve(OffmeshLinks.Num());
OffMeshData.AreaClassToIdMap = &AdditionalCachedData.AreaClassToIdMap;
OffMeshData.FlagsPerArea = AdditionalCachedData.FlagsPerOffMeshLinkArea;
const FSimpleLinkNavModifier* LinkModifier = OffmeshLinks.GetData();
const float DefaultSnapHeight = TileConfig.walkableClimb * TileConfig.ch;
for (int32 LinkModifierIndex = 0; LinkModifierIndex < OffmeshLinks.Num(); ++LinkModifierIndex, ++LinkModifier)
{
OffMeshData.AddLinks(LinkModifier->Links, LinkModifier->LocalToWorld, TileConfig.AgentIndex, DefaultSnapHeight);
#if GENERATE_SEGMENT_LINKS
OffMeshData.AddSegmentLinks(LinkModifier->SegmentLinks, LinkModifier->LocalToWorld, TileConfig.AgentIndex, DefaultSnapHeight);
#endif // GENERATE_SEGMENT_LINKS
}
}
// fill flags, or else detour won't be able to find polygons
// Update poly flags from areas.
for (int32 i = 0; i < GenerationContext.PolyMesh->npolys; i++)
{
GenerationContext.PolyMesh->flags[i] = AdditionalCachedData.FlagsPerArea[GenerationContext.PolyMesh->areas[i]];
}
dtNavMeshCreateParams Params;
memset(&Params, 0, sizeof(Params));
Params.verts = GenerationContext.PolyMesh->verts;
Params.vertCount = GenerationContext.PolyMesh->nverts;
Params.polys = GenerationContext.PolyMesh->polys;
Params.polyAreas = GenerationContext.PolyMesh->areas;
Params.polyFlags = GenerationContext.PolyMesh->flags;
Params.polyCount = GenerationContext.PolyMesh->npolys;
Params.nvp = GenerationContext.PolyMesh->nvp;
if (TileConfig.bGenerateDetailedMesh)
{
Params.detailMeshes = GenerationContext.DetailMesh->meshes;
Params.detailVerts = GenerationContext.DetailMesh->verts;
Params.detailVertsCount = GenerationContext.DetailMesh->nverts;
Params.detailTris = GenerationContext.DetailMesh->tris;
Params.detailTriCount = GenerationContext.DetailMesh->ntris;
}
Params.offMeshCons = OffMeshData.LinkParams.GetData();
Params.offMeshConCount = OffMeshData.LinkParams.Num();
Params.walkableHeight = TileConfig.AgentHeight;
Params.walkableRadius = TileConfig.AgentRadius;
Params.walkableClimb = TileConfig.AgentMaxClimb;
Params.tileX = TileX;
Params.tileY = TileY;
Params.tileLayer = iLayer;
rcVcopy(Params.bmin, GenerationContext.Layer->header->bmin);
rcVcopy(Params.bmax, GenerationContext.Layer->header->bmax);
Params.cs = TileConfig.cs;
Params.ch = TileConfig.ch;
Params.buildBvTree = TileConfig.bGenerateBVTree;
#if GENERATE_CLUSTER_LINKS
Params.clusterCount = GenerationContext.ClusterSet->nclusters;
Params.polyClusters = GenerationContext.ClusterSet->polyMap;
#endif
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastCreateNavMeshData);
if (!dtCreateNavMeshData(&Params, &NavData, &NavDataSize))
{
BuildContext.log(RC_LOG_ERROR, "Could not build Detour navmesh.");
return false;
}
}
GenerationContext.NavigationData.Add(FNavMeshTileData(NavData, NavDataSize, iLayer, CompressedData.LayerBBox));
const float ModkB = 1.0f / 1024.0f;
BuildContext.log(RC_LOG_PROGRESS, ">> Layer[%d] = Verts(%d) Polys(%d) Memory(%.2fkB) Cache(%.2fkB)",
iLayer, GenerationContext.PolyMesh->nverts, GenerationContext.PolyMesh->npolys,
GenerationContext.NavigationData.Last().DataSize * ModkB, CompressedLayers[iLayer].DataSize * ModkB);
}
// prepare navigation data of actually rebuild layers for transfer
NavigationData = GenerationContext.NavigationData;
return true;
}
void FRecastTileGenerator::MarkDynamicAreas(dtTileCacheLayer& Layer)
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastMarkAreas);
if (Modifiers.Num())
{
if (AdditionalCachedData.bUseSortFunction && AdditionalCachedData.ActorOwner && Modifiers.Num() > 1)
{
AdditionalCachedData.ActorOwner->SortAreasForGenerator(Modifiers);
}
// 1: if navmesh is using low areas, apply only low area replacements
if (TileConfig.bMarkLowHeightAreas)
{
const int32 LowAreaId = RECAST_LOW_AREA;
for (int32 ModIdx = 0; ModIdx < Modifiers.Num(); ModIdx++)
{
FRecastAreaNavModifierElement& Element = Modifiers[ModIdx];
for (int32 AreaIdx = Element.Areas.Num() - 1; AreaIdx >= 0; AreaIdx--)
{
const FAreaNavModifier& AreaMod = Element.Areas[AreaIdx];
if (AreaMod.GetApplyMode() == ENavigationAreaMode::ApplyInLowPass ||
AreaMod.GetApplyMode() == ENavigationAreaMode::ReplaceInLowPass)
{
const int32* AreaIDPtr = AdditionalCachedData.AreaClassToIdMap.Find(AreaMod.GetAreaClass());
// replace area will be fixed as LowAreaId during this pass, regardless settings in area modifier
const int32* ReplaceAreaIDPtr = (AreaMod.GetApplyMode() == ENavigationAreaMode::ReplaceInLowPass) ? &LowAreaId : nullptr;
if (AreaIDPtr != nullptr)
{
for (const FTransform& LocalToWorld : Element.PerInstanceTransform)
{
MarkDynamicArea(AreaMod, LocalToWorld, Layer, *AreaIDPtr, ReplaceAreaIDPtr);
}
if (Element.PerInstanceTransform.Num() == 0)
{
MarkDynamicArea(AreaMod, FTransform::Identity, Layer, *AreaIDPtr, ReplaceAreaIDPtr);
}
}
}
}
}
// 2. remove all low area marking
dtReplaceArea(Layer, RECAST_NULL_AREA, RECAST_LOW_AREA);
}
// 3. apply remaining modifiers
for (const FRecastAreaNavModifierElement& Element : Modifiers)
{
for (const FAreaNavModifier& Area : Element.Areas)
{
if (Area.GetApplyMode() == ENavigationAreaMode::ApplyInLowPass || Area.GetApplyMode() == ENavigationAreaMode::ReplaceInLowPass)
{
continue;
}
const int32* AreaIDPtr = AdditionalCachedData.AreaClassToIdMap.Find(Area.GetAreaClass());
const int32* ReplaceIDPtr = (Area.GetApplyMode() == ENavigationAreaMode::Replace) && Area.GetAreaClassToReplace() ?
AdditionalCachedData.AreaClassToIdMap.Find(Area.GetAreaClassToReplace()) : nullptr;
if (AreaIDPtr)
{
for (const FTransform& LocalToWorld : Element.PerInstanceTransform)
{
MarkDynamicArea(Area, LocalToWorld, Layer, *AreaIDPtr, ReplaceIDPtr);
}
if (Element.PerInstanceTransform.Num() == 0)
{
MarkDynamicArea(Area, FTransform::Identity, Layer, *AreaIDPtr, ReplaceIDPtr);
}
}
}
}
}
else
{
if (TileConfig.bMarkLowHeightAreas)
{
dtReplaceArea(Layer, RECAST_NULL_AREA, RECAST_LOW_AREA);
}
}
}
void FRecastTileGenerator::MarkDynamicArea(const FAreaNavModifier& Modifier, const FTransform& LocalToWorld, dtTileCacheLayer& Layer)
{
const int32* AreaIDPtr = AdditionalCachedData.AreaClassToIdMap.Find(Modifier.GetAreaClass());
const int32* ReplaceIDPtr = Modifier.GetAreaClassToReplace() ? AdditionalCachedData.AreaClassToIdMap.Find(Modifier.GetAreaClassToReplace()) : nullptr;
if (AreaIDPtr)
{
MarkDynamicArea(Modifier, LocalToWorld, Layer, *AreaIDPtr, ReplaceIDPtr);
}
}
void FRecastTileGenerator::MarkDynamicArea(const FAreaNavModifier& Modifier, const FTransform& LocalToWorld, dtTileCacheLayer& Layer, const int32 AreaID, const int32* ReplaceIDPtr)
{
const float ExpandBy = TileConfig.AgentRadius;
// Expand by 1 cell height up and down to cover for voxel grid inaccuracy
const float OffsetZMax = TileConfig.ch;
const float OffsetZMin = TileConfig.ch + (Modifier.ShouldIncludeAgentHeight() ? TileConfig.AgentHeight : 0.0f);
// Check whether modifier affects this layer
const FBox LayerUnrealBounds = Recast2UnrealBox(Layer.header->bmin, Layer.header->bmax);
FBox ModifierBounds = Modifier.GetBounds().TransformBy(LocalToWorld);
ModifierBounds.Min -= FVector(ExpandBy, ExpandBy, OffsetZMin);
ModifierBounds.Max += FVector(ExpandBy, ExpandBy, OffsetZMax);
if (!LayerUnrealBounds.Intersect(ModifierBounds))
{
return;
}
const float* LayerRecastOrig = Layer.header->bmin;
switch (Modifier.GetShapeType())
{
case ENavigationShapeType::Cylinder:
{
FCylinderNavAreaData CylinderData;
Modifier.GetCylinder(CylinderData);
// Only scaling and translation
FVector Scale3D = LocalToWorld.GetScale3D().GetAbs();
CylinderData.Height *= Scale3D.Z;
CylinderData.Radius *= FMath::Max(Scale3D.X, Scale3D.Y);
CylinderData.Origin = LocalToWorld.TransformPosition(CylinderData.Origin);
const float OffsetZMid = (OffsetZMax - OffsetZMin) * 0.5f;
CylinderData.Origin.Z += OffsetZMid;
CylinderData.Height += FMath::Abs(OffsetZMid) * 2.f;
CylinderData.Radius += ExpandBy;
FVector RecastPos = Unreal2RecastPoint(CylinderData.Origin);
if (ReplaceIDPtr)
{
dtReplaceCylinderArea(Layer, LayerRecastOrig, TileConfig.cs, TileConfig.ch,
&(RecastPos.X), CylinderData.Radius, CylinderData.Height, AreaID, *ReplaceIDPtr);
}
else
{
dtMarkCylinderArea(Layer, LayerRecastOrig, TileConfig.cs, TileConfig.ch,
&(RecastPos.X), CylinderData.Radius, CylinderData.Height, AreaID);
}
}
break;
case ENavigationShapeType::Box:
{
FBoxNavAreaData BoxData;
Modifier.GetBox(BoxData);
FBox WorldBox = FBox::BuildAABB(BoxData.Origin, BoxData.Extent).TransformBy(LocalToWorld);
WorldBox = WorldBox.ExpandBy(FVector(ExpandBy, ExpandBy, 0));
WorldBox.Min.Z -= OffsetZMin;
WorldBox.Max.Z += OffsetZMax;
FBox RacastBox = Unreal2RecastBox(WorldBox);
FVector RecastPos;
FVector RecastExtent;
RacastBox.GetCenterAndExtents(RecastPos, RecastExtent);
if (ReplaceIDPtr)
{
dtReplaceBoxArea(Layer, LayerRecastOrig, TileConfig.cs, TileConfig.ch,
&(RecastPos.X), &(RecastExtent.X), AreaID, *ReplaceIDPtr);
}
else
{
dtMarkBoxArea(Layer, LayerRecastOrig, TileConfig.cs, TileConfig.ch,
&(RecastPos.X), &(RecastExtent.X), AreaID);
}
}
break;
case ENavigationShapeType::Convex:
{
FConvexNavAreaData ConvexData;
Modifier.GetConvex(ConvexData);
// Only scaling and translation
PartialTransformConvexHull(ConvexData, LocalToWorld);
TArray<FVector> ConvexVerts;
GrowConvexHull(ExpandBy, ConvexData.Points, ConvexVerts);
ConvexData.MinZ -= OffsetZMin;
ConvexData.MaxZ += OffsetZMax;
if (ConvexVerts.Num())
{
TArray<float> ConvexCoords;
ConvexCoords.AddZeroed(ConvexVerts.Num() * 3);
float* ItCoord = ConvexCoords.GetData();
for (int32 i = 0; i < ConvexVerts.Num(); i++)
{
const FVector RecastV = Unreal2RecastPoint(ConvexVerts[i]);
*ItCoord = RecastV.X; ItCoord++;
*ItCoord = RecastV.Y; ItCoord++;
*ItCoord = RecastV.Z; ItCoord++;
}
if (ReplaceIDPtr)
{
dtReplaceConvexArea(Layer, LayerRecastOrig, TileConfig.cs, TileConfig.ch,
ConvexCoords.GetData(), ConvexVerts.Num(), ConvexData.MinZ, ConvexData.MaxZ, AreaID, *ReplaceIDPtr);
}
else
{
dtMarkConvexArea(Layer, LayerRecastOrig, TileConfig.cs, TileConfig.ch,
ConvexCoords.GetData(), ConvexVerts.Num(), ConvexData.MinZ, ConvexData.MaxZ, AreaID);
}
}
}
break;
default: break;
}
}
uint32 FRecastTileGenerator::GetUsedMemCount() const
{
uint32 TotalMemory = 0;
TotalMemory += InclusionBounds.GetAllocatedSize();
TotalMemory += Modifiers.GetAllocatedSize();
TotalMemory += OffmeshLinks.GetAllocatedSize();
TotalMemory += RawGeometry.GetAllocatedSize();
for (const FRecastRawGeometryElement& Element : RawGeometry)
{
TotalMemory += Element.GeomCoords.GetAllocatedSize();
TotalMemory += Element.GeomIndices.GetAllocatedSize();
TotalMemory += Element.PerInstanceTransform.GetAllocatedSize();
}
for (const FRecastAreaNavModifierElement& Element : Modifiers)
{
TotalMemory += Element.Areas.GetAllocatedSize();
TotalMemory += Element.PerInstanceTransform.GetAllocatedSize();
}
const FSimpleLinkNavModifier* SimpleLink = OffmeshLinks.GetData();
for (int32 Index = 0; Index < OffmeshLinks.Num(); ++Index, ++SimpleLink)
{
TotalMemory += SimpleLink->Links.GetAllocatedSize();
}
TotalMemory += CompressedLayers.GetAllocatedSize();
for (int32 i = 0; i < CompressedLayers.Num(); i++)
{
TotalMemory += CompressedLayers[i].DataSize;
}
TotalMemory += NavigationData.GetAllocatedSize();
for (int32 i = 0; i < NavigationData.Num(); i++)
{
TotalMemory += NavigationData[i].DataSize;
}
return TotalMemory;
}
void FRecastTileGenerator::AddReferencedObjects(FReferenceCollector& Collector)
{
for (auto& RelevantData : NavigationRelevantData)
{
UObject* Owner = RelevantData->GetOwner();
if (Owner)
{
Collector.AddReferencedObject(Owner);
}
}
}
FString FRecastTileGenerator::GetReferencerName() const
{
return TEXT("FRecastTileGenerator");
}
static int32 CaclulateMaxTilesCount(const TNavStatArray<FBox>& NavigableAreas, float TileSizeinWorldUnits, float AvgLayersPerGridCell)
{
int32 GridCellsCount = 0;
for (FBox AreaBounds : NavigableAreas)
{
// TODO: need more precise calculation, currently we don't take into account that volumes can be overlapped
FBox RCBox = Unreal2RecastBox(AreaBounds);
int32 XSize = FMath::CeilToInt(RCBox.GetSize().X/TileSizeinWorldUnits) + 1;
int32 YSize = FMath::CeilToInt(RCBox.GetSize().Z/TileSizeinWorldUnits) + 1;
GridCellsCount+= (XSize*YSize);
}
return FMath::CeilToInt(GridCellsCount * AvgLayersPerGridCell);
}
// Whether navmesh is static, does not support rebuild from geometry
static bool IsGameStaticNavMesh(ARecastNavMesh* InNavMesh)
{
return (InNavMesh->GetWorld()->IsGameWorld() && InNavMesh->GetRuntimeGenerationMode() != ERuntimeGenerationType::Dynamic);
}
//----------------------------------------------------------------------//
// FRecastNavMeshGenerator
//----------------------------------------------------------------------//
FRecastNavMeshGenerator::FRecastNavMeshGenerator(ARecastNavMesh& InDestNavMesh)
: NumActiveTiles(0)
, MaxTileGeneratorTasks(1)
, AvgLayersPerTile(8.0f)
, DestNavMesh(&InDestNavMesh)
, bInitialized(false)
, bRestrictBuildingToActiveTiles(false)
, Version(0)
{
#if TIME_SLICE_NAV_REGEN
TimeSliceDuration = 0.0025;
#endif
INC_DWORD_STAT_BY(STAT_NavigationMemory, sizeof(*this));
Init();
int32 MaxTiles = 0;
int32 MaxPolysPerTile = 0;
// recreate navmesh if no data was loaded, or when loaded data doesn't match current grid layout
bool bRecreateNavmesh = true;
if (DestNavMesh->HasValidNavmesh())
{
const bool bGameStaticNavMesh = IsGameStaticNavMesh(DestNavMesh);
const dtNavMeshParams* SavedNavParams = DestNavMesh->GetRecastNavMeshImpl()->DetourNavMesh->getParams();
if (SavedNavParams)
{
if (bGameStaticNavMesh)
{
bRecreateNavmesh = false;
MaxTiles = SavedNavParams->maxTiles;
MaxPolysPerTile = SavedNavParams->maxPolys;
}
else
{
const float TileDim = Config.tileSize * Config.cs;
if (SavedNavParams->tileHeight == TileDim && SavedNavParams->tileWidth == TileDim)
{
const FVector Orig = Recast2UnrealPoint(SavedNavParams->orig);
const FVector OrigError(FMath::Fmod(Orig.X, TileDim), FMath::Fmod(Orig.Y, TileDim), FMath::Fmod(Orig.Z, TileDim));
if (OrigError.IsNearlyZero())
{
bRecreateNavmesh = false;
}
else
{
UE_LOG(LogNavigation, Warning, TEXT("Recreating dtNavMesh instance due to saved navmesh origin (%s, usually the RecastNavMesh location) not being aligned with tile size (%d uu) ")
, *Orig.ToString(), int(TileDim));
}
}
// if new navmesh needs more tiles, force recreating
if (!bRecreateNavmesh)
{
CalcNavMeshProperties(MaxTiles, MaxPolysPerTile);
if (FMath::Log2(MaxTiles) != FMath::Log2(SavedNavParams->maxTiles))
{
bRecreateNavmesh = true;
UE_LOG(LogNavigation, Warning, TEXT("Recreating dtNavMesh instance due mismatch in number of bytes required to store serialized maxTiles (%d, %d bits) vs calculated maxtiles (%d, %d bits)")
, SavedNavParams->maxTiles, FMath::CeilToInt(FMath::Log2(SavedNavParams->maxTiles))
, MaxTiles, FMath::CeilToInt(FMath::Log2(MaxTiles)));
}
}
}
};
}
if (bRecreateNavmesh)
{
// recreate navmesh from scratch if no data was loaded
ConstructTiledNavMesh();
// mark all the areas we need to update, which is the whole (known) navigable space if not restricted to active tiles
const UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
if (NavSys)
{
bRestrictBuildingToActiveTiles = NavSys->IsActiveTilesGenerationEnabled();
}
MarkNavBoundsDirty();
}
else
{
// otherwise just update generator params
Config.MaxPolysPerTile = MaxPolysPerTile;
NumActiveTiles = GetTilesCountHelper(DestNavMesh->GetRecastNavMeshImpl()->DetourNavMesh);
}
}
FRecastNavMeshGenerator::~FRecastNavMeshGenerator()
{
DEC_DWORD_STAT_BY( STAT_NavigationMemory, sizeof(*this) );
}
void FRecastNavMeshGenerator::ConfigureBuildProperties(FRecastBuildConfig& OutConfig)
{
// @TODO those variables should be tweakable per navmesh actor
const float CellSize = DestNavMesh->CellSize;
const float CellHeight = DestNavMesh->CellHeight;
const float AgentHeight = DestNavMesh->AgentHeight;
const float MaxAgentHeight = DestNavMesh->AgentMaxHeight;
const float AgentMaxSlope = DestNavMesh->AgentMaxSlope;
const float AgentMaxClimb = DestNavMesh->AgentMaxStepHeight;
const float AgentRadius = DestNavMesh->AgentRadius;
OutConfig.Reset();
OutConfig.cs = CellSize;
OutConfig.ch = CellHeight;
OutConfig.walkableSlopeAngle = AgentMaxSlope;
OutConfig.walkableHeight = (int32)ceilf(AgentHeight / CellHeight);
OutConfig.walkableClimb = (int32)ceilf(AgentMaxClimb / CellHeight);
const float WalkableRadius = FMath::CeilToFloat(AgentRadius / CellSize);
OutConfig.walkableRadius = WalkableRadius;
// store original sizes
OutConfig.AgentHeight = AgentHeight;
OutConfig.AgentMaxClimb = AgentMaxClimb;
OutConfig.AgentRadius = AgentRadius;
OutConfig.borderSize = WalkableRadius + 3;
OutConfig.maxEdgeLen = (int32)(1200.0f / CellSize);
OutConfig.maxSimplificationError = 1.3f;
// hardcoded, but can be overridden by RecastNavMesh params later
OutConfig.minRegionArea = (int32)rcSqr(0);
OutConfig.mergeRegionArea = (int32)rcSqr(20.f);
OutConfig.maxVertsPerPoly = (int32)MAX_VERTS_PER_POLY;
OutConfig.detailSampleDist = 600.0f;
OutConfig.detailSampleMaxError = 1.0f;
OutConfig.PolyMaxHeight = (int32)ceilf(MaxAgentHeight / CellHeight);
OutConfig.minRegionArea = (int32)rcSqr(DestNavMesh->MinRegionArea / CellSize);
OutConfig.mergeRegionArea = (int32)rcSqr(DestNavMesh->MergeRegionSize / CellSize);
OutConfig.maxSimplificationError = DestNavMesh->MaxSimplificationError;
OutConfig.bPerformVoxelFiltering = DestNavMesh->bPerformVoxelFiltering;
OutConfig.bMarkLowHeightAreas = DestNavMesh->bMarkLowHeightAreas;
OutConfig.bFilterLowSpanSequences = DestNavMesh->bFilterLowSpanSequences;
OutConfig.bFilterLowSpanFromTileCache = DestNavMesh->bFilterLowSpanFromTileCache;
if (DestNavMesh->bMarkLowHeightAreas)
{
OutConfig.walkableHeight = 1;
}
const UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
OutConfig.AgentIndex = NavSys->GetSupportedAgentIndex(DestNavMesh);
OutConfig.tileSize = FMath::TruncToInt(DestNavMesh->TileSizeUU / CellSize);
OutConfig.regionChunkSize = OutConfig.tileSize / DestNavMesh->LayerChunkSplits;
OutConfig.TileCacheChunkSize = OutConfig.tileSize / DestNavMesh->RegionChunkSplits;
OutConfig.regionPartitioning = DestNavMesh->LayerPartitioning;
OutConfig.TileCachePartitionType = DestNavMesh->RegionPartitioning;
}
void FRecastNavMeshGenerator::Init()
{
check(DestNavMesh);
ConfigureBuildProperties(Config);
BBoxGrowth = FVector(2.0f * Config.borderSize * Config.cs);
RcNavMeshOrigin = Unreal2RecastPoint(DestNavMesh->NavMeshOriginOffset);
AdditionalCachedData = FRecastNavMeshCachedData::Construct(DestNavMesh);
if (Config.MaxPolysPerTile <= 0 && DestNavMesh->HasValidNavmesh())
{
const dtNavMeshParams* SavedNavParams = DestNavMesh->GetRecastNavMeshImpl()->DetourNavMesh->getParams();
if (SavedNavParams)
{
Config.MaxPolysPerTile = SavedNavParams->maxPolys;
}
}
UpdateNavigationBounds();
/** setup maximum number of active tile generator*/
const int32 NumberOfWorkerThreads = FTaskGraphInterface::Get().GetNumWorkerThreads();
MaxTileGeneratorTasks = FMath::Min(FMath::Max(NumberOfWorkerThreads * 2, 1), GetOwner() ? GetOwner()->GetMaxSimultaneousTileGenerationJobsCount() : INT_MAX);
UE_LOG(LogNavigation, Log, TEXT("Using max of %d workers to build navigation."), MaxTileGeneratorTasks);
NumActiveTiles = 0;
// prepare voxel cache if needed
if (ARecastNavMesh::IsVoxelCacheEnabled())
{
VoxelCacheContext.Create(Config.tileSize + Config.borderSize * 2, Config.cs, Config.ch);
}
bInitialized = true;
}
void FRecastNavMeshGenerator::UpdateNavigationBounds()
{
const UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
if (NavSys)
{
if (NavSys->ShouldGenerateNavigationEverywhere() == false)
{
FBox BoundsSum(ForceInit);
if (DestNavMesh)
{
TArray<FBox> SupportedBounds;
NavSys->GetNavigationBoundsForNavData(*DestNavMesh, SupportedBounds);
InclusionBounds.Reset(SupportedBounds.Num());
for (const FBox& Box : SupportedBounds)
{
InclusionBounds.Add(Box);
BoundsSum += Box;
}
}
TotalNavBounds = BoundsSum;
}
else
{
InclusionBounds.Reset(1);
TotalNavBounds = NavSys->GetWorldBounds();
if (!TotalNavBounds.IsValid)
{
InclusionBounds.Add(TotalNavBounds);
}
}
}
else
{
TotalNavBounds = FBox(ForceInit);
}
}
bool FRecastNavMeshGenerator::ConstructTiledNavMesh()
{
bool bSuccess = false;
// There is should not be any active build tasks
CancelBuild();
// create new Detour navmesh instance
dtNavMesh* DetourMesh = dtAllocNavMesh();
if (DetourMesh)
{
++Version;
dtNavMeshParams TiledMeshParameters;
FMemory::Memzero(TiledMeshParameters);
rcVcopy(TiledMeshParameters.orig, &RcNavMeshOrigin.X);
TiledMeshParameters.tileWidth = Config.tileSize * Config.cs;
TiledMeshParameters.tileHeight = Config.tileSize * Config.cs;
CalcNavMeshProperties(TiledMeshParameters.maxTiles, TiledMeshParameters.maxPolys);
Config.MaxPolysPerTile = TiledMeshParameters.maxPolys;
if (TiledMeshParameters.maxTiles == 0)
{
UE_LOG(LogNavigation, Warning, TEXT("ConstructTiledNavMesh: Failed to create navmesh of size 0."));
bSuccess = false;
}
else
{
const dtStatus status = DetourMesh->init(&TiledMeshParameters);
if (dtStatusFailed(status))
{
UE_LOG(LogNavigation, Warning, TEXT("ConstructTiledNavMesh: Could not init navmesh."));
bSuccess = false;
}
else
{
bSuccess = true;
NumActiveTiles = GetTilesCountHelper(DetourMesh);
DestNavMesh->GetRecastNavMeshImpl()->SetRecastMesh(DetourMesh);
}
}
if (bSuccess == false)
{
dtFreeNavMesh(DetourMesh);
}
}
else
{
UE_LOG(LogNavigation, Warning, TEXT("ConstructTiledNavMesh: Could not allocate navmesh.") );
bSuccess = false;
}
return bSuccess;
}
void FRecastNavMeshGenerator::CalcPolyRefBits(ARecastNavMesh* NavMeshOwner, int32& MaxTileBits, int32& MaxPolyBits)
{
static const int32 TotalBits = (sizeof(dtPolyRef) * 8);
#if USE_64BIT_ADDRESS
MaxTileBits = NavMeshOwner ? FMath::CeilToFloat(FMath::Log2(NavMeshOwner->GetTileNumberHardLimit())) : 20;
MaxPolyBits = FMath::Min<int32>(32, (TotalBits - DT_MIN_SALT_BITS) - MaxTileBits);
#else
MaxTileBits = 14;
MaxPolyBits = (TotalBits - DT_MIN_SALT_BITS) - MaxTileBits;
#endif//USE_64BIT_ADDRESS
}
void FRecastNavMeshGenerator::CalcNavMeshProperties(int32& MaxTiles, int32& MaxPolys)
{
int32 MaxTileBits = -1;
int32 MaxPolyBits = -1;
// limit max amount of tiles
CalcPolyRefBits(DestNavMesh, MaxTileBits, MaxPolyBits);
const int32 MaxTilesFromMask = (1 << MaxTileBits);
int32 MaxRequestedTiles = 0;
if (DestNavMesh->IsResizable())
{
MaxRequestedTiles = CaclulateMaxTilesCount(InclusionBounds, Config.tileSize * Config.cs, AvgLayersPerTile);
}
else
{
MaxRequestedTiles = DestNavMesh->TilePoolSize;
}
if (MaxRequestedTiles < 0 || MaxRequestedTiles > MaxTilesFromMask)
{
UE_LOG(LogNavigation, Error, TEXT("Navmesh bounds are too large! Limiting requested tiles count (%d) to: (%d)"), MaxRequestedTiles, MaxTilesFromMask);
MaxRequestedTiles = MaxTilesFromMask;
}
// Max tiles and max polys affect how the tile IDs are calculated.
// There are (sizeof(dtPolyRef)*8 - DT_MIN_SALT_BITS) bits available for
// identifying a tile and a polygon.
#if USE_64BIT_ADDRESS
MaxPolys = (MaxPolyBits >= 32) ? INT_MAX : (1 << MaxPolyBits);
#else
MaxPolys = 1 << ((sizeof(dtPolyRef) * 8 - DT_MIN_SALT_BITS) - MaxTileBits);
#endif // USE_64BIT_ADDRESS
MaxTiles = MaxRequestedTiles;
}
bool FRecastNavMeshGenerator::RebuildAll()
{
DestNavMesh->UpdateNavVersion();
// Recreate recast navmesh
DestNavMesh->GetRecastNavMeshImpl()->ReleaseDetourNavMesh();
RcNavMeshOrigin = Unreal2RecastPoint(DestNavMesh->NavMeshOriginOffset);
ConstructTiledNavMesh();
if (MarkNavBoundsDirty() == false)
{
// There are no navigation bounds to build, probably navmesh was resized and we just need to update debug draw
DestNavMesh->RequestDrawingUpdate();
}
return true;
}
void FRecastNavMeshGenerator::EnsureBuildCompletion()
{
const bool bHadTasks = GetNumRemaningBuildTasks() > 0;
const bool bDoAsyncDataGathering = (GatherGeometryOnGameThread() == false);
do
{
const int32 NumTasksToProcess = (bDoAsyncDataGathering ? 1 : MaxTileGeneratorTasks) - RunningDirtyTiles.Num();
ProcessTileTasks(NumTasksToProcess);
// Block until tasks are finished
for (FRunningTileElement& Element : RunningDirtyTiles)
{
Element.AsyncTask->EnsureCompletion();
}
}
while (GetNumRemaningBuildTasks() > 0);
// Update navmesh drawing only if we had something to build
if (bHadTasks)
{
DestNavMesh->RequestDrawingUpdate();
}
}
void FRecastNavMeshGenerator::CancelBuild()
{
DiscardCurrentBuildingTasks();
#if WITH_EDITOR
RecentlyBuiltTiles.Empty();
#endif//WITH_EDITOR
}
void FRecastNavMeshGenerator::TickAsyncBuild(float DeltaSeconds)
{
bool bRequestDrawingUpdate = false;
#if WITH_EDITOR
// Remove expired tiles
{
const double Timestamp = FPlatformTime::Seconds();
const int32 NumPreRemove = RecentlyBuiltTiles.Num();
RecentlyBuiltTiles.RemoveAllSwap([&](const FTileTimestamp& Tile) { return (Timestamp - Tile.Timestamp) > 0.5; });
const int32 NumPostRemove = RecentlyBuiltTiles.Num();
bRequestDrawingUpdate = (NumPreRemove != NumPostRemove);
}
#endif//WITH_EDITOR
// Submit async tile build tasks in case we have dirty tiles and have room for them
const UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
check(NavSys);
const int32 NumRunningTasks = NavSys->GetNumRunningBuildTasks();
// this is a temp solution to enforce only one worker thread if GatherGeometryOnGameThread == false
// due to missing safety features
const bool bDoAsyncDataGathering = GatherGeometryOnGameThread() == false;
const int32 NumTasksToSubmit = (bDoAsyncDataGathering ? 1 : MaxTileGeneratorTasks) - NumRunningTasks;
TArray<uint32> UpdatedTileIndices = ProcessTileTasks(NumTasksToSubmit);
if (UpdatedTileIndices.Num() > 0)
{
// Invalidate active paths that go through regenerated tiles
DestNavMesh->OnNavMeshTilesUpdated(UpdatedTileIndices);
bRequestDrawingUpdate = true;
#if WITH_EDITOR
// Store completed tiles with timestamps to have ability to distinguish during debug draw
const double Timestamp = FPlatformTime::Seconds();
RecentlyBuiltTiles.Reserve(RecentlyBuiltTiles.Num() + UpdatedTileIndices.Num());
for (uint32 TiledIdx : UpdatedTileIndices)
{
FTileTimestamp TileTimestamp;
TileTimestamp.TileIdx = TiledIdx;
TileTimestamp.Timestamp = Timestamp;
RecentlyBuiltTiles.Add(TileTimestamp);
}
#endif//WITH_EDITOR
}
if (bRequestDrawingUpdate)
{
DestNavMesh->RequestDrawingUpdate();
}
}
void FRecastNavMeshGenerator::OnNavigationBoundsChanged()
{
UpdateNavigationBounds();
dtNavMesh* DetourMesh = DestNavMesh->GetRecastNavMeshImpl()->GetRecastMesh();
if (!IsGameStaticNavMesh(DestNavMesh) && DestNavMesh->IsResizable() && DetourMesh)
{
// Check whether Navmesh size needs to be changed
int32 MaxRequestedTiles = CaclulateMaxTilesCount(InclusionBounds, Config.tileSize * Config.cs, AvgLayersPerTile);
if (DetourMesh->getMaxTiles() != MaxRequestedTiles)
{
// Destroy current NavMesh
DestNavMesh->GetRecastNavMeshImpl()->SetRecastMesh(nullptr);
// if there are any valid bounds recreate detour navmesh instance
// and mark all bounds as dirty
if (InclusionBounds.Num() > 0)
{
TArray<FNavigationDirtyArea> AsDirtyAreas;
AsDirtyAreas.Reserve(InclusionBounds.Num());
for (const FBox& BBox : InclusionBounds)
{
AsDirtyAreas.Add(FNavigationDirtyArea(BBox, ENavigationDirtyFlag::NavigationBounds));
}
RebuildDirtyAreas(AsDirtyAreas);
}
}
}
}
void FRecastNavMeshGenerator::RebuildDirtyAreas(const TArray<FNavigationDirtyArea>& InDirtyAreas)
{
dtNavMesh* DetourMesh = DestNavMesh->GetRecastNavMeshImpl()->GetRecastMesh();
if (DetourMesh == nullptr)
{
ConstructTiledNavMesh();
}
MarkDirtyTiles(InDirtyAreas);
}
void FRecastNavMeshGenerator::OnAreaAdded(const UClass* AreaClass, int32 AreaID)
{
AdditionalCachedData.OnAreaAdded(AreaClass, AreaID);
}
int32 FRecastNavMeshGenerator::FindInclusionBoundEncapsulatingBox(const FBox& Box) const
{
for (int32 Index = 0; Index < InclusionBounds.Num(); ++Index)
{
if (DoesBoxContainBox(InclusionBounds[Index], Box))
{
return Index;
}
}
return INDEX_NONE;
}
void FRecastNavMeshGenerator::RestrictBuildingToActiveTiles(bool InRestrictBuildingToActiveTiles)
{
if (bRestrictBuildingToActiveTiles != InRestrictBuildingToActiveTiles)
{
bRestrictBuildingToActiveTiles = InRestrictBuildingToActiveTiles;
if (InRestrictBuildingToActiveTiles)
{
// gather non-empty tiles and add them to ActiveTiles
const dtNavMesh* DetourMesh = DestNavMesh->GetRecastNavMeshImpl()->GetRecastMesh();
if (DetourMesh != nullptr && DetourMesh->isEmpty() == false)
{
ActiveTiles.Reset();
int32 TileCount = DetourMesh->getMaxTiles();
for (int32 TileIndex = 0; TileIndex < TileCount; ++TileIndex)
{
const dtMeshTile* Tile = DetourMesh->getTile(TileIndex);
if (Tile != nullptr && Tile->header != nullptr && Tile->header->polyCount > 0)
{
ActiveTiles.AddUnique(FIntPoint(Tile->header->x, Tile->header->y));
}
}
}
}
}
}
bool FRecastNavMeshGenerator::IsInActiveSet(const FIntPoint& Tile) const
{
// @TODO checking if given tile is in active tiles needs to be faster
return bRestrictBuildingToActiveTiles == false || ActiveTiles.Find(Tile) != INDEX_NONE;
}
//@TODO Investigate removing from RunningDirtyTiles here too (or atleast not using the results in any way)
void FRecastNavMeshGenerator::RemoveTiles(const TArray<FIntPoint>& Tiles)
{
for (const FIntPoint& TileXY : Tiles)
{
RemoveTileLayers(TileXY.X, TileXY.Y);
if (PendingDirtyTiles.Num() > 0)
{
FPendingTileElement DirtyTile;
DirtyTile.Coord = TileXY;
PendingDirtyTiles.Remove(DirtyTile);
}
#if TIME_SLICE_NAV_REGEN
if (TileGeneratorSync.Get())
{
if (TileGeneratorSync->GetTileX() == TileXY.X && TileGeneratorSync->GetTileY() == TileXY.Y)
{
TileGeneratorSync.Reset();
}
}
#endif
}
}
void FRecastNavMeshGenerator::ReAddTiles(const TArray<FIntPoint>& Tiles)
{
static const FVector Expansion(1, 1, BIG_NUMBER);
// a little trick here - adding a dirty area so that navmesh building figures it out on its own
dtNavMesh* DetourMesh = DestNavMesh->GetRecastNavMeshImpl()->GetRecastMesh();
const dtNavMeshParams* SavedNavParams = DestNavMesh->GetRecastNavMeshImpl()->DetourNavMesh->getParams();
const float TileDim = Config.tileSize * Config.cs;
TSet<FPendingTileElement> DirtyTiles;
// @note we act on assumption all items in Tiles are unique
for (const FIntPoint& TileCoords : Tiles)
{
FPendingTileElement Element;
Element.Coord = TileCoords;
Element.bRebuildGeometry = true;
DirtyTiles.Add(Element);
}
int32 NumTilesMarked = DirtyTiles.Num();
// Merge all pending tiles into one container
for (const FPendingTileElement& Element : PendingDirtyTiles)
{
FPendingTileElement* ExistingElement = DirtyTiles.Find(Element);
if (ExistingElement)
{
ExistingElement->bRebuildGeometry |= Element.bRebuildGeometry;
// Append area bounds to existing list
if (ExistingElement->bRebuildGeometry == false)
{
ExistingElement->DirtyAreas.Append(Element.DirtyAreas);
}
else
{
ExistingElement->DirtyAreas.Empty();
}
}
else
{
DirtyTiles.Add(Element);
}
}
// Dump results into array
PendingDirtyTiles.Empty(DirtyTiles.Num());
for (const FPendingTileElement& Element : DirtyTiles)
{
PendingDirtyTiles.Add(Element);
}
// Sort tiles by proximity to players
if (NumTilesMarked > 0)
{
SortPendingBuildTiles();
}
/*TArray<FNavigationDirtyArea> DirtyAreasContainer;
DirtyAreasContainer.Reserve(Tiles.Num());
TSet<FPendingTileElement> DirtyTiles;
for (const FIntPoint& TileCoords : Tiles)
{
const FVector TileCenter = Recast2UnrealPoint(SavedNavParams->orig) + FVector(TileDim * float(TileCoords.X), TileDim * float(TileCoords.Y), 0);
FNavigationDirtyArea DirtyArea(FBox(TileCenter - Expansion, TileCenter - 1), ENavigationDirtyFlag::All);
DirtyAreasContainer.Add(DirtyArea);
}
MarkDirtyTiles(DirtyAreasContainer);*/
}
namespace RecastTileVersionHelper
{
inline uint32 GetUpdatedTileId(dtPolyRef& TileRef, dtNavMesh* DetourMesh)
{
uint32 DecodedTileId = 0, DecodedPolyId = 0, DecodedSaltId = 0;
DetourMesh->decodePolyId(TileRef, DecodedSaltId, DecodedTileId, DecodedPolyId);
DecodedSaltId = (DecodedSaltId + 1) & ((1 << DetourMesh->getSaltBits()) - 1);
if (DecodedSaltId == 0)
{
DecodedSaltId++;
}
TileRef = DetourMesh->encodePolyId(DecodedSaltId, DecodedTileId, DecodedPolyId);
return DecodedTileId;
}
}
TArray<uint32> FRecastNavMeshGenerator::RemoveTileLayers(const int32 TileX, const int32 TileY, TMap<int32, dtPolyRef>* OldLayerTileIdMap)
{
dtNavMesh* DetourMesh = DestNavMesh->GetRecastNavMeshImpl()->GetRecastMesh();
TArray<uint32> UpdatedIndices;
if (DetourMesh != nullptr && DetourMesh->isEmpty() == false)
{
const int32 NumLayers = DetourMesh->getTileCountAt(TileX, TileY);
if (NumLayers > 0)
{
TArray<dtMeshTile*> Tiles;
Tiles.AddZeroed(NumLayers);
DetourMesh->getTilesAt(TileX, TileY, (const dtMeshTile**)Tiles.GetData(), NumLayers);
for (int32 i = 0; i < NumLayers; i++)
{
const int32 LayerIndex = Tiles[i]->header->layer;
dtPolyRef TileRef = DetourMesh->getTileRef(Tiles[i]);
NumActiveTiles--;
UE_LOG(LogNavigation, Log, TEXT("%s> Tile (%d,%d:%d), removing TileRef: 0x%X (active:%d)"),
*DestNavMesh->GetName(), TileX, TileY, LayerIndex, TileRef, NumActiveTiles);
DetourMesh->removeTile(TileRef, nullptr, nullptr);
uint32 TileId = RecastTileVersionHelper::GetUpdatedTileId(TileRef, DetourMesh);
UpdatedIndices.AddUnique(TileId);
if (OldLayerTileIdMap)
{
OldLayerTileIdMap->Add(LayerIndex, TileRef);
}
}
}
// Remove compressed tile cache layers
DestNavMesh->RemoveTileCacheLayers(TileX, TileY);
}
return UpdatedIndices;
}
TArray<uint32> FRecastNavMeshGenerator::AddGeneratedTiles(FRecastTileGenerator& TileGenerator)
{
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastAddGeneratedTiles);
TMap<int32, dtPolyRef> OldLayerTileIdMap;
TArray<uint32> ResultTileIndices;
const int32 TileX = TileGenerator.GetTileX();
const int32 TileY = TileGenerator.GetTileY();
if (TileGenerator.IsFullyRegenerated())
{
// remove all layers
ResultTileIndices = RemoveTileLayers(TileX, TileY, &OldLayerTileIdMap);
}
dtNavMesh* DetourMesh = DestNavMesh->GetRecastNavMeshImpl()->GetRecastMesh();
const int32 FirstDirtyTileIndex = TileGenerator.GetDirtyLayersMask().Find(true);
if (DetourMesh != nullptr
// no longer testing this here, we can live with a stray unwanted tile here
// and there. It will be removed the next time around the invokers get
// updated
// && IsInActiveSet(FIntPoint(TileX, TileY))
&& FirstDirtyTileIndex != INDEX_NONE)
{
TArray<FNavMeshTileData> TileLayers = TileGenerator.GetNavigationData();
ResultTileIndices.Reserve(TileLayers.Num());
struct FLayerIndexFinder
{
int32 LayerIndex;
explicit FLayerIndexFinder(const int32 InLayerIndex) : LayerIndex(InLayerIndex) {}
bool operator()(const FNavMeshTileData& LayerData) const
{
return LayerData.LayerIndex == LayerIndex;
}
};
for (int32 LayerIndex = FirstDirtyTileIndex; LayerIndex < TileGenerator.GetDirtyLayersMask().Num(); ++LayerIndex)
{
if (TileGenerator.IsLayerChanged(LayerIndex))
{
dtTileRef OldTileRef = DetourMesh->getTileRefAt(TileX, TileY, LayerIndex);
const int32 LayerDataIndex = TileLayers.IndexOfByPredicate(FLayerIndexFinder(LayerIndex));
if (LayerDataIndex != INDEX_NONE)
{
FNavMeshTileData& LayerData = TileLayers[LayerDataIndex];
if (OldTileRef)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_NavMesh_RemoveOldTIle);
NumActiveTiles--;
UE_LOG(LogNavigation, Log, TEXT("%s> Tile (%d,%d:%d), removing TileRef: 0x%X (active:%d)"),
*DestNavMesh->GetName(), TileX, TileY, LayerIndex, OldTileRef, NumActiveTiles);
DetourMesh->removeTile(OldTileRef, nullptr, nullptr);
const uint32 TileId = RecastTileVersionHelper::GetUpdatedTileId(OldTileRef, DetourMesh);
ResultTileIndices.AddUnique(TileId);
}
else
{
OldTileRef = OldLayerTileIdMap.FindRef(LayerIndex);
}
if (LayerData.IsValid())
{
bool bRejectNavmesh = false;
dtTileRef ResultTileRef = 0;
dtStatus status = 0;
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_NavMesh_AddTileToDetourMesh);
// let navmesh know it's tile generator who owns the data
status = DetourMesh->addTile(LayerData.GetData(), LayerData.DataSize, DT_TILE_FREE_DATA, OldTileRef, &ResultTileRef);
// if tile index was already taken by other layer try adding it on first free entry (salt was already updated by whatever took that spot)
if (dtStatusFailed(status) && dtStatusDetail(status, DT_OUT_OF_MEMORY) && OldTileRef)
{
OldTileRef = 0;
status = DetourMesh->addTile(LayerData.GetData(), LayerData.DataSize, DT_TILE_FREE_DATA, OldTileRef, &ResultTileRef);
}
}
if (dtStatusFailed(status))
{
if (dtStatusDetail(status, DT_OUT_OF_MEMORY))
{
UE_LOG(LogNavigation, Error, TEXT("%s> Tile (%d,%d:%d), tile limit reached!! (%d)"),
*DestNavMesh->GetName(), TileX, TileY, LayerIndex, DetourMesh->getMaxTiles());
}
}
else
{
ResultTileIndices.AddUnique(DetourMesh->decodePolyIdTile(ResultTileRef));
NumActiveTiles++;
UE_LOG(LogNavigation, Log, TEXT("%s> Tile (%d,%d:%d), added TileRef: 0x%X (active:%d)"),
*DestNavMesh->GetName(), TileX, TileY, LayerIndex, ResultTileRef, NumActiveTiles);
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_NavMesh_ReleaseTileLayers);
// NavMesh took the ownership of generated data, so we don't need to deallocate it
uint8* ReleasedData = LayerData.Release();
}
}
}
}
else
{
// remove the layer since it ended up empty
DetourMesh->removeTile(OldTileRef, nullptr, nullptr);
const uint32 TileId = RecastTileVersionHelper::GetUpdatedTileId(OldTileRef, DetourMesh);
ResultTileIndices.AddUnique(TileId);
}
}
}
}
return ResultTileIndices;
}
void FRecastNavMeshGenerator::DiscardCurrentBuildingTasks()
{
PendingDirtyTiles.Empty();
for (FRunningTileElement& Element : RunningDirtyTiles)
{
if (Element.AsyncTask)
{
Element.AsyncTask->EnsureCompletion();
delete Element.AsyncTask;
Element.AsyncTask = nullptr;
}
}
#if TIME_SLICE_NAV_REGEN
TileGeneratorSync.Reset();
#endif
RunningDirtyTiles.Empty();
}
bool FRecastNavMeshGenerator::HasDirtyTiles() const
{
return (PendingDirtyTiles.Num() > 0
|| RunningDirtyTiles.Num() > 0
#if TIME_SLICE_NAV_REGEN
|| TileGeneratorSync.Get() != nullptr
#endif
);
}
FBox FRecastNavMeshGenerator::GrowBoundingBox(const FBox& BBox, bool bIncludeAgentHeight) const
{
const FVector BBoxGrowOffsetMin = FVector(0, 0, bIncludeAgentHeight ? Config.AgentHeight : 0.0f);
return FBox(BBox.Min - BBoxGrowth - BBoxGrowOffsetMin, BBox.Max + BBoxGrowth);
}
static bool IntersectBounds(const FBox& TestBox, const TNavStatArray<FBox>& Bounds)
{
for (const FBox& Box : Bounds)
{
if (Box.Intersect(TestBox))
{
return true;
}
}
return false;
}
namespace
{
FBox CalculateBoxIntersection(const FBox& BoxA, const FBox& BoxB)
{
// assumes boxes overlap
ensure(BoxA.Intersect(BoxB));
return FBox(FVector(FMath::Max(BoxA.Min.X, BoxB.Min.X)
, FMath::Max(BoxA.Min.Y, BoxB.Min.Y)
, FMath::Max(BoxA.Min.Z, BoxB.Min.Z))
, FVector(FMath::Min(BoxA.Max.X, BoxB.Max.X)
, FMath::Min(BoxA.Max.Y, BoxB.Max.Y)
, FMath::Min(BoxA.Max.Z, BoxB.Max.Z))
);
}
}
bool FRecastNavMeshGenerator::HasDirtyTiles(const FBox& AreaBounds) const
{
if (HasDirtyTiles() == false)
{
return false;
}
bool bRetDirty = false;
const float TileSizeInWorldUnits = Config.tileSize * Config.cs;
const FRcTileBox TileBox(AreaBounds, RcNavMeshOrigin, TileSizeInWorldUnits);
for (int32 Index = 0; bRetDirty == false && Index < PendingDirtyTiles.Num(); ++Index)
{
bRetDirty = TileBox.Contains(PendingDirtyTiles[Index].Coord);
}
for (int32 Index = 0; bRetDirty == false && Index < RunningDirtyTiles.Num(); ++Index)
{
bRetDirty = TileBox.Contains(RunningDirtyTiles[Index].Coord);
}
return bRetDirty;
}
int32 FRecastNavMeshGenerator::GetDirtyTilesCount(const FBox& AreaBounds) const
{
const float TileSizeInWorldUnits = Config.tileSize * Config.cs;
const FRcTileBox TileBox(AreaBounds, RcNavMeshOrigin, TileSizeInWorldUnits);
int32 DirtyPendingCount = 0;
for (const FPendingTileElement& PendingElement : PendingDirtyTiles)
{
DirtyPendingCount += TileBox.Contains(PendingElement.Coord) ? 1 : 0;
}
int32 RunningCount = 0;
for (const FRunningTileElement& RunningElement : RunningDirtyTiles)
{
RunningCount += TileBox.Contains(RunningElement.Coord) ? 1 : 0;
}
return DirtyPendingCount + RunningCount;
}
bool FRecastNavMeshGenerator::MarkNavBoundsDirty()
{
// if rebuilding all no point in keeping "old" invalidated areas
TArray<FNavigationDirtyArea> DirtyAreas;
for (FBox AreaBounds : InclusionBounds)
{
FNavigationDirtyArea DirtyArea(AreaBounds, ENavigationDirtyFlag::All | ENavigationDirtyFlag::NavigationBounds);
DirtyAreas.Add(DirtyArea);
}
if (DirtyAreas.Num())
{
MarkDirtyTiles(DirtyAreas);
return true;
}
return false;
}
void FRecastNavMeshGenerator::MarkDirtyTiles(const TArray<FNavigationDirtyArea>& DirtyAreas)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_MarkDirtyTiles);
check(bInitialized);
const float TileSizeInWorldUnits = Config.tileSize * Config.cs;
check(TileSizeInWorldUnits > 0);
const bool bGameStaticNavMesh = IsGameStaticNavMesh(DestNavMesh);
// find all tiles that need regeneration
TSet<FPendingTileElement> DirtyTiles;
for (const FNavigationDirtyArea& DirtyArea : DirtyAreas)
{
// Static navmeshes accept only area modifiers updates
if (bGameStaticNavMesh && (!DirtyArea.HasFlag(ENavigationDirtyFlag::DynamicModifier) || DirtyArea.HasFlag(ENavigationDirtyFlag::NavigationBounds)))
{
continue;
}
bool bDoTileInclusionTest = false;
FBox AdjustedAreaBounds = DirtyArea.Bounds;
// if it's not expanding the navigatble area
if (DirtyArea.HasFlag(ENavigationDirtyFlag::NavigationBounds) == false)
{
// and is outside of current bounds
if (GetTotalBounds().Intersect(DirtyArea.Bounds) == false)
{
// skip it
continue;
}
const FBox CutDownArea = CalculateBoxIntersection(GetTotalBounds(), DirtyArea.Bounds);
AdjustedAreaBounds = GrowBoundingBox(CutDownArea, DirtyArea.HasFlag(ENavigationDirtyFlag::UseAgentHeight));
// @TODO this and the following test share some work in common
if (IntersectBounds(AdjustedAreaBounds, InclusionBounds) == false)
{
continue;
}
// check if any of inclusion volumes encapsulates this box
// using CutDownArea not AdjustedAreaBounds since if the area is on the border of navigable space
// then FindInclusionBoundEncapsulatingBox can produce false negative
bDoTileInclusionTest = (FindInclusionBoundEncapsulatingBox(CutDownArea) == INDEX_NONE);
}
const FRcTileBox TileBox(AdjustedAreaBounds, RcNavMeshOrigin, TileSizeInWorldUnits);
for (int32 TileY = TileBox.YMin; TileY <= TileBox.YMax; ++TileY)
{
for (int32 TileX = TileBox.XMin; TileX <= TileBox.XMax; ++TileX)
{
if (IsInActiveSet(FIntPoint(TileX, TileY)) == false)
{
continue;
}
if (bDoTileInclusionTest == true && DirtyArea.HasFlag(ENavigationDirtyFlag::NavigationBounds) == false)
{
const FBox TileBounds = CalculateTileBounds(TileX, TileY, RcNavMeshOrigin, TotalNavBounds, TileSizeInWorldUnits);
// do per tile check since we can have lots of tiles inbetween navigable bounds volumes
if (IntersectBounds(TileBounds, InclusionBounds) == false)
{
// Skip this tile
continue;
}
}
FPendingTileElement Element;
Element.Coord = FIntPoint(TileX, TileY);
Element.bRebuildGeometry = DirtyArea.HasFlag(ENavigationDirtyFlag::Geometry) || DirtyArea.HasFlag(ENavigationDirtyFlag::NavigationBounds);
if (Element.bRebuildGeometry == false)
{
Element.DirtyAreas.Add(AdjustedAreaBounds);
}
FPendingTileElement* ExistingElement = DirtyTiles.Find(Element);
if (ExistingElement)
{
ExistingElement->bRebuildGeometry|= Element.bRebuildGeometry;
// Append area bounds to existing list
if (ExistingElement->bRebuildGeometry == false)
{
ExistingElement->DirtyAreas.Append(Element.DirtyAreas);
}
else
{
ExistingElement->DirtyAreas.Empty();
}
}
else
{
DirtyTiles.Add(Element);
}
}
}
}
int32 NumTilesMarked = DirtyTiles.Num();
// Merge all pending tiles into one container
for (const FPendingTileElement& Element : PendingDirtyTiles)
{
FPendingTileElement* ExistingElement = DirtyTiles.Find(Element);
if (ExistingElement)
{
ExistingElement->bRebuildGeometry|= Element.bRebuildGeometry;
// Append area bounds to existing list
if (ExistingElement->bRebuildGeometry == false)
{
ExistingElement->DirtyAreas.Append(Element.DirtyAreas);
}
else
{
ExistingElement->DirtyAreas.Empty();
}
}
else
{
DirtyTiles.Add(Element);
}
}
// Dump results into array
PendingDirtyTiles.Empty(DirtyTiles.Num());
for(const FPendingTileElement& Element : DirtyTiles)
{
PendingDirtyTiles.Add(Element);
}
// Sort tiles by proximity to players
if (NumTilesMarked > 0)
{
SortPendingBuildTiles();
}
}
void FRecastNavMeshGenerator::SortPendingBuildTiles()
{
TArray<FVector2D> SeedLocations;
UWorld* CurWorld = GetWorld();
if (CurWorld == nullptr)
{
return;
}
// Collect players positions
for (FConstPlayerControllerIterator PlayerIt = CurWorld->GetPlayerControllerIterator(); PlayerIt; ++PlayerIt)
{
APlayerController* PC = PlayerIt->Get();
if (PC && PC->GetPawn() != NULL)
{
const FVector2D SeedLoc(PC->GetPawn()->GetActorLocation());
SeedLocations.Add(SeedLoc);
}
}
if (SeedLocations.Num() == 0)
{
// Use navmesh origin for sorting
SeedLocations.Add(FVector2D::ZeroVector);
}
if (SeedLocations.Num() > 0)
{
const float TileSizeInWorldUnits = Config.tileSize * Config.cs;
// Calculate shortest distances between tiles and players
for (FPendingTileElement& Element : PendingDirtyTiles)
{
const FBox TileBox = CalculateTileBounds(Element.Coord.X, Element.Coord.Y, FVector::ZeroVector, TotalNavBounds, TileSizeInWorldUnits);
FVector2D TileCenter2D = FVector2D(TileBox.GetCenter());
for (FVector2D SeedLocation : SeedLocations)
{
const float DistSq = FVector2D::DistSquared(TileCenter2D, SeedLocation);
if (DistSq < Element.SeedDistance)
{
Element.SeedDistance = DistSq;
}
}
}
// nearest tiles should be at the end of the list
PendingDirtyTiles.Sort();
}
}
TSharedRef<FRecastTileGenerator> FRecastNavMeshGenerator::CreateTileGenerator(const FIntPoint& Coord, const TArray<FBox>& DirtyAreas)
{
TSharedRef<FRecastTileGenerator> TileGenerator = MakeShareable(new FRecastTileGenerator(*this, Coord));
TileGenerator->Setup(*this, DirtyAreas);
return TileGenerator;
}
void FRecastNavMeshGenerator::RemoveLayers(const FIntPoint& Tile, TArray<uint32>& UpdatedTiles)
{
// If there is nothing to generate remove all tiles from navmesh at specified grid coordinates
UpdatedTiles.Append(
RemoveTileLayers(Tile.X, Tile.Y)
);
DestNavMesh->MarkEmptyTileCacheLayers(Tile.X, Tile.Y);
}
#if RECAST_ASYNC_REBUILDING
TArray<uint32> FRecastNavMeshGenerator::ProcessTileTasksAsync(const int32 NumTasksToProcess)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_ProcessTileTasksAsync);
TArray<uint32> UpdatedTiles;
const bool bGameStaticNavMesh = IsGameStaticNavMesh(DestNavMesh);
int32 NumProcessedTasks = 0;
// Submit pending tile elements
for (int32 ElementIdx = PendingDirtyTiles.Num()-1; ElementIdx >= 0 && NumProcessedTasks < NumTasksToProcess; ElementIdx--)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_ProcessTileTasks_NewTasks);
FPendingTileElement& PendingElement = PendingDirtyTiles[ElementIdx];
FRunningTileElement RunningElement(PendingElement.Coord);
// Make sure that we are not submitting generator for grid cell that is currently being regenerated
if (!RunningDirtyTiles.Contains(RunningElement))
{
// Spawn async task
TUniquePtr<FRecastTileGeneratorTask> TileTask = MakeUnique<FRecastTileGeneratorTask>(CreateTileGenerator(PendingElement.Coord, PendingElement.DirtyAreas));
// Start it in background in case it has something to build
if (TileTask->GetTask().TileGenerator->HasDataToBuild())
{
RunningElement.AsyncTask = TileTask.Release();
RunningElement.AsyncTask->StartBackgroundTask();
RunningDirtyTiles.Add(RunningElement);
}
else if (!bGameStaticNavMesh)
{
RemoveLayers(PendingElement.Coord, UpdatedTiles);
}
// Remove submitted element from pending list
PendingDirtyTiles.RemoveAt(ElementIdx, 1, /*bAllowShrinking=*/false);
NumProcessedTasks++;
}
}
// Release memory, list could be quite big after map load
if (NumProcessedTasks > 0 && PendingDirtyTiles.Num() == 0)
{
PendingDirtyTiles.Empty(32);
}
// Collect completed tasks and apply generated data to navmesh
for (int32 Idx = RunningDirtyTiles.Num() - 1; Idx >=0; --Idx)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_ProcessTileTasks_FinishedTasks);
FRunningTileElement& Element = RunningDirtyTiles[Idx];
check(Element.AsyncTask);
if (Element.AsyncTask->IsDone())
{
// Add generated tiles to navmesh
if (!Element.bShouldDiscard)
{
FRecastTileGenerator& TileGenerator = *(Element.AsyncTask->GetTask().TileGenerator);
TArray<uint32> UpdatedTileIndices = AddGeneratedTiles(TileGenerator);
UpdatedTiles.Append(UpdatedTileIndices);
// Store compressed tile cache layers so it can be reused later
if (TileGenerator.GetCompressedLayers().Num())
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_StoringCompressedLayers);
DestNavMesh->AddTileCacheLayers(Element.Coord.X, Element.Coord.Y, TileGenerator.GetCompressedLayers());
}
else
{
DestNavMesh->MarkEmptyTileCacheLayers(Element.Coord.X, Element.Coord.Y);
}
}
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_TileGeneratorRemoval);
// Destroy tile generator task
delete Element.AsyncTask;
Element.AsyncTask = nullptr;
// Remove completed tile element from a list of running tasks
RunningDirtyTiles.RemoveAtSwap(Idx, 1, false);
}
}
}
return UpdatedTiles;
}
#endif
#if TIME_SLICE_NAV_REGEN
bool FRecastNavMeshGenerator::IsTimeSliceDurationExceeded(const double StartTime) const
{
const double CurTime = FPlatformTime::Seconds();
return CurTime - StartTime >= TimeSliceDuration;
}
#endif
#if !RECAST_ASYNC_REBUILDING
TArray<uint32> FRecastNavMeshGenerator::ProcessTileTasksSync(const int32 NumTasksToProcess)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_ProcessTileTasksSync);
TArray<uint32> UpdatedTiles;
const bool bGameStaticNavMesh = IsGameStaticNavMesh(DestNavMesh);
#if TIME_SLICE_NAV_REGEN
const double StartTime = FPlatformTime::Seconds();
//if we are time slice processing a tile (ie we have already done some processing on this tile last frame)
bool bIsTimeSliceProcessingTile = TileGeneratorSync.Get() != nullptr;
#endif
int32 NumProcessedTasks = 0;
//if bIsTimeSliceProcessingTile is true then this will be the Idx of the next Element to process not the Idx of TileGeneratorSync
int32 ElementIdx = PendingDirtyTiles.Num() - 1;
if ((ElementIdx >= 0
&& NumProcessedTasks < NumTasksToProcess)
#if TIME_SLICE_NAV_REGEN
|| bIsTimeSliceProcessingTile
#endif
)
{
// Submit pending tile elements
while (true)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_ProcessTileTasks_NewTasks);
FIntPoint TileLocation;
//this will only be set up when bIsTimeSliceProcessingTile == false,
//its currently not needed at other times.
TArray<FBox> DirtyAreas;
#if TIME_SLICE_NAV_REGEN
if (bIsTimeSliceProcessingTile)
{
TileLocation.X = TileGeneratorSync->GetTileX();
TileLocation.Y = TileGeneratorSync->GetTileY();
}
else
#endif //TIME_SLICE_NAV_REGEN
{
FPendingTileElement& PendingElement = PendingDirtyTiles[ElementIdx];
TileLocation.X = PendingElement.Coord.X;
TileLocation.Y = PendingElement.Coord.Y;
DirtyAreas = MoveTemp(PendingElement.DirtyAreas);
// Remove submitted element from pending list, this simplifies time sliced tile processing
PendingDirtyTiles.RemoveAt(ElementIdx);
// Release memory, list could be quite big after map load
if (PendingDirtyTiles.Num() == 0)
{
PendingDirtyTiles.Empty(32);
}
//see definition of bIsTimeSliceProcessingTile
ensureMsgf(TileGeneratorSync.Get() == nullptr, TEXT("TileGeneratorSync should be NULL if we aren't currently time slicing this tile"));
TileGeneratorSync = CreateTileGenerator(TileLocation, DirtyAreas);
#if TIME_SLICE_NAV_REGEN
if (IsTimeSliceDurationExceeded(StartTime))
{
break;
}
#endif //TIME_SLICE_NAV_REGEN
}
FRecastTileGenerator& TileGeneratorRef = *TileGeneratorSync.Get();
ensureMsgf(TileGeneratorRef.GetTileX() == TileLocation.X && TileGeneratorRef.GetTileY() == TileLocation.Y,
TEXT("X Loc Mismatch between TileGeneratorRef and PendingElement"));
if (TileGeneratorRef.HasDataToBuild())
{
#if TIME_SLICE_NAV_REGEN
bool bContinueTimeSliceProcessing = true;
ETimeSliceWorkResult TimeSliceWorkResult = ETimeSliceWorkResult::CallAgain;
if (!TileGeneratorRef.HasDoneWork())
{
while (TimeSliceWorkResult == ETimeSliceWorkResult::CallAgain && bContinueTimeSliceProcessing)
{
TimeSliceWorkResult = TileGeneratorRef.DoWork();
bContinueTimeSliceProcessing = !IsTimeSliceDurationExceeded(StartTime);
}
if (!bContinueTimeSliceProcessing)
{
break;
}
}
#else //TIME_SLICE_NAV_REGEN
TileGeneratorRef.DoWork();
#endif //TIME_SLICE_NAV_REGEN
//@TODO Investigate preventing further processing of tiles when generation has failed
const TArray<uint32> UpdatedTileIndices = AddGeneratedTiles(TileGeneratorRef);
UpdatedTiles.Append(UpdatedTileIndices);
// Store compressed tile cache layers so it can be reused later
if (TileGeneratorRef.GetCompressedLayers().Num())
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_StoringCompressedLayers);
DestNavMesh->AddTileCacheLayers(TileLocation.X, TileLocation.Y, TileGeneratorRef.GetCompressedLayers());
}
else
{
DestNavMesh->MarkEmptyTileCacheLayers(TileLocation.X, TileLocation.Y);
}
}
else if (!bGameStaticNavMesh)
{
RemoveLayers(TileLocation, UpdatedTiles);
}
TileGeneratorSync = nullptr;
NumProcessedTasks++;
#if TIME_SLICE_NAV_REGEN
//only subtract from the ElementIdx if we aren't time slice Processing this tile (ie the tile hasn't already had some
//processing done on it last frame)
if (!bIsTimeSliceProcessingTile)
{
--ElementIdx;
}
bIsTimeSliceProcessingTile = false;
#else //TIME_SLICE_NAV_REGEN
--ElementIdx;
#endif //TIME_SLICE_NAV_REGEN
if (NumProcessedTasks >= NumTasksToProcess || ElementIdx < 0)
{
break;
}
#if TIME_SLICE_NAV_REGEN
else
{
//only perform the time slice check if we are going to iterate again
if (IsTimeSliceDurationExceeded(StartTime))
{
break;
}
}
#endif //TIME_SLICE_NAV_REGEN
}
}
return UpdatedTiles;
}
#endif
TArray<uint32> FRecastNavMeshGenerator::ProcessTileTasks(const int32 NumTasksToProcess)
{
const bool bHasTasksAtStart = GetNumRemaningBuildTasks() > 0;
#if RECAST_ASYNC_REBUILDING
TArray<uint32> UpdatedTiles = ProcessTileTasksAsync(NumTasksToProcess);
#else
TArray<uint32> UpdatedTiles = ProcessTileTasksSync(NumTasksToProcess);
#endif
// Notify owner in case all tasks has been completed
const bool bHasTasksAtEnd = GetNumRemaningBuildTasks() > 0;
if (bHasTasksAtStart && !bHasTasksAtEnd)
{
DestNavMesh->OnNavMeshGenerationFinished();
}
#if !UE_BUILD_SHIPPING && OUTPUT_NAV_TILE_LAYER_COMPRESSION_DATA && FRAMEPRO_ENABLED
//only do this if framepro is recording as its an expensive operation
if (FFrameProProfiler::IsFrameProRecording())
{
int32 TileCacheSize = DestNavMesh->GetCompressedTileCacheSize();
FPlatformMisc::CustomNamedStat("TotalTileCacheSize", static_cast<float>(TileCacheSize), "NavMesh", "Bytes");
}
#endif
return UpdatedTiles;
}
void FRecastNavMeshGenerator::ExportComponentGeometry(UActorComponent* Component, FNavigationRelevantData& Data)
{
FRecastGeometryExport GeomExport(Data);
RecastGeometryExport::ExportComponent(Component, GeomExport);
RecastGeometryExport::CovertCoordDataToRecast(GeomExport.VertexBuffer);
RecastGeometryExport::StoreCollisionCache(GeomExport);
}
void FRecastNavMeshGenerator::ExportVertexSoupGeometry(const TArray<FVector>& Verts, FNavigationRelevantData& Data)
{
FRecastGeometryExport GeomExport(Data);
RecastGeometryExport::ExportVertexSoup(Verts, GeomExport.VertexBuffer, GeomExport.IndexBuffer, GeomExport.Data->Bounds);
RecastGeometryExport::StoreCollisionCache(GeomExport);
}
void FRecastNavMeshGenerator::ExportRigidBodyGeometry(UBodySetup& BodySetup, TNavStatArray<FVector>& OutVertexBuffer, TNavStatArray<int32>& OutIndexBuffer, const FTransform& LocalToWorld)
{
TNavStatArray<float> VertCoords;
FBox TempBounds;
RecastGeometryExport::ExportRigidBodySetup(BodySetup, VertCoords, OutIndexBuffer, TempBounds, LocalToWorld);
OutVertexBuffer.Reserve(OutVertexBuffer.Num() + (VertCoords.Num() / 3));
for (int32 i = 0; i < VertCoords.Num(); i += 3)
{
OutVertexBuffer.Add(FVector(VertCoords[i + 0], VertCoords[i + 1], VertCoords[i + 2]));
}
}
void FRecastNavMeshGenerator::ExportRigidBodyGeometry(UBodySetup& BodySetup, TNavStatArray<FVector>& OutTriMeshVertexBuffer, TNavStatArray<int32>& OutTriMeshIndexBuffer
, TNavStatArray<FVector>& OutConvexVertexBuffer, TNavStatArray<int32>& OutConvexIndexBuffer, TNavStatArray<int32>& OutShapeBuffer
, const FTransform& LocalToWorld)
{
BodySetup.CreatePhysicsMeshes();
TNavStatArray<float> VertCoords;
FBox TempBounds;
VertCoords.Reset();
RecastGeometryExport::ExportRigidBodyTriMesh(BodySetup, VertCoords, OutTriMeshIndexBuffer, TempBounds, LocalToWorld);
OutTriMeshVertexBuffer.Reserve(OutTriMeshVertexBuffer.Num() + (VertCoords.Num() / 3));
for (int32 i = 0; i < VertCoords.Num(); i += 3)
{
OutTriMeshVertexBuffer.Add(FVector(VertCoords[i + 0], VertCoords[i + 1], VertCoords[i + 2]));
}
const int32 NumExistingVerts = OutConvexVertexBuffer.Num();
VertCoords.Reset();
RecastGeometryExport::ExportRigidBodyConvexElements(BodySetup, VertCoords, OutConvexIndexBuffer, OutShapeBuffer, TempBounds, LocalToWorld);
RecastGeometryExport::ExportRigidBodyBoxElements(BodySetup.AggGeom, VertCoords, OutConvexIndexBuffer, OutShapeBuffer, TempBounds, LocalToWorld, NumExistingVerts);
RecastGeometryExport::ExportRigidBodySphylElements(BodySetup.AggGeom, VertCoords, OutConvexIndexBuffer, OutShapeBuffer, TempBounds, LocalToWorld, NumExistingVerts);
RecastGeometryExport::ExportRigidBodySphereElements(BodySetup.AggGeom, VertCoords, OutConvexIndexBuffer, OutShapeBuffer, TempBounds, LocalToWorld, NumExistingVerts);
OutConvexVertexBuffer.Reserve(OutConvexVertexBuffer.Num() + (VertCoords.Num() / 3));
for (int32 i = 0; i < VertCoords.Num(); i += 3)
{
OutConvexVertexBuffer.Add(FVector(VertCoords[i + 0], VertCoords[i + 1], VertCoords[i + 2]));
}
}
void FRecastNavMeshGenerator::ExportAggregatedGeometry(const FKAggregateGeom& AggGeom, TNavStatArray<FVector>& OutConvexVertexBuffer, TNavStatArray<int32>& OutConvexIndexBuffer, TNavStatArray<int32>& OutShapeBuffer, const FTransform& LocalToWorld)
{
TNavStatArray<float> VertCoords;
FBox TempBounds;
const int32 NumExistingVerts = OutConvexVertexBuffer.Num();
// convex and tri mesh are NOT supported, since they require BodySetup.CreatePhysicsMeshes() call
// only simple shapes
RecastGeometryExport::ExportRigidBodyBoxElements(AggGeom, VertCoords, OutConvexIndexBuffer, OutShapeBuffer, TempBounds, LocalToWorld, NumExistingVerts);
RecastGeometryExport::ExportRigidBodySphylElements(AggGeom, VertCoords, OutConvexIndexBuffer, OutShapeBuffer, TempBounds, LocalToWorld, NumExistingVerts);
RecastGeometryExport::ExportRigidBodySphereElements(AggGeom, VertCoords, OutConvexIndexBuffer, OutShapeBuffer, TempBounds, LocalToWorld, NumExistingVerts);
OutConvexVertexBuffer.Reserve(OutConvexVertexBuffer.Num() + (VertCoords.Num() / 3));
for (int32 i = 0; i < VertCoords.Num(); i += 3)
{
OutConvexVertexBuffer.Add(FVector(VertCoords[i + 0], VertCoords[i + 1], VertCoords[i + 2]));
}
}
bool FRecastNavMeshGenerator::IsBuildInProgress(bool bCheckDirtyToo) const
{
return RunningDirtyTiles.Num()
|| (bCheckDirtyToo && PendingDirtyTiles.Num())
#if TIME_SLICE_NAV_REGEN
|| TileGeneratorSync.Get()
#endif
;
}
int32 FRecastNavMeshGenerator::GetNumRemaningBuildTasks() const
{
return RunningDirtyTiles.Num()
+ PendingDirtyTiles.Num()
#if TIME_SLICE_NAV_REGEN
+ (TileGeneratorSync.Get() ? 1 : 0)
#endif
;
}
int32 FRecastNavMeshGenerator::GetNumRunningBuildTasks() const
{
return RunningDirtyTiles.Num()
#if TIME_SLICE_NAV_REGEN
+ (TileGeneratorSync.Get() ? 1 : 0)
#endif
;
}
bool FRecastNavMeshGenerator::GatherGeometryOnGameThread() const
{
return DestNavMesh == nullptr || DestNavMesh->ShouldGatherDataOnGameThread() == true;
}
bool FRecastNavMeshGenerator::IsTileChanged(int32 TileIdx) const
{
#if WITH_EDITOR
// Check recently built tiles
if (TileIdx > 0)
{
FTileTimestamp TileTimestamp;
TileTimestamp.TileIdx = static_cast<uint32>(TileIdx);
if (RecentlyBuiltTiles.Contains(TileTimestamp))
{
return true;
}
}
#endif//WITH_EDITOR
return false;
}
uint32 FRecastNavMeshGenerator::LogMemUsed() const
{
UE_LOG(LogNavigation, Display, TEXT(" FRecastNavMeshGenerator: self %d"), sizeof(FRecastNavMeshGenerator));
uint32 GeneratorsMem = 0;
for (const FRunningTileElement& Element : RunningDirtyTiles)
{
GeneratorsMem += Element.AsyncTask->GetTask().TileGenerator->UsedMemoryOnStartup;
#if TIME_SLICE_NAV_REGEN
GeneratorsMem += TileGeneratorSync->UsedMemoryOnStartup;
#endif
}
UE_LOG(LogNavigation, Display, TEXT(" FRecastNavMeshGenerator: Total Generator\'s size %u, count %d"), GeneratorsMem, RunningDirtyTiles.Num());
return GeneratorsMem + sizeof(FRecastNavMeshGenerator) + PendingDirtyTiles.GetAllocatedSize() + RunningDirtyTiles.GetAllocatedSize();
}
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) && ENABLE_VISUAL_LOG
void FRecastNavMeshGenerator::GrabDebugSnapshot(struct FVisualLogEntry* Snapshot, const FBox& BoundingBox, const FName& CategoryName, ELogVerbosity::Type LogVerbosity) const
{
const UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
const FNavigationOctree* NavOctree = NavSys ? NavSys->GetNavOctree() : NULL;
if (Snapshot == nullptr)
{
return;
}
if (NavOctree == NULL)
{
UE_LOG(LogNavigation, Error, TEXT("Failed to vlog navigation data due to %s being NULL"), NavSys == NULL ? TEXT("NavigationSystem") : TEXT("NavOctree"));
return;
}
ELogVerbosity::Type NavAreaVerbosity = FMath::Clamp(ELogVerbosity::Type(LogVerbosity + 1), ELogVerbosity::NoLogging, ELogVerbosity::VeryVerbose);
for (int32 Index = 0; Index < NavSys->NavDataSet.Num(); ++Index)
{
TArray<FVector> CoordBuffer;
TArray<int32> Indices;
TNavStatArray<FVector> Faces;
const ARecastNavMesh* NavData = Cast<const ARecastNavMesh>(NavSys->NavDataSet[Index]);
if (NavData)
{
for (FNavigationOctree::TConstElementBoxIterator<FNavigationOctree::DefaultStackAllocator> It(*NavOctree, BoundingBox);
It.HasPendingElements();
It.Advance())
{
const FNavigationOctreeElement& Element = It.GetCurrentElement();
const bool bExportGeometry = Element.Data->HasGeometry() && Element.ShouldUseGeometry(DestNavMesh->GetConfig());
if (bExportGeometry && Element.Data->CollisionData.Num())
{
FRecastGeometryCache CachedGeometry(Element.Data->CollisionData.GetData());
const uint32 NumVerts = CachedGeometry.Header.NumVerts;
CoordBuffer.Reset(NumVerts);
for (uint32 VertIdx = 0; VertIdx < NumVerts * 3; VertIdx += 3)
{
CoordBuffer.Add(Recast2UnrealPoint(&CachedGeometry.Verts[VertIdx]));
}
const uint32 NumIndices = CachedGeometry.Header.NumFaces * 3;
Indices.SetNum(NumIndices, false);
for (uint32 IndicesIdx = 0; IndicesIdx < NumIndices; ++IndicesIdx)
{
Indices[IndicesIdx] = CachedGeometry.Indices[IndicesIdx];
}
Snapshot->AddElement(CoordBuffer, Indices, CategoryName, LogVerbosity, FColorList::LightGrey.WithAlpha(255));
}
else
{
TArray<FVector> Verts;
const TArray<FAreaNavModifier>& AreaMods = Element.Data->Modifiers.GetAreas();
for (int32 i = 0; i < AreaMods.Num(); i++)
{
if (AreaMods[i].GetShapeType() == ENavigationShapeType::Unknown)
{
continue;
}
const uint8 AreaId = NavData->GetAreaID(AreaMods[i].GetAreaClass());
const UClass* AreaClass = NavData->GetAreaClass(AreaId);
const UNavArea* DefArea = AreaClass ? ((UClass*)AreaClass)->GetDefaultObject<UNavArea>() : NULL;
const FColor PolygonColor = AreaClass != FNavigationSystem::GetDefaultWalkableArea() ? (DefArea ? DefArea->DrawColor : NavData->GetConfig().Color) : FColorList::Cyan;
if (AreaMods[i].GetShapeType() == ENavigationShapeType::Box)
{
FBoxNavAreaData Box;
AreaMods[i].GetBox(Box);
Snapshot->AddElement(FBox::BuildAABB(Box.Origin, Box.Extent), FMatrix::Identity, CategoryName, NavAreaVerbosity, PolygonColor.WithAlpha(255));
}
else if (AreaMods[i].GetShapeType() == ENavigationShapeType::Cylinder)
{
FCylinderNavAreaData Cylinder;
AreaMods[i].GetCylinder(Cylinder);
Snapshot->AddElement(Cylinder.Origin, Cylinder.Origin + FVector(0, 0, Cylinder.Height), Cylinder.Radius, CategoryName, NavAreaVerbosity, PolygonColor.WithAlpha(255));
}
else
{
FConvexNavAreaData Convex;
AreaMods[i].GetConvex(Convex);
Verts.Reset();
GrowConvexHull(NavData->AgentRadius, Convex.Points, Verts);
Snapshot->AddElement(
Verts,
Convex.MinZ - NavData->CellHeight,
Convex.MaxZ + NavData->CellHeight,
CategoryName, NavAreaVerbosity, PolygonColor.WithAlpha(255));
}
}
}
}
}
}
}
#endif
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) && ENABLE_VISUAL_LOG
void FRecastNavMeshGenerator::ExportNavigationData(const FString& FileName) const
{
const UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
const FNavigationOctree* NavOctree = NavSys ? NavSys->GetNavOctree() : NULL;
if (NavOctree == NULL)
{
UE_LOG(LogNavigation, Error, TEXT("Failed to export navigation data due to %s being NULL"), NavSys == NULL ? TEXT("NavigationSystem") : TEXT("NavOctree"));
return;
}
const double StartExportTime = FPlatformTime::Seconds();
FString CurrentTimeStr = FDateTime::Now().ToString();
for (int32 Index = 0; Index < NavSys->NavDataSet.Num(); ++Index)
{
// feed data from octtree and mark for rebuild
TNavStatArray<float> CoordBuffer;
TNavStatArray<int32> IndexBuffer;
const ARecastNavMesh* NavData = Cast<const ARecastNavMesh>(NavSys->NavDataSet[Index]);
if (NavData)
{
struct FAreaExportData
{
FConvexNavAreaData Convex;
uint8 AreaId;
};
TArray<FAreaExportData> AreaExport;
for(FNavigationOctree::TConstElementBoxIterator<FNavigationOctree::DefaultStackAllocator> It(*NavOctree, TotalNavBounds);
It.HasPendingElements();
It.Advance())
{
const FNavigationOctreeElement& Element = It.GetCurrentElement();
const bool bExportGeometry = Element.Data->HasGeometry() && Element.ShouldUseGeometry(DestNavMesh->GetConfig());
if (bExportGeometry && Element.Data->CollisionData.Num())
{
FRecastGeometryCache CachedGeometry(Element.Data->CollisionData.GetData());
IndexBuffer.Reserve( IndexBuffer.Num() + (CachedGeometry.Header.NumFaces * 3 ));
CoordBuffer.Reserve( CoordBuffer.Num() + (CachedGeometry.Header.NumVerts * 3 ));
for (int32 i = 0; i < CachedGeometry.Header.NumFaces * 3; i++)
{
IndexBuffer.Add(CachedGeometry.Indices[i] + CoordBuffer.Num() / 3);
}
for (int32 i = 0; i < CachedGeometry.Header.NumVerts * 3; i++)
{
CoordBuffer.Add(CachedGeometry.Verts[i]);
}
}
else
{
const TArray<FAreaNavModifier>& AreaMods = Element.Data->Modifiers.GetAreas();
for (int32 i = 0; i < AreaMods.Num(); i++)
{
FAreaExportData ExportInfo;
ExportInfo.AreaId = NavData->GetAreaID(AreaMods[i].GetAreaClass());
if (AreaMods[i].GetShapeType() == ENavigationShapeType::Convex)
{
AreaMods[i].GetConvex(ExportInfo.Convex);
TArray<FVector> ConvexVerts;
GrowConvexHull(NavData->AgentRadius, ExportInfo.Convex.Points, ConvexVerts);
ExportInfo.Convex.MinZ -= NavData->CellHeight;
ExportInfo.Convex.MaxZ += NavData->CellHeight;
ExportInfo.Convex.Points = ConvexVerts;
AreaExport.Add(ExportInfo);
}
}
}
}
UWorld* NavigationWorld = GetWorld();
for (int32 LevelIndex = 0; LevelIndex < NavigationWorld->GetNumLevels(); ++LevelIndex)
{
const ULevel* const Level = NavigationWorld->GetLevel(LevelIndex);
if (Level == NULL)
{
continue;
}
const TArray<FVector>* LevelGeom = Level->GetStaticNavigableGeometry();
if (LevelGeom != NULL && LevelGeom->Num() > 0)
{
TNavStatArray<FVector> Verts;
TNavStatArray<int32> Faces;
// For every ULevel in World take its pre-generated static geometry vertex soup
RecastGeometryExport::TransformVertexSoupToRecast(*LevelGeom, Verts, Faces);
IndexBuffer.Reserve( IndexBuffer.Num() + Faces.Num() );
CoordBuffer.Reserve( CoordBuffer.Num() + Verts.Num() * 3);
for (int32 i = 0; i < Faces.Num(); i++)
{
IndexBuffer.Add(Faces[i] + CoordBuffer.Num() / 3);
}
for (int32 i = 0; i < Verts.Num(); i++)
{
CoordBuffer.Add(Verts[i].X);
CoordBuffer.Add(Verts[i].Y);
CoordBuffer.Add(Verts[i].Z);
}
}
}
FString AreaExportStr;
for (int32 i = 0; i < AreaExport.Num(); i++)
{
const FAreaExportData& ExportInfo = AreaExport[i];
AreaExportStr += FString::Printf(TEXT("\nAE %d %d %f %f\n"),
ExportInfo.AreaId, ExportInfo.Convex.Points.Num(), ExportInfo.Convex.MinZ, ExportInfo.Convex.MaxZ);
for (int32 iv = 0; iv < ExportInfo.Convex.Points.Num(); iv++)
{
FVector Pt = Unreal2RecastPoint(ExportInfo.Convex.Points[iv]);
AreaExportStr += FString::Printf(TEXT("Av %f %f %f\n"), Pt.X, Pt.Y, Pt.Z);
}
}
FString AdditionalData;
if (AreaExport.Num())
{
AdditionalData += "# Area export\n";
AdditionalData += AreaExportStr;
AdditionalData += "\n";
}
AdditionalData += "# RecastDemo specific data\n";
#if 0
// use this bounds to have accurate navigation data bounds
const FVector Center = Unreal2RecastPoint(NavData->GetBounds().GetCenter());
FVector Extent = FVector(NavData->GetBounds().GetExtent());
Extent = FVector(Extent.X, Extent.Z, Extent.Y);
#else
// this bounds match navigation bounds from level
FBox RCNavBounds = Unreal2RecastBox(TotalNavBounds);
const FVector Center = RCNavBounds.GetCenter();
const FVector Extent = RCNavBounds.GetExtent();
#endif
const FBox Box = FBox::BuildAABB(Center, Extent);
AdditionalData += FString::Printf(
TEXT("rd_bbox %7.7f %7.7f %7.7f %7.7f %7.7f %7.7f\n"),
Box.Min.X, Box.Min.Y, Box.Min.Z,
Box.Max.X, Box.Max.Y, Box.Max.Z
);
const FRecastNavMeshGenerator* CurrentGen = static_cast<const FRecastNavMeshGenerator*>(NavData->GetGenerator());
check(CurrentGen);
AdditionalData += FString::Printf(TEXT("# AgentHeight\n"));
AdditionalData += FString::Printf(TEXT("rd_agh %5.5f\n"), CurrentGen->Config.AgentHeight);
AdditionalData += FString::Printf(TEXT("# AgentRadius\n"));
AdditionalData += FString::Printf(TEXT("rd_agr %5.5f\n"), CurrentGen->Config.AgentRadius);
AdditionalData += FString::Printf(TEXT("# Cell Size\n"));
AdditionalData += FString::Printf(TEXT("rd_cs %5.5f\n"), CurrentGen->Config.cs);
AdditionalData += FString::Printf(TEXT("# Cell Height\n"));
AdditionalData += FString::Printf(TEXT("rd_ch %5.5f\n"), CurrentGen->Config.ch);
AdditionalData += FString::Printf(TEXT("# Agent max climb\n"));
AdditionalData += FString::Printf(TEXT("rd_amc %d\n"), (int)CurrentGen->Config.AgentMaxClimb);
AdditionalData += FString::Printf(TEXT("# Agent max slope\n"));
AdditionalData += FString::Printf(TEXT("rd_ams %5.5f\n"), CurrentGen->Config.walkableSlopeAngle);
AdditionalData += FString::Printf(TEXT("# Region min size\n"));
AdditionalData += FString::Printf(TEXT("rd_rmis %d\n"), (uint32)FMath::Sqrt(CurrentGen->Config.minRegionArea));
AdditionalData += FString::Printf(TEXT("# Region merge size\n"));
AdditionalData += FString::Printf(TEXT("rd_rmas %d\n"), (uint32)FMath::Sqrt(CurrentGen->Config.mergeRegionArea));
AdditionalData += FString::Printf(TEXT("# Max edge len\n"));
AdditionalData += FString::Printf(TEXT("rd_mel %d\n"), CurrentGen->Config.maxEdgeLen);
AdditionalData += FString::Printf(TEXT("# Perform Voxel Filtering\n"));
AdditionalData += FString::Printf(TEXT("rd_pvf %d\n"), CurrentGen->Config.bPerformVoxelFiltering);
AdditionalData += FString::Printf(TEXT("# Generate Detailed Mesh\n"));
AdditionalData += FString::Printf(TEXT("rd_gdm %d\n"), CurrentGen->Config.bGenerateDetailedMesh);
AdditionalData += FString::Printf(TEXT("# MaxPolysPerTile\n"));
AdditionalData += FString::Printf(TEXT("rd_mppt %d\n"), CurrentGen->Config.MaxPolysPerTile);
AdditionalData += FString::Printf(TEXT("# maxVertsPerPoly\n"));
AdditionalData += FString::Printf(TEXT("rd_mvpp %d\n"), CurrentGen->Config.maxVertsPerPoly);
AdditionalData += FString::Printf(TEXT("# Tile size\n"));
AdditionalData += FString::Printf(TEXT("rd_ts %d\n"), CurrentGen->Config.tileSize);
AdditionalData += FString::Printf(TEXT("\n"));
const FString FilePathName = FileName + FString::Printf(TEXT("_NavDataSet%d_%s.obj"), Index, *CurrentTimeStr) ;
ExportGeomToOBJFile(FilePathName, CoordBuffer, IndexBuffer, AdditionalData);
}
}
UE_LOG(LogNavigation, Log, TEXT("ExportNavigation time: %.3f sec ."), FPlatformTime::Seconds() - StartExportTime);
}
#endif
static class FNavigationGeomExec : private FSelfRegisteringExec
{
public:
/** Console commands, see embeded usage statement **/
virtual bool Exec( UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar ) override
{
#if ALLOW_DEBUG_FILES && !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
bool bCorrectCmd = FParse::Command(&Cmd, TEXT("ExportNavigation"));
if (bCorrectCmd && !InWorld)
{
UE_LOG(LogNavigation, Error, TEXT("Failed to export navigation data due to missing UWorld"));
}
else if (InWorld && bCorrectCmd)
{
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(InWorld);
if (NavSys)
{
const ANavigationData* NavData = NavSys->GetDefaultNavDataInstance();
if (NavData)
{
if (const FNavDataGenerator* Generator = NavData->GetGenerator())
{
const FString Name = NavData->GetName();
Generator->ExportNavigationData( FString::Printf( TEXT("%s/%s"), *FPaths::ProjectSavedDir(), *Name ));
return true;
}
else
{
UE_LOG(LogNavigation, Error, TEXT("Failed to export navigation data due to missing generator"));
}
}
else
{
UE_LOG(LogNavigation, Error, TEXT("Failed to export navigation data due to navigation data"));
}
}
else
{
UE_LOG(LogNavigation, Error, TEXT("Failed to export navigation data due to missing navigation system"));
}
}
#endif // ALLOW_DEBUG_FILES && WITH_EDITOR
return false;
}
} NavigationGeomExec;
#endif // WITH_RECAST