8153 lines
285 KiB
C++
8153 lines
285 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "NavMesh/RecastNavMeshGenerator.h"
|
|
|
|
#include "DynamicMeshBuilder.h"
|
|
#include "AI/Navigation/NavRelevantInterface.h"
|
|
#include "Compression/OodleDataCompression.h"
|
|
#include "Engine/Level.h"
|
|
#include "GameFramework/Pawn.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Serialization/MemoryWriter.h"
|
|
#include "GameFramework/PlayerController.h"
|
|
#include "Engine/Engine.h"
|
|
#include "Logging/LogScopedCategoryAndVerbosityOverride.h"
|
|
#include "NavigationSystem.h"
|
|
#include "FramePro/FrameProProfiler.h"
|
|
#include "NavMesh/RecastVersion.h"
|
|
#include "UObject/GarbageCollection.h"
|
|
|
|
#if WITH_RECAST
|
|
|
|
#include "Chaos/HeightField.h"
|
|
#include "Chaos/TriangleMeshImplicitObject.h"
|
|
#include "NavMesh/PImplRecastNavMesh.h"
|
|
#include "NavMesh/RecastGeometryExport.h"
|
|
#include "VisualLogger/VisualLogger.h"
|
|
|
|
// recast includes
|
|
#include "Detour/DetourNavMeshBuilder.h"
|
|
#include "Detour/DetourNavLinkBuilder.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"
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
#include "DebugUtils/DebugDraw.h"
|
|
#include "DebugUtils/RecastDebugDraw.h"
|
|
#include "DebugUtils/DetourDebugDraw.h"
|
|
#include "DebugUtils/DetourNavLinkDebugDraw.h"
|
|
#endif //RECAST_INTERNAL_DEBUG_DATA
|
|
|
|
#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 SHOW_NAV_EXPORT_PREVIEW 0
|
|
|
|
CSV_DEFINE_CATEGORY(NAVREGEN, false);
|
|
|
|
struct dtTileCacheAlloc;
|
|
|
|
//Experimental debug tools
|
|
static int32 GNavmeshSynchronousTileGeneration = 0;
|
|
static FAutoConsoleVariableRef NavmeshVarSynchronous(TEXT("ai.nav.GNavmeshSynchronousTileGeneration"), GNavmeshSynchronousTileGeneration, TEXT(""), ECVF_Default);
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
static int32 GNavmeshDebugTileX = MAX_int32;
|
|
static int32 GNavmeshDebugTileY = MAX_int32;
|
|
static bool GNavmeshGenerateDebugTileOnly = false;
|
|
static FAutoConsoleVariableRef NavmeshVarDebugTileX(TEXT("ai.nav.GNavmeshDebugTileX"), GNavmeshDebugTileX, TEXT(""), ECVF_Default);
|
|
static FAutoConsoleVariableRef NavmeshVarDebugTileY(TEXT("ai.nav.GNavmeshDebugTileY"), GNavmeshDebugTileY, TEXT(""), ECVF_Default);
|
|
static FAutoConsoleVariableRef NavmeshVarGenerateDebugTileOnly(TEXT("ai.nav.GNavmeshGenerateDebugTileOnly"), GNavmeshGenerateDebugTileOnly, TEXT(""), ECVF_Default);
|
|
#endif //RECAST_INTERNAL_DEBUG_DATA
|
|
|
|
// Hotfixing this flag without rebuilding the data will cause decompression errors, equivalent to not having prebuilt navmesh data at all.
|
|
static bool GNavmeshUseOodleCompression = true;
|
|
static FAutoConsoleVariableRef NavmeshVarOodleCompression(TEXT("ai.nav.NavmeshUseOodleCompression"), GNavmeshUseOodleCompression, TEXT("Use Oodle for run-time tile cache compression/decompression. Optimized for size in editor, optimized for speed in standalone."), ECVF_Default);
|
|
|
|
namespace UE::NavMesh::Private
|
|
{
|
|
static float RecentlyBuildTileDisplayTime = 0.2f;
|
|
static FAutoConsoleVariableRef CVarRecentlyBuildTileDisplayTime(TEXT("ai.nav.RecentlyBuildTileDisplayTime"), RecentlyBuildTileDisplayTime, TEXT("Time (in seconds) to display tiles that have recently been built."), ECVF_Default);
|
|
|
|
static bool bUseTightBoundExpansion = true;
|
|
static FAutoConsoleVariableRef CVarUseTightBoundExpansion(TEXT("ai.nav.UseTightBoundExpansion"), bUseTightBoundExpansion, TEXT("Active by default. Use an expansion of one AgentRadius. Set to false to revert to the previous behavior (2 AgentRadius)."), ECVF_Default);
|
|
|
|
static bool bUseAsymetricBorderSizes = false;
|
|
static FAutoConsoleVariableRef CVarUseAsymetricBorderSizes(TEXT("ai.nav.UseAsymetricBorderSizes"), bUseAsymetricBorderSizes, TEXT("Active by default. When generating links, use asymetric tile border sizes to improve generation speed."), ECVF_Default);
|
|
|
|
static bool bAllowLinkGeneration = true;
|
|
static FAutoConsoleVariableRef CVarAllowLinkGeneration(TEXT("ai.nav.AllowLinkGeneration"), bAllowLinkGeneration, TEXT("Set to false to force disabling link generation."), ECVF_Default);
|
|
|
|
}
|
|
|
|
static FOodleDataCompression::ECompressor GNavmeshTileCacheCompressor = FOodleDataCompression::ECompressor::Mermaid;
|
|
static FOodleDataCompression::ECompressionLevel GNavmeshTileCacheCompressionLevel = FOodleDataCompression::ECompressionLevel::HyperFast1;
|
|
|
|
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 InFileName - full name of OBJ file with extension
|
|
* @param GeomCoords - list of vertices
|
|
* @param GeomFaces - list of triangles (3 vert indices for each)
|
|
*/
|
|
static void ExportGeomToOBJFile(const FString& InFileName, const TNavStatArray<FVector::FReal>& 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, EAllowShrinking::No);
|
|
}
|
|
};
|
|
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
|
|
}
|
|
|
|
FRecastVoxelCache::FRecastVoxelCache(const uint8* Memory)
|
|
{
|
|
uint8* BytesArr = (uint8*)Memory;
|
|
if (Memory)
|
|
{
|
|
NumTiles = *((int32*)BytesArr); BytesArr += sizeof(int32);
|
|
if (NumTiles > 0)
|
|
{
|
|
Tiles = (FTileInfo*)BytesArr;
|
|
|
|
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;
|
|
}
|
|
|
|
iTile->NextTile = 0;
|
|
}
|
|
else
|
|
{
|
|
Tiles = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NumTiles = 0;
|
|
Tiles = 0;
|
|
}
|
|
}
|
|
|
|
FRecastGeometryCache::FRecastGeometryCache(const uint8* Memory)
|
|
{
|
|
Header = *((FHeader*)Memory);
|
|
Verts = (FVector::FReal*)(Memory + sizeof(FRecastGeometryCache));
|
|
Indices = (int32*)(Memory + sizeof(FRecastGeometryCache) + (sizeof(FVector::FReal) * Header.NumVerts * 3));
|
|
}
|
|
|
|
rcRasterizationFlags FRecastRawGeometryElement::GetRasterizationFlags(const FCompositeNavModifier& InModifier)
|
|
{
|
|
int32 Flags = 0;
|
|
if (InModifier.GetFillCollisionUnderneathForNavmesh())
|
|
{
|
|
// To prevent navmesh generation under the geometry, set the RC_PROJECT_TO_BOTTOM flag to true.
|
|
// This rasterize triangles as filled columns down to the HF lower bound.
|
|
Flags |= RC_PROJECT_TO_BOTTOM;
|
|
}
|
|
if (InModifier.GetRasterizeAsFilledConvexVolume())
|
|
{
|
|
// To rasterized as filled convex volume, set RC_RASTERIZE_AS_FILLED_CONVEX flag to true.
|
|
// It will rasterize all the triangles of an ElementData into a 2d grid of 1 span per cell thus making sure that the interior is full
|
|
Flags |= RC_RASTERIZE_AS_FILLED_CONVEX;
|
|
}
|
|
|
|
return rcRasterizationFlags(Flags);
|
|
}
|
|
|
|
namespace RecastGeometryExport {
|
|
|
|
#if SHOW_NAV_EXPORT_PREVIEW
|
|
static UWorld* FindEditorWorld()
|
|
{
|
|
if (GEngine)
|
|
{
|
|
for (const FWorldContext& Context : GEngine->GetWorldContexts())
|
|
{
|
|
if (Context.WorldType == EWorldType::Editor)
|
|
{
|
|
return Context.World();
|
|
}
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
#endif //SHOW_NAV_EXPORT_PREVIEW
|
|
|
|
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(FVector::FReal) * 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);
|
|
}
|
|
|
|
void ExportChaosTriMesh(const Chaos::FTriangleMeshImplicitObject* const TriMesh, const FTransform& LocalToWorld
|
|
, TNavStatArray<FVector::FReal>& VertexBuffer, TNavStatArray<int32>& IndexBuffer
|
|
, FBox& UnrealBounds)
|
|
{
|
|
if (TriMesh == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
using namespace Chaos;
|
|
|
|
int32 VertOffset = VertexBuffer.Num() / 3;
|
|
|
|
auto LambdaHelper = [&](const auto& Triangles)
|
|
{
|
|
int32 NumTris = Triangles.Num();
|
|
const Chaos::FTriangleMeshImplicitObject::ParticlesType& Vertices = TriMesh->Particles();
|
|
|
|
VertexBuffer.Reserve(VertexBuffer.Num() + NumTris * 9);
|
|
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 (int32 TriIdx = 0; TriIdx < NumTris; ++TriIdx)
|
|
{
|
|
for (int32 i = 0; i < 3; i++)
|
|
{
|
|
const FVector UnrealCoords = LocalToWorld.TransformPosition((FVector)Vertices.GetX(Triangles[TriIdx][i]));
|
|
UnrealBounds += UnrealCoords;
|
|
|
|
VertexBuffer.Add(UnrealCoords.X);
|
|
VertexBuffer.Add(UnrealCoords.Y);
|
|
VertexBuffer.Add(UnrealCoords.Z);
|
|
}
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
|
|
const FTrimeshIndexBuffer& IdxBuffer = TriMesh->Elements();
|
|
if(IdxBuffer.RequiresLargeIndices())
|
|
{
|
|
LambdaHelper(IdxBuffer.GetLargeIndexBuffer());
|
|
}
|
|
else
|
|
{
|
|
LambdaHelper(IdxBuffer.GetSmallIndexBuffer());
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void ExportChaosConvexMesh(const FKConvexElem* const Convex, const FTransform& LocalToWorld
|
|
, TNavStatArray<FVector::FReal>& VertexBuffer, TNavStatArray<int32>& IndexBuffer
|
|
, FBox& UnrealBounds)
|
|
{
|
|
using namespace Chaos;
|
|
|
|
if (Convex == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_NavMesh_ExportChaosConvexMesh);
|
|
|
|
int32 VertOffset = VertexBuffer.Num() / 3;
|
|
|
|
VertexBuffer.Reserve(VertexBuffer.Num() + Convex->VertexData.Num() * 3);
|
|
IndexBuffer.Reserve(IndexBuffer.Num() + Convex->IndexData.Num());
|
|
|
|
#if SHOW_NAV_EXPORT_PREVIEW
|
|
UWorld* DebugWorld = FindEditorWorld();
|
|
#endif // SHOW_NAV_EXPORT_PREVIEW
|
|
|
|
if (Convex->VertexData.Num())
|
|
{
|
|
if(Convex->IndexData.Num() == 0)
|
|
{
|
|
UE_LOG(LogNavigation, Verbose, TEXT("Zero indices in convex."));
|
|
return;
|
|
}
|
|
|
|
if(Convex->IndexData.Num() % 3 != 0)
|
|
{
|
|
UE_LOG(LogNavigation, Verbose, TEXT("Invalid indices in convex."));
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (const FVector& Vertex : Convex->VertexData)
|
|
{
|
|
const FVector UnrealCoord = LocalToWorld.TransformPosition(Vertex);
|
|
UnrealBounds += UnrealCoord;
|
|
|
|
VertexBuffer.Add(UnrealCoord.X);
|
|
VertexBuffer.Add(UnrealCoord.Y);
|
|
VertexBuffer.Add(UnrealCoord.Z);
|
|
}
|
|
|
|
if (Convex->IndexData.Num() % 3 == 0)
|
|
{
|
|
for (int32 i = 0; i < Convex->IndexData.Num(); i += 3)
|
|
{
|
|
IndexBuffer.Add(VertOffset + Convex->IndexData[i]);
|
|
IndexBuffer.Add(VertOffset + Convex->IndexData[i + 2]);
|
|
IndexBuffer.Add(VertOffset + Convex->IndexData[i + 1]);
|
|
}
|
|
}
|
|
|
|
#if SHOW_NAV_EXPORT_PREVIEW
|
|
if (DebugWorld)
|
|
{
|
|
for (int32 Index = VertOffset; Index < VertexBuffer.Num(); Index += 3)
|
|
{
|
|
FVector V0(VertexBuffer[IndexBuffer[Index] * 3], VertexBuffer[IndexBuffer[Index] * 3 + 1], VertexBuffer[IndexBuffer[Index] * 3] + 2);
|
|
FVector V1(VertexBuffer[IndexBuffer[Index + 1] * 3], VertexBuffer[IndexBuffer[Index + 1] * 3 + 1], VertexBuffer[IndexBuffer[Index + 1] * 3] + 2);
|
|
FVector V2(VertexBuffer[IndexBuffer[Index + 2] * 3], VertexBuffer[IndexBuffer[Index + 2] * 3 + 1], VertexBuffer[IndexBuffer[Index + 2] * 3] + 2);
|
|
|
|
DrawDebugLine(DebugWorld, V0, V1, FColor::Blue, true);
|
|
DrawDebugLine(DebugWorld, V1, V2, FColor::Blue, true);
|
|
DrawDebugLine(DebugWorld, V2, V0, FColor::Blue, true);
|
|
}
|
|
}
|
|
#endif // SHOW_NAV_EXPORT_PREVIEW
|
|
}
|
|
|
|
void ExportChaosHeightField(const Chaos::FHeightField* const HeightField, const FTransform& LocalToWorld
|
|
, TNavStatArray<FVector::FReal>& VertexBuffer, TNavStatArray<int32>& IndexBuffer
|
|
, FBox& UnrealBounds)
|
|
{
|
|
using namespace Chaos;
|
|
|
|
if(HeightField == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_NavMesh_ExportChaosHeightField);
|
|
|
|
const int32 NumRows = HeightField->GetNumRows();
|
|
const int32 NumCols = HeightField->GetNumCols();
|
|
const int32 VertexCount = NumRows * NumCols;
|
|
|
|
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 = Y * NumCols + X; // #PHYSTODO bMirrored support
|
|
|
|
const FVector UnrealCoords = LocalToWorld.TransformPosition(FVector(X, Y, HeightField->GetHeight(SampleIdx)));
|
|
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++)
|
|
{
|
|
if(HeightField->IsHole(X, Y))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const int32 I0 = Y * NumCols + X; // #PHYSTODO bMirrored support
|
|
int32 I1 = I0 + 1;
|
|
int32 I2 = I0 + NumCols;
|
|
const int32 I3 = I2 + 1;
|
|
|
|
if(bMirrored)
|
|
{
|
|
// Flip the winding so the triangles face the right way after scaling
|
|
Swap(I1, I2);
|
|
}
|
|
|
|
IndexBuffer.Add(VertOffset + I0);
|
|
IndexBuffer.Add(VertOffset + I3);
|
|
IndexBuffer.Add(VertOffset + I1);
|
|
|
|
IndexBuffer.Add(VertOffset + I0);
|
|
IndexBuffer.Add(VertOffset + I2);
|
|
IndexBuffer.Add(VertOffset + I3);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ExportChaosHeightFieldSlice(const FNavHeightfieldSamples& PrefetchedHeightfieldSamples, const int32 NumRows, const int32 NumCols, const FTransform& LocalToWorld
|
|
, TNavStatArray<FVector::FReal>& VertexBuffer, TNavStatArray<int32>& IndexBuffer, const FBox& SliceBox
|
|
, FBox& UnrealBounds)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_NavMesh_ExportChaosHeightFieldSlice);
|
|
|
|
// 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::FloorToInt32(LocalBox.Min.X) - 1, 0, NumCols);
|
|
const int32 MinY = FMath::Clamp(FMath::FloorToInt32(LocalBox.Min.Y) - 1, 0, NumRows);
|
|
const int32 MaxX = FMath::Clamp(FMath::CeilToInt32(LocalBox.Max.X) + 1, 0, NumCols);
|
|
const int32 MaxY = FMath::Clamp(FMath::CeilToInt32(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 = CoordY * NumCols + CoordX; // #PHYSTODO bMirrored support
|
|
|
|
|
|
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 = CoordY * (NumCols-1) + CoordX; // #PHYSTODO bMirrored support
|
|
|
|
const bool bIsHole = PrefetchedHeightfieldSamples.Holes[SampleIdx];
|
|
if (bIsHole)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const int32 I0 = IdxY * SizeX + IdxX;
|
|
int32 I1 = I0 + 1;
|
|
int32 I2 = I0 + SizeX;
|
|
const int32 I3 = I2 + 1;
|
|
if (bMirrored)
|
|
{
|
|
Swap(I1, I2);
|
|
}
|
|
|
|
IndexBuffer.Add(VertOffset + I0);
|
|
IndexBuffer.Add(VertOffset + I3);
|
|
IndexBuffer.Add(VertOffset + I1);
|
|
|
|
IndexBuffer.Add(VertOffset + I0);
|
|
IndexBuffer.Add(VertOffset + I2);
|
|
IndexBuffer.Add(VertOffset + I3);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void ExportCustomMesh(const FVector* InVertices, int32 NumVerts, const int32* InIndices, int32 NumIndices, const FTransform& LocalToWorld,
|
|
TNavStatArray<FVector::FReal>& VertexBuffer, TNavStatArray<int32>& IndexBuffer, FBox& UnrealBounds)
|
|
{
|
|
if (NumVerts <= 0 || NumIndices <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (InVertices == nullptr || InIndices == nullptr)
|
|
{
|
|
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<FVector::FReal>& 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<FVector::FReal>& VertexBuffer, TNavStatArray<int32>& IndexBuffer,
|
|
TNavStatArray<int32>& ShapeBuffer, FBox& UnrealBounds, const FTransform& LocalToWorld)
|
|
{
|
|
const int32 ConvexCount = BodySetup.AggGeom.ConvexElems.Num();
|
|
FKConvexElem const* ConvexElem = BodySetup.AggGeom.ConvexElems.GetData();
|
|
|
|
for (int32 i = 0; i < ConvexCount; ++i, ++ConvexElem)
|
|
{
|
|
// Store index of first vertex in shape buffer
|
|
ShapeBuffer.Add(VertexBuffer.Num() / 3);
|
|
|
|
if (ConvexElem->GetChaosConvexMesh())
|
|
{
|
|
ExportChaosConvexMesh(ConvexElem, ConvexElem->GetTransform() * LocalToWorld, VertexBuffer, IndexBuffer, UnrealBounds);
|
|
}
|
|
}
|
|
}
|
|
|
|
FORCEINLINE_DEBUGGABLE void ExportRigidBodyTriMesh(UBodySetup& BodySetup, TNavStatArray<FVector::FReal>& VertexBuffer, TNavStatArray<int32>& IndexBuffer,
|
|
FBox& UnrealBounds, const FTransform& LocalToWorld)
|
|
{
|
|
if (BodySetup.GetCollisionTraceFlag() == CTF_UseComplexAsSimple)
|
|
{
|
|
for(const auto& TriMesh : BodySetup.TriMeshGeometries)
|
|
{
|
|
ExportChaosTriMesh(TriMesh.GetReference(), LocalToWorld, VertexBuffer, IndexBuffer, UnrealBounds);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ExportRigidBodyBoxElements(const FKAggregateGeom& AggGeom, TNavStatArray<FVector::FReal>& 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 < UE_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<FVector::FReal>& 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++)
|
|
{
|
|
FVector::FReal Angle;
|
|
FVector::FReal ZOffset;
|
|
if (RingIdx <= NumSides/4)
|
|
{
|
|
Angle = ((FVector::FReal)RingIdx/(NumRings-1)) * (FVector::FReal)DOUBLE_PI;
|
|
ZOffset = 0.5f * SphylInfo.Length;
|
|
}
|
|
else
|
|
{
|
|
Angle = ((FVector::FReal)(RingIdx-1)/(NumRings-1)) * (FVector::FReal)DOUBLE_PI;
|
|
ZOffset = -0.5f * 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<FVector::FReal>& 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++)
|
|
{
|
|
FVector::FReal Angle = ((FVector::FReal)RingIdx/NumRings) * (FVector::FReal)DOUBLE_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<FVector::FReal>& 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();
|
|
}
|
|
|
|
void ExportObject(const FNavigationElement& Element, FRecastGeometryExport& GeomExport)
|
|
{
|
|
const EHasCustomNavigableGeometry::Type GeometryExportType = Element.GetGeometryExportType();
|
|
if (GeometryExportType == EHasCustomNavigableGeometry::DontExport)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool bDefaultGeometryExportRequired = true;
|
|
if (GeometryExportType != EHasCustomNavigableGeometry::No)
|
|
{
|
|
Element.CustomGeometryExportDelegate.ExecuteIfBound(Element, GeomExport, bDefaultGeometryExportRequired);
|
|
}
|
|
|
|
if (UBodySetup* BodySetup = Element.GetBodySetup())
|
|
{
|
|
if (bDefaultGeometryExportRequired)
|
|
{
|
|
ExportRigidBodySetup(*BodySetup, GeomExport.VertexBuffer, GeomExport.IndexBuffer, GeomExport.Data->Bounds, Element.GetTransform());
|
|
}
|
|
GeomExport.SlopeOverride = BodySetup->WalkableSlopeOverride;
|
|
}
|
|
}
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
FORCEINLINE_DEBUGGABLE void ValidateGeometryExport(const FRecastGeometryExport& GeomExport)
|
|
{
|
|
if (const UObject* Owner = GeomExport.Data->SourceElement->GetWeakUObject().Get())
|
|
{
|
|
if (const UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(Owner->GetWorld()))
|
|
{
|
|
constexpr int32 CoordinatePerTriangle = 9;
|
|
if (NavSys->GeometryExportTriangleCountWarningThreshold > 0
|
|
&& (GeomExport.VertexBuffer.Num() / CoordinatePerTriangle) > NavSys->GeometryExportTriangleCountWarningThreshold)
|
|
{
|
|
static uint32 LastNameHash = 0;
|
|
const FString FullName = GeomExport.Data->SourceElement.Get().GetFullName();
|
|
const uint32 NameHash = GetTypeHash(FullName);
|
|
if (NameHash != LastNameHash)
|
|
{
|
|
UE_LOG(LogNavigation,
|
|
Warning,
|
|
TEXT("Exporting collision geometry with too many triangles (%i). This might cause performance and memory issues."
|
|
" Add a simple collision or change GeometryExportVertexCountWarningThreshold. See '%s'."),
|
|
GeomExport.VertexBuffer.Num() / CoordinatePerTriangle, *FullName);
|
|
}
|
|
LastNameHash = NameHash;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif //!UE_BUILD_SHIPPING
|
|
|
|
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 ConvertCoordDataToRecast(TNavStatArray<FVector::FReal>& Coords)
|
|
{
|
|
if (Coords.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FVector::FReal* CoordPtr = Coords.GetData();
|
|
const int32 MaxIt = Coords.Num() / 3;
|
|
for (int32 i = 0; i < MaxIt; i++)
|
|
{
|
|
CoordPtr[0] = -CoordPtr[0];
|
|
|
|
const FVector::FReal TmpV = -CoordPtr[1];
|
|
CoordPtr[1] = CoordPtr[2];
|
|
CoordPtr[2] = TmpV;
|
|
|
|
CoordPtr += 3;
|
|
}
|
|
}
|
|
|
|
void ExportVertexSoup(const TArray<FVector>& VertexSoup, TNavStatArray<FVector::FReal>& 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
|
|
|
|
FRecastGeometryExport::FRecastGeometryExport(FNavigationRelevantData& InData)
|
|
: Data(&InData)
|
|
{
|
|
Data->Bounds = FBox(ForceInit);
|
|
}
|
|
|
|
void FRecastGeometryExport::ExportChaosTriMesh(const Chaos::FTriangleMeshImplicitObject* const TriMesh, const FTransform& LocalToWorld)
|
|
{
|
|
RecastGeometryExport::ExportChaosTriMesh(TriMesh, LocalToWorld, VertexBuffer, IndexBuffer, Data->Bounds);
|
|
}
|
|
|
|
void FRecastGeometryExport::ExportChaosConvexMesh(const FKConvexElem* const Convex, const FTransform& LocalToWorld)
|
|
{
|
|
RecastGeometryExport::ExportChaosConvexMesh(Convex, LocalToWorld, VertexBuffer, IndexBuffer, Data->Bounds);
|
|
}
|
|
|
|
void FRecastGeometryExport::ExportChaosHeightField(const Chaos::FHeightField* const Heightfield, const FTransform& LocalToWorld)
|
|
{
|
|
RecastGeometryExport::ExportChaosHeightField(Heightfield, LocalToWorld, VertexBuffer, IndexBuffer, Data->Bounds);
|
|
}
|
|
|
|
void FRecastGeometryExport::ExportChaosHeightFieldSlice(const FNavHeightfieldSamples& PrefetchedHeightfieldSamples, const int32 NumRows, const int32 NumCols, const FTransform& LocalToWorld, const FBox& SliceBox)
|
|
{
|
|
RecastGeometryExport::ExportChaosHeightFieldSlice(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)
|
|
{
|
|
if (NumIndices % 3 != 0)
|
|
{
|
|
UE_LOG(LogNavigation, Warning, TEXT("%hs: InIndices doesn't represent a list of triangles. Skipping it [Data Owner: %s]"), __FUNCTION__, *GetDataOwnerName());
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void FRecastGeometryExport::ConvertVertexBufferToRecast()
|
|
{
|
|
if (VertexBuffer.Num() % 3 != 0)
|
|
{
|
|
UE_LOG(LogNavigation, Warning, TEXT("%hs: try to convert a vertex buffer that doesn't contain a list of vertex triplets. Skipping it [Data Owner: %s]"), __FUNCTION__, *GetDataOwnerName());
|
|
return;
|
|
}
|
|
|
|
RecastGeometryExport::ConvertCoordDataToRecast(VertexBuffer);
|
|
}
|
|
|
|
void FRecastGeometryExport::StoreCollisionCache()
|
|
{
|
|
RecastGeometryExport::StoreCollisionCache(*this);
|
|
}
|
|
|
|
void FRecastGeometryExport::TransformVertexSoupToRecast(const TArray<FVector>& VertexSoup, TNavStatArray<FVector>& Verts, TNavStatArray<int32>& Faces)
|
|
{
|
|
RecastGeometryExport::TransformVertexSoupToRecast(VertexSoup, Verts, Faces);
|
|
}
|
|
|
|
FString FRecastGeometryExport::GetDataOwnerName() const
|
|
{
|
|
return Data ? Data->SourceElement.Get().GetName() : TEXT("No Data");
|
|
}
|
|
|
|
FORCEINLINE void GrowConvexHull(const FVector::FReal 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 FVector::FReal A1 = Line1.P2.X - Line1.P1.X;
|
|
const FVector::FReal B1 = Line2.P1.X - Line2.P2.X;
|
|
const FVector::FReal C1 = Line2.P1.X - Line1.P1.X;
|
|
|
|
const FVector::FReal A2 = Line1.P2.Y - Line1.P1.Y;
|
|
const FVector::FReal B2 = Line2.P1.Y - Line2.P2.Y;
|
|
const FVector::FReal C2 = Line2.P1.Y - Line1.P1.Y;
|
|
|
|
const FVector::FReal Denominator = A2*B1 - A1*B2;
|
|
if (Denominator != 0)
|
|
{
|
|
const FVector::FReal 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.0));
|
|
|
|
FVector::FReal RotationAngle = TNumericLimits<FVector::FReal>::Max();
|
|
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 FVector::FReal 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 FVector::FReal ExpansionThreshold = 2 * ExpandBy;
|
|
const FVector::FReal 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 FVector::FReal 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 AddLink(const FNavigationLink& Link, const FTransform& LocalToWorld, float DefaultSnapHeight, TFunctionRef<void(dtOffMeshLinkCreateParams&)> AddAreaID)
|
|
{
|
|
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) |
|
|
(Link.bIsGenerated ? DT_OFFMESH_CON_GENERATED : 0);
|
|
|
|
NewInfo.snapRadius = Link.SnapRadius;
|
|
NewInfo.snapHeight = Link.bUseSnapHeight ? Link.SnapHeight : DefaultSnapHeight;
|
|
NewInfo.userID = Link.NavLinkId.GetId();
|
|
|
|
AddAreaID(NewInfo);
|
|
|
|
// snap area is currently not supported for regular (point-point) offmesh links
|
|
|
|
LinkParams.Add(NewInfo);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
AddLink(Link, LocalToWorld, DefaultSnapHeight, [&] (dtOffMeshLinkCreateParams& NewInfo)
|
|
{
|
|
UClass* AreaClass = Link.GetAreaClass();
|
|
const int32* AreaID = AreaClassToIdMap->Find(AreaClass);
|
|
if (AreaID != nullptr)
|
|
{
|
|
NewInfo.area = IntCastChecked<unsigned char>(*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));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void AddLinks(const TArray<FGeneratedNavigationLink>& Links, const FTransform& LocalToWorld, int32 AgentIndex, float DefaultSnapHeight)
|
|
{
|
|
for (int32 LinkIndex = 0; LinkIndex < Links.Num(); ++LinkIndex)
|
|
{
|
|
const FGeneratedNavigationLink& Link = Links[LinkIndex];
|
|
if (!Link.SupportedAgents.Contains(AgentIndex))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AddLink(Link, LocalToWorld, DefaultSnapHeight, [&] (dtOffMeshLinkCreateParams& NewInfo)
|
|
{
|
|
// area and polyFlag have already been resolved for generated links, jut copy them
|
|
NewInfo.area = Link.generatedLinkArea;
|
|
NewInfo.polyFlag = Link.generatedLinkPolyFlag;
|
|
});
|
|
}
|
|
}
|
|
|
|
#if WITH_NAVMESH_SEGMENT_LINKS
|
|
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.NavLinkId.GetId();
|
|
|
|
UClass* AreaClass = Link.GetAreaClass();
|
|
const int32* AreaID = AreaClassToIdMap->Find(AreaClass);
|
|
if (AreaID != NULL)
|
|
{
|
|
NewInfo.area = IntCastChecked<unsigned char>(*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);
|
|
}
|
|
}
|
|
#endif // WITH_NAVMESH_SEGMENT_LINKS
|
|
|
|
protected:
|
|
|
|
void StoreUnrealPoint(FVector::FReal* 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(FRecastTileGenerator& InTileGenerator)
|
|
: rcContext(true)
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
, InternalDebugData(InTileGenerator.GetMutableDebugData())
|
|
#endif
|
|
{
|
|
}
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
FRecastInternalDebugData& InternalDebugData;
|
|
#endif
|
|
|
|
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: %hs"), Msg);
|
|
break;
|
|
case RC_LOG_WARNING:
|
|
UE_LOG(LogNavigation, Log, TEXT("Recast: %hs"), Msg);
|
|
break;
|
|
default:
|
|
UE_LOG(LogNavigation, VeryVerbose, TEXT("Recast: %hs"), Msg);
|
|
break;
|
|
}
|
|
}
|
|
|
|
virtual void doDtLog(const char* Msg, const int32 /*len*/)
|
|
{
|
|
UE_LOG(LogNavigation, Error, TEXT("Recast: %hs"), Msg);
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------//
|
|
struct FTileCacheCompressor : public dtTileCacheCompressor
|
|
{
|
|
struct FCompressedCacheHeader
|
|
{
|
|
int32 UncompressedSize;
|
|
};
|
|
|
|
virtual int32 maxCompressedSize(const int32 bufferSize)
|
|
{
|
|
if (GNavmeshUseOodleCompression)
|
|
{
|
|
return FOodleDataCompression::CompressedBufferSizeNeeded(bufferSize) + sizeof(FCompressedCacheHeader);
|
|
}
|
|
else
|
|
{
|
|
return FMath::TruncToInt(static_cast<float>(bufferSize) * 1.1f) + sizeof(FCompressedCacheHeader);
|
|
}
|
|
}
|
|
|
|
virtual dtStatus compress(const uint8* buffer, const int32 bufferSize,
|
|
uint8* compressed, const int32 maxCompressedSize, int32* outCompressedSize)
|
|
{
|
|
const int32 HeaderSize = sizeof(FCompressedCacheHeader);
|
|
|
|
FCompressedCacheHeader DataHeader;
|
|
DataHeader.UncompressedSize = bufferSize;
|
|
FMemory::Memcpy((void*)compressed, &DataHeader, HeaderSize);
|
|
|
|
uint8* DataPtr = compressed + HeaderSize;
|
|
int32 DataSize = maxCompressedSize - HeaderSize;
|
|
|
|
if (GNavmeshUseOodleCompression)
|
|
{
|
|
const int64 CompressedSize = FOodleDataCompression::CompressParallel((void*)DataPtr, DataSize, (const void*)buffer, bufferSize, GNavmeshTileCacheCompressor, GNavmeshTileCacheCompressionLevel);
|
|
if (CompressedSize > 0)
|
|
{
|
|
*outCompressedSize = IntCastChecked<int32>(CompressedSize + HeaderSize);
|
|
return DT_SUCCESS;
|
|
}
|
|
else
|
|
{
|
|
return DT_FAILURE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (FCompression::CompressMemory(NAME_Zlib, (void*)DataPtr, DataSize, (const void*)buffer, bufferSize, COMPRESS_BiasMemory))
|
|
{
|
|
*outCompressedSize = DataSize + HeaderSize;
|
|
return DT_SUCCESS;
|
|
}
|
|
else
|
|
{
|
|
return DT_FAILURE;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
virtual dtStatus decompress(const uint8* compressed, const int32 compressedSize,
|
|
uint8* buffer, const int32 maxBufferSize, int32* outDecompressedSize)
|
|
{
|
|
const int32 HeaderSize = sizeof(FCompressedCacheHeader);
|
|
|
|
FCompressedCacheHeader DataHeader;
|
|
FMemory::Memcpy(&DataHeader, (void*)compressed, HeaderSize);
|
|
|
|
const uint8* DataPtr = compressed + HeaderSize;
|
|
const int32 DataSize = compressedSize - HeaderSize;
|
|
|
|
if (GNavmeshUseOodleCompression)
|
|
{
|
|
if (FOodleDataCompression::Decompress((void*)buffer, DataHeader.UncompressedSize, (const void*)DataPtr, DataSize))
|
|
{
|
|
*outDecompressedSize = DataHeader.UncompressedSize;
|
|
return DT_SUCCESS;
|
|
}
|
|
else
|
|
{
|
|
return DT_FAILURE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (FCompression::UncompressMemory(NAME_Zlib, (void*)buffer, DataHeader.UncompressedSize, (const void*)DataPtr, DataSize))
|
|
{
|
|
*outDecompressedSize = DataHeader.UncompressedSize;
|
|
return DT_SUCCESS;
|
|
}
|
|
else
|
|
{
|
|
return DT_FAILURE;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
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, DT_ALLOC_TEMP);
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------//
|
|
// FVoxelCacheRasterizeContext
|
|
//----------------------------------------------------------------------//
|
|
|
|
struct FVoxelCacheRasterizeContext
|
|
{
|
|
FVoxelCacheRasterizeContext()
|
|
{
|
|
RasterizeHF = NULL;
|
|
}
|
|
|
|
~FVoxelCacheRasterizeContext()
|
|
{
|
|
rcFreeHeightField(RasterizeHF);
|
|
RasterizeHF = 0;
|
|
}
|
|
|
|
void Create(int32 FieldSize, FVector::FReal CellSize, FVector::FReal CellHeight)
|
|
{
|
|
if (RasterizeHF == NULL)
|
|
{
|
|
const FVector::FReal DummyBounds[3] = { 0 };
|
|
const bool bAllocateTempSpanColumns = true; // we can't know here if at some point a NavElementData will need it, so we need to allocate it
|
|
|
|
RasterizeHF = rcAllocHeightfield();
|
|
rcCreateHeightfield(NULL, *RasterizeHF, FieldSize, FieldSize, DummyBounds, DummyBounds, CellSize, CellHeight, bAllocateTempSpanColumns);
|
|
}
|
|
}
|
|
|
|
void Reset()
|
|
{
|
|
rcResetHeightfield(*RasterizeHF);
|
|
}
|
|
|
|
void SetupForTile(const FVector::FReal* TileBMin, const FVector::FReal* TileBMax, const FVector::FReal RasterizationLowPadding, const FVector::FReal RasterizationHighPadding)
|
|
{
|
|
Reset();
|
|
|
|
rcVcopy(RasterizeHF->bmin, TileBMin);
|
|
rcVcopy(RasterizeHF->bmax, TileBMax);
|
|
|
|
RasterizeHF->bmin[0] -= RasterizationLowPadding;
|
|
RasterizeHF->bmin[2] -= RasterizationLowPadding;
|
|
RasterizeHF->bmax[0] += RasterizationHighPadding;
|
|
RasterizeHF->bmax[2] += RasterizationHighPadding;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
//----------------------------------------------------------------------//
|
|
// FRecastTileGenerator
|
|
//----------------------------------------------------------------------//
|
|
|
|
FRecastTileGenerator::FRecastTileGenerator(FRecastNavMeshGenerator& ParentGenerator, const FIntPoint& Location, const double PendingTileCreationTime)
|
|
: TimeSliceManager(ParentGenerator.GetTimeSliceManager())
|
|
, RasterizeGeometryWorldRecastCoordsBounds(ForceInit)
|
|
{
|
|
bUpdateGeometry = true;
|
|
bHasLowAreaModifiers = false;
|
|
bHasGeometryToRasterizeAsFilledConvexVolume = false;
|
|
|
|
TileX = Location.X;
|
|
TileY = Location.Y;
|
|
|
|
TileCreationTime = PendingTileCreationTime;
|
|
|
|
// Copy tile config from parent generator
|
|
TileConfig = ParentGenerator.GetConfig();
|
|
|
|
TileDebugSettings = ParentGenerator.GetTileDebugSettings();
|
|
Version = ParentGenerator.GetVersion();
|
|
AdditionalCachedData = ParentGenerator.GetAdditionalCachedData();
|
|
|
|
ParentGeneratorWeakPtr = ((FNavDataGenerator&)ParentGenerator).AsShared();
|
|
|
|
RasterizeGeomRecastState = ERasterizeGeomRecastTimeSlicedState::MarkWalkableTriangles;
|
|
RasterizeGeomState = ERasterizeGeomTimeSlicedState::RasterizeGeometryTransformCoordsAndFlipIndices;
|
|
GenerateRecastFilterState = EGenerateRecastFilterTimeSlicedState::FilterLowHangingWalkableObstacles;
|
|
GenRecastFilterLedgeSpansYStart = 0;
|
|
DoWorkTimeSlicedState = EDoWorkTimeSlicedState::GatherGeometryFromSources;
|
|
GenerateTileTimeSlicedState = EGenerateTileTimeSlicedState::GenerateCompressedLayers;
|
|
|
|
GenerateNavDataTimeSlicedState = EGenerateNavDataTimeSlicedState::Init;
|
|
GenNavDataLayerTimeSlicedIdx = 0;
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::Init;
|
|
RasterizeTrianglesTimeSlicedRawGeomIdx = 0;
|
|
RasterizeTrianglesTimeSlicedInstTransformIdx = 0;
|
|
|
|
check(ParentGenerator.GetOwner());
|
|
TileTimeSliceSettings.FilterLedgeSpansMaxYProcess = ParentGenerator.GetOwner()->TimeSliceFilterLedgeSpansMaxYProcess;
|
|
|
|
SolidHF = nullptr;
|
|
CompactHF = nullptr;
|
|
}
|
|
|
|
FRecastTileGenerator::~FRecastTileGenerator()
|
|
{
|
|
rcFreeHeightField(SolidHF);
|
|
rcFreeCompactHeightfield(CompactHF);
|
|
|
|
GenNavDataTimeSlicedGenerationContext.Reset();
|
|
GenNavDataTimeSlicedAllocator.Reset();
|
|
GenCompressedlayersTimeSlicedRasterContext.Reset();
|
|
}
|
|
|
|
void FRecastTileGenerator::Setup(const FRecastNavMeshGenerator& ParentGenerator, const TArray<FBox>& DirtyAreas)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_FRecastTileGenerator_Setup);
|
|
|
|
const FVector RcNavMeshOrigin = ParentGenerator.GetRcNavMeshOrigin();
|
|
const FBox NavTotalBounds = ParentGenerator.GetTotalBounds();
|
|
const FVector::FReal TileSizeUU = TileConfig.GetTileSizeUU();
|
|
|
|
NavDataConfig = ParentGenerator.GetOwner()->GetConfig();
|
|
|
|
TileBB = CalculateTileBounds(TileX, TileY, RcNavMeshOrigin, NavTotalBounds, TileSizeUU);
|
|
|
|
if (UE::NavMesh::Private::bUseTightBoundExpansion)
|
|
{
|
|
TileBBExpandedForAgent = ParentGenerator.GrowBoundingBox(TileBB, /*bIncludeAgentHeight*/ false);
|
|
}
|
|
else
|
|
{
|
|
// Deprecated
|
|
TileBBExpandedForAgent = TileBB.ExpandBy(NavDataConfig.AgentRadius * 2 + TileConfig.cs);
|
|
}
|
|
|
|
const FBox RCBox = Unreal2RecastBox(TileBB);
|
|
const FVector Min32(RCBox.Min);
|
|
const FVector Max32(RCBox.Max);
|
|
rcVcopy(TileConfig.bmin, &Min32.X);
|
|
rcVcopy(TileConfig.bmax, &Max32.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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there are no DirtyAreas, we expect there is a geometry change (also see usage of bRegenerateCompressedLayers).
|
|
// Else it's a modifier change.
|
|
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 || TileConfig.bGenerateLinks || CompressedLayers.Num() == 0);
|
|
|
|
// Gather geometry for tile if it's inside navigable bounds
|
|
// DirtyLayers might not be initialized if InclusionBounds are empty
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool bGatherGeometryNow = ParentGenerator.GatherGeometryOnGameThread();
|
|
#if !RECAST_ASYNC_REBUILDING
|
|
if (ParentGenerator.IsTimeSliceRegenActive())
|
|
{
|
|
bGatherGeometryNow = false;
|
|
}
|
|
#endif // !RECAST_ASYNC_REBUILDING
|
|
|
|
if (bGatherGeometryNow)
|
|
{
|
|
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);
|
|
}
|
|
|
|
FBox FRecastTileGenerator::CalculateTileBounds(int32 X, int32 Y, const FVector& RcNavMeshOrigin, const FBox& TotalNavBounds, FVector::FReal 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;
|
|
}
|
|
|
|
ETimeSliceWorkResult FRecastTileGenerator::DoWorkTimeSliced()
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_DoWork);
|
|
|
|
TSharedPtr<FNavDataGenerator, ESPMode::ThreadSafe> ParentGenerator = ParentGeneratorWeakPtr.Pin();
|
|
ETimeSliceWorkResult WorkResult = ETimeSliceWorkResult::Succeeded;
|
|
|
|
check(TimeSliceManager);
|
|
|
|
if (ParentGenerator.IsValid())
|
|
{
|
|
switch (DoWorkTimeSlicedState)
|
|
{
|
|
case EDoWorkTimeSlicedState::Invalid:
|
|
{
|
|
ensureMsgf(false, TEXT("Invalid EDoWorkTimeSlicedState, has this function been called when its already finished processing?"));
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
case EDoWorkTimeSlicedState::GatherGeometryFromSources:
|
|
{
|
|
if (InclusionBounds.Num())
|
|
{
|
|
WorkResult = GatherGeometryFromSourcesTimeSliced();
|
|
|
|
// Needs to occur after DemandLazyDataGathering
|
|
TSharedPtr<FRecastNavMeshGenerator> RecastParentGenerator = StaticCastSharedPtr<FRecastNavMeshGenerator>(ParentGenerator);
|
|
SetupTileConfigFromHighestResolution(*RecastParentGenerator);
|
|
|
|
if (WorkResult == ETimeSliceWorkResult::CallAgainNextTimeSlice)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
DoWorkTimeSlicedState = EDoWorkTimeSlicedState::GenerateTile;
|
|
} //fall through to next state
|
|
case EDoWorkTimeSlicedState::GenerateTile:
|
|
{
|
|
WorkResult = GenerateTileTimeSliced();
|
|
|
|
if (WorkResult != ETimeSliceWorkResult::CallAgainNextTimeSlice)
|
|
{
|
|
DumpAsyncData();
|
|
DumpSyncData(); // Currently, TIME_SLICE_NAV_REGEN can only be active if not async.
|
|
|
|
DoWorkTimeSlicedState = EDoWorkTimeSlicedState::Invalid;//Set to Invalid as we never want to call this again on this instance
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
{
|
|
ensureMsgf(false, TEXT("unhandled EDoWorkTimeSlicedState"));
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
}
|
|
}
|
|
|
|
return WorkResult;
|
|
}
|
|
|
|
bool FRecastTileGenerator::DoWork()
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_DoWork);
|
|
|
|
TSharedPtr<FNavDataGenerator, ESPMode::ThreadSafe> ParentGenerator = ParentGeneratorWeakPtr.Pin();
|
|
bool bSuccess = true;
|
|
|
|
if (ParentGenerator.IsValid())
|
|
{
|
|
if (InclusionBounds.Num())
|
|
{
|
|
GatherGeometryFromSources();
|
|
|
|
// Needs to occur after DemandLazyDataGathering
|
|
TSharedPtr<FRecastNavMeshGenerator> RecastParentGenerator = StaticCastSharedPtr<FRecastNavMeshGenerator>(ParentGenerator);
|
|
SetupTileConfigFromHighestResolution(*RecastParentGenerator);
|
|
}
|
|
|
|
bSuccess = GenerateTile();
|
|
|
|
DumpAsyncData();
|
|
}
|
|
|
|
return bSuccess;
|
|
}
|
|
|
|
void FRecastTileGenerator::DumpAsyncData()
|
|
{
|
|
RawGeometry.Empty();
|
|
Modifiers.Empty();
|
|
OffmeshLinks.Empty();
|
|
|
|
NavSystem = nullptr;
|
|
}
|
|
|
|
void FRecastTileGenerator::DumpSyncData()
|
|
{
|
|
ensure(IsInGameThread());
|
|
NavigationRelevantData.Empty();
|
|
}
|
|
|
|
void FRecastTileGenerator::SetupTileConfigFromHighestResolution(const FRecastNavMeshGenerator& ParentGenerator)
|
|
{
|
|
ENavigationDataResolution HighestResolution = ENavigationDataResolution::Low;
|
|
bool bNewResolutionFound = false;
|
|
|
|
for (const FRecastAreaNavModifierElement& Element : Modifiers)
|
|
{
|
|
if (Element.NavMeshResolution != ENavigationDataResolution::Invalid)
|
|
{
|
|
HighestResolution = FMath::Max(HighestResolution, Element.NavMeshResolution);
|
|
bNewResolutionFound = true;
|
|
}
|
|
}
|
|
|
|
check(HighestResolution != ENavigationDataResolution::Invalid);
|
|
if (bNewResolutionFound && ParentGenerator.GetOwner()->NavMeshResolutionParams[(uint8)HighestResolution].IsValid())
|
|
{
|
|
// Update the TileConfig
|
|
ParentGenerator.SetupTileConfig(HighestResolution, TileConfig);
|
|
}
|
|
}
|
|
|
|
void FRecastTileGenerator::GatherGeometryFromSources()
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_GatherGeometryFromSources);
|
|
|
|
UNavigationSystemV1* NavSys = NavSystem.Get();
|
|
if (NavSys == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (TSharedRef<FNavigationRelevantData, ESPMode::ThreadSafe>& ElementData : NavigationRelevantData)
|
|
{
|
|
GatherNavigationDataGeometry(ElementData, *NavSys, NavDataConfig, bUpdateGeometry);
|
|
}
|
|
}
|
|
|
|
ETimeSliceWorkResult FRecastTileGenerator::GatherGeometryFromSourcesTimeSliced()
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_GatherGeometryFromSources);
|
|
|
|
UNavigationSystemV1* NavSys = NavSystem.Get();
|
|
if (NavSys == nullptr)
|
|
{
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
|
|
while(NavigationRelevantData.Num())
|
|
{
|
|
GatherNavigationDataGeometry(NavigationRelevantData.Pop(EAllowShrinking::No), *NavSys, NavDataConfig, bUpdateGeometry);
|
|
|
|
MARK_TIMESLICE_SECTION_DEBUG(TimeSliceManager->GetTimeSlicer(), GatherGeometryFromSources);
|
|
|
|
if (TimeSliceManager->GetTimeSlicer().TestTimeSliceFinished())
|
|
{
|
|
return ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
}
|
|
}
|
|
|
|
return ETimeSliceWorkResult::Succeeded;
|
|
}
|
|
|
|
void FRecastTileGenerator::PrepareGeometrySources(const FRecastNavMeshGenerator& ParentGenerator, bool bGeometryChanged)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_PrepareGeometrySources);
|
|
|
|
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(ParentGenerator.GetWorld());
|
|
const FNavigationOctree* NavOctreeInstance = NavSys ? NavSys->GetNavOctree() : nullptr;
|
|
check(NavOctreeInstance);
|
|
NavigationRelevantData.Reset();
|
|
NavSystem = NavSys;
|
|
bUpdateGeometry = bGeometryChanged;
|
|
|
|
const ARecastNavMesh* const OwnerNav = ParentGenerator.GetOwner();
|
|
const bool bUseVirtualGeometryFilteringAndDirtying = OwnerNav->bUseVirtualGeometryFilteringAndDirtying;
|
|
|
|
NavOctreeInstance->FindElementsWithBoundsTest(ParentGenerator.GrowBoundingBox(TileBB, /*bIncludeAgentHeight*/ false),
|
|
[&ParentGenerator, this, bGeometryChanged, bUseVirtualGeometryFilteringAndDirtying](const FNavigationOctreeElement& Element)
|
|
{
|
|
const bool bShouldUse = bUseVirtualGeometryFilteringAndDirtying ?
|
|
ParentGenerator.ShouldGenerateGeometryForOctreeElement(Element, NavDataConfig) :
|
|
Element.ShouldUseGeometry(NavDataConfig);
|
|
if (bShouldUse)
|
|
{
|
|
const bool bExportGeometry = bGeometryChanged && (Element.Data->HasGeometry() || Element.Data->IsPendingLazyGeometryGathering());
|
|
if (bExportGeometry ||
|
|
Element.Data->NeedAnyPendingLazyModifiersGathering() ||
|
|
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());
|
|
const FNavigationOctree* NavigationOctree = NavSys ? NavSys->GetNavOctree() : nullptr;
|
|
if (NavigationOctree == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
const ARecastNavMesh* const OwnerNav = ParentGenerator.GetOwner();
|
|
const bool bUseVirtualGeometryFilteringAndDirtying = OwnerNav->bUseVirtualGeometryFilteringAndDirtying;
|
|
const FNavDataConfig& OwnerNavDataConfig = OwnerNav->GetConfig();
|
|
|
|
TArray<TSharedRef<FNavigationRelevantData, ESPMode::ThreadSafe>> RelevantDataArray;
|
|
|
|
UE_SUPPRESS(LogNavigation, VeryVerbose, UE_VLOG_BOX(OwnerNav, LogNavigation, VeryVerbose, TileBB, FColor::White, TEXT("Tile (%i, %i)"), TileX, TileY));
|
|
|
|
const FBox NewBounds = ParentGenerator.GrowBoundingBox(TileBB, /*bIncludeAgentHeight*/ false);
|
|
|
|
NavigationOctree->FindElementsWithBoundsTest(NewBounds,
|
|
[&RelevantDataArray, &OwnerNavDataConfig, &ParentGenerator, this, bUseVirtualGeometryFilteringAndDirtying](const FNavigationOctreeElement& Element)
|
|
{
|
|
const bool bShouldUse = bUseVirtualGeometryFilteringAndDirtying ?
|
|
ParentGenerator.ShouldGenerateGeometryForOctreeElement(Element, OwnerNavDataConfig) :
|
|
Element.ShouldUseGeometry(OwnerNavDataConfig);
|
|
if (bShouldUse)
|
|
{
|
|
RelevantDataArray.Add(Element.Data);
|
|
}
|
|
});
|
|
|
|
ENavigationDataResolution HighestResolution = ENavigationDataResolution::Low;
|
|
bool bNewResolutionFound = false;
|
|
|
|
for (TSharedRef<FNavigationRelevantData, ESPMode::ThreadSafe>& ElementData : RelevantDataArray)
|
|
{
|
|
GatherNavigationDataGeometry(ElementData, *NavSys, OwnerNavDataConfig, bGeometryChanged);
|
|
|
|
// Keep highest resolution that is not the default.
|
|
const ENavigationDataResolution Resolution = ElementData->Modifiers.GetNavMeshResolution();
|
|
if (Resolution != ENavigationDataResolution::Invalid)
|
|
{
|
|
HighestResolution = FMath::Max(HighestResolution, Resolution);
|
|
bNewResolutionFound = true;
|
|
}
|
|
}
|
|
|
|
check(HighestResolution != ENavigationDataResolution::Invalid);
|
|
if (bNewResolutionFound && ParentGenerator.GetOwner()->NavMeshResolutionParams[(uint8)HighestResolution].IsValid())
|
|
{
|
|
// Update the TileConfig
|
|
ParentGenerator.SetupTileConfig(HighestResolution, TileConfig);
|
|
}
|
|
}
|
|
|
|
void FRecastTileGenerator::GatherNavigationDataGeometry(const TSharedRef<FNavigationRelevantData, ESPMode::ThreadSafe>& ElementDataRef, UNavigationSystemV1& NavSys, const FNavDataConfig& OwnerNavDataConfig, const bool bGeometryChanged)
|
|
{
|
|
bool bDumpGeometryData = false;
|
|
FNavigationRelevantData& ElementData = ElementDataRef.Get();
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
if (!IsTileDebugAllowingGeneration())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (IsTileDebugActive())
|
|
{
|
|
UE_LOG(LogNavigation, Log, TEXT("Gathering geometry for tile (%i,%i): %s.\n"
|
|
" Bounds: %s\n"
|
|
" Geometry: Has=%s Pending=%s Slice=%s\n"
|
|
" Modifier: Has=%s Pending=%s"),
|
|
TileX, TileY, *ElementData.SourceElement.Get().GetFullName(),
|
|
*ElementData.Bounds.ToString(),
|
|
*LexToString(ElementData.HasGeometry()), *LexToString(ElementData.IsPendingLazyGeometryGathering()), *LexToString(ElementData.SupportsGatheringGeometrySlices()),
|
|
*LexToString(ElementData.HasModifiers()), *LexToString(ElementData.NeedAnyPendingLazyModifiersGathering()));
|
|
}
|
|
#endif // RECAST_INTERNAL_DEBUG_DATA
|
|
|
|
if (ElementData.IsPendingLazyGeometryGathering() || ElementData.NeedAnyPendingLazyModifiersGathering())
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_LazyGeometryExport);
|
|
NavSys.DemandLazyDataGathering(ElementData);
|
|
}
|
|
|
|
if (ElementData.IsPendingLazyGeometryGathering() && ElementData.SupportsGatheringGeometrySlices())
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_LandscapeSlicesExporting);
|
|
|
|
FRecastGeometryExport GeomExport(ElementData);
|
|
|
|
// adding a small bump to avoid special case of zero-expansion when tile bounds
|
|
// overlap landscape's tile bounds
|
|
ElementData.SourceElement->GeometrySliceExportDelegate.ExecuteIfBound(ElementData.SourceElement.Get(), GeomExport, TileBBExpandedForAgent);
|
|
RecastGeometryExport::ConvertCoordDataToRecast(GeomExport.VertexBuffer);
|
|
RecastGeometryExport::StoreCollisionCache(GeomExport);
|
|
bDumpGeometryData = true;
|
|
}
|
|
|
|
// Temporary change to help narrow down a rare crash:
|
|
// Was: const FCompositeNavModifier ModifierInstance = ElementData->GetModifierForAgent(&OwnerNavDataConfig);
|
|
const FCompositeNavModifier& ModifierInstance = ElementData.Modifiers.HasMetaAreas()
|
|
? ElementData.Modifiers.GetInstantiatedMetaModifier(&OwnerNavDataConfig, ElementData.SourceElement->GetWeakUObject())
|
|
: ElementData.Modifiers;
|
|
|
|
const bool bExportGeometry = bGeometryChanged && 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, Unreal2RecastBox(ElementData.Bounds), ModifierInstance, SpanData);
|
|
CachedVoxels = SpanData.GetData();
|
|
NumCachedVoxels = SpanData.Num();
|
|
|
|
// encode
|
|
{
|
|
LLM_SCOPE_BYTAG(NavigationOctree);
|
|
|
|
const SIZE_T PrevElementMemory = ElementData.GetAllocatedSize();
|
|
AddVoxelCache(ElementData.VoxelData, CachedVoxels, NumCachedVoxels);
|
|
|
|
const SIZE_T NewElementMemory = ElementData.GetAllocatedSize();
|
|
const SIZE_T ElementMemoryDelta = NewElementMemory - PrevElementMemory;
|
|
INC_MEMORY_STAT_BY(STAT_Navigation_CollisionTreeMemory, ElementMemoryDelta);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ValidateAndAppendGeometry(ElementData, ModifierInstance);
|
|
}
|
|
|
|
if (bDumpGeometryData)
|
|
{
|
|
ElementData.CollisionData.Empty();
|
|
}
|
|
}
|
|
|
|
if (ModifierInstance.IsEmpty() == false)
|
|
{
|
|
AppendModifier(ModifierInstance, ElementData.NavDataPerInstanceTransformDelegate);
|
|
}
|
|
}
|
|
|
|
// Deprecated
|
|
bool FRecastTileGenerator::CreateHeightField(FNavMeshBuildContext& BuildContext, FTileRasterizationContext& RasterContext)
|
|
{
|
|
return CreateHeightField(BuildContext);
|
|
}
|
|
|
|
// Deprecated
|
|
void FRecastTileGenerator::GenerateRecastFilter(FNavMeshBuildContext& BuildContext, FTileRasterizationContext& RasterContext)
|
|
{
|
|
GenerateRecastFilter(BuildContext);
|
|
}
|
|
|
|
// Deprecated
|
|
ETimeSliceWorkResult FRecastTileGenerator::GenerateRecastFilterTimeSliced(FNavMeshBuildContext& BuildContext, FTileRasterizationContext& RasterContext)
|
|
{
|
|
return GenerateRecastFilterTimeSliced(BuildContext);
|
|
}
|
|
|
|
// Deprecated
|
|
bool FRecastTileGenerator::BuildCompactHeightField(FNavMeshBuildContext& BuildContext, FTileRasterizationContext& RasterContext)
|
|
{
|
|
return BuildCompactHeightField(BuildContext);
|
|
}
|
|
|
|
// Deprecated
|
|
bool FRecastTileGenerator::RecastErodeWalkable(FNavMeshBuildContext& BuildContext, FTileRasterizationContext& RasterContext)
|
|
{
|
|
return RecastErodeWalkable(BuildContext);
|
|
}
|
|
|
|
void FRecastTileGenerator::ApplyVoxelFilter(rcHeightfield* HF, FVector::FReal WalkableRadius)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_TileVoxelFilteringAsync);
|
|
|
|
if (HF != NULL)
|
|
{
|
|
const int32 Width = HF->width;
|
|
const int32 Height = HF->height;
|
|
const FVector::FReal CellSize = HF->cs;
|
|
const FVector::FReal CellHeight = HF->ch;
|
|
const FVector::FReal BottomX = HF->bmin[0];
|
|
const FVector::FReal BottomZ = HF->bmin[1];
|
|
const FVector::FReal 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 FVector::FReal 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 FVector::FReal SpanX = -(BottomX + x * CellSize);
|
|
const FVector::FReal 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 FVector::FReal SpanMin = CellHeight * s->data.smin + BottomZ;
|
|
const FVector::FReal 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 FVector::FReal SpanX = -(BottomX + x * CellSize);
|
|
const FVector::FReal 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 FVector::FReal SpanMin = CellHeight * s->data.smin + BottomZ;
|
|
const FVector::FReal 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::InitRasterizationMaskArray(const rcHeightfield* InSolidHF, TInlineMaskArray& OutRasterizationMasks)
|
|
{
|
|
const int CellCount = InSolidHF->width * InSolidHF->height;
|
|
OutRasterizationMasks.SetNumUninitialized(CellCount);
|
|
const uint8 AllowAllFlags = 0xFF;
|
|
FMemory::Memset(OutRasterizationMasks.GetData(), AllowAllFlags, CellCount*sizeof(TInlineMaskArray::ElementType));
|
|
}
|
|
|
|
// Deprecated
|
|
void FRecastTileGenerator::PrepareVoxelCache(const TNavStatArray<uint8>& RawCollisionCache, const FCompositeNavModifier& InModifier, TNavStatArray<rcSpanCache>& SpanData)
|
|
{
|
|
return PrepareVoxelCache(RawCollisionCache, FBox(ForceInit), InModifier, SpanData);
|
|
}
|
|
|
|
void FRecastTileGenerator::PrepareVoxelCache(const TNavStatArray<uint8>& RawCollisionCache, const FBox& RecastBounds, const FCompositeNavModifier& InModifier, TNavStatArray<rcSpanCache>& SpanData)
|
|
{
|
|
// tile's geometry: voxel cache (only for synchronous rebuilds)
|
|
const int32 WalkableClimbVX = TileConfig.walkableClimb;
|
|
const FVector::FReal WalkableSlopeCos = FMath::Cos(FMath::DegreesToRadians(TileConfig.walkableSlopeAngle));
|
|
const FVector::FReal RasterizationLowPadding = TileConfig.borderSize.low * TileConfig.cs;
|
|
const FVector::FReal RasterizationHighPadding = TileConfig.borderSize.high * TileConfig.cs;
|
|
|
|
FRecastGeometryCache CachedCollisions(RawCollisionCache.GetData());
|
|
|
|
VoxelCacheContext.SetupForTile(TileConfig.bmin, TileConfig.bmax, RasterizationLowPadding, RasterizationHighPadding);
|
|
|
|
float SlopeCosPerActor = UE_REAL_TO_FLOAT(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());
|
|
|
|
TInlineMaskArray RasterizationMasks;
|
|
if (InModifier.GetMaskFillCollisionUnderneathForNavmesh())
|
|
{
|
|
const int32 Mask = ~RC_PROJECT_TO_BOTTOM;
|
|
for (const FAreaNavModifier& ModifierArea : InModifier.GetAreas())
|
|
{
|
|
MarkRasterizationMask(0 /*ctx*/, VoxelCacheContext.RasterizeHF, ModifierArea, FTransform::Identity, Mask, RasterizationMasks);
|
|
}
|
|
}
|
|
|
|
const rcRasterizationFlags Flags = FRecastRawGeometryElement::GetRasterizationFlags(InModifier);
|
|
|
|
const FVector::FReal* BoundsMin = RecastBounds.IsValid ? &RecastBounds.Min.X : nullptr;
|
|
const FVector::FReal* BoundsMax = RecastBounds.IsValid ? &RecastBounds.Max.X : nullptr;
|
|
TInlineMaskArray::ElementType* MaskArray = RasterizationMasks.Num() > 0 ? RasterizationMasks.GetData() : nullptr;
|
|
rcRasterizeTriangles(0, CachedCollisions.Verts, CachedCollisions.Header.NumVerts,
|
|
CachedCollisions.Indices, TriAreas.GetData(), CachedCollisions.Header.NumFaces,
|
|
*VoxelCacheContext.RasterizeHF, WalkableClimbVX, Flags, MaskArray, BoundsMin, BoundsMax);
|
|
|
|
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);
|
|
}
|
|
|
|
bHasGeometryToRasterizeAsFilledConvexVolume = bHasGeometryToRasterizeAsFilledConvexVolume || Modifier.GetRasterizeAsFilledConvexVolume();
|
|
|
|
// Navmesh resolutions is a modifier without area, if present, it must not be skipped.
|
|
if (Modifier.GetAreas().Num() == 0 && Modifier.GetNavMeshResolution() == ENavigationDataResolution::Invalid)
|
|
{
|
|
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();
|
|
ModifierElement.bMaskFillCollisionUnderneathForNavmesh = Modifier.GetMaskFillCollisionUnderneathForNavmesh();
|
|
ModifierElement.NavMeshResolution = Modifier.GetNavMeshResolution();
|
|
|
|
Modifiers.Add(MoveTemp(ModifierElement));
|
|
}
|
|
|
|
void FRecastTileGenerator::ValidateAndAppendGeometry(const FNavigationRelevantData& ElementData, const FCompositeNavModifier& InModifier)
|
|
{
|
|
if (ElementData.IsCollisionDataValid())
|
|
{
|
|
AppendGeometry(ElementData, InModifier, ElementData.NavDataPerInstanceTransformDelegate);
|
|
}
|
|
}
|
|
|
|
void FRecastTileGenerator::ValidateAndAppendGeometry(const TSharedRef<FNavigationRelevantData, ESPMode::ThreadSafe>& ElementData, const FCompositeNavModifier& InModifier)
|
|
{
|
|
ValidateAndAppendGeometry(ElementData.Get(), InModifier);
|
|
}
|
|
|
|
void FRecastTileGenerator::AppendGeometry(const FNavigationRelevantData& ElementData, const FCompositeNavModifier& InModifier, const FNavDataPerInstanceTransformDelegate& InTransformsDelegate)
|
|
{
|
|
const TNavStatArray<uint8>& RawCollisionCache = ElementData.CollisionData;
|
|
if (RawCollisionCache.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FRecastRawGeometryElement GeometryElement;
|
|
GeometryElement.RasterizationFlags = FRecastRawGeometryElement::GetRasterizationFlags(InModifier);
|
|
|
|
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)
|
|
{
|
|
UE_LOG(LogNavigationDataBuild, VeryVerbose, TEXT("%hs adding %i vertices from %s."),
|
|
__FUNCTION__, CollisionCache.Header.NumVerts, *ElementData.SourceElement.Get().GetFullName());
|
|
|
|
GeometryElement.GeomCoords.SetNumUninitialized(NumCoords);
|
|
GeometryElement.GeomIndices.SetNumUninitialized(NumIndices);
|
|
|
|
FMemory::Memcpy(GeometryElement.GeomCoords.GetData(), CollisionCache.Verts, sizeof(FVector::FReal) * NumCoords);
|
|
FMemory::Memcpy(GeometryElement.GeomIndices.GetData(), CollisionCache.Indices, sizeof(int32) * NumIndices);
|
|
|
|
GeometryElement.GeomCoordsBounds = Unreal2RecastBox(ElementData.Bounds);
|
|
|
|
RawGeometry.Add(MoveTemp(GeometryElement));
|
|
}
|
|
}
|
|
|
|
ETimeSliceWorkResult FRecastTileGenerator::GenerateTileTimeSliced()
|
|
{
|
|
UE_LOG(LogNavigation, Verbose, TEXT("Building tile (time sliced): (%i,%i)"), TileX, TileY);
|
|
|
|
FNavMeshBuildContext BuildContext(*this);
|
|
ETimeSliceWorkResult WorkResult = ETimeSliceWorkResult::Succeeded;
|
|
|
|
dtLinkBuilderData linkBuiderData;
|
|
linkBuiderData.generatingLinks = TileConfig.bGenerateLinks;
|
|
|
|
check(TimeSliceManager);
|
|
|
|
switch (GenerateTileTimeSlicedState)
|
|
{
|
|
case EGenerateTileTimeSlicedState::Invalid:
|
|
{
|
|
ensureMsgf(false, TEXT("Invalid EGenerateTileTimeSlicedState, has this function been called when its already finished time processong?"));
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
case EGenerateTileTimeSlicedState::GenerateCompressedLayers:
|
|
{
|
|
if (bRegenerateCompressedLayers)
|
|
{
|
|
const ETimeSliceWorkResult WorkResultCompressed = GenerateCompressedLayersTimeSliced(BuildContext);
|
|
|
|
if (WorkResultCompressed == ETimeSliceWorkResult::Succeeded)
|
|
{
|
|
GenerateTileTimeSlicedState = EGenerateTileTimeSlicedState::GenerateNavigationData;
|
|
// Mark all layers as dirty
|
|
DirtyLayers.Init(true, CompressedLayers.Num());
|
|
}
|
|
else if (WorkResultCompressed == ETimeSliceWorkResult::Failed)
|
|
{
|
|
GenerateTileTimeSlicedState = EGenerateTileTimeSlicedState::Invalid;
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
|
|
if (TimeSliceManager->GetTimeSlicer().IsTimeSliceFinishedCached())
|
|
{
|
|
return ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
GenerateTileTimeSlicedState = EGenerateTileTimeSlicedState::GenerateNavigationData;
|
|
}
|
|
} //fall through to next state
|
|
case EGenerateTileTimeSlicedState::GenerateNavigationData:
|
|
{
|
|
WorkResult = GenerateNavigationDataTimeSliced(BuildContext, linkBuiderData);
|
|
|
|
if (WorkResult != ETimeSliceWorkResult::CallAgainNextTimeSlice)
|
|
{
|
|
GenerateTileTimeSlicedState = EGenerateTileTimeSlicedState::Invalid;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
{
|
|
ensureMsgf(false, TEXT("unhandled EGenerateTileTimeSlicedState"));
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
};
|
|
|
|
// it's possible to have valid generation with empty resulting tile (no navigable geometry in tile)
|
|
return WorkResult;
|
|
}
|
|
|
|
bool FRecastTileGenerator::GenerateTile()
|
|
{
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
const double StartStamp = FPlatformTime::Seconds();
|
|
double PostCompressLayerStamp = StartStamp;
|
|
#endif // RECAST_INTERNAL_DEBUG_DATA
|
|
|
|
UE_LOG(LogNavigation, Verbose, TEXT("Building tile: (%i,%i)"), TileX, TileY);
|
|
|
|
FNavMeshBuildContext BuildContext(*this);
|
|
bool bSuccess = true;
|
|
|
|
dtLinkBuilderData LinkBuiderData;
|
|
LinkBuiderData.generatingLinks = TileConfig.bGenerateLinks;
|
|
|
|
if (bRegenerateCompressedLayers)
|
|
{
|
|
CompressedLayers.Reset();
|
|
bSuccess = GenerateCompressedLayers(BuildContext, LinkBuiderData);
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
PostCompressLayerStamp = FPlatformTime::Seconds();
|
|
#endif // RECAST_INTERNAL_DEBUG_DATA
|
|
|
|
if (bSuccess)
|
|
{
|
|
// Mark all layers as dirty
|
|
DirtyLayers.Init(true, CompressedLayers.Num());
|
|
}
|
|
}
|
|
|
|
if (bSuccess)
|
|
{
|
|
bSuccess = GenerateNavigationData(BuildContext, LinkBuiderData);
|
|
}
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
const double EndStamp = FPlatformTime::Seconds();
|
|
BuildContext.InternalDebugData.BuildTime = EndStamp - StartStamp;
|
|
BuildContext.InternalDebugData.BuildCompressedLayerTime = PostCompressLayerStamp - StartStamp;
|
|
BuildContext.InternalDebugData.BuildNavigationDataTime = EndStamp - PostCompressLayerStamp;
|
|
BuildContext.InternalDebugData.Resolution = (unsigned char)TileConfig.TileResolution;
|
|
#endif // RECAST_INTERNAL_DEBUG_DATA
|
|
|
|
// it's possible to have valid generation with empty resulting tile (no navigable geometry in tile)
|
|
return bSuccess;
|
|
}
|
|
|
|
struct FTileRasterizationContext
|
|
{
|
|
FTileRasterizationContext() : LayerSet(nullptr), RasterizationFlags(rcRasterizationFlags(0))
|
|
{
|
|
}
|
|
|
|
~FTileRasterizationContext()
|
|
{
|
|
rcFreeHeightfieldLayerSet(LayerSet);
|
|
}
|
|
|
|
rcRasterizationFlags GetRasterizationFlags() const { return RasterizationFlags; }
|
|
void SetRasterizationFlags(rcRasterizationFlags Value) { RasterizationFlags = Value; }
|
|
|
|
struct rcHeightfieldLayerSet* LayerSet;
|
|
TArray<FNavMeshTileData> Layers;
|
|
FRecastTileGenerator::TInlineMaskArray RasterizationMasks;
|
|
|
|
private:
|
|
rcRasterizationFlags RasterizationFlags;
|
|
};
|
|
|
|
bool FRecastTileGenerator::CreateHeightField(FNavMeshBuildContext& BuildContext)
|
|
{
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
if (!IsTileDebugAllowingGeneration())
|
|
{
|
|
return false;
|
|
}
|
|
#endif // RECAST_INTERNAL_DEBUG_DATA
|
|
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastCreateHeightField);
|
|
|
|
const int size = TileConfig.tileSize + (TileConfig.borderSize.low + TileConfig.borderSize.high);
|
|
TileConfig.width = size;
|
|
TileConfig.height = size;
|
|
|
|
const FVector::FReal BBoxPaddingLow = TileConfig.borderSize.low * TileConfig.cs;
|
|
const FVector::FReal BBoxPaddingHigh = TileConfig.borderSize.high * TileConfig.cs;
|
|
TileConfig.bmin[0] -= BBoxPaddingLow;
|
|
TileConfig.bmin[2] -= BBoxPaddingLow;
|
|
TileConfig.bmax[0] += BBoxPaddingHigh;
|
|
TileConfig.bmax[2] += BBoxPaddingHigh;
|
|
|
|
BuildContext.log(RC_LOG_PROGRESS, "CreateHeightField:");
|
|
BuildContext.log(RC_LOG_PROGRESS, " - %d x %d cells", TileConfig.width, TileConfig.height);
|
|
|
|
const bool bHasGeometry = RawGeometry.Num() > 0;
|
|
|
|
// Allocate voxel heightfield where we rasterize our input data to.
|
|
if (bHasGeometry)
|
|
{
|
|
SolidHF = rcAllocHeightfield();
|
|
if (SolidHF == nullptr)
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "CreateHeightField: Out of memory 'SolidHF'.");
|
|
return false;
|
|
}
|
|
if (!rcCreateHeightfield(&BuildContext, *SolidHF, TileConfig.width, TileConfig.height, TileConfig.bmin, TileConfig.bmax, TileConfig.cs, TileConfig.ch, bHasGeometryToRasterizeAsFilledConvexVolume))
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "CreateHeightField: Could not create solid heightfield.");
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Deprecated
|
|
ETimeSliceWorkResult FRecastTileGenerator::RasterizeGeometryRecastTimeSliced(FNavMeshBuildContext& BuildContext, const TArray<FVector::FReal>& Coords, const TArray<int32>& Indices, const rcRasterizationFlags RasterizationFlags, FTileRasterizationContext& RasterContext)
|
|
{
|
|
return RasterizeGeometryRecastTimeSliced(BuildContext, Coords, Indices, FBox(ForceInit), RasterizationFlags, RasterContext);
|
|
}
|
|
|
|
ETimeSliceWorkResult FRecastTileGenerator::RasterizeGeometryRecastTimeSliced(FNavMeshBuildContext& BuildContext, const TArray<FVector::FReal>& Coords, const TArray<int32>& Indices, const FBox& CoordsBounds, const rcRasterizationFlags RasterizationFlags, FTileRasterizationContext& RasterContext)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_Navigation_RasterizeGeometryRecast);
|
|
|
|
check(TimeSliceManager);
|
|
|
|
const int32 NumFaces = Indices.Num() / 3;
|
|
const int32 NumVerts = Coords.Num() / 3;
|
|
|
|
switch (RasterizeGeomRecastState)
|
|
{
|
|
case ERasterizeGeomRecastTimeSlicedState::MarkWalkableTriangles:
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_Navigation_MarkWalkableTriangles);
|
|
|
|
RasterizeGeomRecastTriAreas.AddZeroed(NumFaces);
|
|
|
|
rcMarkWalkableTriangles(&BuildContext, TileConfig.walkableSlopeAngle,
|
|
Coords.GetData(), NumVerts, Indices.GetData(), NumFaces,
|
|
RasterizeGeomRecastTriAreas.GetData());
|
|
|
|
RasterizeGeomRecastState = ERasterizeGeomRecastTimeSlicedState::RasterizeTriangles;
|
|
|
|
MARK_TIMESLICE_SECTION_DEBUG(TimeSliceManager->GetTimeSlicer(), MarkWalkableTriangles);
|
|
|
|
if (TimeSliceManager->GetTimeSlicer().TestTimeSliceFinished())
|
|
{
|
|
return ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
}
|
|
}// fall through to next state
|
|
case ERasterizeGeomRecastTimeSlicedState::RasterizeTriangles:
|
|
{
|
|
ComputeRasterizationMasks(BuildContext, RasterContext);
|
|
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_Navigation_RasterizeGeomRecastRasterizeTriangles);
|
|
|
|
const FVector::FReal* BoundsMin = CoordsBounds.IsValid ? &CoordsBounds.Min.X : nullptr;
|
|
const FVector::FReal* BoundsMax = CoordsBounds.IsValid ? &CoordsBounds.Max.X : nullptr;
|
|
const TInlineMaskArray::ElementType* MaskArray = RasterContext.RasterizationMasks.Num() > 0 ? RasterContext.RasterizationMasks.GetData() : nullptr;
|
|
rcRasterizeTriangles(&BuildContext,
|
|
Coords.GetData(), NumVerts,
|
|
Indices.GetData(), RasterizeGeomRecastTriAreas.GetData(), NumFaces,
|
|
*SolidHF, TileConfig.walkableClimb, RasterizationFlags, MaskArray, BoundsMin, BoundsMax);
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
BuildContext.InternalDebugData.TriangleCount += NumFaces;
|
|
#endif // RECAST_INTERNAL_DEBUG_DATA
|
|
|
|
RasterizeGeomRecastTriAreas.Reset();
|
|
|
|
//reset this so next call we start by marking walkable triangles
|
|
RasterizeGeomRecastState = ERasterizeGeomRecastTimeSlicedState::MarkWalkableTriangles;
|
|
|
|
MARK_TIMESLICE_SECTION_DEBUG(TimeSliceManager->GetTimeSlicer(), RasterizeTriangles);
|
|
|
|
TimeSliceManager->GetTimeSlicer().TestTimeSliceFinished();
|
|
}
|
|
break;
|
|
default:
|
|
{
|
|
ensureMsgf(false, TEXT("unhandled ERasterizeGeomRecastTimeSlicedState"));
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
}
|
|
return ETimeSliceWorkResult::Succeeded;
|
|
}
|
|
|
|
// Deprecated
|
|
void FRecastTileGenerator::RasterizeGeometryRecast(FNavMeshBuildContext& BuildContext, const TArray<FVector::FReal>& Coords, const TArray<int32>& Indices, const rcRasterizationFlags RasterizationFlags, FTileRasterizationContext& RasterContext)
|
|
{
|
|
return RasterizeGeometryRecast(BuildContext, Coords, Indices, FBox(ForceInit), RasterizationFlags, RasterContext);
|
|
}
|
|
|
|
void FRecastTileGenerator::RasterizeGeometryRecast(FNavMeshBuildContext& BuildContext, const TArray<FVector::FReal>& Coords, const TArray<int32>& Indices, const FBox& CoordsBounds, const rcRasterizationFlags RasterizationFlags, FTileRasterizationContext& RasterContext)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_Navigation_RasterizeGeometryRecast);
|
|
|
|
const int32 NumFaces = Indices.Num() / 3;
|
|
const int32 NumVerts = Coords.Num() / 3;
|
|
|
|
RasterizeGeomRecastTriAreas.AddZeroed(NumFaces);
|
|
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_Navigation_MarkWalkableTriangles);
|
|
|
|
rcMarkWalkableTriangles(&BuildContext, TileConfig.walkableSlopeAngle,
|
|
Coords.GetData(), NumVerts, Indices.GetData(), NumFaces,
|
|
RasterizeGeomRecastTriAreas.GetData());
|
|
}
|
|
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_Navigation_RasterizeGeomRecastRasterizeTriangles);
|
|
|
|
const FVector::FReal* BoundsMin = CoordsBounds.IsValid ? &CoordsBounds.Min.X : nullptr;
|
|
const FVector::FReal* BoundsMax = CoordsBounds.IsValid ? &CoordsBounds.Max.X : nullptr;
|
|
const TInlineMaskArray::ElementType* MaskArray = RasterContext.RasterizationMasks.Num() > 0 ? RasterContext.RasterizationMasks.GetData() : nullptr;
|
|
rcRasterizeTriangles(&BuildContext,
|
|
Coords.GetData(), NumVerts,
|
|
Indices.GetData(), RasterizeGeomRecastTriAreas.GetData(), NumFaces,
|
|
*SolidHF, TileConfig.walkableClimb, RasterizationFlags, MaskArray, BoundsMin, BoundsMax);
|
|
}
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
BuildContext.InternalDebugData.TriangleCount += NumFaces;
|
|
|
|
if (IsTileDebugActive() && TileDebugSettings.bCollisionGeometry)
|
|
{
|
|
TArray<FVector::FReal> Normals;
|
|
Normals.AddZeroed(NumFaces*3);
|
|
|
|
rcCalcTriNormals(Coords.GetData(), NumVerts, Indices.GetData(), NumFaces, Normals.GetData());
|
|
|
|
constexpr FVector::FReal TextureScale = 1.;
|
|
duDebugDrawTriMesh(&BuildContext.InternalDebugData, Coords.GetData(), NumVerts, Indices.GetData(), Normals.GetData(), NumFaces, RasterizeGeomRecastTriAreas.GetData(), TextureScale);
|
|
}
|
|
|
|
BuildContext.InternalDebugData.TriangleCount += NumFaces;
|
|
#endif // RECAST_INTERNAL_DEBUG_DATA
|
|
|
|
RasterizeGeomRecastTriAreas.Reset();
|
|
}
|
|
|
|
// Deprecated
|
|
void FRecastTileGenerator::RasterizeGeometryTransformCoordsAndFlipIndices(const TArray<FVector::FReal>& Coords, const TArray<int32>& Indices, const FTransform& LocalToWorld)
|
|
{
|
|
return RasterizeGeometryTransformCoordsAndFlipIndices(Coords, Indices, FBox(ForceInit), LocalToWorld);
|
|
}
|
|
|
|
void FRecastTileGenerator::RasterizeGeometryTransformCoordsAndFlipIndices(const TArray<FVector::FReal>& Coords, const TArray<int32>& Indices, const FBox& CoordsBounds, const FTransform& LocalToWorld)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_Navigation_RasterizeGeometryTransformCoordsAndFlipIndices);
|
|
|
|
RasterizeGeometryWorldRecastCoords.SetNumUninitialized(Coords.Num(), EAllowShrinking::No);
|
|
RasterizeGeometryWorldRecastCoordsBounds = FBox(ForceInit);
|
|
|
|
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]));
|
|
|
|
RasterizeGeometryWorldRecastCoords[i + 0] = WorldRecastCoord.X;
|
|
RasterizeGeometryWorldRecastCoords[i + 1] = WorldRecastCoord.Y;
|
|
RasterizeGeometryWorldRecastCoords[i + 2] = WorldRecastCoord.Z;
|
|
}
|
|
|
|
//Flip the order of indices for each triangle if the source object is mirrored
|
|
bRasterizeGeometryUseFlippedIndices = false;
|
|
if (LocalToWorld.GetDeterminant() < 0.f)
|
|
{
|
|
bRasterizeGeometryUseFlippedIndices = true;
|
|
RasterizeGeometryFlippedIndices.SetNumUninitialized(Indices.Num(), EAllowShrinking::No);
|
|
|
|
for (int32 i = 0; i < Indices.Num(); i += 3)
|
|
{
|
|
RasterizeGeometryFlippedIndices[i] = Indices[i + 2];
|
|
RasterizeGeometryFlippedIndices[i + 1] = Indices[i + 1];
|
|
RasterizeGeometryFlippedIndices[i + 2] = Indices[i];
|
|
}
|
|
}
|
|
|
|
if (CoordsBounds.IsValid)
|
|
{
|
|
RasterizeGeometryWorldRecastCoordsBounds = Recast2UnrealBox(CoordsBounds).TransformBy(LocalToRecastWorld);
|
|
}
|
|
}
|
|
|
|
// Deprecated
|
|
void FRecastTileGenerator::RasterizeGeometryTransformCoords(const TArray<FVector::FReal>& Coords, const FTransform& LocalToWorld)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_Navigation_RasterizeGeometryTransformCoords);
|
|
|
|
RasterizeGeometryWorldRecastCoords.SetNumUninitialized(Coords.Num(), EAllowShrinking::No);
|
|
RasterizeGeometryWorldRecastCoordsBounds = FBox(ForceInit);
|
|
|
|
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]));
|
|
|
|
RasterizeGeometryWorldRecastCoords[i+0] = WorldRecastCoord.X;
|
|
RasterizeGeometryWorldRecastCoords[i+1] = WorldRecastCoord.Y;
|
|
RasterizeGeometryWorldRecastCoords[i+2] = WorldRecastCoord.Z;
|
|
}
|
|
}
|
|
|
|
// Deprecated
|
|
ETimeSliceWorkResult FRecastTileGenerator::RasterizeGeometryTimeSliced(FNavMeshBuildContext& BuildContext, const TArray<FVector::FReal>& Coords, const TArray<int32>& Indices, const FTransform& LocalToWorld, const rcRasterizationFlags RasterizationFlags, FTileRasterizationContext& RasterContext)
|
|
{
|
|
return RasterizeGeometryTimeSliced(BuildContext, Coords, Indices, FBox(ForceInit), LocalToWorld, RasterizationFlags, RasterContext);
|
|
}
|
|
|
|
ETimeSliceWorkResult FRecastTileGenerator::RasterizeGeometryTimeSliced(FNavMeshBuildContext& BuildContext, const TArray<FVector::FReal>& Coords, const TArray<int32>& Indices, const FBox& CoordsBounds, const FTransform& LocalToWorld, const rcRasterizationFlags RasterizationFlags, FTileRasterizationContext& RasterContext)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_Navigation_RasterizeGeometry);
|
|
|
|
check(TimeSliceManager);
|
|
|
|
ETimeSliceWorkResult WorkResult = ETimeSliceWorkResult::Succeeded;
|
|
|
|
switch (RasterizeGeomState)
|
|
{
|
|
case ERasterizeGeomTimeSlicedState::RasterizeGeometryTransformCoordsAndFlipIndices:
|
|
{
|
|
RasterizeGeometryTransformCoordsAndFlipIndices(Coords, Indices, CoordsBounds, LocalToWorld);
|
|
|
|
RasterizeGeomState = ERasterizeGeomTimeSlicedState::RasterizeGeometryRecast;
|
|
|
|
MARK_TIMESLICE_SECTION_DEBUG(TimeSliceManager->GetTimeSlicer(), RasterizeGeometryTransformCoordsAndFlipIndices);
|
|
|
|
if (TimeSliceManager->GetTimeSlicer().TestTimeSliceFinished())
|
|
{
|
|
return ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
}
|
|
}// fall through to next state
|
|
case ERasterizeGeomTimeSlicedState::RasterizeGeometryRecast:
|
|
{
|
|
const TArray<int32>& RasterizeGeometryIndices = bRasterizeGeometryUseFlippedIndices ? RasterizeGeometryFlippedIndices : Indices;
|
|
WorkResult = RasterizeGeometryRecastTimeSliced(BuildContext, RasterizeGeometryWorldRecastCoords, RasterizeGeometryIndices, RasterizeGeometryWorldRecastCoordsBounds, RasterizationFlags, RasterContext);
|
|
|
|
if (WorkResult != ETimeSliceWorkResult::CallAgainNextTimeSlice)
|
|
{
|
|
//if we have finished rasterizing this geometry then reset RasterizeGeomTimeSlicedState so next time this function is called we go back to RasterizeGeometryTransformCoordsAndFlipIndices first
|
|
RasterizeGeomState = ERasterizeGeomTimeSlicedState::RasterizeGeometryTransformCoordsAndFlipIndices;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
{
|
|
ensureMsgf(false, TEXT("unhandled ERasterizeGeomTimeSlicedState"));
|
|
return ETimeSliceWorkResult(ETimeSliceWorkResult::Failed);
|
|
}
|
|
}
|
|
return WorkResult;
|
|
}
|
|
|
|
void FRecastTileGenerator::RasterizeGeometry(FNavMeshBuildContext& BuildContext, const TArray<FVector::FReal>& Coords, const TArray<int32>& Indices, const FTransform& LocalToWorld, const rcRasterizationFlags RasterizationFlags, FTileRasterizationContext& RasterContext)
|
|
{
|
|
return RasterizeGeometry(BuildContext, Coords, Indices, FBox(ForceInit), LocalToWorld, RasterizationFlags, RasterContext);
|
|
}
|
|
|
|
void FRecastTileGenerator::RasterizeGeometry(FNavMeshBuildContext& BuildContext, const TArray<FVector::FReal>& Coords, const TArray<int32>& Indices, const FBox& CoordsBounds, const FTransform& LocalToWorld, const rcRasterizationFlags RasterizationFlags, FTileRasterizationContext& RasterContext)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_Navigation_RasterizeGeometry);
|
|
|
|
RasterizeGeometryTransformCoordsAndFlipIndices(Coords, Indices, CoordsBounds, LocalToWorld);
|
|
const TArray<int32>& RasterizeGeometryIndices = bRasterizeGeometryUseFlippedIndices ? RasterizeGeometryFlippedIndices : Indices;
|
|
RasterizeGeometryRecast(BuildContext, RasterizeGeometryWorldRecastCoords, RasterizeGeometryIndices, RasterizeGeometryWorldRecastCoordsBounds, RasterizationFlags, RasterContext);
|
|
}
|
|
|
|
ETimeSliceWorkResult FRecastTileGenerator::RasterizeTrianglesTimeSliced(FNavMeshBuildContext& BuildContext, FTileRasterizationContext& RasterContext)
|
|
{
|
|
// Rasterize geometry
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastRasterizeTriangles)
|
|
|
|
check(TimeSliceManager);
|
|
|
|
while (RasterizeTrianglesTimeSlicedRawGeomIdx < RawGeometry.Num())
|
|
{
|
|
const FRecastRawGeometryElement& Element = RawGeometry[RasterizeTrianglesTimeSlicedRawGeomIdx];
|
|
if (Element.PerInstanceTransform.Num() > 0)
|
|
{
|
|
while (RasterizeTrianglesTimeSlicedInstTransformIdx < Element.PerInstanceTransform.Num())
|
|
{
|
|
const FTransform& InstanceTransform = Element.PerInstanceTransform[RasterizeTrianglesTimeSlicedInstTransformIdx];
|
|
const ETimeSliceWorkResult WorkResult = RasterizeGeometryTimeSliced(BuildContext, Element.GeomCoords, Element.GeomIndices, Element.GeomCoordsBounds, InstanceTransform, Element.RasterizationFlags, RasterContext);
|
|
|
|
//the original code just kept calling the RasterizeGeometry() functions and had no return type,
|
|
//so we will process the next layer (if we are not needing to process this layer again next time slice)
|
|
if (TimeSliceManager->GetTimeSlicer().IsTimeSliceFinishedCached())
|
|
{
|
|
if (WorkResult != ETimeSliceWorkResult::CallAgainNextTimeSlice)
|
|
{
|
|
++RasterizeTrianglesTimeSlicedInstTransformIdx;
|
|
}
|
|
|
|
return ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
}
|
|
|
|
++RasterizeTrianglesTimeSlicedInstTransformIdx;
|
|
}
|
|
//reset RasterizeTrianglesTimeSlicedIdx
|
|
RasterizeTrianglesTimeSlicedInstTransformIdx = 0;
|
|
}
|
|
else
|
|
{
|
|
const ETimeSliceWorkResult WorkResult = RasterizeGeometryRecastTimeSliced(BuildContext, Element.GeomCoords, Element.GeomIndices, Element.GeomCoordsBounds, Element.RasterizationFlags, RasterContext);
|
|
|
|
if (TimeSliceManager->GetTimeSlicer().IsTimeSliceFinishedCached())
|
|
{
|
|
if (WorkResult != ETimeSliceWorkResult::CallAgainNextTimeSlice)
|
|
{
|
|
++RasterizeTrianglesTimeSlicedRawGeomIdx;
|
|
}
|
|
return ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
}
|
|
}
|
|
++RasterizeTrianglesTimeSlicedRawGeomIdx;
|
|
}
|
|
|
|
//return sucess as non timesliced functionality does not detect failure here
|
|
return ETimeSliceWorkResult::Succeeded;
|
|
}
|
|
|
|
void FRecastTileGenerator::RasterizeTriangles(FNavMeshBuildContext& BuildContext, FTileRasterizationContext& RasterContext)
|
|
{
|
|
// Rasterize geometry
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastRasterizeTriangles)
|
|
|
|
for (int32 RawGeomIdx = 0; RawGeomIdx < RawGeometry.Num(); ++RawGeomIdx)
|
|
{
|
|
const FRecastRawGeometryElement& Element = RawGeometry[RawGeomIdx];
|
|
if (Element.PerInstanceTransform.Num() > 0)
|
|
{
|
|
for (const FTransform& InstanceTransform : Element.PerInstanceTransform)
|
|
{
|
|
RasterizeGeometry(BuildContext, Element.GeomCoords, Element.GeomIndices, Element.GeomCoordsBounds, InstanceTransform, Element.RasterizationFlags, RasterContext);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RasterizeGeometryRecast(BuildContext, Element.GeomCoords, Element.GeomIndices, Element.GeomCoordsBounds, Element.RasterizationFlags, RasterContext);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FRecastTileGenerator::GenerateRecastFilter(FNavMeshBuildContext& BuildContext)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastFilter)
|
|
|
|
// TileConfig.walkableHeight is set to 1 when marking low spans, calculate real value for filtering
|
|
const int32 FilterWalkableHeight = FMath::CeilToInt(TileConfig.AgentHeight / static_cast<float>(TileConfig.ch));
|
|
|
|
// 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, *SolidHF);
|
|
}
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_FilterLedgeSpans)
|
|
|
|
rcFilterLedgeSpans(&BuildContext, TileConfig.walkableHeight, TileConfig.walkableClimb,
|
|
(rcNeighborSlopeFilterMode)TileConfig.LedgeSlopeFilterMode, TileConfig.maxStepFromWalkableSlope, TileConfig.ch, *SolidHF);
|
|
}
|
|
if (!TileConfig.bMarkLowHeightAreas)
|
|
{
|
|
rcFilterWalkableLowHeightSpans(&BuildContext, TileConfig.walkableHeight, *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, *SolidHF);
|
|
}
|
|
else
|
|
{
|
|
rcFilterWalkableLowHeightSpans(&BuildContext, FilterWalkableHeight, *SolidHF);
|
|
}
|
|
}
|
|
}
|
|
|
|
ETimeSliceWorkResult FRecastTileGenerator::GenerateRecastFilterTimeSliced(FNavMeshBuildContext& BuildContext)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastFilter)
|
|
|
|
check(TimeSliceManager);
|
|
|
|
ETimeSliceWorkResult WorkResult = ETimeSliceWorkResult::Succeeded;
|
|
|
|
switch (GenerateRecastFilterState)
|
|
{
|
|
case EGenerateRecastFilterTimeSlicedState::FilterLowHangingWalkableObstacles:
|
|
{
|
|
// 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, *SolidHF);
|
|
GenerateRecastFilterState = EGenerateRecastFilterTimeSlicedState::FilterLedgeSpans;
|
|
}// fall through to next state
|
|
case EGenerateRecastFilterTimeSlicedState::FilterLedgeSpans:
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_FilterLedgeSpans)
|
|
|
|
bool DoIter = true;
|
|
|
|
do
|
|
{
|
|
rcFilterLedgeSpans(&BuildContext, TileConfig.walkableHeight, TileConfig.walkableClimb,
|
|
(rcNeighborSlopeFilterMode)TileConfig.LedgeSlopeFilterMode, TileConfig.maxStepFromWalkableSlope, TileConfig.ch,
|
|
GenRecastFilterLedgeSpansYStart, TileTimeSliceSettings.FilterLedgeSpansMaxYProcess, *SolidHF);
|
|
|
|
GenRecastFilterLedgeSpansYStart += TileTimeSliceSettings.FilterLedgeSpansMaxYProcess;
|
|
|
|
if (GenRecastFilterLedgeSpansYStart >= SolidHF->height)
|
|
{
|
|
GenRecastFilterLedgeSpansYStart = 0;
|
|
DoIter = false;
|
|
GenerateRecastFilterState = EGenerateRecastFilterTimeSlicedState::FilterWalkableLowHeightSpans;
|
|
}
|
|
|
|
MARK_TIMESLICE_SECTION_DEBUG(TimeSliceManager->GetTimeSlicer(), FilterLedgeSpans);
|
|
|
|
// Only FilterLedge Spans has been found to be slow so we only actually test the timeslice here for this function
|
|
if (TimeSliceManager->GetTimeSlicer().TestTimeSliceFinished())
|
|
{
|
|
return ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
}
|
|
} while (DoIter);
|
|
}// fall through to next state
|
|
case EGenerateRecastFilterTimeSlicedState::FilterWalkableLowHeightSpans:
|
|
{
|
|
// TileConfig.walkableHeight is set to 1 when marking low spans, calculate real value for filtering
|
|
const int32 FilterWalkableHeight = FMath::CeilToInt(TileConfig.AgentHeight / static_cast<float>(TileConfig.ch));
|
|
|
|
if (!TileConfig.bMarkLowHeightAreas)
|
|
{
|
|
rcFilterWalkableLowHeightSpans(&BuildContext, TileConfig.walkableHeight, *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, *SolidHF);
|
|
}
|
|
else
|
|
{
|
|
rcFilterWalkableLowHeightSpans(&BuildContext, FilterWalkableHeight, *SolidHF);
|
|
}
|
|
}
|
|
GenerateRecastFilterState = EGenerateRecastFilterTimeSlicedState::FilterLowHangingWalkableObstacles;
|
|
}
|
|
break;
|
|
default:
|
|
{
|
|
ensureMsgf(false, TEXT("unhandled EGenerateRecastFilterTimeSlicedState"));
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
}
|
|
|
|
return ETimeSliceWorkResult::Succeeded;
|
|
}
|
|
|
|
bool FRecastTileGenerator::BuildCompactHeightField(FNavMeshBuildContext& BuildContext)
|
|
{
|
|
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.
|
|
CompactHF = rcAllocCompactHeightfield();
|
|
if (CompactHF == nullptr)
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "BuildCompactHeightField: Out of memory 'CompactHF'.");
|
|
return false;
|
|
}
|
|
if (!rcBuildCompactHeightfield(&BuildContext, TileConfig.walkableHeight, TileConfig.walkableClimb, *SolidHF, *CompactHF))
|
|
{
|
|
const int SpanCount = rcGetHeightFieldSpanCount(&BuildContext, *SolidHF);
|
|
if (SpanCount > 0)
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "BuildCompactHeightField: 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, "BuildCompactHeightField: no walkable spans - aborting");
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool FRecastTileGenerator::RecastErodeWalkable(FNavMeshBuildContext& BuildContext)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastErodeWalkable);
|
|
|
|
// TileConfig.walkableHeight is set to 1 when marking low spans, calculate real value for filtering
|
|
const int32 FilterWalkableHeight = FMath::CeilToInt(TileConfig.AgentHeight / static_cast<float>(TileConfig.ch));
|
|
|
|
if (static_cast<float>(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, *CompactHF) :
|
|
rcErodeWalkableArea(&BuildContext, TileConfig.walkableRadius, *CompactHF);
|
|
|
|
if (!bEroded)
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "GenerateCompressedLayers: Could not erode.");
|
|
return false;
|
|
}
|
|
|
|
}
|
|
else if (TileConfig.bMarkLowHeightAreas)
|
|
{
|
|
rcMarkLowAreas(&BuildContext, FilterWalkableHeight, RECAST_LOW_AREA, *CompactHF);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FRecastTileGenerator::RecastBuildLayers(FNavMeshBuildContext& BuildContext, FTileRasterizationContext& RasterContext)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildLayers);
|
|
|
|
RasterContext.LayerSet = rcAllocHeightfieldLayerSet();
|
|
if (RasterContext.LayerSet == nullptr)
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "RecastBuildLayers: Out of memory 'LayerSet'.");
|
|
return false;
|
|
}
|
|
|
|
if (TileConfig.regionPartitioning == RC_REGION_MONOTONE)
|
|
{
|
|
if (!rcBuildHeightfieldLayersMonotone(&BuildContext, *CompactHF, TileConfig.borderSize, TileConfig.walkableHeight, *RasterContext.LayerSet))
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "RecastBuildLayers: Could not build heightfield layers.");
|
|
return false;
|
|
|
|
}
|
|
}
|
|
else if (TileConfig.regionPartitioning == RC_REGION_WATERSHED)
|
|
{
|
|
if (!rcBuildDistanceField(&BuildContext, *CompactHF))
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "RecastBuildLayers: Could not build distance field.");
|
|
return false;
|
|
}
|
|
|
|
if (!rcBuildHeightfieldLayers(&BuildContext, *CompactHF, TileConfig.borderSize, TileConfig.walkableHeight, *RasterContext.LayerSet))
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "RecastBuildLayers: Could not build heightfield layers.");
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!rcBuildHeightfieldLayersChunky(&BuildContext, *CompactHF, TileConfig.borderSize, TileConfig.walkableHeight, TileConfig.regionChunkSize, *RasterContext.LayerSet))
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "RecastBuildLayers: Could not build heightfield layers.");
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool FRecastTileGenerator::RecastBuildTileCache(FNavMeshBuildContext& BuildContext, FTileRasterizationContext& RasterContext)
|
|
{
|
|
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 FVector::FReal StepHeights = TileConfig.AgentMaxClimb;
|
|
|
|
FTileCacheCompressor TileCompressor;
|
|
for (int32 i = 0; i < NumLayers; i++)
|
|
{
|
|
const rcHeightfieldLayer* layer = &RasterContext.LayerSet->layers[i];
|
|
|
|
// Store header
|
|
dtTileCacheLayerHeader header;
|
|
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;
|
|
|
|
// 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 = nullptr;
|
|
int32 TileDataSize = 0;
|
|
const dtStatus status = dtBuildTileCacheLayer(&TileCompressor, &header, layer->heights, layer->areas, layer->cons, &TileData, &TileDataSize);
|
|
if (dtStatusFailed(status))
|
|
{
|
|
dtFree(TileData, DT_ALLOC_PERM_TILE_DATA);
|
|
BuildContext.log(RC_LOG_ERROR, "RecastBuildTileCache: 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_TILE_DATA);
|
|
if (CompressedData == nullptr)
|
|
{
|
|
dtFree(TileData, DT_ALLOC_PERM_TILE_DATA);
|
|
BuildContext.log(RC_LOG_ERROR, "RecastBuildTileCache: Out of memory 'CompressedData'.");
|
|
return false;
|
|
}
|
|
|
|
FMemory::Memcpy(CompressedData, TileData, TileDataSize);
|
|
RasterContext.Layers.Add(FNavMeshTileData(CompressedData, TileDataSize, i, LayerBBox));
|
|
|
|
dtFree(TileData, DT_ALLOC_PERM_TILE_DATA);
|
|
|
|
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,
|
|
static_cast<float>(TileDataSize) * Inv1kB,
|
|
static_cast<float>(UncompressedSize) * Inv1kB,
|
|
static_cast<float>(TileDataSize) / static_cast<float>(UncompressedSize));
|
|
}
|
|
CompressedLayers = MoveTemp(RasterContext.Layers);
|
|
return true;
|
|
}
|
|
|
|
ETimeSliceWorkResult FRecastTileGenerator::GenerateCompressedLayersTimeSliced(FNavMeshBuildContext& BuildContext)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildCompressedLayers);
|
|
|
|
check(TimeSliceManager);
|
|
|
|
FTileRasterizationContext* RasterContext = GenCompressedlayersTimeSlicedRasterContext.Get();
|
|
|
|
switch (GenCompressedLayersTimeSlicedState)
|
|
{
|
|
case EGenerateCompressedLayersTimeSliced::Invalid:
|
|
{
|
|
ensureMsgf(false, TEXT("Invalid EGenerateCompressedLayersTimeSliced, has this function been called when its already finished processing?"));
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
|
|
case EGenerateCompressedLayersTimeSliced::Init:
|
|
{
|
|
CompressedLayers.Reset();
|
|
GenCompressedlayersTimeSlicedRasterContext = MakeUnique<FTileRasterizationContext>();
|
|
RasterContext = GenCompressedlayersTimeSlicedRasterContext.Get();
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::CreateHeightField;
|
|
} // fall through to next state
|
|
case EGenerateCompressedLayersTimeSliced::CreateHeightField:
|
|
{
|
|
if (!CreateHeightField(BuildContext))
|
|
{
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::Invalid;
|
|
//no need to check time slice as not much work done
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::RasterizeTriangles;
|
|
|
|
MARK_TIMESLICE_SECTION_DEBUG(TimeSliceManager->GetTimeSlicer(), CreateHeightField);
|
|
|
|
if (TimeSliceManager->GetTimeSlicer().TestTimeSliceFinished())
|
|
{
|
|
return ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
}
|
|
} // fall through to next state
|
|
case EGenerateCompressedLayersTimeSliced::RasterizeTriangles:
|
|
{
|
|
const ETimeSliceWorkResult WorkResult = RasterizeTrianglesTimeSliced(BuildContext, *RasterContext);
|
|
|
|
//original code did not care about success or failure here
|
|
if (WorkResult != ETimeSliceWorkResult::CallAgainNextTimeSlice)
|
|
{
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::EmptyLayers;
|
|
}
|
|
|
|
if (TimeSliceManager->GetTimeSlicer().IsTimeSliceFinishedCached())
|
|
{
|
|
return ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
}
|
|
} // fall through to next state
|
|
case EGenerateCompressedLayersTimeSliced::EmptyLayers:
|
|
{
|
|
if (!SolidHF || SolidHF->pools == 0)
|
|
{
|
|
BuildContext.log(RC_LOG_WARNING, "GenerateCompressedLayersTimeSliced: empty tile - aborting");
|
|
|
|
//no need to check time slice as not much work done
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::Invalid;
|
|
return ETimeSliceWorkResult::Succeeded;
|
|
}
|
|
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::VoxelFilter;
|
|
//no need to check time slice as not much work done
|
|
}// fall through to next state
|
|
case EGenerateCompressedLayersTimeSliced::VoxelFilter:
|
|
{
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::RecastFilter;
|
|
// Reject voxels outside generation boundaries
|
|
if (TileConfig.bPerformVoxelFiltering && !bFullyEncapsulatedByInclusionBounds)
|
|
{
|
|
ApplyVoxelFilter(SolidHF, TileConfig.walkableRadius);
|
|
|
|
MARK_TIMESLICE_SECTION_DEBUG(TimeSliceManager->GetTimeSlicer(), VoxelFilter);
|
|
|
|
if (TimeSliceManager->GetTimeSlicer().TestTimeSliceFinished())
|
|
{
|
|
return ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
}
|
|
}
|
|
}// fall through to next state
|
|
case EGenerateCompressedLayersTimeSliced::RecastFilter:
|
|
{
|
|
const ETimeSliceWorkResult WorkResult = GenerateRecastFilterTimeSliced(BuildContext);
|
|
|
|
// Non timesliced code this is based on did not care about success or failure here.
|
|
if (WorkResult != ETimeSliceWorkResult::CallAgainNextTimeSlice)
|
|
{
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::CompactHeightField;
|
|
}
|
|
|
|
if (TimeSliceManager->GetTimeSlicer().IsTimeSliceFinishedCached())
|
|
{
|
|
return ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
}
|
|
}// fall through to next state
|
|
case EGenerateCompressedLayersTimeSliced::CompactHeightField:
|
|
{
|
|
if (!BuildCompactHeightField(BuildContext))
|
|
{
|
|
//no need to check time slice as not much work done
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::Invalid;
|
|
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::ErodeWalkable;
|
|
|
|
MARK_TIMESLICE_SECTION_DEBUG(TimeSliceManager->GetTimeSlicer(), CompactHeightField);
|
|
|
|
if (TimeSliceManager->GetTimeSlicer().TestTimeSliceFinished())
|
|
{
|
|
return ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
}
|
|
}// fall through to next state
|
|
case EGenerateCompressedLayersTimeSliced::ErodeWalkable:
|
|
{
|
|
if (!RecastErodeWalkable(BuildContext))
|
|
{
|
|
//no need to check time slice as not much work done
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::Invalid;
|
|
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::BuildLayers;
|
|
|
|
MARK_TIMESLICE_SECTION_DEBUG(TimeSliceManager->GetTimeSlicer(), ErodeWalkable);
|
|
|
|
if (TimeSliceManager->GetTimeSlicer().TestTimeSliceFinished())
|
|
{
|
|
return ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
}
|
|
}// fall through to next state
|
|
case EGenerateCompressedLayersTimeSliced::BuildLayers:
|
|
{
|
|
const bool bRecastBuildLayers = RecastBuildLayers(BuildContext, *RasterContext);
|
|
|
|
MARK_TIMESLICE_SECTION_DEBUG(TimeSliceManager->GetTimeSlicer(), BuildLayers);
|
|
|
|
//this could have done a fair amount of work either way so check time slice
|
|
TimeSliceManager->GetTimeSlicer().TestTimeSliceFinished();
|
|
|
|
if (!bRecastBuildLayers)
|
|
{
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::Invalid;
|
|
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::BuildTileCache;
|
|
|
|
if (TimeSliceManager->GetTimeSlicer().IsTimeSliceFinishedCached())
|
|
{
|
|
return ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
}
|
|
}// fall through to next state
|
|
case EGenerateCompressedLayersTimeSliced::BuildTileCache:
|
|
{
|
|
GenCompressedLayersTimeSlicedState = EGenerateCompressedLayersTimeSliced::Invalid;
|
|
|
|
const bool bRecastBuildTileCache = RecastBuildTileCache(BuildContext, *RasterContext);
|
|
|
|
MARK_TIMESLICE_SECTION_DEBUG(TimeSliceManager->GetTimeSlicer(), BuildTileCache);
|
|
|
|
//this could have done a fair amount of work either way so check time slice
|
|
TimeSliceManager->GetTimeSlicer().TestTimeSliceFinished();
|
|
|
|
if (!bRecastBuildTileCache)
|
|
{
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
{
|
|
ensureMsgf(false, TEXT("unknow EGenerateCompressedLayersTimeSliced state"));
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
}
|
|
|
|
return ETimeSliceWorkResult::Succeeded;
|
|
}
|
|
|
|
// Deprecated
|
|
bool FRecastTileGenerator::GenerateCompressedLayers(FNavMeshBuildContext& BuildContext)
|
|
{
|
|
dtLinkBuilderData LinBuilderData;
|
|
return GenerateCompressedLayers(BuildContext, LinBuilderData);
|
|
}
|
|
|
|
// Deprecated
|
|
bool FRecastTileGenerator::GenerateNavigationDataLayer(FNavMeshBuildContext& BuildContext, FTileCacheCompressor& TileCompressor, FTileCacheAllocator& GenNavAllocator, FTileGenerationContext& GenerationContext, int32 LayerIdx)
|
|
{
|
|
dtLinkBuilderData LinBuilderData;
|
|
return GenerateNavigationDataLayer(BuildContext, TileCompressor, GenNavAllocator, GenerationContext, LinBuilderData, LayerIdx);
|
|
}
|
|
|
|
// Deprecated
|
|
ETimeSliceWorkResult FRecastTileGenerator::GenerateNavigationDataTimeSliced(FNavMeshBuildContext& BuildContext)
|
|
{
|
|
dtLinkBuilderData LinBuilderData;
|
|
return GenerateNavigationDataTimeSliced(BuildContext, LinBuilderData);
|
|
}
|
|
|
|
// Deprecated
|
|
bool FRecastTileGenerator::GenerateNavigationData(FNavMeshBuildContext& BuildContext)
|
|
{
|
|
dtLinkBuilderData LinBuilderData;
|
|
return GenerateNavigationData(BuildContext, LinBuilderData);
|
|
}
|
|
|
|
namespace UE::NavMesh::Private
|
|
{
|
|
// Make sure LinkSpillDistance and CellSize has been computed before computing borders.
|
|
void ComputeConfigBorderSizes(const bool bGeneratingLinks, FRecastBuildConfig& InOutConfig)
|
|
{
|
|
int BorderForLinksVx = 0;
|
|
if (bGeneratingLinks)
|
|
{
|
|
BorderForLinksVx = (int)FMath::CeilToInt((rcReal)InOutConfig.LinkSpillDistance / InOutConfig.cs) + InOutConfig.walkableRadius;
|
|
}
|
|
|
|
// +1 for voxelization rounding, +1 for ledge neighbor access, +1 for occasional errors
|
|
const int BorderForAgentVx = InOutConfig.walkableRadius + 3;
|
|
|
|
// Borders must be at least the size of BorderForAgentVx.
|
|
InOutConfig.borderSize.low = UE::NavMesh::Private::bUseAsymetricBorderSizes ? BorderForAgentVx : FMath::Max(BorderForAgentVx, BorderForLinksVx);
|
|
InOutConfig.borderSize.high = FMath::Max(BorderForAgentVx, BorderForLinksVx);
|
|
}
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
void DrawHeightfield(const EHeightFieldRenderMode RenderMode, duDebugDraw* dd, const rcHeightfield& hf)
|
|
{
|
|
if (RenderMode == EHeightFieldRenderMode::Solid)
|
|
{
|
|
duDebugDrawHeightfieldSolid(dd, hf);
|
|
}
|
|
else
|
|
{
|
|
duDebugDrawHeightfieldWalkable(dd, hf);
|
|
}
|
|
}
|
|
#endif //RECAST_INTERNAL_DEBUG_DATA
|
|
};
|
|
|
|
bool FRecastTileGenerator::GenerateCompressedLayers(FNavMeshBuildContext& BuildContext, const dtLinkBuilderData& InLinkBuilderData)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildCompressedLayers);
|
|
|
|
FTileRasterizationContext RasterContext;
|
|
CompressedLayers.Reset();
|
|
|
|
if (!CreateHeightField(BuildContext))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ComputeRasterizationMasks(BuildContext, RasterContext);
|
|
|
|
RasterizeTriangles(BuildContext, RasterContext);
|
|
if (!SolidHF || SolidHF->pools == 0)
|
|
{
|
|
BuildContext.log(RC_LOG_WARNING, "GenerateCompressedLayers: empty tile - aborting");
|
|
return true;
|
|
}
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
if (IsTileDebugActive())
|
|
{
|
|
if (TileDebugSettings.bHeightfieldFromRasterization)
|
|
{
|
|
UE::NavMesh::Private::DrawHeightfield(TileDebugSettings.HeightFieldRenderMode, &BuildContext.InternalDebugData, *SolidHF);
|
|
}
|
|
if (TileDebugSettings.bHeightfieldBounds)
|
|
{
|
|
duDebugDrawHeightfieldBounds(&BuildContext.InternalDebugData, *SolidHF);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Reject voxels outside generation boundaries
|
|
if (TileConfig.bPerformVoxelFiltering && !bFullyEncapsulatedByInclusionBounds)
|
|
{
|
|
ApplyVoxelFilter(SolidHF, TileConfig.walkableRadius);
|
|
}
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
if (IsTileDebugActive() && TileDebugSettings.bHeightfieldPostInclusionBoundsFiltering)
|
|
{
|
|
UE::NavMesh::Private::DrawHeightfield(TileDebugSettings.HeightFieldRenderMode, &BuildContext.InternalDebugData, *SolidHF);
|
|
}
|
|
#endif
|
|
|
|
GenerateRecastFilter(BuildContext);
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
if (IsTileDebugActive() && TileDebugSettings.bHeightfieldPostHeightFiltering)
|
|
{
|
|
UE::NavMesh::Private::DrawHeightfield(TileDebugSettings.HeightFieldRenderMode, &BuildContext.InternalDebugData, *SolidHF);
|
|
}
|
|
#endif
|
|
|
|
if (!BuildCompactHeightField(BuildContext))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
if (IsTileDebugActive() && TileDebugSettings.bCompactHeightfield)
|
|
{
|
|
duDebugDrawCompactHeightfieldSolid(&BuildContext.InternalDebugData, *CompactHF);
|
|
}
|
|
#endif
|
|
|
|
if (!RecastErodeWalkable(BuildContext))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
if (IsTileDebugActive() && TileDebugSettings.bCompactHeightfieldEroded)
|
|
{
|
|
duDebugDrawCompactHeightfieldSolid(&BuildContext.InternalDebugData, *CompactHF);
|
|
}
|
|
#endif
|
|
|
|
if (!RecastBuildLayers(BuildContext, RasterContext))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
if (IsTileDebugActive())
|
|
{
|
|
if (TileDebugSettings.bHeightFieldLayers)
|
|
{
|
|
duDebugDrawHeightfieldLayers(&BuildContext.InternalDebugData, *RasterContext.LayerSet);
|
|
}
|
|
|
|
if (TileDebugSettings.bCompactHeightfieldRegions)
|
|
{
|
|
duDebugDrawCompactHeightfieldRegions(&BuildContext.InternalDebugData, *CompactHF);
|
|
}
|
|
|
|
if (TileDebugSettings.bCompactHeightfieldDistances)
|
|
{
|
|
duDebugDrawCompactHeightfieldDistance(&BuildContext.InternalDebugData, *CompactHF);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return RecastBuildTileCache(BuildContext, RasterContext);
|
|
}
|
|
|
|
struct FTileGenerationContext
|
|
{
|
|
FTileGenerationContext(dtTileCacheAlloc* MyAllocator) : Allocator(MyAllocator) {}
|
|
|
|
~FTileGenerationContext()
|
|
{
|
|
ResetIntermediateData();
|
|
}
|
|
|
|
void ResetIntermediateData()
|
|
{
|
|
if (Allocator)
|
|
{
|
|
dtFreeTileCacheLayer(Allocator, Layer);
|
|
Layer = nullptr;
|
|
dtFreeTileCacheDistanceField(Allocator, DistanceField);
|
|
DistanceField = nullptr;
|
|
dtFreeTileCacheContourSet(Allocator, ContourSet);
|
|
ContourSet = nullptr;
|
|
#if WITH_NAVMESH_CLUSTER_LINKS
|
|
dtFreeTileCacheClusterSet(Allocator, ClusterSet);
|
|
ClusterSet = nullptr;
|
|
#endif // WITH_NAVMESH_CLUSTER_LINKS
|
|
dtFreeTileCachePolyMesh(Allocator, PolyMesh);
|
|
PolyMesh = nullptr;
|
|
dtFreeTileCachePolyMeshDetail(Allocator, DetailMesh);
|
|
DetailMesh = nullptr;
|
|
// don't clear NavigationData here!
|
|
}
|
|
}
|
|
|
|
struct dtTileCacheAlloc* Allocator = nullptr;
|
|
struct dtTileCacheLayer* Layer = nullptr;
|
|
struct dtTileCacheDistanceField* DistanceField = nullptr;
|
|
struct dtTileCacheContourSet* ContourSet = nullptr;
|
|
#if WITH_NAVMESH_CLUSTER_LINKS
|
|
struct dtTileCacheClusterSet* ClusterSet = nullptr;
|
|
#endif //WITH_NAVMESH_CLUSTER_LINKS
|
|
struct dtTileCachePolyMesh* PolyMesh = nullptr;
|
|
struct dtTileCachePolyMeshDetail* DetailMesh = nullptr;
|
|
TArray<FNavMeshTileData> NavigationData;
|
|
};
|
|
|
|
dtStatus FRecastTileGenerator::BuildTileCacheLinks(FNavMeshBuildContext& BuildContext, dtTileCacheAlloc* alloc, const dtTileCacheLayer& layer,
|
|
const dtTileCacheContourSet& lcset, TArray<FGeneratedNavigationLink>& OutGeneratedLinks) const
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FRecastTileGenerator::BuildTileCacheLinks);
|
|
BuildContext.log(RC_LOG_PROGRESS, "Building Links:");
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
|
|
duDebugDraw* dd = nullptr;
|
|
int32 DebugEdge = -1;
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
if (IsTileDebugActive() && (TileDebugSettings.LinkGenerationDebugFlags != 0))
|
|
{
|
|
DebugEdge = TileDebugSettings.LinkGenerationSelectedEdge;
|
|
dd = &BuildContext.InternalDebugData;
|
|
}
|
|
const uint16 DebugFlags = TileDebugSettings.LinkGenerationDebugFlags;
|
|
#endif // RECAST_INTERNAL_DEBUG_DATA
|
|
|
|
dtAssert(alloc);
|
|
|
|
auto LogOnExit = [&]()
|
|
{
|
|
const double LinkBuildTime = FPlatformTime::Seconds()-StartTime;
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
BuildContext.InternalDebugData.BuildLinkTime += LinkBuildTime;
|
|
#endif
|
|
BuildContext.log(RC_LOG_PROGRESS, " BuildTileCacheLinks time: %0.3fms.", LinkBuildTime*1000 );
|
|
};
|
|
|
|
if (!SolidHF || !CompactHF)
|
|
{
|
|
LogOnExit();
|
|
return DT_FAILURE;
|
|
}
|
|
|
|
const dtReal* orig = layer.header->bmin;
|
|
|
|
dtLinkBuilderConfig linkBuilderConfig;
|
|
linkBuilderConfig.jumpDownConfig = TileConfig.JumpDownConfig;
|
|
if (linkBuilderConfig.jumpDownConfig.enabled)
|
|
{
|
|
linkBuilderConfig.jumpDownConfig.init();
|
|
}
|
|
|
|
linkBuilderConfig.jumpOverConfig = TileConfig.JumpOverConfig;
|
|
|
|
linkBuilderConfig.agentRadius = TileConfig.walkableRadius * TileConfig.cs;
|
|
linkBuilderConfig.agentHeight = TileConfig.walkableHeight * TileConfig.ch;
|
|
linkBuilderConfig.agentClimb = TileConfig.walkableClimb * TileConfig.ch;
|
|
linkBuilderConfig.cellSize = TileConfig.cs;
|
|
linkBuilderConfig.cellHeight = TileConfig.ch;
|
|
|
|
dtNavLinkBuilder linkBuilder;
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildLinks_FindEdges);
|
|
|
|
if (!linkBuilder.findEdges(BuildContext, TileConfig, linkBuilderConfig, lcset, orig, SolidHF, CompactHF))
|
|
{
|
|
LogOnExit();
|
|
return DT_FAILURE;
|
|
}
|
|
|
|
BuildContext.log(RC_LOG_PROGRESS, " Found %i edges.", linkBuilder.getEdgeCount());
|
|
}
|
|
|
|
if (linkBuilder.getEdgeCount() == 0)
|
|
{
|
|
LogOnExit();
|
|
return DT_SUCCESS;
|
|
}
|
|
|
|
if (DebugEdge == -1)
|
|
{
|
|
rcContext& context = BuildContext;
|
|
if (linkBuilderConfig.jumpDownConfig.enabled)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(RecastBuildLinks_JumpDown);
|
|
linkBuilder.buildForAllEdges(context, linkBuilderConfig, DT_LINK_ACTION_JUMP_DOWN);
|
|
}
|
|
|
|
if (linkBuilderConfig.jumpOverConfig.enabled)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(RecastBuildLinks_JumpOver);
|
|
linkBuilder.buildForAllEdges(context, linkBuilderConfig, DT_LINK_ACTION_JUMP_OVER);
|
|
}
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
duDebugDrawNavLinkBuilder(dd, linkBuilder, DebugFlags, nullptr);
|
|
}
|
|
else
|
|
{
|
|
if (linkBuilderConfig.jumpDownConfig.enabled)
|
|
{
|
|
dtNavLinkBuilder::EdgeSampler sampler1;
|
|
linkBuilder.debugBuildEdge(linkBuilderConfig, DT_LINK_ACTION_JUMP_DOWN, DebugEdge, sampler1);
|
|
duDebugDrawNavLinkBuilder(dd, linkBuilder, DebugFlags, &sampler1);
|
|
}
|
|
|
|
if (linkBuilderConfig.jumpOverConfig.enabled)
|
|
{
|
|
dtNavLinkBuilder::EdgeSampler sampler2;
|
|
linkBuilder.debugBuildEdge(linkBuilderConfig, DT_LINK_ACTION_JUMP_OVER, DebugEdge, sampler2);
|
|
duDebugDrawNavLinkBuilder(dd, linkBuilder, DebugFlags, &sampler2);
|
|
}
|
|
#endif // RECAST_INTERNAL_DEBUG_DATA
|
|
}
|
|
|
|
if (linkBuilderConfig.jumpDownConfig.enabled)
|
|
{
|
|
auto AddLinkSharedLambda = [&OutGeneratedLinks](const dtNavLinkBuilderJumpDownConfig& config) -> FGeneratedNavigationLink&
|
|
{
|
|
FGeneratedNavigationLink& NewLink = OutGeneratedLinks.Emplace_GetRef();
|
|
NewLink.bIsGenerated = true;
|
|
NewLink.NavLinkId = FNavLinkId(config.linkUserId);
|
|
return NewLink;
|
|
};
|
|
|
|
// Used when we're provided with identical up and down areas. In this situation, a single bidirectional link is sufficient for our needs
|
|
auto AddBidirectionalLinkLambda = [&AddLinkSharedLambda](const dtNavLinkBuilderJumpDownConfig& config, const dtReal* posA, const dtReal* posB)
|
|
{
|
|
FGeneratedNavigationLink& NewLink = AddLinkSharedLambda(config);
|
|
NewLink.generatedLinkArea = config.downDirArea;
|
|
NewLink.generatedLinkPolyFlag = config.downDirPolyFlag;
|
|
NewLink.Left = Recast2UnrealPoint(posA);
|
|
NewLink.Right = Recast2UnrealPoint(posB);
|
|
};
|
|
|
|
// Used when we're provided with different up and down areas. In this situation, we need to create two different one-way link representations in order to meet our needs
|
|
auto AddDirectionalLinksLambda = [&AddLinkSharedLambda](const dtNavLinkBuilderJumpDownConfig& config, const dtReal* posA, const dtReal* posB)
|
|
{
|
|
const FVector UnrealPosA = Recast2UnrealPoint(posA);
|
|
const FVector UnrealPosB = Recast2UnrealPoint(posB);
|
|
|
|
const bool bABDown = UnrealPosA.Z > UnrealPosB.Z;
|
|
|
|
const FVector DownLinkLeft = bABDown ? UnrealPosA : UnrealPosB;
|
|
const FVector DownLinkRight = bABDown ? UnrealPosB : UnrealPosA;
|
|
|
|
|
|
if (config.downDirArea != RECAST_NULL_AREA)
|
|
{
|
|
FGeneratedNavigationLink& DownLink = AddLinkSharedLambda(config);
|
|
DownLink.generatedLinkArea = config.downDirArea;
|
|
DownLink.generatedLinkPolyFlag = config.downDirPolyFlag;
|
|
DownLink.Left = DownLinkLeft;
|
|
DownLink.Right = DownLinkRight;
|
|
DownLink.Direction = ENavLinkDirection::LeftToRight;
|
|
}
|
|
|
|
if (config.upDirArea != RECAST_NULL_AREA)
|
|
{
|
|
// Note that in order for this link to be recognized by the navigation system, it needs to have unique left/right points, which is why the left and right points are flipped and we're marking this link as left-to-right
|
|
FGeneratedNavigationLink& UpLink = AddLinkSharedLambda(config);
|
|
UpLink.generatedLinkArea = config.upDirArea;
|
|
UpLink.generatedLinkPolyFlag = config.upDirPolyFlag;
|
|
UpLink.Left = DownLinkRight;
|
|
UpLink.Right = DownLinkLeft;
|
|
UpLink.Direction = ENavLinkDirection::LeftToRight;
|
|
}
|
|
};
|
|
|
|
const bool bBidirectionalJumpDownLinks = linkBuilderConfig.jumpDownConfig.downDirArea == linkBuilderConfig.jumpDownConfig.upDirArea;
|
|
TFunctionRef<void(const dtNavLinkBuilderJumpDownConfig&, const dtReal*, const dtReal*)> AddLinkLambda = bBidirectionalJumpDownLinks ? TFunctionRef<void(const dtNavLinkBuilderJumpDownConfig&, const dtReal*, const dtReal*)>(AddBidirectionalLinkLambda) : TFunctionRef<void(const dtNavLinkBuilderJumpDownConfig&, const dtReal*, const dtReal*)>(AddDirectionalLinksLambda);
|
|
|
|
// Make FGeneratedNavigationLinks
|
|
for (const dtNavLinkBuilder::JumpLink& link : linkBuilder.m_links)
|
|
{
|
|
if (link.flags == dtNavLinkBuilder::FILTERED)
|
|
continue;
|
|
|
|
// Only "JumpDownConfig are expected for now
|
|
if (link.action != DT_LINK_ACTION_JUMP_DOWN)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (TileConfig.JumpDownConfig.linkBuilderFlags & DT_NAVLINK_CREATE_CENTER_POINT_LINK)
|
|
{
|
|
// Make a link using the center of the range.
|
|
dtReal midA[3];
|
|
dtVlerp(midA, &link.spine0[0], &link.spine1[0], 0.5);
|
|
dtReal midB[3];
|
|
dtVlerp(midB, &link.spine0[(link.nspine-1)*3], &link.spine1[(link.nspine-1)*3], 0.5);
|
|
// Since trajectory validation starts at agentClimb height to ignore small bumps, remove the offset for the actual link height.
|
|
midA[1] -= linkBuilderConfig.agentClimb;
|
|
midB[1] -= linkBuilderConfig.agentClimb;
|
|
AddLinkLambda(linkBuilderConfig.jumpDownConfig, midA, midB);
|
|
}
|
|
|
|
if (TileConfig.JumpDownConfig.linkBuilderFlags & DT_NAVLINK_CREATE_EXTREMITY_LINKS)
|
|
{
|
|
dtReal posA[3];
|
|
dtReal posB[3];
|
|
dtVcopy(posA, &link.spine0[0]);
|
|
dtVcopy(posB, &link.spine0[(link.nspine-1)*3]);
|
|
posA[1] -= linkBuilderConfig.agentClimb;
|
|
posB[1] -= linkBuilderConfig.agentClimb;
|
|
AddLinkLambda(linkBuilderConfig.jumpDownConfig, posA, posB);
|
|
|
|
dtVcopy(posA, &link.spine1[0]);
|
|
dtVcopy(posB, &link.spine1[(link.nspine-1)*3]);
|
|
posA[1] -= linkBuilderConfig.agentClimb;
|
|
posB[1] -= linkBuilderConfig.agentClimb;
|
|
AddLinkLambda(linkBuilderConfig.jumpDownConfig, posA, posB);
|
|
}
|
|
}
|
|
}
|
|
|
|
LogOnExit();
|
|
return DT_SUCCESS;
|
|
}
|
|
|
|
bool FRecastTileGenerator::GenerateNavigationDataLayer(FNavMeshBuildContext& BuildContext, FTileCacheCompressor& TileCompressor,
|
|
FTileCacheAllocator& GenNavAllocator, FTileGenerationContext& GenerationContext, const dtLinkBuilderData& InLinkBuilderData, int32 LayerIdx)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_GenerateNavigationDataLayer)
|
|
|
|
dtStatus status = DT_SUCCESS;
|
|
|
|
FNavMeshTileData& CompressedData = CompressedLayers[LayerIdx];
|
|
GenerationContext.ResetIntermediateData();
|
|
|
|
// Decompress tile layer data.
|
|
status = dtDecompressTileCacheLayer(&GenNavAllocator, &TileCompressor, (const unsigned char*)CompressedData.GetData(), CompressedData.DataSize, &GenerationContext.Layer);
|
|
if (dtStatusFailed(status))
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationDataLayer: 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(&GenNavAllocator, TileConfig.minRegionArea, TileConfig.mergeRegionArea, *GenerationContext.Layer);
|
|
}
|
|
else if (TileConfig.TileCachePartitionType == RC_REGION_WATERSHED)
|
|
{
|
|
GenerationContext.DistanceField = dtAllocTileCacheDistanceField(&GenNavAllocator);
|
|
if (GenerationContext.DistanceField == nullptr)
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationDataLayer: Out of memory 'DistanceField'.");
|
|
return false;
|
|
}
|
|
|
|
status = dtBuildTileCacheDistanceField(&GenNavAllocator, *GenerationContext.Layer, *GenerationContext.DistanceField);
|
|
if (dtStatusFailed(status))
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationDataLayer: Failed to build distance field.");
|
|
return false;
|
|
}
|
|
|
|
status = dtBuildTileCacheRegions(&GenNavAllocator, TileConfig.minRegionArea, TileConfig.mergeRegionArea, *GenerationContext.Layer, *GenerationContext.DistanceField);
|
|
}
|
|
else
|
|
{
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
if (IsTileDebugActive() && TileDebugSettings.bTileCacheLayerAreas)
|
|
{
|
|
duDebugDrawTileCacheLayerAreas(&BuildContext.InternalDebugData, *GenerationContext.Layer, TileConfig.cs, TileConfig.ch);
|
|
}
|
|
#endif
|
|
|
|
status = dtBuildTileCacheRegionsChunky(&GenNavAllocator, TileConfig.minRegionArea, TileConfig.mergeRegionArea, *GenerationContext.Layer, TileConfig.TileCacheChunkSize);
|
|
}
|
|
|
|
if (dtStatusFailed(status))
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationDataLayer: Failed to build regions.");
|
|
return false;
|
|
}
|
|
|
|
// skip empty layer
|
|
if (GenerationContext.Layer->regCount <= 0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
if (IsTileDebugActive() && TileDebugSettings.bTileCacheLayerRegions)
|
|
{
|
|
duDebugDrawTileCacheLayerRegions(&BuildContext.InternalDebugData, *GenerationContext.Layer, TileConfig.cs, TileConfig.ch);
|
|
}
|
|
#endif
|
|
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildContours);
|
|
// Build contour set
|
|
GenerationContext.ContourSet = dtAllocTileCacheContourSet(&GenNavAllocator);
|
|
if (GenerationContext.ContourSet == nullptr)
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationDataLayer: Out of memory 'ContourSet'.");
|
|
return false;
|
|
}
|
|
|
|
bool bSkipContourSimplification = false;
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
bSkipContourSimplification = IsTileDebugActive() && TileDebugSettings.bSkipContourSimplification;
|
|
#endif
|
|
|
|
#if WITH_NAVMESH_CLUSTER_LINKS
|
|
GenerationContext.ClusterSet = dtAllocTileCacheClusterSet(&GenNavAllocator);
|
|
if (GenerationContext.ClusterSet == nullptr)
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationDataLayer: Out of memory 'ClusterSet'.");
|
|
return false;
|
|
}
|
|
|
|
status = dtBuildTileCacheContours(&GenNavAllocator, *GenerationContext.Layer,
|
|
TileConfig.walkableClimb, TileConfig.maxSimplificationError, TileConfig.simplificationElevationRatio,
|
|
TileConfig.cs, TileConfig.ch,*GenerationContext.ContourSet, *GenerationContext.ClusterSet, bSkipContourSimplification);
|
|
#else
|
|
status = dtBuildTileCacheContours(&GenNavAllocator, *GenerationContext.Layer,
|
|
TileConfig.walkableClimb, TileConfig.maxSimplificationError, TileConfig.simplificationElevationRatio,
|
|
TileConfig.cs, TileConfig.ch, *GenerationContext.ContourSet, bSkipContourSimplification);
|
|
#endif //WITH_NAVMESH_CLUSTER_LINKS
|
|
|
|
if (dtStatusFailed(status))
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationDataLayer: 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)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
if (IsTileDebugActive() && TileDebugSettings.bTileCacheContours)
|
|
{
|
|
duDebugDrawTileCacheContours(&BuildContext.InternalDebugData, *GenerationContext.ContourSet, LayerIdx, GenerationContext.Layer->header->bmin, TileConfig.cs, TileConfig.ch);
|
|
}
|
|
#endif
|
|
|
|
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildPolyMesh);
|
|
// Build poly mesh
|
|
GenerationContext.PolyMesh = dtAllocTileCachePolyMesh(&GenNavAllocator);
|
|
if (GenerationContext.PolyMesh == nullptr)
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: Out of memory 'PolyMesh'.");
|
|
return false;
|
|
}
|
|
|
|
status = dtBuildTileCachePolyMesh(&GenNavAllocator, &BuildContext, *GenerationContext.ContourSet, *GenerationContext.PolyMesh, TileConfig.walkableClimb);
|
|
if (dtStatusFailed(status))
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: Failed to generate poly mesh.");
|
|
return false;
|
|
}
|
|
|
|
#if WITH_NAVMESH_CLUSTER_LINKS
|
|
status = dtBuildTileCacheClusters(&GenNavAllocator, *GenerationContext.ClusterSet, *GenerationContext.PolyMesh);
|
|
if (dtStatusFailed(status))
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: Failed to update cluster set.");
|
|
return false;
|
|
}
|
|
#endif // WITH_NAVMESH_CLUSTER_LINKS
|
|
}
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
if (IsTileDebugActive() && TileDebugSettings.bTileCachePolyMesh)
|
|
{
|
|
duDebugDrawTileCachePolyMesh(&BuildContext.InternalDebugData, *GenerationContext.PolyMesh, GenerationContext.Layer->header->bmin, TileConfig.cs, TileConfig.ch);
|
|
}
|
|
#endif
|
|
|
|
// Build detail mesh
|
|
if (TileConfig.bGenerateDetailedMesh)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildPolyDetail);
|
|
|
|
// Build detail mesh.
|
|
GenerationContext.DetailMesh = dtAllocTileCachePolyMeshDetail(&GenNavAllocator);
|
|
if (GenerationContext.DetailMesh == nullptr)
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationData: Out of memory 'DetailMesh'.");
|
|
return false;
|
|
}
|
|
|
|
// Fills GenerationContext.PolyMesh with connectivity information.
|
|
status = dtBuildTileCachePolyMeshDetail(&GenNavAllocator, 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;
|
|
}
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
if (IsTileDebugActive() && TileDebugSettings.bTileCacheDetailMesh)
|
|
{
|
|
duDebugDrawTileCacheDetailMesh(&BuildContext.InternalDebugData, *GenerationContext.DetailMesh);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// Build Links
|
|
TArray<FGeneratedNavigationLink> GeneratedLinks;
|
|
if (InLinkBuilderData.generatingLinks)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildLinks);
|
|
|
|
if (SolidHF && CompactHF)
|
|
{
|
|
status = BuildTileCacheLinks(BuildContext, &GenNavAllocator, *GenerationContext.Layer, *GenerationContext.ContourSet, GeneratedLinks);
|
|
}
|
|
|
|
if (dtStatusFailed(status))
|
|
{
|
|
BuildContext.log(RC_LOG_ERROR, "GenerateNavigationDataLayer: Failed to generate links (0x%08X).", status);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
unsigned char* NavData = nullptr;
|
|
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;
|
|
}
|
|
|
|
const float DefaultSnapHeight = static_cast<float>(TileConfig.walkableClimb) * static_cast<float>(TileConfig.ch);
|
|
|
|
// if we didn't fail already then it's high time we created data for off-mesh links
|
|
FOffMeshData OffMeshData;
|
|
if (!OffmeshLinks.IsEmpty() || !GeneratedLinks.IsEmpty())
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastGatherOffMeshData);
|
|
|
|
OffMeshData.Reserve(OffmeshLinks.Num() + GeneratedLinks.Num());
|
|
OffMeshData.AreaClassToIdMap = &AdditionalCachedData.AreaClassToIdMap;
|
|
OffMeshData.FlagsPerArea = AdditionalCachedData.FlagsPerOffMeshLinkArea;
|
|
const FSimpleLinkNavModifier* LinkModifier = OffmeshLinks.GetData();
|
|
|
|
for (int32 LinkModifierIndex = 0; LinkModifierIndex < OffmeshLinks.Num(); ++LinkModifierIndex, ++LinkModifier)
|
|
{
|
|
OffMeshData.AddLinks(LinkModifier->Links, LinkModifier->LocalToWorld, TileConfig.AgentIndex, DefaultSnapHeight);
|
|
#if WITH_NAVMESH_SEGMENT_LINKS
|
|
OffMeshData.AddSegmentLinks(LinkModifier->SegmentLinks, LinkModifier->LocalToWorld, TileConfig.AgentIndex, DefaultSnapHeight);
|
|
#endif // WITH_NAVMESH_SEGMENT_LINKS
|
|
}
|
|
|
|
OffMeshData.AddLinks(GeneratedLinks, FTransform::Identity, TileConfig.AgentIndex, DefaultSnapHeight);
|
|
}
|
|
|
|
// 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 = LayerIdx;
|
|
rcVcopy(Params.bmin, GenerationContext.Layer->header->bmin);
|
|
rcVcopy(Params.bmax, GenerationContext.Layer->header->bmax);
|
|
Params.cs = TileConfig.cs;
|
|
Params.ch = TileConfig.ch;
|
|
Params.tileResolutionLevel = (unsigned char)TileConfig.TileResolution;
|
|
Params.buildBvTree = TileConfig.bGenerateBVTree;
|
|
|
|
#if WITH_NAVMESH_CLUSTER_LINKS
|
|
Params.clusterCount = IntCastChecked<unsigned short>(GenerationContext.ClusterSet->nclusters);
|
|
Params.polyClusters = GenerationContext.ClusterSet->polyMap;
|
|
#endif // WITH_NAVMESH_CLUSTER_LINKS
|
|
|
|
{
|
|
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, LayerIdx, CompressedData.LayerBBox));
|
|
|
|
const float ModkB = 1.0f / 1024.0f;
|
|
BuildContext.log(RC_LOG_PROGRESS, ">> Layer[%d] = Verts(%d) Polys(%d) Memory(%.2fkB) Cache(%.2fkB)",
|
|
LayerIdx, GenerationContext.PolyMesh->nverts, GenerationContext.PolyMesh->npolys,
|
|
static_cast<float>(GenerationContext.NavigationData.Last().DataSize) * ModkB,
|
|
static_cast<float>(CompressedLayers[LayerIdx].DataSize) * ModkB);
|
|
|
|
return true;
|
|
}
|
|
|
|
ETimeSliceWorkResult FRecastTileGenerator::GenerateNavigationDataTimeSliced(FNavMeshBuildContext& BuildContext, const dtLinkBuilderData& InLinkBuilderData)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildNavigation);
|
|
|
|
check(TimeSliceManager);
|
|
ETimeSliceWorkResult WorkResult = ETimeSliceWorkResult::Succeeded;
|
|
|
|
switch (GenerateNavDataTimeSlicedState)
|
|
{
|
|
case EGenerateNavDataTimeSlicedState::Invalid:
|
|
{
|
|
ensureMsgf(false, TEXT("Invalid EGenerateNavDataTimeSlicedState, has this function been called when its already finished processing?"));
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
|
|
case EGenerateNavDataTimeSlicedState::Init:
|
|
{
|
|
GenNavDataTimeSlicedAllocator = MakeUnique<FTileCacheAllocator>();
|
|
GenNavDataTimeSlicedGenerationContext = MakeUnique<FTileGenerationContext>(GenNavDataTimeSlicedAllocator.Get());
|
|
GenNavDataTimeSlicedGenerationContext->NavigationData.Reserve(CompressedLayers.Num());
|
|
GenerateNavDataTimeSlicedState = EGenerateNavDataTimeSlicedState::GenerateLayers;
|
|
}//fall through to next state
|
|
case EGenerateNavDataTimeSlicedState::GenerateLayers:
|
|
{
|
|
for (; GenNavDataLayerTimeSlicedIdx < CompressedLayers.Num() && GenNavDataLayerTimeSlicedIdx < DirtyLayers.Num(); GenNavDataLayerTimeSlicedIdx++)
|
|
{
|
|
if (DirtyLayers[GenNavDataLayerTimeSlicedIdx] == false || !CompressedLayers[GenNavDataLayerTimeSlicedIdx].IsValid())
|
|
{
|
|
// skip layers not marked for rebuild
|
|
continue;
|
|
}
|
|
|
|
if (TimeSliceManager->GetTimeSlicer().IsTimeSliceFinishedCached())
|
|
{
|
|
WorkResult = ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
break;
|
|
}
|
|
|
|
FTileCacheCompressor TileCompressor;
|
|
const bool bGenDataLayer = GenerateNavigationDataLayer(BuildContext, TileCompressor, *GenNavDataTimeSlicedAllocator,
|
|
*GenNavDataTimeSlicedGenerationContext, InLinkBuilderData, GenNavDataLayerTimeSlicedIdx);
|
|
|
|
MARK_TIMESLICE_SECTION_DEBUG(TimeSliceManager->GetTimeSlicer(), GenerateLayers);
|
|
|
|
//carry on iterating but don't do any more work if the time slice is finished (as we may not need to in which case we can avoid calling this function again)
|
|
TimeSliceManager->GetTimeSlicer().TestTimeSliceFinished();
|
|
|
|
if (!bGenDataLayer)
|
|
{
|
|
WorkResult = ETimeSliceWorkResult::Failed;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (WorkResult != ETimeSliceWorkResult::CallAgainNextTimeSlice)
|
|
{
|
|
GenNavDataLayerTimeSlicedIdx = 0;
|
|
GenerateNavDataTimeSlicedState = EGenerateNavDataTimeSlicedState::Invalid;
|
|
|
|
if (WorkResult == ETimeSliceWorkResult::Succeeded)
|
|
{
|
|
NavigationData = MoveTemp(GenNavDataTimeSlicedGenerationContext->NavigationData);
|
|
}
|
|
GenNavDataTimeSlicedGenerationContext->ResetIntermediateData();
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
{
|
|
ensureMsgf(false, TEXT("unhandled EGenerateNavDataTimeSlicedState"));
|
|
return ETimeSliceWorkResult::Failed;
|
|
}
|
|
}
|
|
|
|
return WorkResult;
|
|
}
|
|
|
|
bool FRecastTileGenerator::GenerateNavigationData(FNavMeshBuildContext& BuildContext, const dtLinkBuilderData& InLinkBuilderData)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastBuildNavigation);
|
|
|
|
FTileCacheAllocator GenNavAllocator;
|
|
FTileGenerationContext GenerationContext(&GenNavAllocator);
|
|
GenerationContext.NavigationData.Reserve(CompressedLayers.Num());
|
|
FTileCacheCompressor TileCompressor;
|
|
bool bGenDataLayer = true;
|
|
dtStatus status = DT_SUCCESS;
|
|
|
|
for (int32 LayerIdx = 0; LayerIdx < CompressedLayers.Num() && LayerIdx < DirtyLayers.Num(); LayerIdx++)
|
|
{
|
|
if (DirtyLayers[LayerIdx] == false || !CompressedLayers[LayerIdx].IsValid())
|
|
{
|
|
// skip layers not marked for rebuild
|
|
continue;
|
|
}
|
|
|
|
bGenDataLayer = GenerateNavigationDataLayer(BuildContext, TileCompressor, GenNavAllocator, GenerationContext, InLinkBuilderData, LayerIdx);
|
|
|
|
if (!bGenDataLayer)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bGenDataLayer)
|
|
{
|
|
NavigationData = MoveTemp(GenerationContext.NavigationData);
|
|
}
|
|
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(FRecastTileGenerator_GenerateNavigationData_Free);
|
|
GenerationContext.ResetIntermediateData();
|
|
}
|
|
|
|
return bGenDataLayer;
|
|
}
|
|
|
|
void FRecastTileGenerator::ComputeRasterizationMasks(FNavMeshBuildContext& BuildContext, FTileRasterizationContext& RasterContext)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastComputeRasterizationMasks);
|
|
|
|
UE_LOG(LogNavigationDataBuild, VeryVerbose, TEXT(" %s"), ANSI_TO_TCHAR(__FUNCTION__));
|
|
|
|
for (const FRecastAreaNavModifierElement& ModifierElement : Modifiers)
|
|
{
|
|
if (ModifierElement.bMaskFillCollisionUnderneathForNavmesh)
|
|
{
|
|
if (SolidHF == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const int32 Mask = ~RC_PROJECT_TO_BOTTOM;
|
|
for (const FAreaNavModifier& ModifierArea : ModifierElement.Areas)
|
|
{
|
|
for (const FTransform& LocalToWorld : ModifierElement.PerInstanceTransform)
|
|
{
|
|
MarkRasterizationMask(&BuildContext, SolidHF, ModifierArea, LocalToWorld, Mask, RasterContext.RasterizationMasks);
|
|
}
|
|
|
|
if (ModifierElement.PerInstanceTransform.Num() == 0)
|
|
{
|
|
MarkRasterizationMask(&BuildContext, SolidHF, ModifierArea, FTransform::Identity, Mask, RasterContext.RasterizationMasks);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FRecastTileGenerator::MarkDynamicAreas(dtTileCacheLayer& Layer)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastMarkAreas);
|
|
|
|
if (Modifiers.Num())
|
|
{
|
|
if (AdditionalCachedData.bUseSortFunction && Modifiers.Num() > 1)
|
|
{
|
|
FGCScopeGuard GCScopeGuard;
|
|
if (const ARecastNavMesh* ActorOwner = AdditionalCachedData.ActorOwner.Get())
|
|
{
|
|
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 MarkBoxMask(const FVector::FReal* pos, const FVector::FReal* extent, const int mask, rcHeightfield& hf, int* rasterizationMasks)
|
|
{
|
|
FVector::FReal* orig = hf.bmin;
|
|
FVector::FReal bmin[3], bmax[3];
|
|
rcVsub(bmin, pos, extent);
|
|
rcVadd(bmax, pos, extent);
|
|
|
|
const int w = hf.width;
|
|
const int h = hf.height;
|
|
const FVector::FReal ics = 1.0f/hf.cs;
|
|
|
|
int minx = (int)FMath::Floor((bmin[0]-orig[0])*ics);
|
|
int minz = (int)FMath::Floor((bmin[2]-orig[2])*ics);
|
|
int maxx = (int)FMath::Floor((bmax[0]-orig[0])*ics);
|
|
int maxz = (int)FMath::Floor((bmax[2]-orig[2])*ics);
|
|
|
|
if (maxx < 0) return;
|
|
if (minx >= w) return;
|
|
if (maxz < 0) return;
|
|
if (minz >= h) return;
|
|
|
|
if (minx < 0) minx = 0;
|
|
if (maxx >= w) maxx = w-1;
|
|
if (minz < 0) minz = 0;
|
|
if (maxz >= h) maxz = h-1;
|
|
|
|
for (int z = minz; z <= maxz; ++z)
|
|
{
|
|
for (int x = minx; x <= maxx; ++x)
|
|
{
|
|
rasterizationMasks[x+z*w] &= mask;
|
|
}
|
|
}
|
|
}
|
|
|
|
int PointInPoly(const FVector::FReal* verts, int nv, const FVector::FReal* p)
|
|
{
|
|
int i, j, c = 0;
|
|
for (i = 0, j = nv-1; i < nv; j = i++)
|
|
{
|
|
const FVector::FReal* vi = &verts[i*3];
|
|
const FVector::FReal* vj = &verts[j*3];
|
|
if (((vi[2] > p[2]) != (vj[2] > p[2])) &&
|
|
(p[0] < (vj[0]-vi[0]) * (p[2]-vi[2]) / (vj[2]-vi[2]) + vi[0]))
|
|
c = !c;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
// Similar to rcMarkConvexPolyArea
|
|
void MarkConvexMask(const int mask, const FVector::FReal* verts, const int nv, rcHeightfield& hf, int* rasterizationMasks)
|
|
{
|
|
FVector::FReal* orig = hf.bmin;
|
|
FVector::FReal bmin[3], bmax[3];
|
|
rcVcopy(bmin, verts);
|
|
rcVcopy(bmax, verts);
|
|
for (int i = 1; i < nv; ++i)
|
|
{
|
|
rcVmin(bmin, &verts[i*3]);
|
|
rcVmax(bmax, &verts[i*3]);
|
|
}
|
|
|
|
const int w = hf.width;
|
|
const int h = hf.height;
|
|
const FVector::FReal ics = 1.0f/hf.cs;
|
|
|
|
int minx = (int)((bmin[0]-orig[0])*ics);
|
|
int minz = (int)((bmin[2]-orig[2])*ics);
|
|
int maxx = (int)((bmax[0]-orig[0])*ics);
|
|
int maxz = (int)((bmax[2]-orig[2])*ics);
|
|
|
|
if (maxx < 0) return;
|
|
if (minx >= w) return;
|
|
if (maxz < 0) return;
|
|
if (minz >= h) return;
|
|
|
|
if (minx < 0) minx = 0;
|
|
if (maxx >= w) maxx = w-1;
|
|
if (minz < 0) minz = 0;
|
|
if (maxz >= h) maxz = h-1;
|
|
|
|
for (int z = minz; z <= maxz; ++z)
|
|
{
|
|
for (int x = minx; x <= maxx; ++x)
|
|
{
|
|
FVector::FReal p[3];
|
|
p[0] = orig[0] + ((FVector::FReal)x+0.5f)*hf.cs;
|
|
p[1] = 0.0f;
|
|
p[2] = orig[2] + ((FVector::FReal)z+0.5f)*hf.cs;
|
|
if (PointInPoly(verts, nv, p))
|
|
{
|
|
rasterizationMasks[x+z*w] &= mask;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FRecastTileGenerator::MarkRasterizationMask(rcContext* /*BuildContext*/, rcHeightfield* InSolidHF,
|
|
const FAreaNavModifier& Modifier, const FTransform& LocalToWorld, const int32 Mask, TInlineMaskArray& OutMaskArray)
|
|
{
|
|
FBox ModifierBounds = Modifier.GetBounds().TransformBy(LocalToWorld);
|
|
if (!ModifierBounds.Intersect(TileBB))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Init on first use
|
|
if (OutMaskArray.Num() == 0)
|
|
{
|
|
InitRasterizationMaskArray(InSolidHF, OutMaskArray);
|
|
}
|
|
|
|
switch (Modifier.GetShapeType())
|
|
{
|
|
case ENavigationShapeType::Box:
|
|
{
|
|
FBoxNavAreaData BoxData;
|
|
Modifier.GetBox(BoxData);
|
|
FBox WorldBox = FBox::BuildAABB(BoxData.Origin, BoxData.Extent).TransformBy(LocalToWorld);
|
|
FBox RecastBox = Unreal2RecastBox(WorldBox);
|
|
FVector RecastPos;
|
|
FVector RecastExtent;
|
|
RecastBox.GetCenterAndExtents(RecastPos, RecastExtent);
|
|
check(OutMaskArray.Num() == InSolidHF->width*InSolidHF->height);
|
|
MarkBoxMask(&(RecastPos.X), &(RecastExtent.X), Mask, *InSolidHF, OutMaskArray.GetData());
|
|
}
|
|
break;
|
|
|
|
case ENavigationShapeType::Convex:
|
|
{
|
|
FConvexNavAreaData ConvexData;
|
|
Modifier.GetConvex(ConvexData);
|
|
TArray<FVector> ConvexVerts;
|
|
const FVector::FReal Expand = 0.f;
|
|
|
|
const TArray<FVector> Points = UE::LWC::ConvertArrayType<FVector>(ConvexData.Points);
|
|
GrowConvexHull(Expand, Points, ConvexVerts);
|
|
|
|
if (ConvexVerts.Num())
|
|
{
|
|
TArray<FVector::FReal> ConvexCoords;
|
|
ConvexCoords.AddZeroed(ConvexVerts.Num() * 3);
|
|
FVector::FReal* 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++;
|
|
}
|
|
|
|
MarkConvexMask(Mask, ConvexCoords.GetData(), ConvexVerts.Num(), *InSolidHF, OutMaskArray.GetData());
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FRecastTileGenerator::MarkDynamicArea(const FAreaNavModifier& Modifier, const FTransform& LocalToWorld, dtTileCacheLayer& Layer, const int32 AreaID, const int32* ReplaceIDPtr)
|
|
{
|
|
const float ExpandBy = TileConfig.AgentRadius;
|
|
|
|
// If requested, expand by 1 cell height
|
|
const bool bExpandTop = TileConfig.bUseExtraTopCellWhenMarkingAreas || Modifier.ShouldExpandTopByCellHeight();
|
|
const FVector::FReal OffsetZMax = (bExpandTop ? TileConfig.ch : 0.f);
|
|
const FVector::FReal 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 FVector::FReal* 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 = UE_REAL_TO_FLOAT(CylinderData.Height * Scale3D.Z);
|
|
CylinderData.Radius = UE_REAL_TO_FLOAT(CylinderData.Radius * FMath::Max(Scale3D.X, Scale3D.Y));
|
|
CylinderData.Origin = LocalToWorld.TransformPosition(CylinderData.Origin);
|
|
|
|
const float OffsetZMid = UE_REAL_TO_FLOAT((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, IntCastChecked<unsigned char>(AreaID), IntCastChecked<unsigned char>(*ReplaceIDPtr));
|
|
}
|
|
else
|
|
{
|
|
dtMarkCylinderArea(Layer, LayerRecastOrig, TileConfig.cs, TileConfig.ch,
|
|
&(RecastPos.X), CylinderData.Radius, CylinderData.Height, IntCastChecked<unsigned char>(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;
|
|
|
|
const FBox RecastBox = Unreal2RecastBox(WorldBox);
|
|
FVector RecastPos, RecastExtent;
|
|
RecastBox.GetCenterAndExtents(RecastPos, RecastExtent);
|
|
|
|
if (ReplaceIDPtr)
|
|
{
|
|
dtReplaceBoxArea(Layer, LayerRecastOrig, TileConfig.cs, TileConfig.ch,
|
|
&(RecastPos.X), &(RecastExtent.X), IntCastChecked<unsigned char>(AreaID), IntCastChecked<unsigned char>(*ReplaceIDPtr));
|
|
}
|
|
else
|
|
{
|
|
dtMarkBoxArea(Layer, LayerRecastOrig, TileConfig.cs, TileConfig.ch,
|
|
&(RecastPos.X), &(RecastExtent.X), IntCastChecked<unsigned char>(AreaID));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case ENavigationShapeType::Convex:
|
|
case ENavigationShapeType::InstancedConvex:
|
|
{
|
|
FConvexNavAreaData ConvexData;
|
|
if (Modifier.GetShapeType() == ENavigationShapeType::InstancedConvex)
|
|
{
|
|
Modifier.GetPerInstanceConvex(LocalToWorld, ConvexData);
|
|
}
|
|
else
|
|
{
|
|
Modifier.GetConvex(ConvexData);
|
|
}
|
|
|
|
TArray<FVector> ConvexVerts;
|
|
|
|
const TArray<FVector> Points = UE::LWC::ConvertArrayType<FVector>(ConvexData.Points);
|
|
GrowConvexHull(ExpandBy, Points, ConvexVerts);
|
|
ConvexData.MinZ -= OffsetZMin;
|
|
ConvexData.MaxZ += OffsetZMax;
|
|
|
|
if (ConvexVerts.Num())
|
|
{
|
|
TArray<FVector::FReal> ConvexCoords;
|
|
ConvexCoords.AddZeroed(ConvexVerts.Num() * 3);
|
|
|
|
FVector::FReal* 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, IntCastChecked<unsigned char>(AreaID), IntCastChecked<unsigned char>(*ReplaceIDPtr));
|
|
}
|
|
else
|
|
{
|
|
dtMarkConvexArea(Layer, LayerRecastOrig, TileConfig.cs, TileConfig.ch,
|
|
ConvexCoords.GetData(), ConvexVerts.Num(), ConvexData.MinZ, ConvexData.MaxZ, IntCastChecked<unsigned char>(AreaID));
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
uint32 FRecastTileGenerator::GetUsedMemCount() const
|
|
{
|
|
SIZE_T 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 IntCastChecked<uint32>(TotalMemory);
|
|
}
|
|
|
|
void FRecastTileGenerator::AddReferencedObjects(FReferenceCollector& Collector)
|
|
{
|
|
for (const TSharedRef<FNavigationRelevantData>& RelevantData : NavigationRelevantData)
|
|
{
|
|
// Local variable since 'AddReferencedObject' requires a non-const& parameter
|
|
TWeakObjectPtr<const UObject> WeakObject = RelevantData->SourceElement->GetWeakUObject();
|
|
if (WeakObject.IsValid())
|
|
{
|
|
Collector.AddReferencedObject(WeakObject);
|
|
}
|
|
}
|
|
}
|
|
|
|
FString FRecastTileGenerator::GetReferencerName() const
|
|
{
|
|
return TEXT("FRecastTileGenerator");
|
|
}
|
|
|
|
namespace UE::NavMesh::Private
|
|
{
|
|
void CheckTileIndicesInValidRange(const TNavStatArray<FBox>& NavigableAreas, const ARecastNavMesh& NavMesh)
|
|
{
|
|
auto CheckTileHelper = [&NavMesh](const FVector& Pos) {
|
|
// If there are no NavigableAreas then there won't be any indices for them so default true.
|
|
bool bIndiciesFitInInt32 = false;
|
|
|
|
ensure(NavMesh.CheckTileIndicesInValidRange(Pos, bIndiciesFitInInt32));
|
|
|
|
UE_CLOG(!bIndiciesFitInInt32, LogNavigation, Error, TEXT("Magnitude of Recast tile indicies are too large to fit in an int32 for NavigableAreas extent %s for %s"),*Pos.ToString(), *GetFullNameSafe(&NavMesh));
|
|
};
|
|
|
|
for (const FBox& AreaBounds : NavigableAreas)
|
|
{
|
|
CheckTileHelper(AreaBounds.Min);
|
|
CheckTileHelper(AreaBounds.Max);
|
|
}
|
|
}
|
|
|
|
int32 CalculateMaxTilesCount(const TNavStatArray<FBox>& NavigableAreas, FVector::FReal TileSizeInWorldUnits, FVector::FReal AvgLayersPerGridCell, const uint32 NavMeshVersion)
|
|
{
|
|
int64 GridCellsCount = 0;
|
|
for (int32 Index = 0; Index < NavigableAreas.Num(); ++Index)
|
|
{
|
|
const FBox& AreaBounds = NavigableAreas[Index];
|
|
if (NavMeshVersion >= NAVMESHVER_MAXTILES_COUNT_SKIP_INCLUSION)
|
|
{
|
|
bool bIsInsideAnotherArea = false;
|
|
for (int32 OtherIndex = 0; OtherIndex < NavigableAreas.Num(); ++OtherIndex)
|
|
{
|
|
if (Index == OtherIndex)
|
|
{
|
|
continue;
|
|
}
|
|
const FBox& OtherBox = NavigableAreas[OtherIndex];
|
|
if (OtherBox.IsInsideXY(AreaBounds))
|
|
{
|
|
// The current Area Bounds is fully contained in another Area, so we don't need to count it.
|
|
// This doesn't take into account partial overlaps
|
|
bIsInsideAnotherArea = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bIsInsideAnotherArea)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// TODO: need more precise calculation, currently we don't take into account that volumes can be overlapped
|
|
const FBox RCBox = Unreal2RecastBox(AreaBounds);
|
|
|
|
if (NavMeshVersion >= NAVMESHVER_MAXTILES_COUNT_CHANGE)
|
|
{
|
|
// Keep this as an integer division to avoid imprecision between platforms and targets (since MaxTilesCount is compared with stored data).
|
|
const int64 TileSizeUU = (int64)TileSizeInWorldUnits;
|
|
const int64 XSize = (FMath::CeilToInt(RCBox.GetSize().X) / TileSizeUU) + 1;
|
|
const int64 YSize = (FMath::CeilToInt(RCBox.GetSize().Z) / TileSizeUU) + 1;
|
|
GridCellsCount += (XSize*YSize);
|
|
}
|
|
else
|
|
{
|
|
// Support old navmesh versions
|
|
int64 XSize = FMath::CeilToInt(RCBox.GetSize().X/TileSizeInWorldUnits) + 1;
|
|
int64 YSize = FMath::CeilToInt(RCBox.GetSize().Z/TileSizeInWorldUnits) + 1;
|
|
GridCellsCount += (XSize*YSize);
|
|
}
|
|
}
|
|
|
|
return IntCastChecked<int32>(FMath::CeilToInt(static_cast<FVector::FReal>(GridCellsCount) * AvgLayersPerGridCell));
|
|
}
|
|
} // UE::NavMesh::Private
|
|
|
|
// 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)
|
|
{
|
|
INC_DWORD_STAT_BY(STAT_NavigationMemory, sizeof(*this));
|
|
}
|
|
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS // Needed for ActiveTiles
|
|
FRecastNavMeshGenerator::~FRecastNavMeshGenerator()
|
|
{
|
|
UE_CLOG(RunningDirtyTiles.Num() > 0, LogNavigation, Log, TEXT("Discarding %d build tasks"), RunningDirtyTiles.Num());
|
|
CancelBuild();
|
|
DEC_DWORD_STAT_BY( STAT_NavigationMemory, sizeof(*this) );
|
|
}
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
|
|
void FRecastNavMeshGenerator::SetupTileConfig(const ENavigationDataResolution TileResolution, FRecastBuildConfig& OutConfig) const
|
|
{
|
|
check(GetOwner());
|
|
ensure(GetConfig().cs == GetOwner()->GetCellSize(ENavigationDataResolution::Default));
|
|
const float CellSize = GetOwner()->GetCellSize(TileResolution);
|
|
const float CellHeight = GetOwner()->GetCellHeight(TileResolution);
|
|
const float AgentMaxStepHeight = GetOwner()->GetAgentMaxStepHeight(TileResolution);
|
|
|
|
// Update all settings that depends directly or indirectly of the CellSize
|
|
OutConfig.TileResolution = TileResolution;
|
|
OutConfig.cs = CellSize;
|
|
OutConfig.walkableRadius = FMath::CeilToInt(DestNavMesh->AgentRadius / CellSize);
|
|
OutConfig.maxStepFromWalkableSlope = OutConfig.cs * FMath::Tan(FMath::DegreesToRadians(OutConfig.walkableSlopeAngle));
|
|
|
|
UE::NavMesh::Private::ComputeConfigBorderSizes(IsGeneratingLinks(), OutConfig);
|
|
|
|
OutConfig.maxEdgeLen = (int32)(1200.0f / CellSize);
|
|
|
|
OutConfig.minRegionArea = (int32)rcSqr(DestNavMesh->MinRegionArea / CellSize);
|
|
OutConfig.mergeRegionArea = (int32)rcSqr(DestNavMesh->MergeRegionSize / CellSize);
|
|
|
|
OutConfig.tileSize = FMath::Max(FMath::TruncToInt(DestNavMesh->TileSizeUU / CellSize), 1);
|
|
UE_CLOG(OutConfig.tileSize == 1, LogNavigation, Error, TEXT("RecastNavMesh TileSize of 1 is highly discouraged. This occurence indicates an issue with RecastNavMesh\'s generation properties (specifically TileSizeUU: %f, CellSize: %f). Please ensure their correctness.")
|
|
, DestNavMesh->TileSizeUU, CellSize);
|
|
|
|
OutConfig.regionChunkSize = FMath::Max(1, OutConfig.tileSize / FMath::Max(1, DestNavMesh->LayerChunkSplits));
|
|
OutConfig.TileCacheChunkSize = FMath::Max(1, OutConfig.tileSize / FMath::Max(1, DestNavMesh->RegionChunkSplits));
|
|
|
|
// Update all settings that depends directly or indirectly of the CellHeight
|
|
OutConfig.ch = CellHeight;
|
|
OutConfig.walkableHeight = DestNavMesh->bMarkLowHeightAreas ? 1 : FMath::CeilToInt(DestNavMesh->AgentHeight / CellHeight);
|
|
OutConfig.walkableClimb = FMath::CeilToInt(AgentMaxStepHeight / CellHeight);
|
|
|
|
// Update all settings that depends directly or indirectly of AgentMaxStepHeight
|
|
OutConfig.AgentMaxClimb = AgentMaxStepHeight;
|
|
|
|
OutConfig.bIsTileSetupConfigCompleted = true;
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::ConfigureBuildProperties(FRecastBuildConfig& OutConfig)
|
|
{
|
|
// @TODO those variables should be tweakable per navmesh actor
|
|
const float CellSize = DestNavMesh->GetCellSize(ENavigationDataResolution::Default);
|
|
ensure(CellSize != 0.f);
|
|
const float CellHeight = DestNavMesh->GetCellHeight(ENavigationDataResolution::Default);
|
|
const float AgentHeight = DestNavMesh->AgentHeight;
|
|
const float AgentMaxSlope = DestNavMesh->AgentMaxSlope;
|
|
const float AgentMaxClimb = DestNavMesh->GetAgentMaxStepHeight(ENavigationDataResolution::Default);
|
|
const float AgentRadius = DestNavMesh->AgentRadius;
|
|
|
|
OutConfig.Reset();
|
|
|
|
OutConfig.cs = CellSize;
|
|
OutConfig.ch = CellHeight;
|
|
OutConfig.walkableSlopeAngle = AgentMaxSlope;
|
|
OutConfig.walkableHeight = FMath::CeilToInt(AgentHeight / CellHeight);
|
|
OutConfig.walkableClimb = FMath::CeilToInt(AgentMaxClimb / CellHeight);
|
|
OutConfig.walkableRadius = FMath::CeilToInt(AgentRadius / CellSize);
|
|
OutConfig.maxStepFromWalkableSlope = OutConfig.cs * FMath::Tan(FMath::DegreesToRadians(OutConfig.walkableSlopeAngle));
|
|
|
|
// For each navmesh resolutions, validate that AgentMaxStepHeight is high enough for the AgentMaxSlope angle
|
|
for (int32 Index = 0; Index < (uint8)ENavigationDataResolution::MAX; Index++)
|
|
{
|
|
const ENavigationDataResolution Resolution = (ENavigationDataResolution)Index;
|
|
|
|
const float MaxStepHeight = DestNavMesh->GetAgentMaxStepHeight(Resolution);
|
|
const float TempCellHeight = DestNavMesh->GetCellHeight(Resolution);
|
|
const int WalkableClimbVx = FMath::CeilToInt(MaxStepHeight / TempCellHeight);
|
|
|
|
// Compute the required climb to prevent direct neighbor filtering in rcFilterLedgeSpansImp (minh < -walkableClimb).
|
|
// See comment: "The current span is close to a ledge if the drop to any neighbour span is less than the walkableClimb."
|
|
const float RequiredClimb = DestNavMesh->GetCellSize(Resolution) * FMath::Tan(FMath::DegreesToRadians(AgentMaxSlope));
|
|
const int RequiredClimbVx = FMath::CeilToInt(RequiredClimb / TempCellHeight);
|
|
|
|
if (WalkableClimbVx < RequiredClimbVx)
|
|
{
|
|
// This is a log since we need to let the user decide which one of the parameters needs to be changed (if any).
|
|
UE_LOG(LogNavigationDataBuild, Log, TEXT("%s: AgentMaxStepHeight (%f) for resolution %s is not high enough in steep slopes (AgentMaxSlope is %f). "
|
|
"Use AgentMaxStepHeight bigger than %f or a smaller AgentMaxSlope to avoid undesirable navmesh holes in steep slopes. "
|
|
"This can also be avoided by using smaller CellSize and CellHeight."),
|
|
*GetNameSafe(DestNavMesh), MaxStepHeight,
|
|
*UEnum::GetDisplayValueAsText(Resolution).ToString(), AgentMaxSlope, static_cast<float>(RequiredClimbVx-1)*TempCellHeight);
|
|
}
|
|
}
|
|
|
|
if (IsGeneratingLinks())
|
|
{
|
|
// NavLink builder configuration
|
|
const FNavLinkGenerationJumpDownConfig& JumpDown = DestNavMesh->NavLinkJumpDownConfig;
|
|
JumpDown.CopyToDetourConfig(OutConfig.JumpDownConfig);
|
|
|
|
const float JumpDownSpillDistance = JumpDown.bEnabled ? JumpDown.JumpLength - JumpDown.JumpDistanceFromEdge : 0.f;
|
|
constexpr float JumpOverSpillDistance = 0.f; //JumpOver.bEnabled ? JumpOver.JumpDistanceFromGapCenter : 0.f; // @todo: jump over config is not exposed for now
|
|
OutConfig.LinkSpillDistance = FMath::Max(JumpDownSpillDistance, JumpOverSpillDistance);
|
|
}
|
|
|
|
// store original sizes
|
|
OutConfig.AgentHeight = AgentHeight;
|
|
OutConfig.AgentMaxClimb = AgentMaxClimb;
|
|
OutConfig.AgentRadius = AgentRadius;
|
|
|
|
UE::NavMesh::Private::ComputeConfigBorderSizes(DestNavMesh->bGenerateNavLinks, OutConfig);
|
|
|
|
OutConfig.maxEdgeLen = (int32)(1200.0f / CellSize);
|
|
|
|
// 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.simplificationElevationRatio = DestNavMesh->SimplificationElevationRatio;
|
|
OutConfig.bPerformVoxelFiltering = DestNavMesh->bPerformVoxelFiltering;
|
|
OutConfig.bMarkLowHeightAreas = DestNavMesh->bMarkLowHeightAreas;
|
|
OutConfig.bUseExtraTopCellWhenMarkingAreas = DestNavMesh->bUseExtraTopCellWhenMarkingAreas;
|
|
OutConfig.bFilterLowSpanSequences = DestNavMesh->bFilterLowSpanSequences;
|
|
OutConfig.bFilterLowSpanFromTileCache = DestNavMesh->bFilterLowSpanFromTileCache;
|
|
if (DestNavMesh->bMarkLowHeightAreas)
|
|
{
|
|
OutConfig.walkableHeight = 1;
|
|
}
|
|
|
|
OutConfig.bGenerateLinks = IsGeneratingLinks();
|
|
|
|
const UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
|
|
OutConfig.AgentIndex = NavSys ? NavSys->GetSupportedAgentIndex(DestNavMesh) : 0;
|
|
|
|
OutConfig.tileSize = FMath::Max(FMath::TruncToInt(DestNavMesh->TileSizeUU / CellSize), 1);
|
|
UE_CLOG(OutConfig.tileSize == 1, LogNavigation, Error, TEXT("RecastNavMesh TileSize of 1 is highly discouraged. This occurence indicates an issue with RecastNavMesh\'s generation properties (specifically TileSizeUU: %f, CellSize: %f). Please ensure their correctness.")
|
|
, DestNavMesh->TileSizeUU, CellSize);
|
|
|
|
OutConfig.regionChunkSize = FMath::Max(1, OutConfig.tileSize / FMath::Max(1, DestNavMesh->LayerChunkSplits));
|
|
OutConfig.TileCacheChunkSize = FMath::Max(1, OutConfig.tileSize / FMath::Max(1, DestNavMesh->RegionChunkSplits));
|
|
OutConfig.LedgeSlopeFilterMode = DestNavMesh->LedgeSlopeFilterMode;
|
|
OutConfig.regionPartitioning = DestNavMesh->LayerPartitioning;
|
|
OutConfig.TileCachePartitionType = DestNavMesh->RegionPartitioning;
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::Init()
|
|
{
|
|
check(DestNavMesh);
|
|
|
|
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
|
|
|
|
if (NavSys)
|
|
{
|
|
SyncTimeSlicedData.TimeSliceManager = &(NavSys->GetMutableNavRegenTimeSliceManager());
|
|
}
|
|
else
|
|
{
|
|
//no time slice manager no time sliced regen
|
|
SyncTimeSlicedData.bTimeSliceRegenActive = false;
|
|
}
|
|
|
|
ConfigureBuildProperties(Config);
|
|
|
|
if (UE::NavMesh::Private::bUseTightBoundExpansion)
|
|
{
|
|
// Recast axis are inverted so growth direction are inverted here (using the positive side border for the low box growth and vice versa).
|
|
const rcReal HorizontalGrowhtLow = Config.borderSize.high * Config.cs;
|
|
BBoxGrowthLow = FVector(HorizontalGrowhtLow, HorizontalGrowhtLow, Config.cs);
|
|
|
|
const rcReal HorizontalGrowthHigh = Config.borderSize.low * Config.cs;
|
|
BBoxGrowthHigh = FVector(HorizontalGrowthHigh, HorizontalGrowthHigh, Config.cs);
|
|
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
BBoxGrowth = FVector(static_cast<rcReal>(FMath::Max(Config.borderSize.low, Config.borderSize.high)) * Config.cs);
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
}
|
|
else
|
|
{
|
|
// Not using bUseTightBoundExpansion is deprecated, setting values mimicking previous behavior.
|
|
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
BBoxGrowth = FVector(2.0 * static_cast<rcReal>(FMath::Max(Config.borderSize.low, Config.borderSize.high)) * Config.cs);
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
|
|
BBoxGrowthLow = FVector(2.0 * static_cast<rcReal>(Config.borderSize.high) * Config.cs);
|
|
BBoxGrowthHigh = FVector(2.0 * static_cast<rcReal>(Config.borderSize.low) * Config.cs);
|
|
}
|
|
RcNavMeshOrigin = Unreal2RecastPoint(DestNavMesh->NavMeshOriginOffset);
|
|
|
|
AdditionalCachedData = FRecastNavMeshCachedData::Construct(DestNavMesh);
|
|
|
|
// Must be called after AdditionalCachedData is set.
|
|
ResolveGeneratedLinkAreas(Config);
|
|
|
|
if (Config.MaxPolysPerTile <= 0 && DestNavMesh->HasValidNavmesh())
|
|
{
|
|
const dtNavMeshParams* SavedNavParams = DestNavMesh->GetRecastNavMeshImpl()->DetourNavMesh->getParams();
|
|
if (SavedNavParams)
|
|
{
|
|
Config.MaxPolysPerTile = SavedNavParams->maxPolys;
|
|
}
|
|
}
|
|
|
|
ensure(DestNavMesh->GetRecastMesh() == nullptr || DestNavMesh->GetRecastMesh()->getBVQuantFactor((unsigned char)ENavigationDataResolution::Default) != 0);
|
|
|
|
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.low + Config.borderSize.high, Config.cs, Config.ch);
|
|
}
|
|
|
|
bInitialized = true;
|
|
|
|
|
|
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 FVector::FReal TileDim = Config.GetTileSizeUU();
|
|
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 %s due to saved navmesh origin (%s, usually the RecastNavMesh location) not being aligned with tile size (%d uu) ")
|
|
, *GetNameSafe(DestNavMesh), *Orig.ToString(), int(TileDim));
|
|
}
|
|
}
|
|
|
|
// if new navmesh needs more tiles, force recreating
|
|
if (!bRecreateNavmesh)
|
|
{
|
|
CalcNavMeshProperties(MaxTiles, MaxPolysPerTile);
|
|
if (FMath::CeilToInt(FMath::Log2(static_cast<float>(MaxTiles))) != FMath::CeilToInt(FMath::Log2(static_cast<float>(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(static_cast<float>(SavedNavParams->maxTiles)))
|
|
, MaxTiles, FMath::CeilToInt(FMath::Log2(static_cast<float>(MaxTiles))));
|
|
}
|
|
|
|
UE::NavMesh::Private::CheckTileIndicesInValidRange(InclusionBounds, *DestNavMesh);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
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
|
|
if (NavSys && NavSys->IsActiveTilesGenerationEnabled() == false)
|
|
{
|
|
MarkNavBoundsDirty();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// otherwise just update generator params
|
|
Config.MaxPolysPerTile = MaxPolysPerTile;
|
|
NumActiveTiles = GetTilesCountHelper(DestNavMesh->GetRecastNavMeshImpl()->DetourNavMesh);
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
FVector NMOrigin = RcNavMeshOrigin;
|
|
rcVcopy(TiledMeshParameters.orig, &NMOrigin.X);
|
|
|
|
TiledMeshParameters.tileWidth = Config.GetTileSizeUU();
|
|
TiledMeshParameters.tileHeight = Config.GetTileSizeUU();
|
|
|
|
CalcNavMeshProperties(TiledMeshParameters.maxTiles, TiledMeshParameters.maxPolys);
|
|
Config.MaxPolysPerTile = TiledMeshParameters.maxPolys;
|
|
|
|
TiledMeshParameters.walkableClimb = Config.AgentMaxClimb;
|
|
TiledMeshParameters.walkableHeight = Config.AgentHeight;
|
|
TiledMeshParameters.walkableRadius = Config.AgentRadius;
|
|
TiledMeshParameters.resolutionParams[(uint8)ENavigationDataResolution::Low].bvQuantFactor = 1.f / DestNavMesh->GetCellSize(ENavigationDataResolution::Low);
|
|
TiledMeshParameters.resolutionParams[(uint8)ENavigationDataResolution::Default].bvQuantFactor = 1.f / DestNavMesh->GetCellSize(ENavigationDataResolution::Default);
|
|
TiledMeshParameters.resolutionParams[(uint8)ENavigationDataResolution::High].bvQuantFactor = 1.f / DestNavMesh->GetCellSize(ENavigationDataResolution::High);
|
|
|
|
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);
|
|
|
|
UE::NavMesh::Private::CheckTileIndicesInValidRange(InclusionBounds, *DestNavMesh);
|
|
}
|
|
}
|
|
|
|
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 ? static_cast<int32>(FMath::CeilToFloat(FMath::Log2(static_cast<float>(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
|
|
}
|
|
|
|
bool FRecastNavMeshGenerator::IsGeneratingLinks() const
|
|
{
|
|
return DestNavMesh && DestNavMesh->bGenerateNavLinks && UE::NavMesh::Private::bAllowLinkGeneration;
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::ResolveGeneratedLinkAreas(FRecastBuildConfig& OutConfig)
|
|
{
|
|
if (IsGeneratingLinks())
|
|
{
|
|
auto GetAreaIDAndPolyFlags = [this](const TSubclassOf<UNavAreaBase>& AreaClass, unsigned char& OutAreaID, ARecastNavMesh::FNavPolyFlags& OutPolyFlags)
|
|
{
|
|
if (AreaClass)
|
|
{
|
|
const int32* AreaIDPtr = AdditionalCachedData.AreaClassToIdMap.Find(AreaClass);
|
|
if (AreaIDPtr != nullptr)
|
|
{
|
|
OutAreaID = IntCastChecked<unsigned char>(*AreaIDPtr);
|
|
OutPolyFlags = AdditionalCachedData.FlagsPerArea[*AreaIDPtr];
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogNavigation, Warning, TEXT("FRecastTileGenerator: Trying to use undefined area class while resolving generated links areas. (%s)"), *GetNameSafe(AreaClass));
|
|
}
|
|
}
|
|
};
|
|
|
|
const FNavLinkGenerationJumpDownConfig& JumpDown = DestNavMesh->NavLinkJumpDownConfig;
|
|
GetAreaIDAndPolyFlags(JumpDown.DownDirectionAreaClass, OutConfig.JumpDownConfig.downDirArea, OutConfig.JumpDownConfig.downDirPolyFlag);
|
|
GetAreaIDAndPolyFlags(JumpDown.UpDirectionAreaClass, OutConfig.JumpDownConfig.upDirArea, OutConfig.JumpDownConfig.upDirPolyFlag);
|
|
|
|
if (OutConfig.JumpDownConfig.downDirArea == RECAST_NULL_AREA && OutConfig.JumpDownConfig.upDirArea == RECAST_NULL_AREA)
|
|
{
|
|
UE_LOG(LogNavigation, Warning, TEXT("FRecastTileGenerator: no area class has been defined for the jump down links. Disabling them"));
|
|
OutConfig.JumpDownConfig.enabled = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = UE::NavMesh::Private::CalculateMaxTilesCount(InclusionBounds, Config.GetTileSizeUU(), AvgLayersPerTile, DestNavMesh->NavMeshVersion);
|
|
}
|
|
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) for %s. To resolve this, try using bigger tiles or increasing the TileNumberHardLimit in the NavMesh properties."),
|
|
MaxRequestedTiles, MaxTilesFromMask, *GetFullNameSafe(DestNavMesh));
|
|
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();
|
|
DestNavMesh->bHasNoTileData = false;
|
|
|
|
RcNavMeshOrigin = Unreal2RecastPoint(DestNavMesh->NavMeshOriginOffset);
|
|
|
|
ConstructTiledNavMesh();
|
|
|
|
if (MarkNavBoundsDirty() == false)
|
|
{
|
|
// There are no navigation bounds to build, probably navmesh was resized and we just need to update debug draw
|
|
DestNavMesh->RequestDrawingUpdate();
|
|
}
|
|
else
|
|
{
|
|
RebuildAllStartTime = FPlatformTime::Seconds();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::EnsureBuildCompletion()
|
|
{
|
|
const bool bHadTasks = GetNumRemaningBuildTasks() > 0;
|
|
|
|
const bool bDoAsyncDataGathering = (GatherGeometryOnGameThread() == false);
|
|
do
|
|
{
|
|
const int32 NumTasksToProcess = (bDoAsyncDataGathering ? 1 : MaxTileGeneratorTasks) - RunningDirtyTiles.Num();
|
|
ProcessTileTasksAndGetUpdatedTiles(NumTasksToProcess);
|
|
|
|
// Block until tasks are finished
|
|
for (TRunningTileElement<FRecastTileGeneratorWrapper>& 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)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_TickAsyncBuild);
|
|
|
|
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) > UE::NavMesh::Private::RecentlyBuildTileDisplayTime; });
|
|
|
|
const int32 NumPostRemove = RecentlyBuiltTiles.Num();
|
|
bRequestDrawingUpdate = (NumPreRemove != NumPostRemove);
|
|
}
|
|
#endif//WITH_EDITOR
|
|
|
|
const UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
|
|
if (!ensureMsgf(NavSys != nullptr, TEXT("FRecastNavMeshGenerator can't find valid navigation system: Owner=[%s] World=[%s]"), *GetFullNameSafe(GetOwner()), *GetFullNameSafe(GetWorld())))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Submit async tile build tasks in case we have dirty tiles and have room for them
|
|
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<FNavTileRef> UpdatedTileRefs = ProcessTileTasksAndGetUpdatedTiles(NumTasksToSubmit);
|
|
|
|
if (UpdatedTileRefs.Num() > 0)
|
|
{
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_OnNavMeshTilesUpdated);
|
|
|
|
// Invalidate active paths that go through regenerated tiles
|
|
DestNavMesh->OnNavMeshTilesUpdated(UpdatedTileRefs);
|
|
}
|
|
|
|
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() + UpdatedTileRefs.Num());
|
|
for (const FNavTileRef TileRef : UpdatedTileRefs)
|
|
{
|
|
UE_SUPPRESS(LogNavigation, VeryVerbose,
|
|
{
|
|
if (DestNavMesh->GetRecastNavMeshImpl())
|
|
{
|
|
if (const dtNavMesh* DetourMesh = DestNavMesh->GetRecastNavMeshImpl()->GetRecastMesh())
|
|
{
|
|
const uint32 TileIndex = DetourMesh->decodePolyIdTile((dtTileRef)TileRef);
|
|
const uint32 Salt = DetourMesh->decodePolyIdSalt((dtTileRef)TileRef);
|
|
UE_LOG(LogNavigation, VeryVerbose, TEXT("%s Adding to RecentlyBuiltTiles TileId: %d Salt: %d TileRef: 0x%llx"), ANSI_TO_TCHAR(__FUNCTION__), TileIndex, Salt, (dtTileRef)TileRef);
|
|
}
|
|
}
|
|
});
|
|
|
|
FTileTimestamp TileTimestamp;
|
|
TileTimestamp.NavTileRef = TileRef;
|
|
TileTimestamp.Timestamp = Timestamp;
|
|
RecentlyBuiltTiles.Add(TileTimestamp);
|
|
}
|
|
#endif//WITH_EDITOR
|
|
}
|
|
|
|
if (bRequestDrawingUpdate)
|
|
{
|
|
DestNavMesh->RequestDrawingUpdate();
|
|
}
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::OnNavigationBoundsChanged()
|
|
{
|
|
check(DestNavMesh);
|
|
|
|
UpdateNavigationBounds();
|
|
|
|
dtNavMesh* DetourMesh = DestNavMesh->GetRecastNavMeshImpl() ? DestNavMesh->GetRecastNavMeshImpl()->GetRecastMesh() : nullptr;
|
|
if (!IsGameStaticNavMesh(DestNavMesh) && DestNavMesh->IsResizable() && DetourMesh)
|
|
{
|
|
const UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
|
|
if (NavSys && !NavSys->IsNavigationBuildingLocked())
|
|
{
|
|
// Check whether Navmesh size needs to be changed
|
|
const int32 MaxRequestedTiles = UE::NavMesh::Private::CalculateMaxTilesCount(InclusionBounds, Config.GetTileSizeUU(), AvgLayersPerTile, DestNavMesh->NavMeshVersion);
|
|
if (DetourMesh->getMaxTiles() != MaxRequestedTiles)
|
|
{
|
|
UE_LOG(LogNavigation, Log, TEXT("%s> Navigation bounds changed, rebuilding navmesh"), *DestNavMesh->GetName());
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
UE::NavMesh::Private::CheckTileIndicesInValidRange(InclusionBounds, *DestNavMesh);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::OnAreaRemoved(const UClass* AreaClass)
|
|
{
|
|
AdditionalCachedData.OnAreaRemoved(AreaClass);
|
|
}
|
|
|
|
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 ActiveTileSet
|
|
|
|
const dtNavMesh* DetourMesh = DestNavMesh->GetRecastNavMeshImpl()->GetRecastMesh();
|
|
|
|
if (DetourMesh != nullptr && DetourMesh->isEmpty() == false)
|
|
{
|
|
ActiveTileSet.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)
|
|
{
|
|
ActiveTileSet.FindOrAdd(FIntPoint(Tile->header->x, Tile->header->y));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FRecastNavMeshGenerator::IsInActiveSet(const FIntPoint& Tile) const
|
|
{
|
|
return bRestrictBuildingToActiveTiles == false || ActiveTileSet.Contains(Tile);
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::ResetTimeSlicedTileGeneratorSync()
|
|
{
|
|
SyncTimeSlicedData.TileGeneratorSync.Reset();
|
|
|
|
//reset variables used for timeslicing TileGenratorSync
|
|
SyncTimeSlicedData.ProcessTileTasksSyncState = EProcessTileTasksSyncTimeSlicedState::Init;
|
|
SyncTimeSlicedData.UpdatedTilesCache.Reset();
|
|
SyncTimeSlicedData.OldLayerTileIdMapCached.Reset();
|
|
SyncTimeSlicedData.ResultTileRefsCached.Reset();
|
|
SyncTimeSlicedData.AddGeneratedTilesState = EAddGeneratedTilesTimeSlicedState::Init;
|
|
SyncTimeSlicedData.AddGenTilesLayerIndex = 0;
|
|
}
|
|
|
|
//@TODO Investigate removing from RunningDirtyTiles here too (or at least not using the results in any way)
|
|
void FRecastNavMeshGenerator::RemoveTiles(const TArray<FIntPoint>& Tiles)
|
|
{
|
|
dtNavMesh* DetourMesh = DestNavMesh->GetRecastNavMeshImpl()->GetRecastMesh();
|
|
const bool bIsDetourMeshValid = DetourMesh && !DetourMesh->isEmpty();
|
|
|
|
for (const FIntPoint& TileXY : Tiles)
|
|
{
|
|
if (bIsDetourMeshValid)
|
|
{
|
|
RemoveTileLayers(DetourMesh, TileXY.X, TileXY.Y);
|
|
}
|
|
|
|
if (PendingDirtyTiles.Num() > 0)
|
|
{
|
|
FPendingTileElement DirtyTile;
|
|
DirtyTile.Coord = TileXY;
|
|
PendingDirtyTiles.Remove(DirtyTile);
|
|
}
|
|
|
|
if (SyncTimeSlicedData.TileGeneratorSync.IsValid())
|
|
{
|
|
if (SyncTimeSlicedData.TileGeneratorSync->GetTileX() == TileXY.X && SyncTimeSlicedData.TileGeneratorSync->GetTileY() == TileXY.Y)
|
|
{
|
|
ResetTimeSlicedTileGeneratorSync();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::ReAddTiles(const TArray<FNavMeshDirtyTileElement>& 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();
|
|
|
|
TSet<FPendingTileElement> DirtyTiles;
|
|
|
|
const double CurrentTimeSeconds = FPlatformTime::Seconds();
|
|
const FVector::FReal NearDistanceSquared = FMath::Square(DestNavMesh->InvokerTilePriorityBumpDistanceThresholdInTileUnits*GetConfig().GetTileSizeUU());
|
|
const uint8 PriorityIncrease = DestNavMesh->InvokerTilePriorityBumpIncrease;
|
|
|
|
// @note we act on assumption all items in Tiles are unique
|
|
for (const FNavMeshDirtyTileElement& Tile : Tiles)
|
|
{
|
|
FPendingTileElement Element;
|
|
Element.Coord = Tile.Coordinates;
|
|
#if !UE_BUILD_SHIPPING
|
|
Element.DebugInvokerDistanceSquared = Tile.InvokerDistanceSquared;
|
|
Element.DebugInvokerPriority = Tile.InvokerPriority;
|
|
#endif // !UE_BUILD_SHIPPING
|
|
|
|
// Bump sorting priority for tiles near invokers
|
|
if (Tile.InvokerDistanceSquared < NearDistanceSquared)
|
|
{
|
|
Element.SortingPriority = (ENavigationInvokerPriority)FMath::Min((int)Tile.InvokerPriority+PriorityIncrease, (int)ENavigationInvokerPriority::MAX-1);
|
|
}
|
|
else
|
|
{
|
|
Element.SortingPriority = Tile.InvokerPriority;
|
|
}
|
|
|
|
Element.bRebuildGeometry = true;
|
|
Element.CreationTime = CurrentTimeSeconds;
|
|
DirtyTiles.Add(Element);
|
|
}
|
|
|
|
const int32 NumTilesMarked = DirtyTiles.Num();
|
|
|
|
// Merge all pending tiles into one container
|
|
for (const FPendingTileElement& Element : PendingDirtyTiles)
|
|
{
|
|
FPendingTileElement* ExistingElement = DirtyTiles.Find(Element);
|
|
if (ExistingElement)
|
|
{
|
|
#if !UE_BUILD_SHIPPING
|
|
ExistingElement->DebugInvokerDistanceSquared = FMath::Min(ExistingElement->DebugInvokerDistanceSquared, Element.DebugInvokerDistanceSquared);
|
|
ExistingElement->DebugInvokerPriority = FMath::Max(ExistingElement->DebugInvokerPriority, Element.DebugInvokerPriority);
|
|
#endif // !UE_BUILD_SHIPPING
|
|
ExistingElement->SortingPriority = FMath::Max(ExistingElement->SortingPriority, Element.SortingPriority);
|
|
ExistingElement->bRebuildGeometry |= Element.bRebuildGeometry;
|
|
ExistingElement->CreationTime = FMath::Min(Element.CreationTime, ExistingElement->CreationTime);
|
|
// 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<FNavTileRef> FRecastNavMeshGenerator::RemoveTileLayersAndGetUpdatedTiles(const int32 TileX, const int32 TileY, TMap<int32, dtPolyRef>* OldLayerTileIdMap)
|
|
{
|
|
dtNavMesh* DetourMesh = DestNavMesh->GetRecastNavMeshImpl()->GetRecastMesh();
|
|
TArray<FNavTileRef> 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--;
|
|
DestNavMesh->LogRecastTile(ANSI_TO_TCHAR(__FUNCTION__), FName(""), FName("removing"), *DetourMesh, TileX, TileY, LayerIndex, TileRef);
|
|
|
|
DetourMesh->removeTile(TileRef, nullptr, nullptr);
|
|
|
|
UpdatedIndices.AddUnique(FNavTileRef(TileRef));
|
|
|
|
RecastTileVersionHelper::GetUpdatedTileId(TileRef, DetourMesh); // Updates TileRef
|
|
|
|
if (OldLayerTileIdMap)
|
|
{
|
|
OldLayerTileIdMap->Add(LayerIndex, TileRef);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove compressed tile cache layers
|
|
DestNavMesh->RemoveTileCacheLayers(TileX, TileY);
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
DestNavMesh->RemoveTileDebugData(TileX, TileY);
|
|
#endif
|
|
}
|
|
|
|
return UpdatedIndices;
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::RemoveTileLayers(dtNavMesh* DetourMesh, const int32 TileX, const int32 TileY)
|
|
{
|
|
check(DetourMesh && !DetourMesh->isEmpty())
|
|
|
|
const int32 NumLayers = DetourMesh->getTileCountAt(TileX, TileY);
|
|
|
|
if (NumLayers > 0)
|
|
{
|
|
TArray<dtMeshTile*, TInlineAllocator<16>> Tiles;
|
|
Tiles.AddZeroed(NumLayers);
|
|
DetourMesh->getTilesAt(TileX, TileY, (const dtMeshTile**)Tiles.GetData(), NumLayers);
|
|
|
|
for (int32 i = 0; i < NumLayers; i++)
|
|
{
|
|
const dtPolyRef TileRef = DetourMesh->getTileRef(Tiles[i]);
|
|
NumActiveTiles--;
|
|
|
|
UE_SUPPRESS(LogNavigation, VeryVerbose,
|
|
{
|
|
const int32 LayerIndex = Tiles[i]->header->layer;
|
|
DestNavMesh->LogRecastTile(ANSI_TO_TCHAR(__FUNCTION__), FName(""), FName("removing"), *DetourMesh, TileX, TileY, LayerIndex, TileRef);
|
|
});
|
|
|
|
DetourMesh->removeTile(TileRef, nullptr, nullptr);
|
|
}
|
|
}
|
|
|
|
// Remove compressed tile cache layers
|
|
DestNavMesh->RemoveTileCacheLayers(TileX, TileY);
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
DestNavMesh->RemoveTileDebugData(TileX, TileY);
|
|
#endif
|
|
}
|
|
|
|
FRecastNavMeshGenerator::FSyncTimeSlicedData::FSyncTimeSlicedData()
|
|
: CurrentTileRegenDuration(0.)
|
|
#if TIME_SLICE_NAV_REGEN
|
|
, bTimeSliceRegenActive(true)
|
|
, bNextTimeSliceRegenActive(true)
|
|
#else
|
|
, bTimeSliceRegenActive(false)
|
|
, bNextTimeSliceRegenActive(false)
|
|
#endif
|
|
, ProcessTileTasksSyncState(EProcessTileTasksSyncTimeSlicedState::Init)
|
|
, AddGeneratedTilesState(EAddGeneratedTilesTimeSlicedState::Init)
|
|
, AddGenTilesLayerIndex(0)
|
|
, TimeSliceManager(nullptr)
|
|
{
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::AddGeneratedTileLayer(int32 LayerIndex, FRecastTileGenerator& TileGenerator, const TMap<int32, dtPolyRef>& OldLayerTileIdMap, TArray<FNavTileRef>& OutResultTileRefs)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastAddGeneratedTileLayer);
|
|
|
|
struct FLayerIndexFinder
|
|
{
|
|
int32 LayerIndex;
|
|
explicit FLayerIndexFinder(const int32 InLayerIndex) : LayerIndex(InLayerIndex) {}
|
|
bool operator()(const FNavMeshTileData& LayerData) const
|
|
{
|
|
return LayerData.LayerIndex == LayerIndex;
|
|
}
|
|
};
|
|
|
|
const int32 TileX = TileGenerator.GetTileX();
|
|
const int32 TileY = TileGenerator.GetTileY();
|
|
dtNavMesh* DetourMesh = DestNavMesh->GetRecastNavMeshImpl()->GetRecastMesh();
|
|
TArray<FNavMeshTileData>& TileLayers = TileGenerator.GetNavigationData();
|
|
dtTileRef OldTileRef = DetourMesh->getTileRefAt(TileX, TileY, LayerIndex);
|
|
const int32 LayerDataIndex = TileLayers.IndexOfByPredicate(FLayerIndexFinder(LayerIndex));
|
|
|
|
if (LayerDataIndex != INDEX_NONE)
|
|
{
|
|
FNavMeshTileData& LayerData = TileLayers[LayerDataIndex];
|
|
if (OldTileRef)
|
|
{
|
|
NumActiveTiles--;
|
|
DestNavMesh->LogRecastTile(ANSI_TO_TCHAR(__FUNCTION__), FName(""), FName("removing"), *DetourMesh, TileX, TileY, LayerIndex, OldTileRef);
|
|
|
|
DetourMesh->removeTile(OldTileRef, nullptr, nullptr);
|
|
|
|
OutResultTileRefs.AddUnique(FNavTileRef(OldTileRef));
|
|
|
|
RecastTileVersionHelper::GetUpdatedTileId(OldTileRef, DetourMesh); // Updates OldTileRef
|
|
}
|
|
else
|
|
{
|
|
OldTileRef = OldLayerTileIdMap.FindRef(LayerIndex);
|
|
}
|
|
|
|
if (LayerData.IsValid())
|
|
{
|
|
bool bRejectNavmesh = false;
|
|
dtTileRef ResultTileRef = 0;
|
|
|
|
dtStatus status = 0;
|
|
|
|
{
|
|
// 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> Failed to add tile (%d,%d:%d), %d tile limit reached! (from %s). If using FixedTilePoolSize, try increasing the TilePoolSize or using bigger tiles."),
|
|
*DestNavMesh->GetName(), TileX, TileY, LayerIndex, DetourMesh->getMaxTiles(), ANSI_TO_TCHAR(__FUNCTION__));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OutResultTileRefs.AddUnique(FNavTileRef(ResultTileRef));
|
|
NumActiveTiles++;
|
|
|
|
DestNavMesh->LogRecastTile(ANSI_TO_TCHAR(__FUNCTION__), FName(""), FName("added generated"), *DetourMesh, TileX, TileY, LayerIndex, ResultTileRef);
|
|
|
|
{
|
|
// 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);
|
|
|
|
OutResultTileRefs.AddUnique(FNavTileRef(OldTileRef));
|
|
}
|
|
}
|
|
|
|
bool FRecastNavMeshGenerator::IsAllowedToAddTileLayers(const FIntPoint Tile) const
|
|
{
|
|
check(DestNavMesh);
|
|
return !DestNavMesh->IsWorldPartitionedDynamicNavmesh() || IsInActiveSet(Tile) || (GetWorld() && !GetWorld()->IsGameWorld());
|
|
}
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
// Deprecated
|
|
void FRecastNavMeshGenerator::LogDirtyAreas(const TMap<FPendingTileElement, TArray<FNavigationDirtyAreaPerTileDebugInformation>>& DirtyAreasDebuggingInformation) const
|
|
{
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::LogDirtyAreas(const UObject& OwnerNav,
|
|
const TMap<FPendingTileElement, TArray<FNavigationDirtyAreaPerTileDebugInformation>>& DirtyAreasDebuggingInformation) const
|
|
{
|
|
// Helper struct used to collate the raw information provided to the method, needed for the log results
|
|
struct FNavigationDirtyAreaDebugInformation
|
|
{
|
|
FNavigationDirtyArea DirtyArea;
|
|
int32 NewlyAddedDirtyTiles = 0;
|
|
int32 TotalDirtyTiles = 0;
|
|
};
|
|
|
|
// Array used to describe per dirty area the amount of dirtied tiles
|
|
TArray<FNavigationDirtyAreaDebugInformation> DirtyAreaToDirtyTilesCount;
|
|
for (const TPair<FPendingTileElement, TArray<FNavigationDirtyAreaPerTileDebugInformation>>& DirtyAreasDebuggingInformationPair : DirtyAreasDebuggingInformation)
|
|
{
|
|
for (const FNavigationDirtyAreaPerTileDebugInformation& DirtyAreaPerTileDebugInformation : DirtyAreasDebuggingInformationPair.Value)
|
|
{
|
|
FNavigationDirtyAreaDebugInformation* DirtyResultsTuple = DirtyAreaToDirtyTilesCount.FindByPredicate([&DirtyAreaPerTileDebugInformation](const FNavigationDirtyAreaDebugInformation& OtherDirtyResultsTuple)
|
|
{
|
|
return OtherDirtyResultsTuple.DirtyArea == DirtyAreaPerTileDebugInformation.DirtyArea;
|
|
});
|
|
|
|
if (!DirtyResultsTuple)
|
|
{
|
|
DirtyResultsTuple = &DirtyAreaToDirtyTilesCount.Add_GetRef({DirtyAreaPerTileDebugInformation.DirtyArea, 0, 0});
|
|
}
|
|
|
|
if (!DirtyAreaPerTileDebugInformation.bTileWasAlreadyAdded)
|
|
{
|
|
DirtyResultsTuple->NewlyAddedDirtyTiles++;
|
|
}
|
|
DirtyResultsTuple->TotalDirtyTiles++;
|
|
}
|
|
}
|
|
|
|
for (const auto& [DirtyArea, NewlyAddedDirtyTiles, TotalDirtyTiles] : DirtyAreaToDirtyTilesCount)
|
|
{
|
|
const FVector2D BoundsSize(DirtyArea.Bounds.GetSize());
|
|
|
|
UE_LOG(LogNavigationDirtyArea, VeryVerbose,
|
|
TEXT("(navmesh: %-30s) Dirty area trying to dirty %2d tiles (out of which %2d are newly added/not pending) | Source = %s | Bounds size = %s)"),
|
|
*GetNameSafe(GetOwner()), TotalDirtyTiles, NewlyAddedDirtyTiles, *DirtyArea.GetSourceDescription(), *BoundsSize.ToString());
|
|
|
|
UE_VLOG_BOX(&OwnerNav, LogNavigationDirtyArea, VeryVerbose, DirtyArea.Bounds, FColor::Purple,
|
|
TEXT("Tiles %d (new: %d), Source: %s"), TotalDirtyTiles, NewlyAddedDirtyTiles, *DirtyArea.GetSourceDescription());
|
|
}
|
|
}
|
|
#endif
|
|
|
|
ETimeSliceWorkResult FRecastNavMeshGenerator::AddGeneratedTilesTimeSliced(FRecastTileGenerator& TileGenerator, TArray<FNavTileRef>& OutResultTileRefs)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastAddGeneratedTiles);
|
|
|
|
check(SyncTimeSlicedData.TimeSliceManager);
|
|
|
|
const int32 TileX = TileGenerator.GetTileX();
|
|
const int32 TileY = TileGenerator.GetTileY();
|
|
dtNavMesh* DetourMesh = DestNavMesh->GetRecastNavMeshImpl()->GetRecastMesh();
|
|
TArray<FNavMeshTileData>& TileLayers = TileGenerator.GetNavigationData();
|
|
ETimeSliceWorkResult WorkResult = ETimeSliceWorkResult::Succeeded;
|
|
bool bIteratedThroughDirtyLayers = true;
|
|
|
|
switch (SyncTimeSlicedData.AddGeneratedTilesState)
|
|
{
|
|
case EAddGeneratedTilesTimeSlicedState::Init:
|
|
{
|
|
SyncTimeSlicedData.ResultTileRefsCached.Reset();
|
|
SyncTimeSlicedData.ResultTileRefsCached.Reserve(TileLayers.Num());
|
|
SyncTimeSlicedData.OldLayerTileIdMapCached.Reset();
|
|
SyncTimeSlicedData.OldLayerTileIdMapCached.Reserve(TileLayers.Num());
|
|
SyncTimeSlicedData.AddGenTilesLayerIndex = TileGenerator.GetDirtyLayersMask().Find(true);
|
|
if (TileGenerator.IsFullyRegenerated())
|
|
{
|
|
// remove all layers
|
|
SyncTimeSlicedData.ResultTileRefsCached = RemoveTileLayersAndGetUpdatedTiles(TileX, TileY, &SyncTimeSlicedData.OldLayerTileIdMapCached);
|
|
}
|
|
|
|
SyncTimeSlicedData.AddGeneratedTilesState = EAddGeneratedTilesTimeSlicedState::AddTiles;
|
|
}//fall through to next state
|
|
case EAddGeneratedTilesTimeSlicedState::AddTiles:
|
|
{
|
|
if (DetourMesh != nullptr
|
|
&& IsAllowedToAddTileLayers(FIntPoint(TileX, TileY))
|
|
&& SyncTimeSlicedData.AddGenTilesLayerIndex != INDEX_NONE)
|
|
{
|
|
for (; SyncTimeSlicedData.AddGenTilesLayerIndex < TileGenerator.GetDirtyLayersMask().Num(); ++SyncTimeSlicedData.AddGenTilesLayerIndex)
|
|
{
|
|
if (TileGenerator.IsLayerChanged(SyncTimeSlicedData.AddGenTilesLayerIndex))
|
|
{
|
|
if (SyncTimeSlicedData.TimeSliceManager->GetTimeSlicer().IsTimeSliceFinishedCached())
|
|
{
|
|
WorkResult = ETimeSliceWorkResult::CallAgainNextTimeSlice;
|
|
break;
|
|
}
|
|
|
|
AddGeneratedTileLayer(SyncTimeSlicedData.AddGenTilesLayerIndex, TileGenerator, SyncTimeSlicedData.OldLayerTileIdMapCached, SyncTimeSlicedData.ResultTileRefsCached);
|
|
|
|
MARK_TIMESLICE_SECTION_DEBUG(SyncTimeSlicedData.TimeSliceManager->GetTimeSlicer(), AddTiles);
|
|
|
|
SyncTimeSlicedData.TimeSliceManager->GetTimeSlicer().TestTimeSliceFinished();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
WorkResult = ETimeSliceWorkResult::Failed;
|
|
bIteratedThroughDirtyLayers = false;
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
{
|
|
ensureMsgf(false, TEXT("unhandled EAddGeneratedTilesTimeSlicedState"));
|
|
WorkResult = ETimeSliceWorkResult::Failed;
|
|
}
|
|
}
|
|
|
|
if (SyncTimeSlicedData.AddGenTilesLayerIndex == TileGenerator.GetDirtyLayersMask().Num() || !bIteratedThroughDirtyLayers)
|
|
{
|
|
SyncTimeSlicedData.AddGenTilesLayerIndex = 0;
|
|
SyncTimeSlicedData.AddGeneratedTilesState = EAddGeneratedTilesTimeSlicedState::Init;
|
|
|
|
OutResultTileRefs = MoveTemp(SyncTimeSlicedData.ResultTileRefsCached);
|
|
}
|
|
|
|
return WorkResult;
|
|
}
|
|
|
|
TArray<FNavTileRef> FRecastNavMeshGenerator::AddGeneratedTilesAndGetUpdatedTiles(FRecastTileGenerator& TileGenerator)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RecastAddGeneratedTiles);
|
|
|
|
TMap<int32, dtPolyRef> OldLayerTileIdMap;
|
|
TArray<FNavTileRef> ResultTileRefs;
|
|
const int32 TileX = TileGenerator.GetTileX();
|
|
const int32 TileY = TileGenerator.GetTileY();
|
|
|
|
if (TileGenerator.IsFullyRegenerated())
|
|
{
|
|
// remove all layers
|
|
ResultTileRefs = RemoveTileLayersAndGetUpdatedTiles(TileX, TileY, &OldLayerTileIdMap);
|
|
}
|
|
|
|
dtNavMesh* DetourMesh = DestNavMesh->GetRecastNavMeshImpl()->GetRecastMesh();
|
|
const int32 FirstDirtyTileIndex = TileGenerator.GetDirtyLayersMask().Find(true);
|
|
|
|
if (DetourMesh != nullptr
|
|
&& IsAllowedToAddTileLayers(FIntPoint(TileX, TileY))
|
|
&& FirstDirtyTileIndex != INDEX_NONE)
|
|
{
|
|
TArray<FNavMeshTileData> TileLayers = TileGenerator.GetNavigationData();
|
|
ResultTileRefs.Reserve(TileLayers.Num());
|
|
|
|
for (int32 LayerIndex = FirstDirtyTileIndex; LayerIndex < TileGenerator.GetDirtyLayersMask().Num(); ++LayerIndex)
|
|
{
|
|
if (TileGenerator.IsLayerChanged(LayerIndex))
|
|
{
|
|
AddGeneratedTileLayer(LayerIndex, TileGenerator, OldLayerTileIdMap, ResultTileRefs);
|
|
}
|
|
}
|
|
}
|
|
|
|
return ResultTileRefs;
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::DiscardCurrentBuildingTasks()
|
|
{
|
|
PendingDirtyTiles.Empty();
|
|
|
|
for (TRunningTileElement<FRecastTileGeneratorWrapper>& Element : RunningDirtyTiles)
|
|
{
|
|
if (Element.AsyncTask)
|
|
{
|
|
Element.AsyncTask->EnsureCompletion();
|
|
delete Element.AsyncTask;
|
|
Element.AsyncTask = nullptr;
|
|
}
|
|
}
|
|
|
|
ResetTimeSlicedTileGeneratorSync();
|
|
|
|
RunningDirtyTiles.Empty();
|
|
}
|
|
|
|
bool FRecastNavMeshGenerator::HasDirtyTiles() const
|
|
{
|
|
return (PendingDirtyTiles.Num() > 0
|
|
|| RunningDirtyTiles.Num() > 0
|
|
|| SyncTimeSlicedData.TileGeneratorSync.IsValid()
|
|
);
|
|
}
|
|
|
|
FBox FRecastNavMeshGenerator::GrowBoundingBox(const FBox& BBox, bool bIncludeAgentHeight) const
|
|
{
|
|
const FVector BBoxGrowOffsetMin = FVector(0, 0, bIncludeAgentHeight ? Config.AgentHeight : 0.0f);
|
|
|
|
return FBox(BBox.Min - BBoxGrowthLow - BBoxGrowOffsetMin, BBox.Max + BBoxGrowthHigh);
|
|
}
|
|
|
|
FBox FRecastNavMeshGenerator::GrowDirtyBounds(const FBox& BBox, bool bIncludeAgentHeight) const
|
|
{
|
|
const FVector BBoxGrowOffsetMin = FVector(0, 0, bIncludeAgentHeight ? Config.AgentHeight : 0.0f);
|
|
|
|
return FBox(BBox.Min - BBoxGrowthHigh - BBoxGrowOffsetMin, BBox.Max + BBoxGrowthLow);
|
|
}
|
|
|
|
bool FRecastNavMeshGenerator::ShouldGenerateGeometryForOctreeElement(const FNavigationOctreeElement& Element, const FNavDataConfig& NavDataConfig) const
|
|
{
|
|
return Element.ShouldUseGeometry(NavDataConfig);
|
|
}
|
|
|
|
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 (!ensureMsgf(AreaBounds.IsValid, TEXT("%hs AreaBounds is not valid"), __FUNCTION__))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (HasDirtyTiles() == false)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool bRetDirty = false;
|
|
const FVector::FReal TileSizeInWorldUnits = Config.GetTileSizeUU();
|
|
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
|
|
{
|
|
if (!ensureMsgf(AreaBounds.IsValid, TEXT("%hs AreaBounds is not valid"), __FUNCTION__))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
const FVector::FReal TileSizeInWorldUnits = Config.GetTileSizeUU();
|
|
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 TRunningTileElement<FRecastTileGeneratorWrapper>& 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 FVector::FReal TileSizeInWorldUnits = Config.GetTileSizeUU();
|
|
check(TileSizeInWorldUnits > 0);
|
|
|
|
const bool bGameStaticNavMesh = IsGameStaticNavMesh(DestNavMesh);
|
|
|
|
const UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
|
|
const ARecastNavMesh* const OwnerNav = GetOwner();
|
|
const bool bUseVirtualGeometryFilteringAndDirtying = OwnerNav != nullptr && OwnerNav->bUseVirtualGeometryFilteringAndDirtying;
|
|
// Those are set only if bUseVirtualGeometryFilteringAndDirtying is enabled since we do not use them for anything else
|
|
const FNavigationOctree* const NavOctreeInstance = (bUseVirtualGeometryFilteringAndDirtying && NavSys) ? NavSys->GetNavOctree() : nullptr;
|
|
const FNavDataConfig* const NavDataConfig = bUseVirtualGeometryFilteringAndDirtying ? &DestNavMesh->GetConfig() : nullptr;
|
|
// ~
|
|
|
|
const double CurrentTimeSeconds = FPlatformTime::Seconds();
|
|
|
|
// find all tiles that need regeneration
|
|
TSet<FPendingTileElement> DirtyTiles;
|
|
#if !UE_BUILD_SHIPPING
|
|
// Used for debug purposes to track the number of new dirty tiles per area; Updated only if LogNavigationDirtyArea is VeryVerbose
|
|
TMap<FPendingTileElement, TArray<FNavigationDirtyAreaPerTileDebugInformation>> DirtyAreasDebugging;
|
|
#endif
|
|
|
|
if (!bRestrictBuildingToActiveTiles || !ActiveTileSet.IsEmpty())
|
|
{
|
|
for (const FNavigationDirtyArea& DirtyArea : DirtyAreas)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_DirtyArea);
|
|
|
|
if (!ensureMsgf(DirtyArea.Bounds.IsValid && !DirtyArea.Bounds.ContainsNaN(), TEXT("%hs Attempting to use DirtyArea.Bounds which are not valid%s. Bounds: %s, Source: %s"),
|
|
__FUNCTION__, DirtyArea.Bounds.ContainsNaN() ? TEXT(" (contains NaN)") : TEXT(""), *DirtyArea.Bounds.ToString(), *DirtyArea.GetSourceDescription()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Game world static navmeshes accept only area modifiers updates
|
|
if (bGameStaticNavMesh && (!DirtyArea.HasFlag(ENavigationDirtyFlag::DynamicModifier) || DirtyArea.HasFlag(ENavigationDirtyFlag::NavigationBounds)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
UE_VLOG_BOX(OwnerNav, LogNavigation, VeryVerbose, DirtyArea.Bounds, FColor::Blue, TEXT("DirtyArea %s"), *DirtyArea.GetSourceDescription());
|
|
|
|
// (if bUseVirtualGeometryFilteringAndDirtying is true) Ignore dirty areas flagged by a source object that is not supposed to apply to this navmesh
|
|
if (bUseVirtualGeometryFilteringAndDirtying && NavSys && NavOctreeInstance && NavDataConfig)
|
|
{
|
|
const FNavigationElement* SourceElement = DirtyArea.OptionalSourceElement.Get();
|
|
if (SourceElement
|
|
&& !ShouldDirtyTilesRequestedByElement(*NavSys, *NavOctreeInstance, SourceElement->GetHandle(), *NavDataConfig))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
bool bDoTileInclusionTest = false;
|
|
FBox AdjustedAreaBounds = DirtyArea.Bounds;
|
|
|
|
// if it's not expanding the navigable 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 = GrowDirtyBounds(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);
|
|
}
|
|
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_CheckTilesInBounds);
|
|
|
|
uint32 PendingTilesMarked = 0;
|
|
|
|
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)
|
|
{
|
|
UE_SUPPRESS(LogNavigation, VeryVerbose,
|
|
{
|
|
const FBox TileBounds = FRecastTileGenerator::CalculateTileBounds(TileX, TileY, RcNavMeshOrigin, TotalNavBounds, TileSizeInWorldUnits);
|
|
UE_VLOG_BOX(OwnerNav, LogNavigation, VeryVerbose, TileBounds, FColor::Red, TEXT("Not in active set"));
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (bDoTileInclusionTest == true && DirtyArea.HasFlag(ENavigationDirtyFlag::NavigationBounds) == false)
|
|
{
|
|
const FBox TileBounds = FRecastTileGenerator::CalculateTileBounds(TileX, TileY, RcNavMeshOrigin, TotalNavBounds, TileSizeInWorldUnits);
|
|
|
|
// do per tile check since we can have lots of tiles in between navigable bounds volumes
|
|
if (IntersectBounds(TileBounds, InclusionBounds) == false)
|
|
{
|
|
// Skip this tile
|
|
continue;
|
|
}
|
|
}
|
|
|
|
FPendingTileElement Element;
|
|
Element.Coord = FIntPoint(TileX, TileY);
|
|
// Make sure to prevent bRebuildGeometry for game world static navmeshes.
|
|
// Game world static navmeshes accept only area modifiers updates. Rebuilding geometry would bRegenerateCompressedLayers without having the geometry for them.
|
|
Element.bRebuildGeometry = !bGameStaticNavMesh && (DirtyArea.HasFlag(ENavigationDirtyFlag::Geometry) || DirtyArea.HasFlag(ENavigationDirtyFlag::NavigationBounds));
|
|
Element.CreationTime = CurrentTimeSeconds;
|
|
if (Element.bRebuildGeometry == false)
|
|
{
|
|
Element.DirtyAreas.Add(AdjustedAreaBounds);
|
|
}
|
|
PendingTilesMarked++;
|
|
|
|
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);
|
|
}
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
UE_SUPPRESS(LogNavigationDirtyArea, VeryVerbose,
|
|
{
|
|
const bool bAlreadyAdded = ExistingElement != nullptr;
|
|
DirtyAreasDebugging.FindOrAdd(Element).Add({DirtyArea, bAlreadyAdded});
|
|
});
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
// Warn if this is from a big dirty area
|
|
UE_SUPPRESS(LogNavigationDirtyArea, Warning,
|
|
{
|
|
if (PendingTilesMarked > 0)
|
|
{
|
|
if (NavSys == nullptr)
|
|
{
|
|
// NavSys might not have been initialized yet if not using bUseVirtualGeometryFilteringAndDirtying
|
|
NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
|
|
}
|
|
|
|
// If not using active tile generation, those are reported earlier in FNavigationDirtyAreasController::AddArea
|
|
if (NavSys && NavSys->GetOperationMode() == FNavigationSystemRunMode::GameMode && NavSys->IsActiveTilesGenerationEnabled() &&
|
|
NavSys->GetDirtyAreaWarningSizeThreshold() > 0.f && AdjustedAreaBounds.GetSize().GetMax() > NavSys->GetDirtyAreaWarningSizeThreshold())
|
|
{
|
|
const FVector2D AdjustedAreaBoundsSize(AdjustedAreaBounds.GetSize());
|
|
|
|
UE_LOG(LogNavigationDirtyArea, Warning,
|
|
TEXT("(navmesh: %-30s) Added an oversized dirty area | Tiles marked: %2u | Source Element = %s | Bounds size = %s | Threshold: %.0f"),
|
|
*GetNameSafe(GetOwner()), PendingTilesMarked, *DirtyArea.GetSourceDescription(),
|
|
*AdjustedAreaBoundsSize.ToString(), NavSys->GetDirtyAreaWarningSizeThreshold());
|
|
}
|
|
}
|
|
});
|
|
#endif // !UE_BUILD_SHIPPING
|
|
|
|
} // QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_DirtyArea);
|
|
}
|
|
}
|
|
|
|
int32 NumTilesMarked = DirtyTiles.Num();
|
|
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_Merging);
|
|
|
|
// Merge new dirty tiles info with existing pending elements
|
|
for (FPendingTileElement& ExistingElement : PendingDirtyTiles)
|
|
{
|
|
FSetElementId Id = DirtyTiles.FindId(ExistingElement);
|
|
if (Id.IsValidId())
|
|
{
|
|
const FPendingTileElement& DirtyElement = DirtyTiles[Id];
|
|
|
|
ExistingElement.bRebuildGeometry |= DirtyElement.bRebuildGeometry;
|
|
ExistingElement.CreationTime = FMath::Min(DirtyElement.CreationTime, ExistingElement.CreationTime);
|
|
// Append area bounds to existing list
|
|
if (ExistingElement.bRebuildGeometry == false)
|
|
{
|
|
ExistingElement.DirtyAreas.Append(DirtyElement.DirtyAreas);
|
|
}
|
|
else
|
|
{
|
|
ExistingElement.DirtyAreas.Empty();
|
|
}
|
|
DirtyTiles.Remove(Id);
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
UE_SUPPRESS(LogNavigationDirtyArea, VeryVerbose,
|
|
{
|
|
// Flag everything in the map to set a true boolean (since it was already inserted)
|
|
for (FNavigationDirtyAreaPerTileDebugInformation& Pair : DirtyAreasDebugging.FindChecked(ExistingElement))
|
|
{
|
|
Pair.bTileWasAlreadyAdded = true;
|
|
}
|
|
});
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
UE_SUPPRESS(LogNavigationDirtyArea, VeryVerbose,
|
|
{
|
|
if (OwnerNav)
|
|
{
|
|
LogDirtyAreas(*OwnerNav, DirtyAreasDebugging);
|
|
}
|
|
});
|
|
#endif
|
|
|
|
// Append remaining new dirty tile elements
|
|
PendingDirtyTiles.Reserve(PendingDirtyTiles.Num() + DirtyTiles.Num());
|
|
for(const FPendingTileElement& Element : DirtyTiles)
|
|
{
|
|
PendingDirtyTiles.Add(Element);
|
|
}
|
|
|
|
// Sort tiles by proximity to players
|
|
if (NumTilesMarked > 0)
|
|
{
|
|
SortPendingBuildTiles();
|
|
}
|
|
}
|
|
|
|
// Deprecated
|
|
bool FRecastNavMeshGenerator::ShouldDirtyTilesRequestedByObject(const UNavigationSystemV1& NavSys,
|
|
const FNavigationOctree& NavOctreeInstance, const UObject& SourceObject, const FNavDataConfig& NavDataConfig) const
|
|
{
|
|
return ShouldDirtyTilesRequestedByElement(NavSys, NavOctreeInstance, FNavigationElementHandle(&SourceObject), NavDataConfig);
|
|
}
|
|
|
|
bool FRecastNavMeshGenerator::ShouldDirtyTilesRequestedByElement(
|
|
const UNavigationSystemV1& NavSys,
|
|
const FNavigationOctree& NavOctreeInstance,
|
|
const FNavigationElementHandle SourceElement,
|
|
const FNavDataConfig& NavDataConfig) const
|
|
{
|
|
const FOctreeElementId2* const OctreeElementId = NavSys.GetNavOctreeIdForElement(SourceElement);
|
|
|
|
return (OctreeElementId == nullptr) || ShouldGenerateGeometryForOctreeElement(NavOctreeInstance.GetElementById(*OctreeElementId), NavDataConfig);
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::SortPendingBuildTiles()
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_SortPendingBuildTiles);
|
|
|
|
if (SortPendingTilesMethod == ENavigationSortPendingTilesMethod::SortWithSeedLocations)
|
|
{
|
|
UWorld* CurWorld = GetWorld();
|
|
if (CurWorld == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TArray<FVector2D> SeedLocations;
|
|
GetSeedLocations(*CurWorld, SeedLocations);
|
|
|
|
if (SeedLocations.Num() == 0)
|
|
{
|
|
// Use navmesh origin for sorting
|
|
SeedLocations.Add(FVector2D(TotalNavBounds.GetCenter()));
|
|
}
|
|
|
|
if (SeedLocations.Num() > 0)
|
|
{
|
|
const FVector::FReal TileSizeInWorldUnits = Config.GetTileSizeUU();
|
|
|
|
// Calculate shortest distances between tiles and players
|
|
for (FPendingTileElement& Element : PendingDirtyTiles)
|
|
{
|
|
const FBox TileBox = FRecastTileGenerator::CalculateTileBounds(Element.Coord.X, Element.Coord.Y, FVector::ZeroVector, TotalNavBounds, TileSizeInWorldUnits);
|
|
FVector2D TileCenter2D = FVector2D(TileBox.GetCenter());
|
|
for (FVector2D SeedLocation : SeedLocations)
|
|
{
|
|
Element.SeedDistance = FMath::Min(Element.SeedDistance, FVector2D::DistSquared(TileCenter2D, SeedLocation));
|
|
}
|
|
}
|
|
|
|
// Nearest tiles should be at the end of the list
|
|
PendingDirtyTiles.Sort();
|
|
}
|
|
}
|
|
else if (SortPendingTilesMethod == ENavigationSortPendingTilesMethod::SortByPriority)
|
|
{
|
|
// Highest priority should be at the end of the list
|
|
PendingDirtyTiles.Sort([](const FPendingTileElement& A, const FPendingTileElement& B){ return A.SortingPriority < B.SortingPriority; });
|
|
|
|
UE_SUPPRESS(LogNavigation, VeryVerbose,
|
|
{
|
|
for (int32 Index = 0; Index < PendingDirtyTiles.Num(); Index++)
|
|
{
|
|
const FPendingTileElement& Element = PendingDirtyTiles[Index];
|
|
const FVector::FReal TileSizeInWorldUnits = Config.GetTileSizeUU();
|
|
const FBox TileBox = FRecastTileGenerator::CalculateTileBounds(Element.Coord.X, Element.Coord.Y, FVector::ZeroVector, TotalNavBounds, TileSizeInWorldUnits);
|
|
const ARecastNavMesh* const OwnerNav = GetOwner();
|
|
UE_VLOG_BOX(OwnerNav, LogNavigation, VeryVerbose, TileBox, FColor::Silver, TEXT("Index: %i, Priority %i"), Index, Element.SortingPriority);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::GetSeedLocations(UWorld& World, TArray<FVector2D>& OutSeedLocations) const
|
|
{
|
|
// Collect players positions
|
|
for (FConstPlayerControllerIterator PlayerIt = World.GetPlayerControllerIterator(); PlayerIt; ++PlayerIt)
|
|
{
|
|
APlayerController* PC = PlayerIt->Get();
|
|
if (PC && PC->GetPawn() != NULL)
|
|
{
|
|
const FVector2D SeedLoc(PC->GetPawn()->GetActorLocation());
|
|
OutSeedLocations.Add(SeedLoc);
|
|
}
|
|
}
|
|
}
|
|
|
|
TSharedRef<FRecastTileGenerator> FRecastNavMeshGenerator::CreateTileGenerator(const FIntPoint& Coord, const TArray<FBox>& DirtyAreas, const double PendingTileCreationTime /*=0.*/)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_CreateTileGenerator);
|
|
|
|
return ConstructTileGeneratorImpl<FRecastTileGenerator>(Coord, DirtyAreas, PendingTileCreationTime);
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::RemoveLayers(const FIntPoint& Tile, TArray<FNavTileRef>& UpdatedTiles)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_RemoveLayers);
|
|
|
|
// If there is nothing to generate remove all tiles from navmesh at specified grid coordinates
|
|
UpdatedTiles.Append(
|
|
RemoveTileLayersAndGetUpdatedTiles(Tile.X, Tile.Y)
|
|
);
|
|
DestNavMesh->MarkEmptyTileCacheLayers(Tile.X, Tile.Y);
|
|
}
|
|
|
|
void FRecastNavMeshGenerator::StoreCompressedTileCacheLayers(const FRecastTileGenerator& TileGenerator, int32 TileX, int32 TileY)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_Navigation_StoringCompressedLayers);
|
|
|
|
// Store compressed tile cache layers so it can be reused later
|
|
if (TileGenerator.GetCompressedLayers().Num())
|
|
{
|
|
DestNavMesh->AddTileCacheLayers(TileX, TileY, TileGenerator.GetCompressedLayers());
|
|
}
|
|
else
|
|
{
|
|
DestNavMesh->MarkEmptyTileCacheLayers(TileX, TileY);
|
|
}
|
|
}
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
void FRecastNavMeshGenerator::StoreDebugData(const FRecastTileGenerator& TileGenerator, int32 TileX, int32 TileY)
|
|
{
|
|
DestNavMesh->AddTileDebugData(TileX, TileY, TileGenerator.GetDebugData());
|
|
}
|
|
#endif
|
|
|
|
#if RECAST_ASYNC_REBUILDING
|
|
TArray<FNavTileRef> FRecastNavMeshGenerator::ProcessTileTasksAsyncAndGetUpdatedTiles(const int32 NumTasksToProcess)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_ProcessTileTasksAsync);
|
|
LLM_SCOPE(ELLMTag::NavigationRecast);
|
|
|
|
TArray<FNavTileRef> 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];
|
|
TRunningTileElement<FRecastTileGeneratorWrapper> 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<FAsyncTask<FRecastTileGeneratorWrapper>> TileTask = MakeUnique<FAsyncTask<FRecastTileGeneratorWrapper>>(CreateTileGenerator(PendingElement.Coord, PendingElement.DirtyAreas, PendingElement.CreationTime));
|
|
|
|
// Start it in background in case it has something to build
|
|
if (TileTask->GetTask().TileGenerator->HasDataToBuild())
|
|
{
|
|
RunningElement.AsyncTask = TileTask.Release();
|
|
|
|
if (!GNavmeshSynchronousTileGeneration)
|
|
{
|
|
RunningElement.AsyncTask->StartBackgroundTask();
|
|
}
|
|
else
|
|
{
|
|
RunningElement.AsyncTask->StartSynchronousTask();
|
|
}
|
|
|
|
static int32 Count = 1;
|
|
UE_LOG(LogNavigationDataBuild, VeryVerbose, TEXT(" Tile generation task #%i)"), Count);
|
|
Count++;
|
|
|
|
RunningDirtyTiles.Add(RunningElement);
|
|
}
|
|
else if (!bGameStaticNavMesh)
|
|
{
|
|
RemoveLayers(PendingElement.Coord, UpdatedTiles);
|
|
}
|
|
|
|
// Remove submitted element from pending list
|
|
PendingDirtyTiles.RemoveAt(ElementIdx, EAllowShrinking::No);
|
|
NumProcessedTasks++;
|
|
}
|
|
}
|
|
|
|
// Release memory, list could be quite big after map load
|
|
if (NumProcessedTasks > 0 && PendingDirtyTiles.Num() == 0)
|
|
{
|
|
PendingDirtyTiles.Empty(64);
|
|
}
|
|
|
|
// 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);
|
|
|
|
TRunningTileElement<FRecastTileGeneratorWrapper>& Element = RunningDirtyTiles[Idx];
|
|
check(Element.AsyncTask);
|
|
|
|
if (Element.AsyncTask->IsDone())
|
|
{
|
|
FRecastTileGenerator& TileGenerator = *(Element.AsyncTask->GetTask().TileGenerator);
|
|
|
|
// Add generated tiles to navmesh
|
|
if (!Element.bShouldDiscard)
|
|
{
|
|
TArray<FNavTileRef> UpdatedTileRefs = AddGeneratedTilesAndGetUpdatedTiles(TileGenerator);
|
|
UpdatedTiles.Append(UpdatedTileRefs);
|
|
|
|
StoreCompressedTileCacheLayers(TileGenerator, Element.Coord.X, Element.Coord.Y);
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
StoreDebugData(TileGenerator, Element.Coord.X, Element.Coord.Y);
|
|
#endif
|
|
}
|
|
|
|
TileGenerator.DumpSyncData();
|
|
|
|
{
|
|
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, EAllowShrinking::No);
|
|
}
|
|
}
|
|
}
|
|
|
|
return UpdatedTiles;
|
|
}
|
|
#endif
|
|
|
|
#if !RECAST_ASYNC_REBUILDING
|
|
TSharedRef<FRecastTileGenerator> FRecastNavMeshGenerator::CreateTileGeneratorFromPendingElement(FIntPoint& OutTileLocation, const int32 ForcedPendingTileIdx)
|
|
{
|
|
ensureMsgf(PendingDirtyTiles.Num() > 0, TEXT("Its an assumption of this function that PendingDirtyTiles.Num() > 0"));
|
|
ensureMsgf(ForcedPendingTileIdx == INDEX_NONE || PendingDirtyTiles.IsValidIndex(ForcedPendingTileIdx), TEXT("The pending tile index provided (%d) is invalid. There are %d pending dirty tiles"), ForcedPendingTileIdx, PendingDirtyTiles.Num());
|
|
|
|
const int32 PendingItemIdx = ForcedPendingTileIdx == INDEX_NONE ? PendingDirtyTiles.Num() - 1 : ForcedPendingTileIdx;
|
|
FPendingTileElement& PendingElement = PendingDirtyTiles[PendingItemIdx];
|
|
|
|
OutTileLocation.X = PendingElement.Coord.X;
|
|
OutTileLocation.Y = PendingElement.Coord.Y;
|
|
|
|
TSharedRef<FRecastTileGenerator> TileGenerator = CreateTileGenerator(PendingElement.Coord, PendingElement.DirtyAreas, PendingElement.CreationTime);
|
|
|
|
PendingDirtyTiles.RemoveAt(PendingItemIdx, EAllowShrinking::No);
|
|
|
|
return TileGenerator;
|
|
}
|
|
|
|
TArray<FNavTileRef> FRecastNavMeshGenerator::ProcessTileTasksSyncTimeSlicedAndGetUpdatedTiles()
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_ProcessTileTasksSyncTimeSliced);
|
|
CSV_SCOPED_TIMING_STAT(NAVREGEN, ProcessTileTasksSyncTimeSliced);
|
|
LLM_SCOPE(ELLMTag::NavigationRecast);
|
|
|
|
check(SyncTimeSlicedData.TimeSliceManager);
|
|
|
|
TArray<FNavTileRef> UpdatedTiles;
|
|
double TimeStartProcessingTileThisFrame = 0.;
|
|
|
|
auto HasWorkToDo = [this](int32& OutNextPendingDirtyTileIndex)
|
|
{
|
|
OutNextPendingDirtyTileIndex = INDEX_NONE;
|
|
if (SyncTimeSlicedData.TileGeneratorSync.IsValid())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
OutNextPendingDirtyTileIndex = GetNextPendingDirtyTileToBuild();
|
|
return OutNextPendingDirtyTileIndex != INDEX_NONE;
|
|
};
|
|
|
|
auto EndFunction = [&, this](bool bCalcTileRegenDuration, bool bStartedTimeSlice)
|
|
{
|
|
// Release memory, list could be quite big after map load
|
|
if (PendingDirtyTiles.Num() == 0)
|
|
{
|
|
PendingDirtyTiles.Empty(64);
|
|
}
|
|
|
|
//this will only be true when we haven't finished generating this tile but are ending
|
|
//the function and need to record the TileRegenDuration so far for the tile
|
|
//being currently processed
|
|
if (bCalcTileRegenDuration)
|
|
{
|
|
SyncTimeSlicedData.CurrentTileRegenDuration += (FPlatformTime::Seconds() - TimeStartProcessingTileThisFrame);
|
|
}
|
|
|
|
if (bStartedTimeSlice)
|
|
{
|
|
SyncTimeSlicedData.TimeSliceManager->GetTimeSlicer().EndTimeSliceAndAdjustDuration();
|
|
}
|
|
|
|
#if ALLOW_TIME_SLICE_DEBUG
|
|
// Reset the debug function to make sure the captured variables can't be used when invalid.
|
|
// This is just bombproofing the code as TestTimeSliceFinished() should not be called until
|
|
// ProcessTileTasksSyncTimeSlicedAndGetUpdatedTiles() is next called.
|
|
SyncTimeSlicedData.TimeSliceManager->GetTimeSlicer().DebugResetLongTimeSliceFunction();
|
|
#endif // ALLOW_TIME_SLICE_DEBUG
|
|
|
|
return UpdatedTiles;
|
|
};
|
|
|
|
#if ALLOW_TIME_SLICE_DEBUG
|
|
auto DebugLongTileRegenFunction = [this](FName DebugSectionName, double Duration)
|
|
{
|
|
const FRecastTileGenerator* const TileGenerator = SyncTimeSlicedData.TileGeneratorSync.Get();
|
|
|
|
// This shouldn't trigger but its fairly easy during development to accidentaly call TestTimeSliceFinished() when TileGenerator == nullptr.
|
|
if (ensure(TileGenerator))
|
|
{
|
|
const FVector Pos = SyncTimeSlicedData.TileGeneratorSync->GetTileBB().GetCenter();
|
|
|
|
check(DestNavMesh);
|
|
|
|
// I'd quite like to make this a Warning, but it would be too frequently logged as things stand.
|
|
UE_LOG(LogNavigation, Verbose, TEXT("Nav mesh data: %s, tile at %d, %d, coordinate %f, %f, %f, section %s is taking %f secs to partially regenerate!"), *DestNavMesh->GetName(), TileGenerator->GetTileX(), TileGenerator->GetTileY(), Pos.X, Pos.Y, Pos.Z, *DebugSectionName.ToString(), Duration);
|
|
UE_VLOG_BOX(DestNavMesh, LogNavigation, Verbose, TileGenerator->GetTileBB(), FColor::Red, TEXT("Nav mesh data : %s, tile at %d, %d, section %s is taking %f secs to partially regenerate!"), *DestNavMesh->GetName(), TileGenerator->GetTileX(), TileGenerator->GetTileY(), *DebugSectionName.ToString(), Duration);
|
|
}
|
|
};
|
|
|
|
SyncTimeSlicedData.TimeSliceManager->GetTimeSlicer().DebugSetLongTimeSliceData(DebugLongTileRegenFunction, DestNavMesh->TimeSliceLongDurationDebug);
|
|
#endif // ALLOW_TIME_SLICE_DEBUG
|
|
|
|
int32 NextPendingDirtyTileIndex = INDEX_NONE;
|
|
const bool bHadWorkToDo = HasWorkToDo(NextPendingDirtyTileIndex);
|
|
|
|
// Only calculate the time slice and process tiles if we have work to do.
|
|
if (bHadWorkToDo)
|
|
{
|
|
SyncTimeSlicedData.TimeSliceManager->GetTimeSlicer().StartTimeSlice();
|
|
|
|
const bool bGameStaticNavMesh = IsGameStaticNavMesh(DestNavMesh);
|
|
|
|
// Submit pending tile elements
|
|
do
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_ProcessTileTasks_NewTasks);
|
|
|
|
FIntPoint TileLocation;
|
|
TimeStartProcessingTileThisFrame = FPlatformTime::Seconds();
|
|
|
|
if (SyncTimeSlicedData.ProcessTileTasksSyncState == EProcessTileTasksSyncTimeSlicedState::Init)
|
|
{
|
|
//if the next time slice regen state is false, we want to go to non time sliced tile regen so break here and switch
|
|
//next frame (as we've finished time slice processing the last tile)
|
|
if (!SyncTimeSlicedData.bNextTimeSliceRegenActive)
|
|
{
|
|
return EndFunction(false /* bCalcTileRegenDuration */, bHadWorkToDo);
|
|
}
|
|
|
|
SyncTimeSlicedData.TileGeneratorSync = CreateTileGeneratorFromPendingElement(TileLocation, NextPendingDirtyTileIndex);
|
|
|
|
check(SyncTimeSlicedData.TileGeneratorSync);
|
|
|
|
SyncTimeSlicedData.CurrentTileRegenDuration = 0.;
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
SyncTimeSlicedData.TileRegenStartFrame = GFrameCounter;
|
|
#endif // !UE_BUILD_SHIPPING
|
|
|
|
if (SyncTimeSlicedData.TileGeneratorSync->HasDataToBuild())
|
|
{
|
|
SyncTimeSlicedData.ProcessTileTasksSyncState = EProcessTileTasksSyncTimeSlicedState::DoWork;
|
|
}
|
|
else
|
|
{
|
|
SyncTimeSlicedData.ProcessTileTasksSyncState = EProcessTileTasksSyncTimeSlicedState::Finish;
|
|
|
|
if (!bGameStaticNavMesh)
|
|
{
|
|
RemoveLayers(TileLocation, UpdatedTiles);
|
|
}
|
|
}
|
|
|
|
MARK_TIMESLICE_SECTION_DEBUG(SyncTimeSlicedData.TimeSliceManager->GetTimeSlicer(), CreateTileGenerator);
|
|
|
|
if (SyncTimeSlicedData.TimeSliceManager->GetTimeSlicer().TestTimeSliceFinished())
|
|
{
|
|
return EndFunction(true /* bCalcTileRegenDuration */, bHadWorkToDo);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
check(SyncTimeSlicedData.TileGeneratorSync);
|
|
|
|
TileLocation.X = SyncTimeSlicedData.TileGeneratorSync->GetTileX();
|
|
TileLocation.Y = SyncTimeSlicedData.TileGeneratorSync->GetTileY();
|
|
}
|
|
|
|
FRecastTileGenerator& TileGeneratorRef = *SyncTimeSlicedData.TileGeneratorSync;
|
|
|
|
switch (SyncTimeSlicedData.ProcessTileTasksSyncState)
|
|
{
|
|
case EProcessTileTasksSyncTimeSlicedState::Init:
|
|
{
|
|
//do nothing
|
|
ensureMsgf(false, TEXT("This State should not be used here!"));
|
|
}
|
|
break;
|
|
|
|
case EProcessTileTasksSyncTimeSlicedState::DoWork:
|
|
{
|
|
const ETimeSliceWorkResult WorkResult = TileGeneratorRef.DoWorkTimeSliced();
|
|
|
|
if (WorkResult != ETimeSliceWorkResult::CallAgainNextTimeSlice)
|
|
{
|
|
SyncTimeSlicedData.ProcessTileTasksSyncState = EProcessTileTasksSyncTimeSlicedState::AddGeneratedTiles;
|
|
}
|
|
|
|
if (SyncTimeSlicedData.TimeSliceManager->GetTimeSlicer().IsTimeSliceFinishedCached())
|
|
{
|
|
return EndFunction(true /* bCalcTileRegenDuration */, bHadWorkToDo);
|
|
}
|
|
}//fall through to next state
|
|
case EProcessTileTasksSyncTimeSlicedState::AddGeneratedTiles:
|
|
{
|
|
const ETimeSliceWorkResult WorkResult = AddGeneratedTilesTimeSliced(TileGeneratorRef, SyncTimeSlicedData.UpdatedTilesCache);
|
|
|
|
if (WorkResult != ETimeSliceWorkResult::CallAgainNextTimeSlice)
|
|
{
|
|
SyncTimeSlicedData.ProcessTileTasksSyncState = EProcessTileTasksSyncTimeSlicedState::StoreCompessedTileCacheLayers;
|
|
}
|
|
|
|
if (SyncTimeSlicedData.TimeSliceManager->GetTimeSlicer().IsTimeSliceFinishedCached())
|
|
{
|
|
return EndFunction(true /* bCalcTileRegenDuration */, bHadWorkToDo);
|
|
}
|
|
}//fall through to next state
|
|
case EProcessTileTasksSyncTimeSlicedState::StoreCompessedTileCacheLayers:
|
|
{
|
|
StoreCompressedTileCacheLayers(TileGeneratorRef, TileLocation.X, TileLocation.Y);
|
|
|
|
//no need to check time slicing as not much work done
|
|
SyncTimeSlicedData.ProcessTileTasksSyncState = EProcessTileTasksSyncTimeSlicedState::AppendUpdateTiles;
|
|
}//fall through to next state
|
|
case EProcessTileTasksSyncTimeSlicedState::AppendUpdateTiles: //this state was added purely to separate the functionality and allow the code to be more easily changed in future.
|
|
{
|
|
UpdatedTiles.Append(SyncTimeSlicedData.UpdatedTilesCache);
|
|
SyncTimeSlicedData.UpdatedTilesCache.Empty();
|
|
|
|
//no need to check time slicing as not much work done
|
|
SyncTimeSlicedData.ProcessTileTasksSyncState = EProcessTileTasksSyncTimeSlicedState::Finish;
|
|
}//fall through to next state
|
|
case EProcessTileTasksSyncTimeSlicedState::Finish:
|
|
{
|
|
//reset state to Init for next tile to be processed
|
|
SyncTimeSlicedData.ProcessTileTasksSyncState = EProcessTileTasksSyncTimeSlicedState::Init;
|
|
|
|
SyncTimeSlicedData.CurrentTileRegenDuration += (FPlatformTime::Seconds() - TimeStartProcessingTileThisFrame);
|
|
#if !UE_BUILD_SHIPPING
|
|
const double TempRegenDuration = SyncTimeSlicedData.CurrentTileRegenDuration;
|
|
#endif
|
|
|
|
SyncTimeSlicedData.TimeSliceManager->PushTileRegenTime(SyncTimeSlicedData.CurrentTileRegenDuration);
|
|
|
|
SyncTimeSlicedData.CurrentTileRegenDuration = 0.;
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
SyncTimeSlicedData.TileRegenEndFrame = GFrameCounter;
|
|
|
|
const UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
|
|
if (NavSys)
|
|
{
|
|
const TSharedPtr<FRecastTileGenerator>& TileGenerator = SyncTimeSlicedData.TileGeneratorSync;
|
|
const double WaitTime = (FPlatformTime::Seconds() - TileGenerator->TileCreationTime);
|
|
const int32 NavDataIndex = NavSys->NavDataSet.Find(DestNavMesh);
|
|
SyncTimeSlicedData.TimeSliceManager->PushTileWaitTime(NavDataIndex, WaitTime);
|
|
|
|
UE_SUPPRESS(LogNavigationHistory, Log,
|
|
{
|
|
FTileHistoryData HistoryData;
|
|
HistoryData.TileX = TileGenerator->TileX;
|
|
HistoryData.TileY = TileGenerator->TileY;
|
|
HistoryData.TileRegenTime = (float)TempRegenDuration;
|
|
HistoryData.TileWaitTime = (float)WaitTime;
|
|
HistoryData.StartRegenFrame = SyncTimeSlicedData.TileRegenStartFrame;
|
|
HistoryData.EndRegenFrame = SyncTimeSlicedData.TileRegenEndFrame;
|
|
SyncTimeSlicedData.TimeSliceManager->PushTileHistoryData(NavDataIndex, HistoryData);
|
|
});
|
|
}
|
|
#endif // !UE_BUILD_SHIPPING
|
|
|
|
MARK_TIMESLICE_SECTION_DEBUG(SyncTimeSlicedData.TimeSliceManager->GetTimeSlicer(), FinishTile);
|
|
|
|
//test time slice
|
|
const bool bTimeSliceFinished = SyncTimeSlicedData.TimeSliceManager->GetTimeSlicer().TestTimeSliceFinished();
|
|
|
|
//reset TileGeneratorSync after the last call to TestTimeSliceFinished for this tile, otherwise we may end up
|
|
//trying to access TileGeneratorSync from DebugLongTileRegenFunction()
|
|
SyncTimeSlicedData.TileGeneratorSync.Reset();
|
|
|
|
if (bTimeSliceFinished)
|
|
{
|
|
//we just calculated and set TileRegenDuration so no need to calculate it again
|
|
return EndFunction(false /* bCalcTileRegenDuration */, bHadWorkToDo);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
{
|
|
ensureMsgf(false, TEXT("unhandled EProcessTileTasksSyncTimeSlicedState"));
|
|
}
|
|
}
|
|
}
|
|
while (HasWorkToDo(NextPendingDirtyTileIndex));
|
|
}
|
|
|
|
// we only hit this if we have processed too many tiles in a frame and we will already
|
|
// have calculated the tile regen duration, or if we have processed no tiles and we also
|
|
// don't want to calculate the tile regen duration
|
|
return EndFunction(false /* bCalcTileRegenDuration */, bHadWorkToDo);
|
|
}
|
|
|
|
int32 FRecastNavMeshGenerator::GetNextPendingDirtyTileToBuild() const
|
|
{
|
|
return PendingDirtyTiles.IsEmpty() ? INDEX_NONE : PendingDirtyTiles.Num() - 1;
|
|
}
|
|
|
|
//this code path is approx 10% faster than ProcessTileTasksSyncTimeSliced, however it spikes far worse for most use cases.
|
|
TArray<FNavTileRef> FRecastNavMeshGenerator::ProcessTileTasksSyncAndGetUpdatedTiles(const int32 NumTasksToProcess)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_ProcessTileTasksSync);
|
|
|
|
const bool bGameStaticNavMesh = IsGameStaticNavMesh(DestNavMesh);
|
|
int32 NumProcessedTasks = 0;
|
|
TArray<FNavTileRef> UpdatedTiles;
|
|
FIntPoint TileLocation;
|
|
|
|
// Submit pending tile elements
|
|
while ((PendingDirtyTiles.Num() > 0 && NumProcessedTasks < NumTasksToProcess))
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_ProcessTileTasks_NewTasks);
|
|
|
|
TSharedRef<FRecastTileGenerator> TileGenerator = CreateTileGeneratorFromPendingElement(TileLocation);
|
|
|
|
FRecastTileGenerator& TileGeneratorRef = *TileGenerator;
|
|
|
|
//Does this remain true whenever we stop time slicing?
|
|
if (TileGeneratorRef.HasDataToBuild())
|
|
{
|
|
TileGeneratorRef.DoWork();
|
|
|
|
UpdatedTiles = AddGeneratedTilesAndGetUpdatedTiles(TileGeneratorRef);
|
|
|
|
StoreCompressedTileCacheLayers(TileGeneratorRef, TileLocation.X, TileLocation.Y);
|
|
}
|
|
else if (!bGameStaticNavMesh)
|
|
{
|
|
RemoveLayers(TileLocation, UpdatedTiles);
|
|
}
|
|
|
|
NumProcessedTasks++;
|
|
}
|
|
|
|
// Release memory, list could be quite big after map load
|
|
if (PendingDirtyTiles.Num() == 0)
|
|
{
|
|
PendingDirtyTiles.Empty(64);
|
|
}
|
|
|
|
return UpdatedTiles;
|
|
}
|
|
#endif
|
|
|
|
TArray<FNavTileRef> FRecastNavMeshGenerator::ProcessTileTasksAndGetUpdatedTiles(const int32 NumTasksToProcess)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_ProcessTileTasks);
|
|
|
|
const bool bHasTasksAtStart = GetNumRemaningBuildTasks() > 0;
|
|
TArray<FNavTileRef> UpdatedTiles;
|
|
|
|
#if RECAST_ASYNC_REBUILDING
|
|
UpdatedTiles = ProcessTileTasksAsyncAndGetUpdatedTiles(NumTasksToProcess);
|
|
#else
|
|
if (SyncTimeSlicedData.TimeSliceManager)
|
|
{
|
|
//only switch bTimeSliceRegen state if we are not time slicing or if we are but aren't part way through time slicing a tile
|
|
if (SyncTimeSlicedData.bTimeSliceRegenActive != SyncTimeSlicedData.bNextTimeSliceRegenActive)
|
|
{
|
|
if (!SyncTimeSlicedData.bTimeSliceRegenActive)
|
|
{
|
|
SyncTimeSlicedData.bTimeSliceRegenActive = SyncTimeSlicedData.bNextTimeSliceRegenActive;
|
|
}
|
|
else if (!SyncTimeSlicedData.TileGeneratorSync.IsValid())//test if we have finished processing a tile
|
|
{
|
|
SyncTimeSlicedData.bTimeSliceRegenActive = SyncTimeSlicedData.bNextTimeSliceRegenActive;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//No time slice manager no timesliced regen
|
|
SyncTimeSlicedData.bTimeSliceRegenActive = false;
|
|
}
|
|
|
|
if (SyncTimeSlicedData.bTimeSliceRegenActive)
|
|
{
|
|
UpdatedTiles = ProcessTileTasksSyncTimeSlicedAndGetUpdatedTiles();
|
|
}
|
|
else
|
|
{
|
|
UpdatedTiles = ProcessTileTasksSyncAndGetUpdatedTiles(NumTasksToProcess);
|
|
}
|
|
#endif
|
|
|
|
#if WITH_NAVMESH_SEGMENT_LINKS
|
|
const dtNavMesh* DetourMesh = DestNavMesh->GetRecastNavMeshImpl()->GetRecastMesh();
|
|
if (DetourMesh)
|
|
{
|
|
TilesPendingSegmentLinkConnections.Reserve(TilesPendingSegmentLinkConnections.Num() + UpdatedTiles.Num());
|
|
for (FNavTileRef TileRef : UpdatedTiles)
|
|
{
|
|
TilesPendingSegmentLinkConnections.Add(TileRef);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Notify owner in case all tasks has been completed
|
|
const bool bHasTasksAtEnd = GetNumRemaningBuildTasks() > 0;
|
|
if (bHasTasksAtStart && !bHasTasksAtEnd)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_OnNavMeshGenerationFinished);
|
|
|
|
if (RebuildAllStartTime != 0)
|
|
{
|
|
UE_LOG(LogNavigationDataBuild, Display, TEXT(" %hs build time: %.2fs"), __FUNCTION__, (FPlatformTime::Seconds() - RebuildAllStartTime));
|
|
RebuildAllStartTime = 0;
|
|
}
|
|
|
|
#if WITH_NAVMESH_SEGMENT_LINKS
|
|
DestNavMesh->CreateSegmentLinkConnections(TilesPendingSegmentLinkConnections);
|
|
TilesPendingSegmentLinkConnections.Empty();
|
|
#endif
|
|
|
|
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())
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RecastNavMeshGenerator_GetCompressedTileCacheSize);
|
|
|
|
int32 TileCacheSize = DestNavMesh->GetCompressedTileCacheSize();
|
|
|
|
FPlatformMisc::CustomNamedStat("TotalTileCacheSize", static_cast<float>(TileCacheSize), "NavMesh", "Bytes");
|
|
}
|
|
#endif
|
|
return UpdatedTiles;
|
|
}
|
|
|
|
#if UE_ENABLE_DEBUG_DRAWING
|
|
void FRecastNavMeshGenerator::GetDebugGeometry(const FNavigationRelevantData& EncodedData, FNavDebugMeshData& DebugMeshData)
|
|
{
|
|
const uint8* RawMemory = EncodedData.CollisionData.GetData();
|
|
if (RawMemory == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
const FRecastGeometryCache::FHeader* HeaderInfo = reinterpret_cast<const FRecastGeometryCache::FHeader*>(RawMemory);
|
|
if (HeaderInfo->NumVerts == 0 || HeaderInfo->NumFaces == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const int32 HeaderSize = sizeof(FRecastGeometryCache);
|
|
const int32 IndicesCount = HeaderInfo->NumFaces * 3;
|
|
|
|
DebugMeshData.Vertices.AddZeroed(HeaderInfo->NumVerts);
|
|
FDynamicMeshVertex* DebugVert = DebugMeshData.Vertices.GetData();
|
|
// we cannot copy verts directly since not only are the EncodedData's verts in
|
|
// a FVector::FReal[3] format, they're also in Recast coords so we need to translate it
|
|
// back to Unreal coords
|
|
const FVector::FReal* VertCoord = reinterpret_cast<const FVector::FReal*>(RawMemory + HeaderSize);
|
|
for (int VertIndex = 0; VertIndex < HeaderInfo->NumVerts; ++VertIndex, ++DebugVert, VertCoord += 3)
|
|
{
|
|
new (DebugVert) FDynamicMeshVertex((FVector3f)Recast2UnrealPoint(VertCoord));
|
|
}
|
|
|
|
DebugMeshData.Indices.AddZeroed(IndicesCount);
|
|
FMemory::Memcpy(DebugMeshData.Indices.GetData(), RawMemory + HeaderSize + HeaderInfo->NumVerts * 3 * sizeof(FVector::FReal), IndicesCount * sizeof(int32));
|
|
}
|
|
#endif // !UE_BUILD_SHIPPING
|
|
|
|
// Deprecated
|
|
void FRecastNavMeshGenerator::ExportComponentGeometry(UActorComponent* InOutComponent, FNavigationRelevantData& OutData)
|
|
{
|
|
if (INavRelevantInterface* NavRelevantInterface = Cast<INavRelevantInterface>(InOutComponent))
|
|
{
|
|
const TSharedRef<const FNavigationElement> TmpElement = FNavigationElement::CreateFromNavRelevantInterface(*NavRelevantInterface);
|
|
FRecastGeometryExport::ExportElementGeometry(TmpElement.Get(), OutData);
|
|
}
|
|
}
|
|
|
|
// Deprecated
|
|
void FRecastNavMeshGenerator::ExportNavRelevantObjectGeometry(INavRelevantInterface& InOutNavRelevantInterface, FNavigationRelevantData& OutData)
|
|
{
|
|
const TSharedRef<const FNavigationElement> TmpElement = FNavigationElement::CreateFromNavRelevantInterface(InOutNavRelevantInterface);
|
|
FRecastGeometryExport::ExportElementGeometry(TmpElement.Get(), OutData);
|
|
}
|
|
|
|
void FRecastGeometryExport::ExportElementGeometry(const FNavigationElement& InElement, FNavigationRelevantData& OutData)
|
|
{
|
|
FRecastGeometryExport GeomExport(OutData);
|
|
RecastGeometryExport::ExportObject(InElement, GeomExport);
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
RecastGeometryExport::ValidateGeometryExport(GeomExport);
|
|
#endif
|
|
|
|
RecastGeometryExport::ConvertCoordDataToRecast(GeomExport.VertexBuffer);
|
|
RecastGeometryExport::StoreCollisionCache(GeomExport);
|
|
}
|
|
|
|
// Deprecated
|
|
void FRecastNavMeshGenerator::ExportVertexSoupGeometry(const TArray<FVector>& InVerts, FNavigationRelevantData& OutData)
|
|
{
|
|
FRecastGeometryExport::ExportVertexSoupGeometry(InVerts, OutData);
|
|
}
|
|
|
|
void FRecastGeometryExport::ExportVertexSoupGeometry(const TArray<FVector>& InVerts, FNavigationRelevantData& OutData)
|
|
{
|
|
FRecastGeometryExport GeomExport(OutData);
|
|
RecastGeometryExport::ExportVertexSoup(InVerts, GeomExport.VertexBuffer, GeomExport.IndexBuffer, GeomExport.Data->Bounds);
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
RecastGeometryExport::ValidateGeometryExport(GeomExport);
|
|
#endif
|
|
|
|
RecastGeometryExport::StoreCollisionCache(GeomExport);
|
|
}
|
|
|
|
// Deprecated
|
|
void FRecastNavMeshGenerator::ExportRigidBodyGeometry(
|
|
UBodySetup& InOutBodySetup,
|
|
TNavStatArray<FVector>& OutVertexBuffer,
|
|
TNavStatArray<int32>& OutIndexBuffer,
|
|
const FTransform& LocalToWorld)
|
|
{
|
|
FBox TempBounds;
|
|
FRecastGeometryExport::ExportRigidBodyGeometry(InOutBodySetup, OutVertexBuffer, OutIndexBuffer, TempBounds, LocalToWorld);
|
|
}
|
|
|
|
// Deprecated
|
|
void FRecastNavMeshGenerator::ExportRigidBodyGeometry(
|
|
UBodySetup& InOutBodySetup,
|
|
TNavStatArray<FVector>& OutVertexBuffer,
|
|
TNavStatArray<int32>& OutIndexBuffer,
|
|
FBox& OutBounds,
|
|
const FTransform& LocalToWorld)
|
|
{
|
|
FRecastGeometryExport::ExportRigidBodyGeometry(InOutBodySetup, OutVertexBuffer, OutIndexBuffer, OutBounds, LocalToWorld);
|
|
}
|
|
|
|
void FRecastGeometryExport::ExportRigidBodyGeometry(UBodySetup& InOutBodySetup, TNavStatArray<FVector>& OutVertexBuffer, TNavStatArray<int32>& OutIndexBuffer, FBox& OutBounds, const FTransform& LocalToWorld)
|
|
{
|
|
TNavStatArray<FVector::FReal> VertCoords;
|
|
RecastGeometryExport::ExportRigidBodySetup(InOutBodySetup, VertCoords, OutIndexBuffer, OutBounds, 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]));
|
|
}
|
|
}
|
|
|
|
// Deprecated
|
|
void FRecastNavMeshGenerator::ExportRigidBodyGeometry(
|
|
UBodySetup& InOutBodySetup,
|
|
TNavStatArray<FVector>& OutTriMeshVertexBuffer,
|
|
TNavStatArray<int32>& OutTriMeshIndexBuffer,
|
|
TNavStatArray<FVector>& OutConvexVertexBuffer,
|
|
TNavStatArray<int32>& OutConvexIndexBuffer,
|
|
TNavStatArray<int32>& OutShapeBuffer,
|
|
const FTransform& LocalToWorld)
|
|
{
|
|
FBox TempBounds;
|
|
FRecastGeometryExport::ExportRigidBodyGeometry(
|
|
InOutBodySetup,
|
|
OutTriMeshVertexBuffer,
|
|
OutTriMeshIndexBuffer,
|
|
OutConvexVertexBuffer,
|
|
OutConvexIndexBuffer,
|
|
OutShapeBuffer,
|
|
TempBounds,
|
|
LocalToWorld);
|
|
}
|
|
|
|
// Deprecated
|
|
void FRecastNavMeshGenerator::ExportRigidBodyGeometry(
|
|
UBodySetup& InOutBodySetup,
|
|
TNavStatArray<FVector>& OutTriMeshVertexBuffer,
|
|
TNavStatArray<int32>& OutTriMeshIndexBuffer,
|
|
TNavStatArray<FVector>& OutConvexVertexBuffer,
|
|
TNavStatArray<int32>& OutConvexIndexBuffer,
|
|
TNavStatArray<int32>& OutShapeBuffer,
|
|
FBox& OutBounds,
|
|
const FTransform& LocalToWorld)
|
|
{
|
|
FRecastGeometryExport::ExportRigidBodyGeometry(
|
|
InOutBodySetup,
|
|
OutTriMeshVertexBuffer,
|
|
OutTriMeshIndexBuffer,
|
|
OutConvexVertexBuffer,
|
|
OutConvexIndexBuffer,
|
|
OutShapeBuffer,
|
|
OutBounds,
|
|
LocalToWorld);
|
|
}
|
|
|
|
void FRecastGeometryExport::ExportRigidBodyGeometry(UBodySetup& InOutBodySetup, TNavStatArray<FVector>& OutTriMeshVertexBuffer, TNavStatArray<int32>& OutTriMeshIndexBuffer, TNavStatArray<FVector>& OutConvexVertexBuffer, TNavStatArray<int32>& OutConvexIndexBuffer, TNavStatArray<int32>& OutShapeBuffer, FBox& OutBounds, const FTransform& LocalToWorld)
|
|
{
|
|
InOutBodySetup.CreatePhysicsMeshes();
|
|
|
|
TNavStatArray<FVector::FReal> VertCoords;
|
|
RecastGeometryExport::ExportRigidBodyTriMesh(InOutBodySetup, VertCoords, OutTriMeshIndexBuffer, OutBounds, 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(InOutBodySetup, VertCoords, OutConvexIndexBuffer, OutShapeBuffer, OutBounds, LocalToWorld);
|
|
RecastGeometryExport::ExportRigidBodyBoxElements(InOutBodySetup.AggGeom, VertCoords, OutConvexIndexBuffer, OutShapeBuffer, OutBounds, LocalToWorld, NumExistingVerts);
|
|
RecastGeometryExport::ExportRigidBodySphylElements(InOutBodySetup.AggGeom, VertCoords, OutConvexIndexBuffer, OutShapeBuffer, OutBounds, LocalToWorld, NumExistingVerts);
|
|
RecastGeometryExport::ExportRigidBodySphereElements(InOutBodySetup.AggGeom, VertCoords, OutConvexIndexBuffer, OutShapeBuffer, OutBounds, 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]));
|
|
}
|
|
}
|
|
|
|
// Deprecated
|
|
void FRecastNavMeshGenerator::ExportAggregatedGeometry(
|
|
const FKAggregateGeom& AggGeom,
|
|
TNavStatArray<FVector>& OutConvexVertexBuffer,
|
|
TNavStatArray<int32>& OutConvexIndexBuffer,
|
|
TNavStatArray<int32>& OutShapeBuffer,
|
|
const FTransform& LocalToWorld)
|
|
{
|
|
FBox TempBounds;
|
|
FRecastGeometryExport::ExportAggregatedGeometry(AggGeom, OutConvexVertexBuffer, OutConvexIndexBuffer, OutShapeBuffer, TempBounds, LocalToWorld);
|
|
}
|
|
|
|
// Deprecated
|
|
void FRecastNavMeshGenerator::ExportAggregatedGeometry(
|
|
const FKAggregateGeom& AggGeom,
|
|
TNavStatArray<FVector>& OutConvexVertexBuffer,
|
|
TNavStatArray<int32>& OutConvexIndexBuffer,
|
|
TNavStatArray<int32>& OutShapeBuffer,
|
|
FBox& OutBounds,
|
|
const FTransform& LocalToWorld)
|
|
{
|
|
FRecastGeometryExport::ExportAggregatedGeometry(AggGeom, OutConvexVertexBuffer, OutConvexIndexBuffer, OutShapeBuffer, OutBounds, LocalToWorld);
|
|
}
|
|
|
|
void FRecastGeometryExport::ExportAggregatedGeometry(const FKAggregateGeom& AggGeom, TNavStatArray<FVector>& OutConvexVertexBuffer, TNavStatArray<int32>& OutConvexIndexBuffer, TNavStatArray<int32>& OutShapeBuffer, FBox& OutBounds, const FTransform& LocalToWorld)
|
|
{
|
|
TNavStatArray<FVector::FReal> VertCoords;
|
|
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, OutBounds, LocalToWorld, NumExistingVerts);
|
|
RecastGeometryExport::ExportRigidBodySphylElements(AggGeom, VertCoords, OutConvexIndexBuffer, OutShapeBuffer, OutBounds, LocalToWorld, NumExistingVerts);
|
|
RecastGeometryExport::ExportRigidBodySphereElements(AggGeom, VertCoords, OutConvexIndexBuffer, OutShapeBuffer, OutBounds, 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::IsBuildInProgressCheckDirty() const
|
|
{
|
|
return RunningDirtyTiles.Num()
|
|
|| PendingDirtyTiles.Num()
|
|
|| SyncTimeSlicedData.TileGeneratorSync.IsValid();
|
|
}
|
|
|
|
#if !RECAST_ASYNC_REBUILDING
|
|
bool FRecastNavMeshGenerator::GetTimeSliceData(int32& OutNumRemainingBuildTasks, double& OutCurrentBuildTaskDuration) const
|
|
{
|
|
//it's probably just faster to calculate these rather than branch and only calculate them if bTimeSliceRegenActive is true;
|
|
OutNumRemainingBuildTasks = GetNumRemaningBuildTasksHelper();
|
|
OutCurrentBuildTaskDuration = SyncTimeSlicedData.CurrentTileRegenDuration;
|
|
return SyncTimeSlicedData.bTimeSliceRegenActive;
|
|
}
|
|
#endif
|
|
|
|
int32 FRecastNavMeshGenerator::GetNumRemaningBuildTasks() const
|
|
{
|
|
return GetNumRemaningBuildTasksHelper();
|
|
}
|
|
|
|
int32 FRecastNavMeshGenerator::GetNumRunningBuildTasks() const
|
|
{
|
|
return RunningDirtyTiles.Num()
|
|
+ (SyncTimeSlicedData.TileGeneratorSync.Get() ? 1 : 0);
|
|
}
|
|
|
|
bool FRecastNavMeshGenerator::GatherGeometryOnGameThread() const
|
|
{
|
|
return DestNavMesh == nullptr || DestNavMesh->ShouldGatherDataOnGameThread() == true;
|
|
}
|
|
|
|
bool FRecastNavMeshGenerator::IsTimeSliceRegenActive() const
|
|
{
|
|
return SyncTimeSlicedData.bTimeSliceRegenActive;
|
|
}
|
|
|
|
bool FRecastNavMeshGenerator::IsTileChanged(const FNavTileRef InTileRef) const
|
|
{
|
|
#if WITH_EDITOR
|
|
// Check recently built tiles
|
|
if (InTileRef.IsValid())
|
|
{
|
|
FTileTimestamp TileTimestamp;
|
|
TileTimestamp.NavTileRef = InTileRef;
|
|
if (RecentlyBuiltTiles.Contains(TileTimestamp))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
#endif//WITH_EDITOR
|
|
|
|
return false;
|
|
}
|
|
|
|
uint32 FRecastNavMeshGenerator::LogMemUsed() const
|
|
{
|
|
UE_LOG(LogNavigation, Display, TEXT(" FRecastNavMeshGenerator: self %llu"), sizeof(FRecastNavMeshGenerator));
|
|
|
|
uint32 GeneratorsMem = 0;
|
|
for (const TRunningTileElement<FRecastTileGeneratorWrapper>& Element : RunningDirtyTiles)
|
|
{
|
|
GeneratorsMem += Element.AsyncTask->GetTask().TileGenerator->UsedMemoryOnStartup;
|
|
if (SyncTimeSlicedData.TileGeneratorSync.IsValid())
|
|
{
|
|
GeneratorsMem += SyncTimeSlicedData.TileGeneratorSync->UsedMemoryOnStartup;
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
const bool bUseVirtualGeometryFilteringAndDirtying = NavData->bUseVirtualGeometryFilteringAndDirtying;
|
|
|
|
NavOctree->FindElementsWithBoundsTest(BoundingBox,
|
|
[this, NavData, &Indices, &CoordBuffer, Snapshot, &CategoryName, LogVerbosity, NavAreaVerbosity, bUseVirtualGeometryFilteringAndDirtying](const FNavigationOctreeElement& Element)
|
|
{
|
|
const bool bExportGeometry = Element.Data->HasGeometry() && (
|
|
bUseVirtualGeometryFilteringAndDirtying ?
|
|
ShouldGenerateGeometryForOctreeElement(Element, NavData->GetConfig()) :
|
|
Element.ShouldUseGeometry(NavData->GetConfig())
|
|
);
|
|
|
|
TArray<FTransform> InstanceTransforms;
|
|
Element.Data->NavDataPerInstanceTransformDelegate.ExecuteIfBound(Element.Bounds.GetBox(), InstanceTransforms);
|
|
|
|
if (bExportGeometry && Element.Data->CollisionData.Num())
|
|
{
|
|
FRecastGeometryCache CachedGeometry(Element.Data->CollisionData.GetData());
|
|
|
|
const uint32 NumIndices = CachedGeometry.Header.NumFaces * 3;
|
|
Indices.SetNum(NumIndices, EAllowShrinking::No);
|
|
for (uint32 IndicesIdx = 0; IndicesIdx < NumIndices; ++IndicesIdx)
|
|
{
|
|
Indices[IndicesIdx] = CachedGeometry.Indices[IndicesIdx];
|
|
}
|
|
|
|
auto AddElementFunc = [&](const FTransform& Transform)
|
|
{
|
|
const uint32 NumVerts = CachedGeometry.Header.NumVerts;
|
|
CoordBuffer.Reset(NumVerts);
|
|
for (uint32 VertIdx = 0; VertIdx < NumVerts * 3; VertIdx += 3)
|
|
{
|
|
CoordBuffer.Add(Transform.TransformPosition(Recast2UnrealPoint(&CachedGeometry.Verts[VertIdx])));
|
|
}
|
|
|
|
Snapshot->AddMesh(CoordBuffer, Indices, CategoryName, LogVerbosity, FColorList::LightGrey.WithAlpha(255));
|
|
};
|
|
|
|
if (InstanceTransforms.Num() == 0)
|
|
{
|
|
AddElementFunc(FTransform::Identity);
|
|
}
|
|
for (const FTransform& InstanceTransform : InstanceTransforms)
|
|
{
|
|
AddElementFunc(InstanceTransform);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TArray<FVector> Verts;
|
|
for (const FAreaNavModifier& AreaMod : Element.Data->Modifiers.GetAreas())
|
|
{
|
|
ENavigationShapeType::Type ShapeType = AreaMod.GetShapeType();
|
|
if (ShapeType == ENavigationShapeType::Unknown)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const int32 AreaId = NavData->GetAreaID(AreaMod.GetAreaClass());
|
|
const UClass* AreaClass = NavData->GetAreaClass(AreaId);
|
|
const UNavArea* DefArea = AreaClass ? ((UClass*)AreaClass)->GetDefaultObject<UNavArea>() : nullptr;
|
|
const FColor PolygonColor = AreaClass != FNavigationSystem::GetDefaultWalkableArea() ? (DefArea ? DefArea->DrawColor : NavData->GetConfig().Color) : FColorList::Cyan;
|
|
|
|
if (ShapeType == ENavigationShapeType::Box)
|
|
{
|
|
FBoxNavAreaData Box;
|
|
AreaMod.GetBox(Box);
|
|
|
|
Snapshot->AddBox(FBox::BuildAABB(Box.Origin, Box.Extent), FMatrix::Identity, CategoryName, NavAreaVerbosity, PolygonColor.WithAlpha(255));
|
|
}
|
|
else if (ShapeType == ENavigationShapeType::Cylinder)
|
|
{
|
|
FCylinderNavAreaData Cylinder;
|
|
AreaMod.GetCylinder(Cylinder);
|
|
|
|
Snapshot->AddCylinder(Cylinder.Origin, Cylinder.Origin + FVector(0, 0, Cylinder.Height), Cylinder.Radius, CategoryName, NavAreaVerbosity, PolygonColor.WithAlpha(255));
|
|
}
|
|
else if (ShapeType == ENavigationShapeType::Convex || ShapeType == ENavigationShapeType::InstancedConvex)
|
|
{
|
|
auto AddElementFunc = [&](const FConvexNavAreaData& InConvexNavAreaData)
|
|
{
|
|
Verts.Reset();
|
|
|
|
const TArray<FVector> Points = UE::LWC::ConvertArrayType<FVector>(InConvexNavAreaData.Points);
|
|
GrowConvexHull(NavData->AgentRadius, Points, Verts);
|
|
|
|
if (Verts.Num())
|
|
{
|
|
const float CellHeight = NavData->GetCellHeight(ENavigationDataResolution::Default);
|
|
Snapshot->AddPulledConvex(
|
|
Verts,
|
|
InConvexNavAreaData.MinZ - CellHeight,
|
|
InConvexNavAreaData.MaxZ + CellHeight,
|
|
CategoryName, NavAreaVerbosity, PolygonColor.WithAlpha(255));
|
|
}
|
|
};
|
|
|
|
if (ShapeType == ENavigationShapeType::Convex)
|
|
{
|
|
FConvexNavAreaData Convex;
|
|
AreaMod.GetConvex(Convex);
|
|
AddElementFunc(Convex);
|
|
}
|
|
else // ShapeType == ENavigationShapeType::InstancedConvex
|
|
{
|
|
for (const FTransform& InstanceTransform : InstanceTransforms)
|
|
{
|
|
FConvexNavAreaData Convex;
|
|
AreaMod.GetPerInstanceConvex(InstanceTransform, Convex);
|
|
AddElementFunc(Convex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
}
|
|
}
|
|
#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<FVector::FReal> CoordBuffer;
|
|
TNavStatArray<int32> IndexBuffer;
|
|
const ARecastNavMesh* NavData = Cast<const ARecastNavMesh>(NavSys->NavDataSet[Index]);
|
|
if (NavData)
|
|
{
|
|
const bool bUseVirtualGeometryFilteringAndDirtying = NavData->bUseVirtualGeometryFilteringAndDirtying;
|
|
|
|
struct FAreaExportData
|
|
{
|
|
FConvexNavAreaData Convex;
|
|
uint8 AreaId;
|
|
};
|
|
TArray<FAreaExportData> AreaExport;
|
|
|
|
NavOctree->FindElementsWithBoundsTest(TotalNavBounds,
|
|
[this, NavData, &IndexBuffer, &CoordBuffer, &AreaExport, bUseVirtualGeometryFilteringAndDirtying](const FNavigationOctreeElement& Element)
|
|
{
|
|
const bool bExportGeometry = Element.Data->HasGeometry() && (
|
|
bUseVirtualGeometryFilteringAndDirtying ?
|
|
ShouldGenerateGeometryForOctreeElement(Element, NavData->GetConfig()) :
|
|
Element.ShouldUseGeometry(NavData->GetConfig())
|
|
);
|
|
|
|
TArray<FTransform> InstanceTransforms;
|
|
Element.Data->NavDataPerInstanceTransformDelegate.ExecuteIfBound(Element.Bounds.GetBox(), InstanceTransforms);
|
|
|
|
if (bExportGeometry && Element.Data->CollisionData.Num())
|
|
{
|
|
const int32 NumInstances = FMath::Max(InstanceTransforms.Num(), 1);
|
|
FRecastGeometryCache CachedGeometry(Element.Data->CollisionData.GetData());
|
|
IndexBuffer.Reserve( IndexBuffer.Num() + (CachedGeometry.Header.NumFaces * 3 ) * NumInstances );
|
|
CoordBuffer.Reserve( CoordBuffer.Num() + (CachedGeometry.Header.NumVerts * 3 ) * NumInstances );
|
|
|
|
if (InstanceTransforms.Num() == 0)
|
|
{
|
|
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]);
|
|
}
|
|
}
|
|
for (const FTransform& InstanceTransform : InstanceTransforms)
|
|
{
|
|
for (int32 i = 0; i < CachedGeometry.Header.NumFaces * 3; i++)
|
|
{
|
|
IndexBuffer.Add(CachedGeometry.Indices[i] + CoordBuffer.Num() / 3);
|
|
}
|
|
|
|
FMatrix LocalToRecastWorld = InstanceTransform.ToMatrixWithScale()*Unreal2RecastMatrix();
|
|
|
|
for (int32 i = 0; i < CachedGeometry.Header.NumVerts * 3; i += 3)
|
|
{
|
|
// collision cache stores coordinates in recast space, convert them to unreal and transform to recast world space
|
|
FVector WorldRecastCoord = LocalToRecastWorld.TransformPosition(Recast2UnrealPoint(&CachedGeometry.Verts[i]));
|
|
|
|
CoordBuffer.Add(WorldRecastCoord.X);
|
|
CoordBuffer.Add(WorldRecastCoord.Y);
|
|
CoordBuffer.Add(WorldRecastCoord.Z);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (const FAreaNavModifier& AreaMod : Element.Data->Modifiers.GetAreas())
|
|
{
|
|
ENavigationShapeType::Type ShapeType = AreaMod.GetShapeType();
|
|
|
|
if (ShapeType == ENavigationShapeType::Convex || ShapeType == ENavigationShapeType::InstancedConvex)
|
|
{
|
|
FAreaExportData ExportInfo;
|
|
const int32 AreaId = NavData->GetAreaID(AreaMod.GetAreaClass());
|
|
ExportInfo.AreaId = AreaId != INDEX_NONE ? IntCastChecked<uint8>(AreaId) : INDEX_NONE;
|
|
|
|
auto AddAreaExportDataFunc = [&](const FConvexNavAreaData& InConvexNavAreaData)
|
|
{
|
|
TArray<FVector> ConvexVerts;
|
|
|
|
const TArray<FVector> Points = UE::LWC::ConvertArrayType<FVector>(ExportInfo.Convex.Points);
|
|
GrowConvexHull(NavData->AgentRadius, Points, ConvexVerts);
|
|
if (ConvexVerts.Num())
|
|
{
|
|
const float CellHeight = NavData->GetCellHeight(ENavigationDataResolution::Default);
|
|
ExportInfo.Convex.MinZ -= CellHeight;
|
|
ExportInfo.Convex.MaxZ += CellHeight;
|
|
|
|
ExportInfo.Convex.Points = UE::LWC::ConvertArrayType<FVector>(ConvexVerts);
|
|
|
|
AreaExport.Add(ExportInfo);
|
|
}
|
|
};
|
|
|
|
if (ShapeType == ENavigationShapeType::Convex)
|
|
{
|
|
AreaMod.GetConvex(ExportInfo.Convex);
|
|
AddAreaExportDataFunc(ExportInfo.Convex);
|
|
}
|
|
else // ShapeType == ENavigationShapeType::InstancedConvex
|
|
{
|
|
for (const FTransform& InstanceTransform : InstanceTransforms)
|
|
{
|
|
AreaMod.GetPerInstanceConvex(InstanceTransform, ExportInfo.Convex);
|
|
AddAreaExportDataFunc(ExportInfo.Convex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
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(static_cast<float>(CurrentGen->Config.minRegionArea)));
|
|
AdditionalData += FString::Printf(TEXT("# Region merge size\n"));
|
|
AdditionalData += FString::Printf(TEXT("rd_rmas %d\n"), (uint32)FMath::Sqrt(static_cast<float>(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
|
|
{
|
|
protected:
|
|
/** Console commands, see embeded usage statement **/
|
|
virtual bool Exec_Dev( UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar ) override
|
|
{
|
|
bool bExported = false;
|
|
#if ALLOW_DEBUG_FILES && ENABLE_VISUAL_LOG
|
|
if (FParse::Command(&Cmd, TEXT("ExportNavigation")))
|
|
{
|
|
if (InWorld == nullptr)
|
|
{
|
|
UE_LOG(LogNavigation, Error, TEXT("Failed to export navigation data due to missing UWorld"));
|
|
}
|
|
else
|
|
{
|
|
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(InWorld);
|
|
if (NavSys)
|
|
{
|
|
for (ANavigationData* NavData : NavSys->NavDataSet)
|
|
{
|
|
if (const FNavDataGenerator* Generator = NavData->GetGenerator())
|
|
{
|
|
Generator->ExportNavigationData(FString::Printf(TEXT("%s/%s"), *FPaths::ProjectSavedDir(), *NavData->GetName()));
|
|
bExported = true;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogNavigation, Error, TEXT("Failed to export navigation data %s due to missing generator"), *NavData->GetName());
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogNavigation, Error, TEXT("Failed to export navigation data due to missing navigation system"));
|
|
}
|
|
}
|
|
}
|
|
#endif // ALLOW_DEBUG_FILES && !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
|
|
return bExported;
|
|
}
|
|
} NavigationGeomExec;
|
|
|
|
#if RECAST_INTERNAL_DEBUG_DATA
|
|
bool FRecastTileGenerator::IsTileDebugActive() const
|
|
{
|
|
return (TileDebugSettings.bEnabled && TileDebugSettings.IsWithinTileCoordinates(TileX, TileY))
|
|
|| (TileX == GNavmeshDebugTileX && TileY == GNavmeshDebugTileY);
|
|
}
|
|
|
|
bool FRecastTileGenerator::IsTileDebugAllowingGeneration() const
|
|
{
|
|
if (TileDebugSettings.bEnabled && TileDebugSettings.bGenerateDebugTileOnly)
|
|
{
|
|
return TileDebugSettings.IsWithinTileCoordinates(TileX, TileY);
|
|
}
|
|
else if (GNavmeshGenerateDebugTileOnly)
|
|
{
|
|
return TileX == GNavmeshDebugTileX && TileY == GNavmeshDebugTileY;
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
#endif //RECAST_INTERNAL_DEBUG_DATA
|
|
#endif // WITH_RECAST
|