Files
UnrealEngineUWP/Engine/Source/Runtime/NavigationSystem/Private/NavMesh/RecastNavMeshGenerator.cpp
guillaume guay a8821f80b4 - Added navigation system config 'bUseNavDataInAdditionalLevelWhenDuplicatedAgent". When set to TRUE, adding or streaming an additional level containing an already registered navigation data, it will unregister current one and register the new one;
- Added navigation system config 'bGenerateNavDataWhenNoCompatibleNavBound". When set to TRUE (default) it will still generate navigation data if any NavBoundVolume is compatible with its NavDataConfig, if false it gets rid of that navigation data;
- Make sure that when "bSpawnNavDataInNavBoundsLevel" is TRUE, it builds the navigation data in the first level containing a NavBoundVolume that actually supports this navigation data config (not any first one);
- Added virtual method on NavigateSystem "OnNavSystemOverriden" to give a chance to inspect previous NavigationSystem when getting overriden by a nav config override;
- Added some missing NavigationSystem clean up logic.

#tests client/server standalone Creative AI, Phoebe and regular + editor
#cr maxime.mercier
[FYI] mieszko.zielinski, stephen.holmes, yoan.stamant


#ROBOMERGE-OWNER: guillaume.guay
#ROBOMERGE-AUTHOR: guillaume.guay
#ROBOMERGE-SOURCE: CL 7679442 via CL 7679443 via CL 7688466
#ROBOMERGE-BOT: (v383-7686620)

[CL 7688470 by guillaume guay in Main branch]
2019-08-01 12:53:04 -04:00

5176 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 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.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)
{
const UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
if (NavSys && !NavSys->ShouldGenerateNavDataWhenNoCompatibleNavBound())
{
// There are no navigation bounds to build, no point keeping the nav mesh
DestNavMesh->Destroy();
}
else
{
// 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