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

645 lines
19 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "NavCollision.h"
#include "Serialization/MemoryWriter.h"
#include "NavigationSystem.h"
#include "Interfaces/Interface_CollisionDataProvider.h"
#include "Engine/StaticMesh.h"
#include "PrimitiveDrawingUtils.h"
#include "NavAreas/NavArea.h"
#include "AI/NavigationSystemHelpers.h"
#include "DerivedDataPluginInterface.h"
#include "DerivedDataCacheInterface.h"
#include "PhysicsEngine/BodySetup.h"
#include "ProfilingDebugging/CookStats.h"
#include "Interfaces/ITargetPlatform.h"
#include "CoreGlobals.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(NavCollision)
#if WITH_EDITOR
#include "DeviceProfiles/DeviceProfile.h"
#include "DeviceProfiles/DeviceProfileManager.h"
#endif // WITH_EDITOR
static TAutoConsoleVariable<int32> CVarNavCollisionAvailable(
TEXT("ai.NavCollisionAvailable"),
1,
TEXT("If set to 0 NavCollision won't be cooked and will be unavailable at runtime.\n"),
/*ECVF_ReadOnly | */ECVF_Scalability);
#if ENABLE_COOK_STATS
namespace NavCollisionCookStats
{
static FCookStats::FDDCResourceUsageStats UsageStats;
static FCookStatsManager::FAutoRegisterCallback RegisterCookStats([](FCookStatsManager::AddStatFuncRef AddStat)
{
UsageStats.LogStats(AddStat, TEXT("NavCollision.Usage"), TEXT(""));
});
}
#endif
static const FName NAVCOLLISION_FORMAT = TEXT("NavCollision_Chaos");
class FNavCollisionDataReader
{
public:
FNavCollisionConvex& TriMeshCollision;
FNavCollisionConvex& ConvexCollision;
TNavStatArray<int32>& ConvexShapeIndices;
FBox& Bounds;
FNavCollisionDataReader(FByteBulkData& InBulkData, FNavCollisionConvex& InTriMeshCollision, FNavCollisionConvex& InConvexCollision, TNavStatArray<int32>& InShapeIndices, FBox& InBounds)
: TriMeshCollision(InTriMeshCollision)
, ConvexCollision(InConvexCollision)
, ConvexShapeIndices(InShapeIndices)
, Bounds(InBounds)
{
// Read cooked data
uint8* DataPtr = (uint8*)InBulkData.Lock( LOCK_READ_ONLY );
FBufferReader Ar( DataPtr, InBulkData.GetBulkDataSize(), false, true );
uint8 bLittleEndian = true;
Ar << bLittleEndian;
Ar.SetByteSwapping( PLATFORM_LITTLE_ENDIAN ? !bLittleEndian : !!bLittleEndian );
Ar << TriMeshCollision.VertexBuffer;
Ar << TriMeshCollision.IndexBuffer;
Ar << ConvexCollision.VertexBuffer;
Ar << ConvexCollision.IndexBuffer;
Ar << ConvexShapeIndices;
Ar << Bounds;
InBulkData.Unlock();
}
};
//----------------------------------------------------------------------//
// FDerivedDataNavCollisionCooker
//----------------------------------------------------------------------//
class FDerivedDataNavCollisionCooker : public FDerivedDataPluginInterface
{
private:
UNavCollision* NavCollisionInstance;
UObject* CollisionDataProvider;
FName Format;
FGuid DataGuid;
FString MeshId;
public:
FDerivedDataNavCollisionCooker(FName InFormat, UNavCollision* InInstance);
virtual const TCHAR* GetPluginName() const override
{
return TEXT("NavCollision");
}
virtual const TCHAR* GetVersionString() const override
{
return TEXT("8F4645C7A18B40C487C7E50E19CD6B6C");
}
virtual FString GetPluginSpecificCacheKeySuffix() const override
{
const uint16 Version = 15;
return FString::Printf( TEXT("%s_%s_%s_%hu")
, *Format.ToString()
, *DataGuid.ToString()
, *MeshId
, Version
);
}
virtual bool IsBuildThreadsafe() const override
{
return false;
}
virtual bool Build( TArray<uint8>& OutData ) override;
virtual FString GetDebugContextString() const override;
/** Return true if we can build **/
bool CanBuild()
{
return true;
}
};
FDerivedDataNavCollisionCooker::FDerivedDataNavCollisionCooker(FName InFormat, UNavCollision* InInstance)
: NavCollisionInstance(InInstance)
, CollisionDataProvider( NULL )
, Format( InFormat )
{
check(NavCollisionInstance != NULL);
CollisionDataProvider = NavCollisionInstance->GetOuter();
DataGuid = NavCollisionInstance->GetGuid();
IInterface_CollisionDataProvider* CDP = Cast<IInterface_CollisionDataProvider>(CollisionDataProvider);
if (CDP)
{
CDP->GetMeshId(MeshId);
}
}
bool FDerivedDataNavCollisionCooker::Build( TArray<uint8>& OutData )
{
TRACE_CPUPROFILER_EVENT_SCOPE(FDerivedDataNavCollisionCooker::Build);
if ((NavCollisionInstance->ConvexShapeIndices.Num() == 0) ||
(NavCollisionInstance->GetTriMeshCollision().VertexBuffer.Num() == 0 && NavCollisionInstance->GetConvexCollision().VertexBuffer.Num() == 0))
{
NavCollisionInstance->GatherCollision();
}
FMemoryWriter Ar( OutData );
uint8 bLittleEndian = PLATFORM_LITTLE_ENDIAN;
Ar << bLittleEndian;
int64 CookedMeshInfoOffset = Ar.Tell();
Ar << NavCollisionInstance->GetMutableTriMeshCollision().VertexBuffer;
Ar << NavCollisionInstance->GetMutableTriMeshCollision().IndexBuffer;
Ar << NavCollisionInstance->GetMutableConvexCollision().VertexBuffer;
Ar << NavCollisionInstance->GetMutableConvexCollision().IndexBuffer;
Ar << NavCollisionInstance->ConvexShapeIndices;
Ar << NavCollisionInstance->Bounds;
// Whatever got cached return true. We want to cache 'failure' too.
return true;
}
FString FDerivedDataNavCollisionCooker::GetDebugContextString() const
{
if (NavCollisionInstance)
{
UObject* Outer = NavCollisionInstance->GetOuter();
if (Outer)
{
return Outer->GetFullName();
}
}
return FDerivedDataPluginInterface::GetDebugContextString();
}
namespace
{
UNavCollisionBase* CreateNewNavCollisionInstance(UObject& Outer)
{
return NewObject<UNavCollision>(&Outer);
}
}
//----------------------------------------------------------------------//
// UNavCollision
//----------------------------------------------------------------------//
UNavCollision::UNavCollision(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
bGatherConvexGeometry = true;
bHasConvexGeometry = false;
bForceGeometryRebuild = false;
bCreateOnClient = true;
}
void UNavCollision::PostInitProperties()
{
Super::PostInitProperties();
// if bCreateOnClient is false we're not even going to bind the delegate
if (HasAnyFlags(RF_ClassDefaultObject)
&& (GIsServer || bCreateOnClient
#if WITH_EDITOR
|| GIsEditor
#endif
)
)
{
UNavCollisionBase::ConstructNewInstanceDelegate = UNavCollisionBase::FConstructNew::CreateStatic(&CreateNewNavCollisionInstance);
}
}
FGuid UNavCollision::GetGuid() const
{
return BodySetupGuid;
}
void UNavCollision::Setup(UBodySetup* BodySetup)
{
// Create meshes from cooked data if not already done
if (bHasConvexGeometry || BodySetup == NULL || BodySetupGuid == BodySetup->BodySetupGuid)
{
return;
}
LLM_SCOPE_BYNAME(TEXT("NavigationCollision"));
BodySetupGuid = BodySetup->BodySetupGuid;
// Make sure all are cleared before we start
ClearCollision();
// Find or create cooked navcollision data
FByteBulkData* FormatData = GetCookedData(NAVCOLLISION_FORMAT);
if (!bForceGeometryRebuild && FormatData)
{
// if it's not being already processed
if (FormatData->IsLocked() == false)
{
// Create physics objects
FNavCollisionDataReader CookedDataReader(*FormatData, TriMeshCollision, ConvexCollision, ConvexShapeIndices, Bounds);
bHasConvexGeometry = true;
}
}
else if (FPlatformProperties::RequiresCookedData() == false)
{
GatherCollision();
}
}
FBox UNavCollision::GetBounds() const
{
return Bounds;
}
void UNavCollision::GatherCollision()
{
ClearCollision();
UStaticMesh* StaticMeshOuter = Cast<UStaticMesh>(GetOuter());
if (bGatherConvexGeometry && StaticMeshOuter && StaticMeshOuter->GetBodySetup())
{
NavigationHelper::GatherCollision(StaticMeshOuter->GetBodySetup(), this);
}
FKAggregateGeom SimpleGeom;
for (int32 Idx = 0; Idx < BoxCollision.Num(); Idx++)
{
const FNavCollisionBox& BoxInfo = BoxCollision[Idx];
const float X = FloatCastChecked<float>(BoxInfo.Extent.X * 2.0, UE::LWC::DefaultFloatPrecision);
const float Y = FloatCastChecked<float>(BoxInfo.Extent.Y * 2.0, UE::LWC::DefaultFloatPrecision);
const float Z = FloatCastChecked<float>(BoxInfo.Extent.Z * 2.0, UE::LWC::DefaultFloatPrecision);
FKBoxElem BoxElem(X, Y, Z);
BoxElem.SetTransform(FTransform(BoxInfo.Offset));
SimpleGeom.BoxElems.Add(BoxElem);
}
// not really a cylinder, but should be close enough
for (int32 Idx = 0; Idx < CylinderCollision.Num(); Idx++)
{
const FNavCollisionCylinder& CylinderInfo = CylinderCollision[Idx];
FKSphylElem SphylElem(CylinderInfo.Radius, CylinderInfo.Height);
SphylElem.SetTransform(FTransform(CylinderInfo.Offset + FVector(0.f, 0.f, 0.5f*CylinderInfo.Height)));
SimpleGeom.SphylElems.Add(SphylElem);
}
if (SimpleGeom.GetElementCount())
{
NavigationHelper::GatherCollision(SimpleGeom, *this);
}
bHasConvexGeometry = (TriMeshCollision.VertexBuffer.Num() > 0) || (ConvexCollision.VertexBuffer.Num() > 0);
}
void UNavCollision::ClearCollision()
{
TriMeshCollision.VertexBuffer.Reset();
TriMeshCollision.IndexBuffer.Reset();
ConvexCollision.VertexBuffer.Reset();
ConvexCollision.IndexBuffer.Reset();
ConvexShapeIndices.Reset();
Bounds = FBox(ForceInitToZero);
bHasConvexGeometry = false;
}
void UNavCollision::GetNavigationModifier(FCompositeNavModifier& Modifier, const FTransform& LocalToWorld)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_NavCollision_GetNavigationModifier);
const TSubclassOf<UNavArea> UseAreaClass = AreaClass ? AreaClass : (const TSubclassOf<UNavArea>)(FNavigationSystem::GetDefaultObstacleArea());
// rebuild collision data if needed
if (!bHasConvexGeometry)
{
GatherCollision();
}
const int32 NumModifiers = (TriMeshCollision.VertexBuffer.Num() ? 1 : 0) + ConvexShapeIndices.Num();
Modifier.ReserveForAdditionalAreas(NumModifiers);
auto AddModFunc = [&](const TNavStatArray<FVector>& VertexBuffer, const int32 FirstVertIndex, const int32 LastVertIndex)
{
FAreaNavModifier AreaMod;
if (Modifier.IsPerInstanceModifier())
{
AreaMod.InitializePerInstanceConvex(VertexBuffer, FirstVertIndex, LastVertIndex, UseAreaClass);
}
else
{
AreaMod.InitializeConvex(VertexBuffer, FirstVertIndex, LastVertIndex, LocalToWorld, UseAreaClass);
}
AreaMod.SetIncludeAgentHeight(true);
Modifier.Add(AreaMod);
};
int32 LastVertIndex = 0;
for (int32 Idx = 0; Idx < ConvexShapeIndices.Num(); Idx++)
{
const int32 FirstVertIndex = LastVertIndex;
LastVertIndex = ConvexShapeIndices.IsValidIndex(Idx + 1) ? ConvexShapeIndices[Idx + 1] : ConvexCollision.VertexBuffer.Num();
// @todo this is a temp fix. A proper fix is making sure ConvexShapeIndices doesn't
// contain any duplicates (which is the original cause of UE-52123)
if (FirstVertIndex < LastVertIndex)
{
AddModFunc(ConvexCollision.VertexBuffer, FirstVertIndex, LastVertIndex);
}
}
if (TriMeshCollision.VertexBuffer.Num() > 0)
{
AddModFunc(TriMeshCollision.VertexBuffer, 0, TriMeshCollision.VertexBuffer.Num() - 1);
}
}
bool UNavCollision::ExportGeometry(const FTransform& LocalToWorld, FNavigableGeometryExport& GeoExport) const
{
if (bHasConvexGeometry)
{
GeoExport.ExportCustomMesh(ConvexCollision.VertexBuffer.GetData(), ConvexCollision.VertexBuffer.Num(),
ConvexCollision.IndexBuffer.GetData(), ConvexCollision.IndexBuffer.Num(),
LocalToWorld);
GeoExport.ExportCustomMesh(TriMeshCollision.VertexBuffer.GetData(), TriMeshCollision.VertexBuffer.Num(),
TriMeshCollision.IndexBuffer.GetData(), TriMeshCollision.IndexBuffer.Num(),
LocalToWorld);
}
return bHasConvexGeometry;
}
void DrawCylinderHelper(FPrimitiveDrawInterface* PDI, const FMatrix& ElemTM, const FVector::FReal Radius, const FVector::FReal Height, const FColor Color)
{
constexpr FVector::FReal AngleDelta = 2.0 * UE_DOUBLE_PI / 16.0;
FVector X, Y, Z;
ElemTM.GetUnitAxes(X, Y, Z);
FVector LastVertex = ElemTM.GetOrigin() + X * Radius;
for(int32 SideIndex = 0;SideIndex < 16;SideIndex++)
{
const FVector Vertex = ElemTM.GetOrigin() +
(X * FMath::Cos(AngleDelta * static_cast<FVector::FReal>(SideIndex + 1)) + Y * FMath::Sin(AngleDelta * static_cast<FVector::FReal>(SideIndex + 1))) * Radius;
PDI->DrawLine(LastVertex,Vertex,Color,SDPG_World);
PDI->DrawLine(LastVertex + Z * Height,Vertex + Z * Height,Color,SDPG_World);
PDI->DrawLine(LastVertex,LastVertex + Z * Height,Color,SDPG_World);
LastVertex = Vertex;
}
}
void DrawBoxHelper(FPrimitiveDrawInterface* PDI, const FMatrix& ElemTM, const FVector& Extent, const FColor Color)
{
FVector B[2], P, Q;
B[0] = Extent; // max
B[1] = -1.0f * Extent; // min
for( int32 i=0; i<2; i++ )
{
for( int32 j=0; j<2; j++ )
{
P.X=B[i].X; Q.X=B[i].X;
P.Y=B[j].Y; Q.Y=B[j].Y;
P.Z=B[0].Z; Q.Z=B[1].Z;
PDI->DrawLine( ElemTM.TransformPosition(P), ElemTM.TransformPosition(Q), Color, SDPG_World);
P.Y=B[i].Y; Q.Y=B[i].Y;
P.Z=B[j].Z; Q.Z=B[j].Z;
P.X=B[0].X; Q.X=B[1].X;
PDI->DrawLine( ElemTM.TransformPosition(P), ElemTM.TransformPosition(Q), Color, SDPG_World);
P.Z=B[i].Z; Q.Z=B[i].Z;
P.X=B[j].X; Q.X=B[j].X;
P.Y=B[0].Y; Q.Y=B[1].Y;
PDI->DrawLine( ElemTM.TransformPosition(P), ElemTM.TransformPosition(Q), Color, SDPG_World);
}
}
}
void UNavCollision::DrawSimpleGeom(FPrimitiveDrawInterface* PDI, const FTransform& Transform, const FColor Color)
{
const FMatrix ParentTM = Transform.ToMatrixWithScale();
for (int32 i = 0; i < CylinderCollision.Num(); i++)
{
FMatrix ElemTM = FTranslationMatrix(CylinderCollision[i].Offset);
ElemTM *= ParentTM;
DrawCylinderHelper(PDI, ElemTM, CylinderCollision[i].Radius, CylinderCollision[i].Height, Color);
}
for (int32 i = 0; i < BoxCollision.Num(); i++)
{
FMatrix ElemTM = FTranslationMatrix(BoxCollision[i].Offset);
ElemTM *= ParentTM;
DrawBoxHelper(PDI, ElemTM, BoxCollision[i].Extent, Color);
}
}
#if WITH_EDITOR
void UNavCollision::InvalidateCollision()
{
ClearCollision();
bForceGeometryRebuild = true;
}
void UNavCollision::InvalidatePhysicsData()
{
ClearCollision();
CookedFormatData.FlushData();
}
#endif // WITH_EDITOR
void UNavCollision::Serialize(FArchive& Ar)
{
Super::Serialize(Ar);
const int32 VerInitial = 1;
const int32 VerAreaClass = 2;
const int32 VerConvexTransforms = 3;
const int32 VerShapeGeoExport = 4;
const int32 VerLatest = VerShapeGeoExport;
// use magic number to determine if serialized stream has version :/
const int32 MagicNum = 0xA237F237;
int64 StreamStartPos = Ar.Tell();
int32 Version = VerLatest;
int32 MyMagicNum = MagicNum;
Ar << MyMagicNum;
if (MyMagicNum != MagicNum)
{
Version = VerInitial;
Ar.Seek(StreamStartPos);
}
else
{
Ar << Version;
}
// loading a dummy GUID to have serialization not break on
// packages serialized before switching over UNavCollision to
// use BodySetup's guid rather than its own one
// motivation: not creating a new engine version
// @NOTE could be addressed during next engine version bump
FGuid Guid;
Ar << Guid;
bool bCooked = Ar.IsCooking();
Ar << bCooked;
if (FPlatformProperties::RequiresCookedData() && !bCooked && Ar.IsLoading())
{
UE_LOG(LogNavigation, Fatal, TEXT("This platform requires cooked packages, and NavCollision data was not cooked into %s."), *GetFullName());
}
const bool bUseConvexCollisionVer3 = bGatherConvexGeometry || (CylinderCollision.Num() == 0 && BoxCollision.Num() == 0);
const bool bUseConvexCollision = bGatherConvexGeometry || (BoxCollision.Num() > 0) || (CylinderCollision.Num() > 0);
const bool bProcessCookedData = (Version >= VerShapeGeoExport) ? bUseConvexCollision : bUseConvexCollisionVer3;
if (bCooked && bProcessCookedData)
{
if (Ar.IsCooking())
{
FName Format = NAVCOLLISION_FORMAT;
GetCookedData(Format); // Get the data from the DDC or build it
TArray<FName> ActualFormatsToSave;
ActualFormatsToSave.Add(Format);
CookedFormatData.Serialize(Ar, this, &ActualFormatsToSave);
}
else
{
CookedFormatData.Serialize(Ar, this);
}
}
if (Version >= VerAreaClass)
{
Ar << AreaClass;
}
if (Version < VerShapeGeoExport && Ar.IsLoading() && GIsEditor)
{
bForceGeometryRebuild = true;
}
}
void UNavCollision::PostLoad()
{
Super::PostLoad();
// Our owner needs to be post-loaded before us else they may not have loaded
// their data yet.
UObject* Outer = GetOuter();
if (Outer)
{
Outer->ConditionalPostLoad();
UStaticMesh* StaticMeshOuter = Cast<UStaticMesh>(Outer);
// It's OK to skip this in case of StaticMesh pending compilation because it is also
// called by UStaticMesh::CreateNavCollision at the end of UStaticMesh's PostLoad.
if (StaticMeshOuter != nullptr && !StaticMeshOuter->IsCompiling())
{
Setup(StaticMeshOuter->GetBodySetup());
}
}
}
FByteBulkData* UNavCollision::GetCookedData(FName Format)
{
TRACE_CPUPROFILER_EVENT_SCOPE(UNavCollision::GetCookedData);
const bool bUseConvexCollision = bGatherConvexGeometry || (BoxCollision.Num() > 0) || (CylinderCollision.Num() > 0);
if (IsTemplate() || !bUseConvexCollision)
{
return nullptr;
}
bool bContainedData = CookedFormatData.Contains(Format);
FByteBulkData* Result = &CookedFormatData.GetFormat(Format);
if (!bContainedData && CVarNavCollisionAvailable.GetValueOnAnyThread() != 0)
{
if (FPlatformProperties::RequiresCookedData())
{
UE_LOG(LogNavigation, Error, TEXT("Attempt to build nav collision data for %s when we are unable to. This platform requires cooked packages."), *GetPathName());
return nullptr;
}
TArray<uint8> OutData;
FDerivedDataNavCollisionCooker* DerivedNavCollisionData = new FDerivedDataNavCollisionCooker(Format, this);
if (DerivedNavCollisionData->CanBuild())
{
bool bDataWasBuilt = false;
COOK_STAT(auto Timer = NavCollisionCookStats::UsageStats.TimeSyncWork());
if (GetDerivedDataCacheRef().GetSynchronous(DerivedNavCollisionData, OutData, &bDataWasBuilt))
{
COOK_STAT(Timer.AddHitOrMiss(bDataWasBuilt ? FCookStats::CallStats::EHitOrMiss::Miss : FCookStats::CallStats::EHitOrMiss::Hit, OutData.Num()));
if (OutData.Num())
{
Result->Lock(LOCK_READ_WRITE);
FMemory::Memcpy(Result->Realloc(OutData.Num()), OutData.GetData(), OutData.Num());
Result->Unlock();
}
}
}
}
UE_CLOG(!Result, LogNavigation, Error, TEXT("Failed to read CoockedDataFormat for %s."), *GetPathName());
return (Result && Result->GetBulkDataSize() > 0) ? Result : nullptr; // we don't return empty bulk data...but we save it to avoid thrashing the DDC
}
void UNavCollision::GetResourceSizeEx(FResourceSizeEx& CumulativeResourceSize)
{
Super::GetResourceSizeEx(CumulativeResourceSize);
if (CookedFormatData.Contains(NAVCOLLISION_FORMAT))
{
const FByteBulkData& FmtData = CookedFormatData.GetFormat(NAVCOLLISION_FORMAT);
CumulativeResourceSize.AddDedicatedSystemMemoryBytes(FmtData.GetBulkDataSize());
}
}
bool UNavCollision::NeedsLoadForTargetPlatform(const class ITargetPlatform* TargetPlatform) const
{
#if WITH_EDITOR
const UDeviceProfile* DeviceProfile = UDeviceProfileManager::Get().FindProfile(TargetPlatform->IniPlatformName());
if (DeviceProfile)
{
int32 CVarNavCollisionAvailableVal = 1;
if (DeviceProfile->GetConsolidatedCVarValue(TEXT("ai.NavCollisionAvailable"), CVarNavCollisionAvailableVal))
{
return CVarNavCollisionAvailableVal != 0;
}
}
#endif // WITH_EDITOR
return true;
}
void UNavCollision::CopyUserSettings(const UNavCollision& OtherData)
{
CylinderCollision = OtherData.CylinderCollision;
BoxCollision = OtherData.BoxCollision;
AreaClass = OtherData.AreaClass;
bIsDynamicObstacle = OtherData.bIsDynamicObstacle;
bGatherConvexGeometry = OtherData.bGatherConvexGeometry;
}