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